Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "aitokenpool"
version = "0.7.1"
version = "0.7.2"
edition = "2021"
description = "AI Token 共享池 — 企业 key 池 + 公共共享市场"
license = "MIT"
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ AITokenPool 是一个开源的 **AI Token 共享平台**:企业版(内部 ke

## 状态

后端 + 前端已全部实现,当前版本 **v0.7.1**(config.toml 模型目录唯一真源):
后端 + 前端已全部实现,当前版本 **v0.7.2**(config.toml 模型目录唯一真源):

- ✅ **P0-A(v0.2.0)**:后端骨架 + TOML 配置(`Config::validate`:points_per_unit>0 / plan→provider 存在 / endpoints≥1 / protocol 枚举)+ SQLite 数据层(幂等迁移,**生产空库不种任何种子数据**)+ 认证(argon2 + Bearer API Key)+ API Key 端点
- ✅ **P0-B(v0.2.1)**:网关转发(OpenAI Chat Completions + Anthropic Messages)+ 路由故障转移(粘性 / 健康冷却 / 3 次切换上限)+ 计量账本(点数计算、90/10 分成、事务性 settle)
Expand All @@ -40,6 +40,7 @@ AITokenPool 是一个开源的 **AI Token 共享平台**:企业版(内部 ke
- ✅ **v0.6.6**:**统一数据目录(rant 2026-08-19T20:53:23)**——`ATP_DATA_DIR`(默认 `./data`;`--data-dir` > env > 默认)下放 config.toml(首次自动复制示例)+ aitokenpool.db + logs/,目录自动创建;数据库路径统一由 data-dir 决定(config `db_path` 忽略);Docker 单卷挂载 `./atp-data:/data`(镜像内置示例配置,首启自动复制);.gitignore/.dockerignore 补 atp-data/;旧 data/ 迁移说明
- ✅ **v0.6.7**:**日志系统(rant 2026-08-19T20:54:26)**——log4rs 替换 env_logger:日志落盘 `<data-dir>/logs/aitokenpool.log` + stdout 双写;按大小滚动(`[log].max_file_size`,默认 10MB)+ `max_backups`(默认 7)自动清理旧日志;`[log]` 配置段(dir / level / file_pattern);Docker 日志随统一目录持久化
- ✅ **v0.7.0**:**缓存命中/未命中分开计费(rant 2026-08-20T10:17:27)**——usage 解析拆分缓存 token(openai `prompt_tokens_details.cached_tokens` / anthropic `cache_read_input_tokens` / responses `input_tokens_details.cached_tokens`);计费 = 未命中 × input_per_m + 命中 × cache_hit_input_per_m(缺省 0 = 命中免费);usage_records 新增 `cached_tokens` 列(迁移 v8);DeepSeek 官方 CNY 定价(deepseek-v4-flash 1.5/0.05/4.5、deepseek-v4-pro 4.5/0.15/13.5,空闲价);管理端模型表单可配缓存命中输入价;流式(SSE 转换 + 透传)同拆
- ✅ **v0.7.2**:**seed 同步删除(rant 2026-08-20T11:58:33)**——启动 `seed_models` 在 upsert 之外同步删除 config 中已移除的模型行(config `[[models]]` 全量权威;市场不再显示幽灵模型;admin 运行时增删改下次启动被 config 覆盖/清掉,持久化自定义模型请写入 config.toml);新增测试 `seed_models_deletes_config_removed_rows`(125 passed)
- ✅ **v0.7.1**:**简化模型配置(rant 2026-08-20T10:27:13)**——模型(厂商/价格/上下文/读图等)全部在 config.toml `[[models]]` 段定义(唯一真源),启动 upsert 进 DB;**移除 data/models.example.json 与 price_overrides 双层机制**(不再有「json 聚合源 + 官方价覆盖」设想);10 个模型迁入 config(DeepSeek 官方 CNY 价 flash 1.5/0.05/4.5、pro 4.5/0.15/13.5);data.js 游客兜底价对齐 config

`ui/` 已由纯静态原型升级为**对接真实 API**(登录、钱包、市场、共享、交易、设置、管理、运营全部真实数据),由后端 `ServeDir` 静态托管,无需单独部署前端。
Expand Down
67 changes: 65 additions & 2 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,8 +416,10 @@ pub(crate) fn seed_test_users(conn: &Connection) -> Result<()> {
Ok(())
}

/// models 表种子(启动时 upsert):config.toml [[models]] 唯一真源
/// models 表种子(启动时同步):config.toml [[models]] 唯一真源
///(rant 2026-08-20T10:27:13:移除 models.json + price_overrides 双层机制)
///(rant 2026-08-20T11:58:33:upsert 之外同步删除 config 已移除的模型,避免市场幽灵模型)
/// 语义:config 全量权威——启动后 models 表 = config [[models]](admin 运行时增改在下次启动被 config 覆盖/清掉)
pub fn seed_models(conn: &Connection, cfg: &crate::config::Config) -> Result<()> {
let mut n = 0u32;
for m in &cfg.models {
Expand Down Expand Up @@ -449,7 +451,34 @@ pub fn seed_models(conn: &Connection, cfg: &crate::config::Config) -> Result<()>
)?;
n += 1;
}
log::info!("models seed:{n} 行 upsert(来源 config.toml [[models]])");
// 同步删除 config 中已移除的模型(模型行无外键引用,usage_records/transactions 均按 model 字符串记)
let mut removed = 0u32;
let stale: Vec<(String, String)> = {
let mut stmt = conn
.prepare("SELECT provider, model FROM models")
.context("prepare models scan")?;
let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))?;
let mut v = Vec::new();
for row in rows {
let (p, m) = row?;
if !cfg
.models
.iter()
.any(|cm| cm.provider == p && cm.model == m)
{
v.push((p, m));
}
}
v
};
for (p, m) in &stale {
conn.execute(
"DELETE FROM models WHERE provider = ?1 AND model = ?2",
rusqlite::params![p, m],
)?;
removed += 1;
}
log::info!("models seed:{n} 行 upsert(来源 config.toml [[models]]),删除 {removed} 行 config 已移除模型");
Ok(())
}

Expand Down Expand Up @@ -636,6 +665,40 @@ mod tests {
let _ = std::fs::remove_file(p);
}

#[test]
fn seed_models_deletes_config_removed_rows() {
// rant 2026-08-20T11:58:33:seed 只 upsert 不删除 → config 移除的模型残留 DB(市场幽灵模型)
let (conn, p) = tmp_db("models_sync");
let cfg = crate::config::Config::load("config/config.example.toml").unwrap();
seed_models(&conn, &cfg).expect("seed models");
let before: i64 = conn
.query_row("SELECT COUNT(*) FROM models", [], |r| r.get(0))
.unwrap();
// 模拟历史残留:手工插入 config 中不存在的模型(旧版本遗留 / 曾手写 DB)
conn.execute(
"INSERT INTO models (provider, model, currency, input_per_m, output_per_m) \
VALUES ('ghost-provider', 'ghost-model', 'USD', 1.0, 1.0)",
[],
)
.unwrap();
// 再 seed:ghost 行应被同步删除,数量回到 config 集合大小
seed_models(&conn, &cfg).expect("二次 seed 应清理 ghost 行");
let after: i64 = conn
.query_row("SELECT COUNT(*) FROM models", [], |r| r.get(0))
.unwrap();
assert_eq!(after, before, "config 已移除模型应被删除(无幽灵行)");
let ghost: i64 = conn
.query_row(
"SELECT COUNT(*) FROM models WHERE provider = 'ghost-provider' AND model = 'ghost-model'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(ghost, 0, "ghost 模型行已删除");
drop(conn);
let _ = std::fs::remove_file(p);
}

#[test]
fn v2_migration_adds_available_and_note_columns() {
// 模拟 v1 旧库:建表后不含 available_* / note 列 → migrate 幂等补列
Expand Down
3 changes: 3 additions & 0 deletions src/routes/admin_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
//! - PATCH /api/admin/models/:id:部分更新(任意字段)
//! - DELETE /api/admin/models/:id:直接删除(模型删除后该 model 的调用无价格行 → 0 计费,README 说明)
//! - 权限:require_admin(role=admin,否则 403)
//! - 注意:config.toml `[[models]]` 为唯一真源——启动 seed_models 会同步删除 config 中已移除的
//! 模型行(rant 2026-08-20T11:58:33),故 admin 运行时增删改在下次启动时会被 config 覆盖/清掉;
//! 持久化自定义模型请写入 config.toml。

use axum::extract::State;
use axum::Json;
Expand Down
Loading