refactor(prices): TOML cloud price table + billing fail-open#580
refactor(prices): TOML cloud price table + billing fail-open#580
Conversation
📝 WalkthroughWalkthrough本PR将价格同步从 LiteLLM 迁移为云端价格表,新增 TOML 支持、云端同步调度、价格源/提供商过滤、capabilities 列与本地化文案更新,并在多处引入相关后端、数据库与测试改动。 Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
Summary of ChangesHello @ding113, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 此拉取请求旨在全面重构价格表模块,以提升系统的健壮性和用户体验。通过将云端价格表切换到更易读和维护的 TOML 格式,并引入计费失败时的“放行”策略,系统能够更好地应对价格数据异常情况,避免服务中断。同时,增强的用户界面和自动同步机制也使得价格管理更加高效和直观。 Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
🧪 测试结果
总体结果: ✅ 所有测试通过 |
There was a problem hiding this comment.
Code Review
这个 PR 的质量非常高,它通过引入 TOML 格式的云端价格表、实现计费容错机制(fail-open)以及优化数据库查询,显著重构和改进了价格模块。主要亮点包括:
- 健壮性提升:当价格查询失败或模型价格缺失时,系统不再返回 500 错误,而是优雅地放行请求并异步触发价格同步,极大地提高了服务的可用性。
- 性能优化:通过批量获取价格数据和使用更高效的 SQL 查询(如
DISTINCT ON),有效地解决了 N+1 查询问题,提升了数据查询性能。 - 代码质量:代码结构清晰,新增了专门的模块处理 TOML 解析和价格同步,并且包含了全面的单元测试和集成测试,确保了新功能和重构的可靠性。
- 用户体验改善:UI 层面增加了新的筛选功能和更清晰的模型能力展示,提升了可管理性。
我提出了一些具体的改进建议,主要集中在数据库查询效率和分布式环境下的任务节流机制,希望能帮助代码更上一层楼。总体而言,这是一次出色的重构工作。
| export async function findLatestPriceByModel(modelName: string): Promise<ModelPrice | null> { | ||
| const [price] = await db | ||
| .select({ | ||
| try { | ||
| const selection = { | ||
| id: modelPrices.id, | ||
| modelName: modelPrices.modelName, | ||
| priceData: modelPrices.priceData, | ||
| source: modelPrices.source, | ||
| createdAt: modelPrices.createdAt, | ||
| updatedAt: modelPrices.updatedAt, | ||
| }) | ||
| .from(modelPrices) | ||
| .where(eq(modelPrices.modelName, modelName)) | ||
| .orderBy(desc(modelPrices.createdAt)) | ||
| .limit(1); | ||
| }; | ||
|
|
||
| if (!price) return null; | ||
| return toModelPrice(price); | ||
| // 本地手动配置优先(哪怕云端数据更新得更晚) | ||
| const [manual] = await db | ||
| .select(selection) | ||
| .from(modelPrices) | ||
| .where(and(eq(modelPrices.modelName, modelName), eq(modelPrices.source, "manual"))) | ||
| .orderBy(sql`${modelPrices.createdAt} DESC NULLS LAST`, desc(modelPrices.id)) | ||
| .limit(1); | ||
|
|
||
| if (manual) return toModelPrice(manual); | ||
|
|
||
| // 兜底:任意来源取最新 | ||
| const [price] = await db | ||
| .select(selection) | ||
| .from(modelPrices) | ||
| .where(eq(modelPrices.modelName, modelName)) | ||
| .orderBy(sql`${modelPrices.createdAt} DESC NULLS LAST`, desc(modelPrices.id)) | ||
| .limit(1); | ||
|
|
||
| if (!price) return null; | ||
| return toModelPrice(price); | ||
| } catch (error) { | ||
| logger.error("[ModelPrice] Failed to query latest price by model", { | ||
| modelName, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }); | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
findLatestPriceByModel 函数的实现可以通过单次数据库查询来优化,以提高性能。当前实现是先查询 manual 来源,如果未找到,再进行第二次查询获取任意来源的最新价格。这在缓存未命中时会导致两次数据库往返。
可以将这两个查询合并为一个,使用 ORDER BY 子句来优先排序 manual 记录,从而在一次查询中就获得正确的结果。这与此文件中 findAllLatestPrices 和 findAllLatestPricesPaginated 函数使用的 DISTINCT ON 结合 ORDER BY 的高效模式是一致的。
考虑到此函数位于计费的热路径上,这个优化可以减少数据库负载和请求延迟。
export async function findLatestPriceByModel(modelName: string): Promise<ModelPrice | null> {
try {
const [price] = await db
.select({
id: modelPrices.id,
modelName: modelPrices.modelName,
priceData: modelPrices.priceData,
source: modelPrices.source,
createdAt: modelPrices.createdAt,
updatedAt: modelPrices.updatedAt,
})
.from(modelPrices)
.where(eq(modelPrices.modelName, modelName))
.orderBy(
sql`(source = 'manual') DESC`,
sql`${modelPrices.createdAt} DESC NULLS LAST`,
desc(modelPrices.id)
)
.limit(1);
if (!price) return null;
return toModelPrice(price);
} catch (error) {
logger.error("[ModelPrice] Failed to query latest price by model", {
modelName,
error: error instanceof Error ? error.message : String(error),
});
return null;
}
}| export function requestCloudPriceTableSync(options: { | ||
| reason: "missing-model" | "scheduled" | "manual"; | ||
| throttleMs?: number; | ||
| }): void { | ||
| const throttleMs = options.throttleMs ?? DEFAULT_THROTTLE_MS; | ||
| const taskId = "cloud-price-table-sync"; | ||
|
|
||
| // 去重:已有任务在跑则不重复触发 | ||
| const active = AsyncTaskManager.getActiveTasks(); | ||
| if (active.some((t) => t.taskId === taskId)) { | ||
| return; | ||
| } | ||
|
|
||
| // 节流:避免短时间内频繁拉取云端价格表 | ||
| const g = globalThis as unknown as { __CCH_CLOUD_PRICE_SYNC_LAST_AT__?: number }; | ||
| const lastAt = g.__CCH_CLOUD_PRICE_SYNC_LAST_AT__ ?? 0; | ||
| const now = Date.now(); | ||
| if (now - lastAt < throttleMs) { | ||
| return; | ||
| } | ||
| g.__CCH_CLOUD_PRICE_SYNC_LAST_AT__ = now; |
There was a problem hiding this comment.
requestCloudPriceTableSync 函数中的节流(throttling)逻辑使用了 globalThis 来存储上次执行的时间戳。这种方法在单进程环境中是有效的,但如果未来应用需要水平扩展到多个服务器实例(例如,在 Kubernetes 中运行多个 Pod),它将无法正常工作。
在多实例部署中,每个实例都会有自己独立的 globalThis 对象,导致节流控制在每个实例内部独立生效,从而可能引发对云端价格表源的请求风暴(Thundering Herd),超出预期的请求频率。
为了构建一个更具扩展性的节流机制,建议考虑使用一个共享的分布式存储(如 Redis)来记录和检查上次同步的时间戳。通过 SETNX 或带有过期时间的 SET 命令,可以实现一个可靠的分布式锁或节流阀。
虽然当前架构可能不需要立即修改,但这是一个重要的架构考虑点,有助于未来的扩展。
| const g = globalThis as unknown as { __CCH_CLOUD_PRICE_SYNC_LAST_AT__?: number }; | ||
| const lastAt = g.__CCH_CLOUD_PRICE_SYNC_LAST_AT__ ?? 0; | ||
| const now = Date.now(); | ||
| if (now - lastAt < throttleMs) { | ||
| return; | ||
| } | ||
| g.__CCH_CLOUD_PRICE_SYNC_LAST_AT__ = now; |
There was a problem hiding this comment.
The throttle mechanism has a critical flaw: __CCH_CLOUD_PRICE_SYNC_LAST_AT__ is set before the sync task starts, but it's never reset or updated after the task completes. This means:
- If a sync task fails, subsequent requests within the throttle window will be blocked even though no sync is running
- The throttle window becomes permanent after the first invocation until the process restarts
- The "missing-model" trigger may be indefinitely blocked if it fires shortly after a "scheduled" trigger
Impact: Price sync requests from billing fail-open logic (line 1734 in response-handler.ts) may be silently dropped, leaving models unpriced indefinitely.
Fix: Update __CCH_CLOUD_PRICE_SYNC_LAST_AT__ only AFTER the sync task completes (success or failure), not before it starts.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/price-sync/cloud-price-updater.ts
Line: 71:77
Comment:
The throttle mechanism has a critical flaw: `__CCH_CLOUD_PRICE_SYNC_LAST_AT__` is set before the sync task starts, but it's never reset or updated after the task completes. This means:
1. If a sync task fails, subsequent requests within the throttle window will be blocked even though no sync is running
2. The throttle window becomes permanent after the first invocation until the process restarts
3. The "missing-model" trigger may be indefinitely blocked if it fires shortly after a "scheduled" trigger
**Impact**: Price sync requests from billing fail-open logic (line 1734 in response-handler.ts) may be silently dropped, leaving models unpriced indefinitely.
**Fix**: Update `__CCH_CLOUD_PRICE_SYNC_LAST_AT__` only AFTER the sync task completes (success or failure), not before it starts.
How can I resolve this? If you propose a fix, please make it concise.|
|
||
| if (expectedUrl && typeof response.url === "string" && response.url) { | ||
| try { | ||
| const finalUrl = new URL(response.url); | ||
| if (finalUrl.protocol !== expectedUrl.protocol || finalUrl.host !== expectedUrl.host) { | ||
| return { ok: false, error: "云端价格表拉取失败:重定向到非预期地址" }; | ||
| } | ||
| } catch { | ||
| // response.url 无法解析时不阻断(仅作安全硬化),继续按原路径处理 | ||
| } |
There was a problem hiding this comment.
The redirect validation only checks protocol and host but not the path. A malicious redirect to https://claude-code-hub.app/malicious/prices.toml would pass this check, potentially serving tampered price data.
Scenario:
- Attacker compromises DNS or performs MITM
- Redirects to
https://claude-code-hub.app/../../evil/prices.toml - Validation passes because host matches
- System ingests malicious pricing data
Fix: Also validate that finalUrl.pathname starts with the expected pathname from expectedUrl, or use exact URL matching instead of host-only comparison.
| if (expectedUrl && typeof response.url === "string" && response.url) { | |
| try { | |
| const finalUrl = new URL(response.url); | |
| if (finalUrl.protocol !== expectedUrl.protocol || finalUrl.host !== expectedUrl.host) { | |
| return { ok: false, error: "云端价格表拉取失败:重定向到非预期地址" }; | |
| } | |
| } catch { | |
| // response.url 无法解析时不阻断(仅作安全硬化),继续按原路径处理 | |
| } | |
| const finalUrl = new URL(response.url); | |
| if (finalUrl.protocol !== expectedUrl.protocol || | |
| finalUrl.host !== expectedUrl.host || | |
| !finalUrl.pathname.startsWith(expectedUrl.pathname)) { | |
| return { ok: false, error: "云端价格表拉取失败:重定向到非预期地址" }; | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/price-sync/cloud-price-table.ts
Line: 75:84
Comment:
The redirect validation only checks protocol and host but not the path. A malicious redirect to `https://claude-code-hub.app/malicious/prices.toml` would pass this check, potentially serving tampered price data.
**Scenario**:
- Attacker compromises DNS or performs MITM
- Redirects to `https://claude-code-hub.app/../../evil/prices.toml`
- Validation passes because host matches
- System ingests malicious pricing data
**Fix**: Also validate that `finalUrl.pathname` starts with the expected pathname from `expectedUrl`, or use exact URL matching instead of host-only comparison.
```suggestion
const finalUrl = new URL(response.url);
if (finalUrl.protocol !== expectedUrl.protocol ||
finalUrl.host !== expectedUrl.host ||
!finalUrl.pathname.startsWith(expectedUrl.pathname)) {
return { ok: false, error: "云端价格表拉取失败:重定向到非预期地址" };
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
src/app/api/prices/route.ts (1)
18-62:page/pageSize需要显式处理 NaN,否则会漏过校验并导致 500。
parseInt("abc", 10)得到NaN,而NaN < 1为 false。Proposed diff
import { type NextRequest, NextResponse } from "next/server"; import { getModelPricesPaginated } from "@/actions/model-prices"; import { getSession } from "@/lib/auth"; import type { PaginationParams } from "@/repository/model-price"; +import { logger } from "@/lib/logger"; export async function GET(request: NextRequest) { try { // 权限检查:只有管理员可以访问价格数据 const session = await getSession(); if (!session || session.user.role !== "admin") { - return NextResponse.json({ ok: false, error: "无权限访问此资源" }, { status: 403 }); + return NextResponse.json({ ok: false, errorCode: "forbidden" }, { status: 403 }); } const { searchParams } = new URL(request.url); // 解析查询参数 - const page = parseInt(searchParams.get("page") || "1", 10); - const pageSize = parseInt(searchParams.get("pageSize") || searchParams.get("size") || "50", 10); - const search = searchParams.get("search") || ""; - const source = searchParams.get("source") || ""; - const litellmProvider = searchParams.get("litellmProvider") || ""; + const page = Number.parseInt(searchParams.get("page") ?? "1", 10); + const pageSize = Number.parseInt( + searchParams.get("pageSize") ?? searchParams.get("size") ?? "50", + 10, + ); + const search = (searchParams.get("search") ?? "").trim(); + const source = (searchParams.get("source") ?? "").trim(); + const litellmProvider = (searchParams.get("litellmProvider") ?? "").trim(); // 参数验证 - if (page < 1) { - return NextResponse.json({ ok: false, error: "页码必须大于0" }, { status: 400 }); + if (!Number.isFinite(page) || page < 1) { + return NextResponse.json({ ok: false, errorCode: "invalid_page" }, { status: 400 }); } - if (pageSize < 1 || pageSize > 200) { - return NextResponse.json({ ok: false, error: "每页大小必须在1-200之间" }, { status: 400 }); + if (!Number.isFinite(pageSize) || pageSize < 1 || pageSize > 200) { + return NextResponse.json({ ok: false, errorCode: "invalid_page_size" }, { status: 400 }); } if (source && source !== "manual" && source !== "litellm") { - return NextResponse.json({ ok: false, error: "source 参数无效" }, { status: 400 }); + return NextResponse.json({ ok: false, errorCode: "invalid_source" }, { status: 400 }); } // 构建分页参数 const paginationParams: PaginationParams = { page, pageSize, - search: search || undefined, // 传递搜索关键词给后端 + search: search || undefined, source: source ? (source as PaginationParams["source"]) : undefined, litellmProvider: litellmProvider || undefined, }; // 获取分页数据(搜索在 SQL 层面执行) const result = await getModelPricesPaginated(paginationParams); return NextResponse.json(result); } catch (error) { - console.error("获取价格数据失败:", error); - return NextResponse.json({ ok: false, error: "服务器内部错误" }, { status: 500 }); + logger.error({ err: error }, "[API] GET /api/prices failed"); + return NextResponse.json({ ok: false, errorCode: "internal_error" }, { status: 500 }); } }另外:这个文件是 TS,按仓库规范“用户可见字符串必须走 i18n”。API 场景建议返回
errorCode,由前端按 locale 翻译(如上 diff),避免在 API 层硬编码中文。messages/en/settings.json (3)
584-605: 冲突弹窗仍在强调 “LiteLLM prices”,与本 PR 的“云端价格表”口径可能不一致如果当前冲突对比已不再是 LiteLLM,而是 Cloud Price Table(或云端表的某个来源),这里的
description/litellmPrice文案建议同步更新,避免用户误解“同步来源”。
644-674: 上传弹窗支持 JSON/TOML 的文案更新到位;“cloud price table”大小写建议统一
latestPriceTable: "cloud price table"作为 UI 标签建议与其它按钮标题风格一致(例如首字母大写),避免观感不一致。Also applies to: 692-696
1-2152: 修复 i18n 多语言键值缺失问题JSON 语法有效 ✓,但多语言覆盖不完整:
缺失必需的语言文件
- 根据 i18n 要求需支持 5 种语言(zh-CN, en, ja, ko, de),但当前缺少 ko 和 de 的 settings.json
- 需创建
messages/ko/settings.json和messages/de/settings.json现有翻译文件键值不齐全
- zh-CN:缺少 16 个键(共 1689 vs 1702)
- ja:缺少 38 个键(共 1664 vs 1702)
- 缺失的键包括:
providers.form.mcpPassthrough*(MCP 透传配置)、config.form.enableHttp2等新增功能的翻译影响
- next-intl 运行时遇到缺失键会报错或显示无翻译内容
- 用户在 ko 和 de 环境中无法使用该设置页面
建议先补全 zh-CN、ja 的翻译,再添加 ko、de 两种语言的完整译文。
messages/zh-TW/settings.json (1)
575-606: 冲突弹窗仍指向 LiteLLM,建议与“雲端價格表”口径对齐如果冲突来源已改为云端表,请同步更新这里的描述与列标题相关文案,避免用户误解。
messages/ja/settings.json (1)
957-1163: ja 文案存在明显“中文/中日混排”残留,建议在本 PR 内修正,否则 ja locale 体验会退化例如
apiAddress: "API 地址"、apiKeyCurrent: "当前密钥:"、以及mcpPassthroughHint中的 “提供的/的” 等,这些会直接展示给 ja 用户。建议至少把本 PR 触达页面的关键字段改成日文。Also applies to: 1525-1644, 2097-2101
src/app/[locale]/settings/prices/_components/price-list.tsx (2)
228-242:formatPrice会把合法的 0 显示成 “-”
if (!value)会把0误判为空值;建议改成仅在null/undefined时返回占位符。Proposed fix
- const formatPrice = (value?: number): string => { - if (!value) return "-"; + const formatPrice = (value?: number): string => { + if (value === undefined || value === null) return "-"; // 将每token的价格转换为每百万token的价格 const pricePerMillion = value * 1000000;
94-175: 统一 URL 和 API 请求的分页参数名
updateURL写入的是size(第 115 行),而fetchPrices发送的是pageSize(第 149 行)。虽然 API 路由有回退逻辑(searchParams.get("pageSize") || searchParams.get("size")),但这种参数命名不一致容易造成混淆和维护问题。建议统一采用pageSize(API 文档中的规范参数),同时移除 API 的回退逻辑以保持一致性。
🤖 Fix all issues with AI agents
In @src/actions/model-prices.ts:
- Around line 34-63: The canonicalize implementation inside isPriceDataEqual
uses a plain object `result` and assigns untrusted keys from the input, which
can cause prototype pollution when keys like "__proto__", "constructor", or
"prototype" are present; update canonicalize (used by stableStringify) to create
dictionary objects via Object.create(null) for `result` and for any nested
plain-object containers, and/or explicitly skip dangerous keys (at minimum
"__proto__", "constructor", "prototype") when iterating Object.keys(obj). Keep
key sorting and the rest of the canonicalization logic intact.
🧹 Nitpick comments (10)
src/repository/model-price.ts (2)
35-72: 建议 logger 记录原始 error 对象(含 stack),便于排障。
现在只打了error.message,线上排查会缺关键堆栈。Proposed diff
- } catch (error) { - logger.error("[ModelPrice] Failed to query latest price by model", { - modelName, - error: error instanceof Error ? error.message : String(error), - }); + } catch (error) { + logger.error( + { + modelName, + err: error, + }, + "[ModelPrice] Failed to query latest price by model", + ); return null; }
104-168: 分页查询的 JSONB 过滤可能需要索引支撑(数据量大时)。
price_data->>'litellm_provider' = ...在量大时容易退化为全表扫;如果这是常用筛选,考虑加表达式索引或生成列。messages/en/settings.json (1)
570-583: 同步区文案已切换到 Cloud Price Table,但失败模型列表可能过长需要 UI 侧截断/折叠策略
failedModels: "Failed models: {models}"若直接拼接完整模型名列表,toast/弹窗可能过长;建议确认 UI 有“最多展示 N 个 + more”的处理(你已在 dialog.results.more 加了文案,看起来方向一致)。messages/zh-TW/settings.json (1)
536-560: prices.filters/badges/capabilities 翻译补齐 OK;建议统一专有名词/术语风格例如 “Prompt 快取 / Schema / Tool choice” 是否要统一为繁体常用译法或保留英文,全文件建议一致(避免同一页出现多种风格)。
src/types/model-price.ts (1)
30-38: 新增 display_name/providers 合理,但建议补充字段语义(尤其 providers vs litellm_provider)当前
litellm_provider?: string与providers?: string[]并存,容易让调用方不清楚优先级与含义(是“支持的上游提供商列表”?还是“价格来源提供商”?)。建议在类型旁用注释明确:
display_name与modelName的关系(展示优先级/是否允许为空)。providers的元素值范围与语义(空数组 vs undefined 的区别)。
基于 learnings,建议明确区分“缺省/未知”和“明确为空”的语义。src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx (1)
110-111: 建议:统一 console 日志语言Console 日志使用了中文字符串 "价格表上传失败:"。虽然这是开发者调试信息而非用户界面文本,但为了代码库的一致性和国际化协作,建议使用英文。
♻️ 可选优化
- console.error("价格表上传失败:", response.error); + console.error("Price table upload failed:", response.error);src/instrumentation.ts (1)
59-59: 建议:考虑将同步间隔配置化。30 分钟的硬编码间隔可以考虑通过环境变量配置,便于不同部署环境调整同步频率。
可选的配置化改进
async function startCloudPriceSyncScheduler(): Promise<void> { // ... try { const { requestCloudPriceTableSync } = await import("@/lib/price-sync/cloud-price-updater"); - const intervalMs = 30 * 60 * 1000; + const intervalMs = (getEnvConfig().CLOUD_PRICE_SYNC_INTERVAL_MINUTES ?? 30) * 60 * 1000;src/app/v1/_lib/proxy/response-handler.ts (1)
1725-1736: 当价格数据缺失时,请求会被放行且不计费,同时绕过速率限制。当前实现在找不到价格数据时会执行以下操作:
- 记录警告日志 ✓(第1727行)
- 异步触发价格表同步(5分钟节流)✓
- 但对该请求的速率限制被跳过 ✗(service.ts 第124、614行明确排除
cost <= 0的请求)这意味着攻击者可以通过使用不存在的模型名称来规避计费AND速率限制,对生产环境造成风险。
建议:
- 为"无价格数据"的请求添加显式速率限制,独立于成本计算
- 考虑实施"计费失败则拒绝"策略用于关键业务场景
- 在监控面板中添加"跳过计费请求"的专项指标,便于快速发现异常
src/actions/model-prices.ts (2)
175-184:revalidatePath失败降级 OK,但建议抽成小工具避免重复当前多处重复 try/catch + 相同日志字段;可以提取
safeRevalidatePath("/settings/prices")之类的小函数减少后续维护成本。Also applies to: 509-516, 546-553
193-224: 上传支持 JSON/TOML 的兼容处理合理(但存在双重 JSON.parse 的小开销)现在是“先探测 JSON 是否可 parse,再交给
processPriceTableInternal再 parse 一次”。若文件较大可考虑直接把 parse 后的对象传入内部处理(或让 internal 接受 object),不过不影响正确性。
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to Reviews > Disable Cache setting
📒 Files selected for processing (26)
messages/en/settings.jsonmessages/ja/settings.jsonmessages/ru/settings.jsonmessages/zh-CN/settings.jsonmessages/zh-TW/settings.jsonpackage.jsonsrc/actions/model-prices.tssrc/app/[locale]/settings/prices/_components/price-list.tsxsrc/app/[locale]/settings/prices/_components/sync-litellm-button.tsxsrc/app/[locale]/settings/prices/_components/upload-price-dialog.tsxsrc/app/[locale]/settings/prices/page.tsxsrc/app/api/prices/route.tssrc/app/v1/_lib/proxy/response-handler.tssrc/app/v1/_lib/proxy/session.tssrc/instrumentation.tssrc/lib/price-sync.tssrc/lib/price-sync/cloud-price-table.tssrc/lib/price-sync/cloud-price-updater.tssrc/lib/utils/price-data.tssrc/repository/model-price.tssrc/types/model-price.tstests/integration/billing-model-source.test.tstests/unit/actions/model-prices.test.tstests/unit/price-sync/cloud-price-table.test.tstests/unit/price-sync/cloud-price-updater.test.tstests/unit/proxy/pricing-no-price.test.ts
💤 Files with no reviewable changes (1)
- src/lib/price-sync.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,ts,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
No emoji characters in any code, comments, or string literals
Files:
tests/unit/proxy/pricing-no-price.test.tssrc/app/v1/_lib/proxy/response-handler.tssrc/app/v1/_lib/proxy/session.tssrc/lib/price-sync/cloud-price-updater.tstests/integration/billing-model-source.test.tssrc/app/[locale]/settings/prices/_components/upload-price-dialog.tsxtests/unit/price-sync/cloud-price-table.test.tssrc/lib/utils/price-data.tstests/unit/price-sync/cloud-price-updater.test.tssrc/types/model-price.tssrc/app/[locale]/settings/prices/_components/sync-litellm-button.tsxsrc/instrumentation.tssrc/app/api/prices/route.tssrc/app/[locale]/settings/prices/_components/price-list.tsxsrc/actions/model-prices.tssrc/repository/model-price.tstests/unit/actions/model-prices.test.tssrc/app/[locale]/settings/prices/page.tsxsrc/lib/price-sync/cloud-price-table.ts
**/*.{ts,tsx,jsx,js}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,jsx,js}: All user-facing strings must use i18n (5 languages supported: zh-CN, en, ja, ko, de). Never hardcode display text
Use path alias @/ to map to ./src/
Use Biome for code formatting with configuration: double quotes, trailing commas, 2-space indent, 100 character line width
Prefer named exports over default exports
Use next-intl for internationalization
Use Next.js 16 App Router with Hono for API routes
Files:
tests/unit/proxy/pricing-no-price.test.tssrc/app/v1/_lib/proxy/response-handler.tssrc/app/v1/_lib/proxy/session.tssrc/lib/price-sync/cloud-price-updater.tstests/integration/billing-model-source.test.tssrc/app/[locale]/settings/prices/_components/upload-price-dialog.tsxtests/unit/price-sync/cloud-price-table.test.tssrc/lib/utils/price-data.tstests/unit/price-sync/cloud-price-updater.test.tssrc/types/model-price.tssrc/app/[locale]/settings/prices/_components/sync-litellm-button.tsxsrc/instrumentation.tssrc/app/api/prices/route.tssrc/app/[locale]/settings/prices/_components/price-list.tsxsrc/actions/model-prices.tssrc/repository/model-price.tstests/unit/actions/model-prices.test.tssrc/app/[locale]/settings/prices/page.tsxsrc/lib/price-sync/cloud-price-table.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Place unit tests in tests/unit/, integration tests in tests/integration/, and source-adjacent tests in src/**/*.test.ts
Files:
tests/unit/proxy/pricing-no-price.test.tstests/integration/billing-model-source.test.tstests/unit/price-sync/cloud-price-table.test.tstests/unit/price-sync/cloud-price-updater.test.tstests/unit/actions/model-prices.test.ts
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use React 19, shadcn/ui, Tailwind CSS, and Recharts for the UI layer
Files:
src/app/[locale]/settings/prices/_components/upload-price-dialog.tsxsrc/app/[locale]/settings/prices/_components/sync-litellm-button.tsxsrc/app/[locale]/settings/prices/_components/price-list.tsxsrc/app/[locale]/settings/prices/page.tsx
🧠 Learnings (11)
📓 Common learnings
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:20:04.478Z
Learning: In the `ding113/claude-code-hub` repository, Server Actions (files under `src/actions/*.ts`) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx:42-53
Timestamp: 2026-01-10T06:20:13.376Z
Learning: In the claude-code-hub project, model pricing display (in files like `src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx`) intentionally uses hardcoded USD currency symbol (`$`) and per-million-token notation (`/M`, `/img`) because the system exclusively tracks LiteLLM pricing in USD and the notation is industry standard. Configurability was deemed unnecessary complexity.
📚 Learning: 2026-01-07T17:05:37.171Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T17:05:37.171Z
Learning: Applies to **/*.{tsx,jsx} : Use React 19, shadcn/ui, Tailwind CSS, and Recharts for the UI layer
Applied to files:
package.jsonsrc/app/[locale]/settings/prices/_components/price-list.tsx
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (e.g., matches DB schema or special UI logic) and undefined signifies 'inherit default'. This pattern should be consistently reflected in type definitions across related fields to preserve semantic clarity between database constraints and UI behavior.
Applied to files:
src/app/v1/_lib/proxy/response-handler.tssrc/app/v1/_lib/proxy/session.tssrc/lib/price-sync/cloud-price-updater.tssrc/lib/utils/price-data.tssrc/types/model-price.tssrc/instrumentation.tssrc/app/api/prices/route.tssrc/actions/model-prices.tssrc/repository/model-price.tssrc/lib/price-sync/cloud-price-table.ts
📚 Learning: 2026-01-10T06:20:32.687Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx:118-125
Timestamp: 2026-01-10T06:20:32.687Z
Learning: In `src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx`, the "Cancel" button in the SyncConflictDialog is intentionally designed to call `onConfirm([])`, which triggers `doSync([])` to continue the sync while skipping (not overwriting) conflicting manual prices. This is the desired product behavior to allow users to proceed with LiteLLM sync for non-conflicting models while preserving their manual price entries.
Applied to files:
src/lib/price-sync/cloud-price-updater.tssrc/app/[locale]/settings/prices/_components/upload-price-dialog.tsxsrc/app/[locale]/settings/prices/_components/sync-litellm-button.tsxsrc/app/[locale]/settings/prices/_components/price-list.tsxsrc/actions/model-prices.tstests/unit/actions/model-prices.test.tssrc/app/[locale]/settings/prices/page.tsx
📚 Learning: 2026-01-10T06:20:13.376Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx:42-53
Timestamp: 2026-01-10T06:20:13.376Z
Learning: In the claude-code-hub project, model pricing display (in files like `src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx`) intentionally uses hardcoded USD currency symbol (`$`) and per-million-token notation (`/M`, `/img`) because the system exclusively tracks LiteLLM pricing in USD and the notation is industry standard. Configurability was deemed unnecessary complexity.
Applied to files:
src/app/[locale]/settings/prices/_components/upload-price-dialog.tsxmessages/zh-TW/settings.jsonsrc/app/[locale]/settings/prices/_components/sync-litellm-button.tsxmessages/ja/settings.jsonsrc/app/[locale]/settings/prices/_components/price-list.tsxsrc/actions/model-prices.tstests/unit/actions/model-prices.test.tsmessages/ru/settings.jsonmessages/en/settings.jsonsrc/app/[locale]/settings/prices/page.tsxmessages/zh-CN/settings.json
📚 Learning: 2026-01-10T06:19:56.528Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/app/[locale]/settings/prices/_components/model-price-dialog.tsx:205-257
Timestamp: 2026-01-10T06:19:56.528Z
Learning: In the pricing module (src/app/[locale]/settings/prices/_components/), currency symbols ("$") and technical unit notations ("/M" for per-million tokens, "/img" for per-image) are intentionally hardcoded. The system uses USD as the fixed currency for all pricing, and these notations are standard industry conventions. These hardcoded values are an accepted exception to the general i18n requirement.
Applied to files:
src/app/[locale]/settings/prices/_components/upload-price-dialog.tsxmessages/zh-TW/settings.jsonmessages/ja/settings.jsonsrc/app/[locale]/settings/prices/_components/price-list.tsxmessages/ru/settings.jsonmessages/en/settings.json
📚 Learning: 2026-01-10T06:20:04.478Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:20:04.478Z
Learning: In the `ding113/claude-code-hub` repository, Server Actions (files under `src/actions/*.ts`) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.
Applied to files:
src/app/[locale]/settings/prices/_components/upload-price-dialog.tsxmessages/zh-TW/settings.jsonsrc/app/[locale]/settings/prices/_components/sync-litellm-button.tsxmessages/ja/settings.jsonmessages/ru/settings.jsonmessages/zh-CN/settings.json
📚 Learning: 2026-01-07T17:05:37.171Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T17:05:37.171Z
Learning: Applies to **/*.{ts,tsx,jsx,js} : All user-facing strings must use i18n (5 languages supported: zh-CN, en, ja, ko, de). Never hardcode display text
Applied to files:
src/app/[locale]/settings/prices/_components/upload-price-dialog.tsxsrc/app/[locale]/settings/prices/_components/sync-litellm-button.tsx
📚 Learning: 2026-01-07T17:05:37.171Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T17:05:37.171Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Place unit tests in tests/unit/, integration tests in tests/integration/, and source-adjacent tests in src/**/*.test.ts
Applied to files:
tests/unit/price-sync/cloud-price-updater.test.ts
📚 Learning: 2026-01-05T03:02:14.502Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx:66-66
Timestamp: 2026-01-05T03:02:14.502Z
Learning: In the claude-code-hub project, the translations.actions.addKey field in UserKeyTableRowProps is defined as optional for backward compatibility, but all actual callers in the codebase provide the complete translations object. The field has been added to all 5 locale files (messages/{locale}/dashboard.json).
Applied to files:
messages/ja/settings.jsonmessages/ru/settings.json
📚 Learning: 2026-01-10T06:19:58.167Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:19:58.167Z
Learning: Do not modify hardcoded Chinese error messages in Server Actions under src/actions/*.ts as part of piecemeal changes. This is a repo-wide architectural decision that requires a coordinated i18n refactor across all Server Action files (e.g., model-prices.ts, users.ts, system-config.ts). Treat i18n refactor as a separate unified task rather than per-PR changes, and plan a project-wide approach for replacing hardcoded strings with localized resources.
Applied to files:
src/actions/model-prices.ts
🧬 Code graph analysis (11)
src/lib/price-sync/cloud-price-updater.ts (5)
src/lib/price-sync/cloud-price-table.ts (3)
CloudPriceTableResult(12-12)fetchCloudPriceTableToml(53-103)parseCloudPriceTableToml(18-51)src/types/model-price.ts (1)
PriceUpdateResult(82-89)src/actions/model-prices.ts (1)
processPriceTableInternal(71-191)src/lib/async-task-manager.ts (1)
AsyncTaskManager(232-233)src/lib/logger.ts (1)
logger(168-187)
tests/integration/billing-model-source.test.ts (3)
src/repository/system-config.ts (1)
getSystemSettings(168-249)src/repository/model-price.ts (1)
findLatestPriceByModel(35-73)src/repository/message.ts (1)
updateMessageRequestCost(91-112)
tests/unit/price-sync/cloud-price-table.test.ts (1)
src/lib/price-sync/cloud-price-table.ts (2)
parseCloudPriceTableToml(18-51)fetchCloudPriceTableToml(53-103)
src/lib/utils/price-data.ts (1)
src/types/model-price.ts (1)
ModelPriceData(4-53)
tests/unit/price-sync/cloud-price-updater.test.ts (5)
src/lib/price-sync/cloud-price-updater.ts (2)
syncCloudPriceTableToDatabase(17-47)requestCloudPriceTableSync(57-101)src/actions/model-prices.ts (1)
processPriceTableInternal(71-191)src/lib/price-sync/cloud-price-table.ts (1)
CloudPriceTableResult(12-12)src/lib/async-task-manager.ts (1)
AsyncTaskManager(232-233)src/lib/logger.ts (1)
logger(168-187)
src/instrumentation.ts (2)
src/lib/price-sync/cloud-price-updater.ts (1)
requestCloudPriceTableSync(57-101)src/lib/logger.ts (1)
logger(168-187)
src/app/api/prices/route.ts (1)
src/repository/model-price.ts (1)
PaginationParams(13-19)
src/repository/model-price.ts (5)
src/drizzle/schema.ts (1)
modelPrices(365-381)src/repository/_shared/transformers.ts (1)
toModelPrice(141-148)src/lib/logger.ts (1)
logger(168-187)src/repository/index.ts (1)
findAllLatestPrices(34-34)src/types/model-price.ts (1)
ModelPrice(63-70)
tests/unit/actions/model-prices.test.ts (1)
src/actions/model-prices.ts (1)
checkLiteLLMSyncConflicts(335-390)
src/app/[locale]/settings/prices/page.tsx (1)
src/actions/model-prices.ts (1)
getModelPricesPaginated(247-272)
src/lib/price-sync/cloud-price-table.ts (1)
src/types/model-price.ts (1)
ModelPriceData(4-53)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: pr-label
- GitHub Check: pr-review
- GitHub Check: pr-description
- GitHub Check: check-codex-status
- GitHub Check: 🌐 API Tests
- GitHub Check: Docker Build Test
- GitHub Check: dev-build-deploy
🔇 Additional comments (41)
package.json (1)
33-40: 不存在意外打入客户端 bundle 的风险。
@iarna/toml的引入完全隔离在服务端:仅在src/lib/price-sync/cloud-price-table.ts中使用,该模块只被服务端模块(src/lib/price-sync/cloud-price-updater.ts和src/actions/model-prices.ts)引入,这些文件都明确标记了"use server"指令。没有任何客户端组件直接或间接地导入该模块,因此该依赖不会增加前端体积。Likely an incorrect or invalid review comment.
src/repository/model-price.ts (1)
13-19: 命名约定处理正确,无需修改。代码已正确实现 API 层(camelCase)与数据库层(snake_case)的命名约定分离:
- 参数接收
litellmProvider(camelCase)- 存储为
litellm_provider(snake_case)- 转换在 src/actions/model-prices.ts:499 明确进行:
litellm_provider: input.litellmProvider || undefined- SQL 查询在 src/repository/model-price.ts:120 正确使用:
price_data->>'litellm_provider'这是标准的、一致的、正确的做法,无需调整。
Likely an incorrect or invalid review comment.
messages/ru/settings.json (1)
536-695: RU 译文已验证:JSON 格式有效,占位符与 zh-CN 完全一致。验证结果:
- ✓ JSON 语法有效
- ✓ 关键占位符均存在且变量正确:
{label}、{status}、{added}、{updated}、{unchanged}、{failed}、{models}、{count}- ✓ 顶层键与 zh-CN 完全匹配
- ✓ prices 段落键完全匹配
无需调整。
messages/zh-CN/settings.json (1)
1257-1415: 新增的prices相关文案及 i18n 键已正确同步至所有语言版本。验证确认:所有 5 个语言文件(en、ja、ru、zh-CN、zh-TW)都包含新增的
prices.filters、prices.badges、prices.capabilities、prices.sync.*和prices.conflict.*键,共 148 个键完全一致。占位符(如{added}、{updated}、{error}、{models}、{count}等)在所有语言中保持一致,不存在缺失或不匹配的情况。JSON 格式有效。messages/en/settings.json (2)
545-569: 新增 filters/badges/capabilities 结构整体合理,但建议确认能力 key 与后端字段一一对应这些 key 看起来是在映射
supports_*字段与 UI 图标提示;请确认不会出现“后端返回 supports_xxx,但这里缺少对应文案 key”的情况(否则会导致缺文案/回退)。
615-638: 表格新增 Capabilities 列与分页 Per page 文案 OK;请确认旧 type 列的渲染已同步移除目前翻译里仍保留 typeChat/typeImage/typeCompletion 等 key(可能仍用于其他地方),但请确认表格列定义确实不再引用旧
type列标题 key,以免出现“标题缺失/重复列”。messages/zh-TW/settings.json (2)
561-574: 同步区文案切换到“雲端價格表”符合目标;failedModels 模板 OK同样建议确认 UI 层对
{models}有长度控制(避免超长提示影响可读性)。
606-629: capabilities 列/分页/上传弹窗(JSON/TOML)/更多操作 文案更新到位这几处改动与 PR 目标一致,且 key 结构与 en/ja 基本对齐。
Also applies to: 635-665, 683-687
messages/ja/settings.json (1)
536-574: 价格页相关(filters/capabilities/同步/分页/上传/更多操作)日文文案整体 OK这些新增 key 与 en/zh-TW 基本一致,覆盖了 JSON/TOML 与能力图标提示的需求。
Also applies to: 606-629, 635-687
src/app/v1/_lib/proxy/session.ts (2)
1-12: 用共享 hasValidPriceData 替代本地实现是正向收敛;请确认无循环依赖且判定逻辑与旧实现一致建议重点确认:
@/lib/utils/price-data不会反向依赖 proxy/session 相关模块;以及 hasValidPriceData 对 image/chat/completion 的“有效价格”判定与原来一致(避免计费链路行为变化)。
708-771: billingModelSource 缓存与并发安全处理 OK;小建议:为 findLatestPriceByModel 加上失败隔离/超时需要在 repository 层确保这里已做 try/catch 包裹系统设置读取并回退到 redirected,整体符合“计费 fail-open”的方向;请确认
findLatestPriceByModel在数据库慢/失败时不会把请求线程拖死(超时/连接池/异常传播应在 repository 层兜住)。src/lib/utils/price-data.ts (1)
7-40: LGTM!验证逻辑完善该函数正确验证价格数据是否包含可计费字段,逻辑清晰:
- 优先检查标准成本字段(input/output token cost、cache cost、image cost)
- 其次检查搜索上下文成本字段
- 验证条件(finite 且 >= 0)涵盖了免费层级和正常定价
实现符合 fail-open 计费策略,能有效区分空对象和有效价格数据。
src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx (2)
87-91: LGTM!文件类型验证正确使用
toLowerCase()进行大小写不敏感的文件扩展名检查,同时支持 JSON 和 TOML 格式,符合 PR 目标。
199-199: LGTM!TOML 支持和 i18n 改进以下更新均符合 PR 目标:
accept属性扩展至.json,.toml- 资源链接更新为 TOML 格式的云端价格表 URL
- 结果列表使用
t("dialog.results.more")实现本地化,替代硬编码字符串Also applies to: 213-213, 242-243, 258-259, 284-285
tests/unit/price-sync/cloud-price-table.test.ts (2)
7-100: LGTM!parseCloudPriceTableToml 测试覆盖全面测试用例涵盖:
- 正常场景:嵌套的 pricing 表、metadata 字段
- 错误场景:缺少 models 表、无效 TOML 语法、空 models
- 安全场景:过滤保留键(
__proto__、constructor、prototype)防止原型污染- 防御场景:通过 mock TOML 解析器验证非对象根节点的处理
测试设计严谨,确保了解析逻辑的健壮性。
102-210: LGTM!fetchCloudPriceTableToml 测试覆盖完整测试场景包括:
- 成功响应与空响应体
- HTTP 错误状态
- 重定向到非预期主机(安全检查)
- 无效 URL 与 fetch 抛错
- 超时/中止信号处理(使用 fake timers)
- 非 Error 类型的异常(throw 字符串)
所有边界情况和异常路径均已覆盖,确保云端价格表拉取的可靠性。
tests/integration/billing-model-source.test.ts (2)
6-6: LGTM!云端价格同步 mock 配置正确新增的测试基础设施设计合理:
cloudPriceSyncRequests数组记录同步请求- Mock
requestCloudPriceTableSync捕获调用并追踪 reasonbeforeEach钩子清理状态,确保测试隔离实现简洁且有效。
Also applies to: 29-34, 92-94
373-445: LGTM!fail-open 计费测试覆盖准确新增测试套件验证了价格缺失/查询失败时的关键行为:
"无价格"场景(lines 423-433):
- ✓ 不写入数据库成本
- ✓ 不追踪限流成本
- ✓ 触发异步云端价格表同步(reason: "missing-model")
"价格查询抛错"场景(lines 435-444):
- ✓ 不写入数据库成本
- ✓ 不追踪限流成本
- ✓ 响应处理不受影响
- 正确区分了数据库错误与价格数据缺失(抛错时不触发同步)
测试逻辑符合 fail-open 设计,确保计费链路容错。
tests/unit/proxy/pricing-no-price.test.ts (2)
4-14: LGTM!Mock 配置使用 vi.hoisted 正确使用
vi.hoisted确保 mock 在模块导入前初始化,避免了顺序问题:
cloudSyncRequests和requestCloudPriceTableSyncMock在 hoisted 作用域中创建beforeEach钩子正确清理状态这是 Vitest 的最佳实践。
Also applies to: 179-182
184-245: LGTM!无价格场景单元测试全面三个测试场景完整验证了 fail-open 计费逻辑:
场景 1:无价格(lines 184-199)
- ✓ 价格查询返回 null
- ✓ 跳过 DB 成本更新和限流追踪
- ✓ 触发云端价格表同步(reason: "missing-model")
场景 2:空对象价格(lines 201-227)
- ✓ 价格数据为
{}(无有效计费字段)- ✓ 视为缺失价格,触发同步
- ✓ 验证
hasValidPriceData验证逻辑场景 3:查询抛错(lines 229-244)
- ✓ 数据库查询失败
- ✓ 跳过计费但不触发同步(区分错误类型)
- ✓ 响应处理不受影响
测试设计准确反映了计费容错策略。
src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx (1)
1-153: LGTM!组件正确地将日志前缀从 LiteLLM 更新为云端价格表,所有用户可见的消息都正确使用了 i18n 翻译键。代码遵循了现有的错误处理模式。
src/app/[locale]/settings/prices/page.tsx (2)
46-58: LGTM!服务端过滤参数解析逻辑正确,
source参数的白名单验证(仅允许manual或litellm)有效防止了无效输入。
100-103: LGTM!新增的初始过滤状态 props 正确传递给
PriceList组件,使用空字符串作为undefined的默认值是合理的设计。tests/unit/price-sync/cloud-price-updater.test.ts (2)
1-161: LGTM!
syncCloudPriceTableToDatabase的测试覆盖了关键路径:HTTP 错误、空响应、缺少 models 表、处理失败、以及成功场景。Mock 设置合理,测试结构清晰。
163-238: LGTM!
requestCloudPriceTableSync的测试正确验证了去重(active task 检查)、节流(throttle window)以及后台任务注册和日志记录行为。src/instrumentation.ts (2)
45-83: LGTM!云端价格表定时同步调度器实现完善:
- 使用
globalThis状态去重防止热重载重复注册- 启动时立即触发一次(
throttleMs: 0)避免 30 分钟空窗期- 失败不阻塞启动,仅记录警告日志
145-155: LGTM!关机时正确清理定时器并重置状态标志,遵循了文件中其他资源清理的一致模式。
src/lib/price-sync/cloud-price-updater.ts (2)
17-47: LGTM!
syncCloudPriceTableToDatabase实现良好:
- 使用结构化
ok/error返回值而非抛出异常,确保调用方主流程不受影响- 动态导入
processPriceTableInternal避免循环依赖- 错误消息详细,便于排查问题
57-100: LGTM!
requestCloudPriceTableSync的去重和节流逻辑设计合理:
- 先检查
AsyncTaskManager中是否有同名任务运行- 再检查全局时间戳进行节流
- 后台任务完成后记录详细日志
src/lib/price-sync/cloud-price-table.ts (3)
14-16: LGTM!
isRecord类型守卫实现正确,用于运行时类型验证。
30-37: 安全性良好:正确防护原型污染攻击。过滤
__proto__、constructor、prototype键并使用Object.create(null)创建无原型对象,有效防止了 TOML 内容中的原型污染攻击。
76-85: 安全硬化:重定向验证。检查响应 URL 的
protocol和host是否与预期一致,可防止 SSRF 通过开放重定向被利用。注释说明了当response.url无法解析时不阻断的设计决策。src/app/v1/_lib/proxy/response-handler.ts (3)
1657-1768: 计费容错机制实现正确。
updateRequestCostFromUsage重构后的逻辑:
- 使用
resolveValidPriceData辅助函数检查价格数据有效性- 主模型价格不存在时回退到备选模型
- 完全无价格时采取"不计费放行"策略并异步触发云端同步
这符合 PR 目标:避免因价格表查询失败导致 500 错误。
1900-1961: LGTM!
trackCostToRedis包裹在 try/catch 中,确保 Redis 追踪失败不会影响请求处理。日志记录了失败原因便于排查。
365-384: LGTM!Session 成本计算包裹在 try/catch 中,失败时记录错误并跳过,不影响主流程。这与 PR 的计费容错目标一致。
tests/unit/actions/model-prices.test.ts (3)
9-60: Mock 结构调整与新依赖注入方式整体可用
findAllLatestPrices与fetchCloudPriceTableToml的 mock 接入方式清晰,beforeEach默认返回空数组也能避免用例间相互污染。Also applies to: 85-90
230-335: TOML 拉取/解析失败分支覆盖到位用例覆盖了:无手动价格、存在冲突、无冲突、拉取失败、TOML 非法等关键分支,能匹配本 PR 的云端 TOML 迁移目标。
338-460: processPriceTableInternal 改用批量“最新价格”数据源后,测试意图明确通过
findAllLatestPricesMock驱动“跳过 manual / 覆盖 manual / 新增模型 / 跳过元数据 / 缺 mode 记 failed”等路径,回归点覆盖合理。src/actions/model-prices.ts (2)
105-114: 批量加载 latest 价格避免 N+1:方向正确
findAllLatestPrices+Map做索引能显著减少同步时的查询次数,逻辑也比逐条查更稳。Also applies to: 151-168
335-358: 云端 TOML 拉取/解析与日志前缀统一:可观测性更好
fetchCloudPriceTableToml+parseCloudPriceTableToml的错误回传与日志结构化字段(added/updated/failed 等)齐全,便于排障。Also applies to: 397-444
src/app/[locale]/settings/prices/_components/price-list.tsx (1)
247-258: display_name 优先展示 + 能力用 icon 呈现的交互实现干净chat mode 不显示 badge、display_name 与 modelName 的双行展示、能力图标的
aria-label/tooltip 文案均通过t()注入,整体实现符合组件目标。Also applies to: 260-284, 430-484, 509-563
| export async function fetchCloudPriceTableToml( | ||
| url: string = CLOUD_PRICE_TABLE_URL | ||
| ): Promise<CloudPriceTableResult<string>> { | ||
| const expectedUrl = (() => { | ||
| try { | ||
| return new URL(url); | ||
| } catch { | ||
| return null; | ||
| } | ||
| })(); | ||
|
|
||
| const controller = new AbortController(); | ||
| const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); | ||
|
|
||
| try { | ||
| const response = await fetch(url, { | ||
| signal: controller.signal, | ||
| headers: { | ||
| Accept: "text/plain", | ||
| }, | ||
| cache: "no-store", | ||
| }); | ||
|
|
||
| if (expectedUrl && typeof response.url === "string" && response.url) { | ||
| try { | ||
| const finalUrl = new URL(response.url); | ||
| if (finalUrl.protocol !== expectedUrl.protocol || finalUrl.host !== expectedUrl.host) { | ||
| return { ok: false, error: "云端价格表拉取失败:重定向到非预期地址" }; | ||
| } | ||
| } catch { | ||
| // response.url 无法解析时不阻断(仅作安全硬化),继续按原路径处理 | ||
| } | ||
| } | ||
|
|
||
| if (!response.ok) { | ||
| return { ok: false, error: `云端价格表拉取失败:HTTP ${response.status}` }; | ||
| } | ||
|
|
||
| const tomlText = await response.text(); | ||
| if (!tomlText.trim()) { | ||
| return { ok: false, error: "云端价格表拉取失败:内容为空" }; | ||
| } | ||
|
|
||
| return { ok: true, data: tomlText }; | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| return { ok: false, error: `云端价格表拉取失败:${message}` }; | ||
| } finally { | ||
| clearTimeout(timeoutId); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/lib/price-sync/cloud-price-table.tsRepository: ding113/claude-code-hub
Length of output: 190
🏁 Script executed:
# Check for i18n usage in the project
rg "next-intl|useTranslations|getTranslations|i18n" --type ts --type tsx -l | head -20Repository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
web_search
@iarna/toml 2.2.5 npm security vulnerabilitiesRepository: ding113/claude-code-hub
Length of output: 175
🏁 Script executed:
# Search for i18n usage patterns in TypeScript/JavaScript files
rg "next-intl|useTranslations|getTranslations" -t js -t ts --max-count 20Repository: ding113/claude-code-hub
Length of output: 50379
🏁 Script executed:
# Check if the file exists and read it
if [ -f "src/lib/price-sync/cloud-price-table.ts" ]; then
cat -n "src/lib/price-sync/cloud-price-table.ts"
else
echo "File not found, searching for similar files..."
find . -name "cloud-price-table*" -type f 2>/dev/null
fiRepository: ding113/claude-code-hub
Length of output: 3970
🌐 Web query:
@iarna/toml 2.2.5 security vulnerabilities CVE
💡 Result:
I found no CVE entries for @iarna/toml version 2.2.5. Package feeds and security scanners list no known vulnerabilities for that release ([npm registry],[Snyk]). CVEs recently published for TOML parsers (e.g., CVE-2025-55195 for @std/toml and CVE-2025-54803 for js-toml) affect other TOML libraries, not @iarna/toml. [1][2][3]
Sources:
- npm package page for @iarna/toml (v2.2.5) — no CVE listed. [1]
- Snyk vulnerability page for @iarna/toml@2.2.5 — no direct vulnerabilities found. [2]
- NVD entries for recent TOML-related CVEs (examples affecting other TOML projects). [3]
所有用户面向错误消息需要使用 i18n,不能硬编码中文字符串
代码中存在多处硬编码的中文错误消息(lines 22, 27, 40, 49, 80, 88, 93, 99),违反了 i18n 代码规范。这些返回给用户的错误信息需要通过 next-intl 进行国际化处理,以支持 zh-CN、en、ja、ko、de 五种语言。建议参考项目中其他模块的 i18n 实现模式,使用 getTranslations() 获取本地化的错误消息。
@iarna/toml 2.2.5 不存在已知安全问题。
There was a problem hiding this comment.
Code Review Summary
This PR implements a comprehensive refactoring of the price table module, switching from LiteLLM JSON to TOML-based cloud price table with fail-open billing. The changes are well-structured, include proper error handling, and have good test coverage. The N+1 query optimization is a notable performance improvement.
PR Size: XL
- Lines changed: 2,663 (2,037 additions, 626 deletions)
- Files changed: 26
Split Suggestion (for future consideration): Given the XL size, future changes of this scope could consider splitting into:
- Core TOML parsing and cloud sync infrastructure
- Fail-open billing implementation
- UI enhancements (filters, capabilities, pagination)
- i18n updates (could be a separate PR)
Issues Found
| Category | Critical | High | Medium | Low |
|---|---|---|---|---|
| Logic/Bugs | 0 | 0 | 0 | 0 |
| Security | 0 | 0 | 0 | 0 |
| Error Handling | 0 | 0 | 0 | 0 |
| Types | 0 | 0 | 0 | 0 |
| Comments/Docs | 0 | 0 | 0 | 0 |
| Tests | 0 | 0 | 0 | 0 |
| Simplification | 0 | 0 | 0 | 0 |
Key Observations (Not Issues)
Positive Patterns Observed:
- Fail-open billing properly implemented - missing prices trigger async sync without blocking requests
- N+1 query fix - batch fetch all prices into Map before processing (O(n) → O(1) queries)
- Prototype pollution protection - Reserved keys (
__proto__,constructor,prototype) are filtered in TOML parsing - Redirect validation - SSRF protection by validating response URL host matches expected URL
- Graceful degradation -
revalidatePathwrapped in try/catch for background task contexts - Proper throttling - Cloud sync uses 5-minute throttle to prevent hammering the server
- Error logging - All error paths have appropriate logging with context
Test Coverage:
- 21 unit tests for cloud-price-table parsing and fetching
- Integration tests for billing model source
- 245 lines of tests for fail-open billing scenarios
Review Coverage
- Logic and correctness - Clean
- Security (OWASP Top 10) - Clean (SSRF protection, prototype pollution prevention)
- Error handling - Clean (fail-open pattern properly implemented)
- Type safety - Clean
- Documentation accuracy - Clean
- Test coverage - Adequate (26 new tests)
- Code clarity - Good
Notes
-
The
isPriceDataEqualfunction now uses stable JSON stringification with sorted keys - this is a good improvement for comparing objects with potentially different key orders from TOML parsing. -
The scheduled sync interval (30 minutes) is appropriately configured with proper cleanup on shutdown signals.
-
i18n coverage is complete across all 5 supported languages (zh-CN, en, ja, ru, zh-TW).
Automated review by Claude AI
There was a problem hiding this comment.
Code Review Summary
This PR implements a comprehensive refactoring of the price table module, switching from LiteLLM JSON to TOML-based cloud price table with fail-open billing. The changes are well-structured, include proper error handling, and have good test coverage. The N+1 query optimization is a notable performance improvement.
PR Size: XL
- Lines changed: 2,663 (2,037 additions, 626 deletions)
- Files changed: 26
Split Suggestion (for future consideration): Given the XL size, future changes of this scope could consider splitting into:
- Core TOML parsing and cloud sync infrastructure
- Fail-open billing implementation
- UI enhancements (filters, capabilities, pagination)
- i18n updates (could be a separate PR)
Issues Found
| Category | Critical | High | Medium | Low |
|---|---|---|---|---|
| Logic/Bugs | 0 | 0 | 0 | 0 |
| Security | 0 | 0 | 0 | 0 |
| Error Handling | 0 | 0 | 0 | 0 |
| Types | 0 | 0 | 0 | 0 |
| Comments/Docs | 0 | 0 | 0 | 0 |
| Tests | 0 | 0 | 0 | 0 |
| Simplification | 0 | 0 | 0 | 0 |
Key Observations (Not Issues)
Positive Patterns Observed:
- Fail-open billing properly implemented - missing prices trigger async sync without blocking requests
- N+1 query fix - batch fetch all prices into Map before processing (O(n) to O(1) queries)
- Prototype pollution protection - Reserved keys (
__proto__,constructor,prototype) are filtered in TOML parsing - Redirect validation - SSRF protection by validating response URL host matches expected URL
- Graceful degradation -
revalidatePathwrapped in try/catch for background task contexts - Proper throttling - Cloud sync uses 5-minute throttle to prevent hammering the server
- Error logging - All error paths have appropriate logging with context
Test Coverage:
- 21 unit tests for cloud-price-table parsing and fetching
- Integration tests for billing model source
- 245 lines of tests for fail-open billing scenarios
Review Coverage
- Logic and correctness - Clean
- Security (OWASP Top 10) - Clean (SSRF protection, prototype pollution prevention)
- Error handling - Clean (fail-open pattern properly implemented)
- Type safety - Clean
- Documentation accuracy - Clean
- Test coverage - Adequate (26 new tests)
- Code clarity - Good
Notes
-
The
isPriceDataEqualfunction now uses stable JSON stringification with sorted keys - this is a good improvement for comparing objects with potentially different key orders from TOML parsing. -
The scheduled sync interval (30 minutes) is appropriately configured with proper cleanup on shutdown signals.
-
i18n coverage is complete across all 5 supported languages (zh-CN, en, ja, ru, zh-TW).
Automated review by Claude AI
| if (!parseResult.ok) { | ||
| return { ok: false, error: parseResult.error }; | ||
| } | ||
| jsonContent = JSON.stringify(parseResult.data.models); |
There was a problem hiding this comment.
[HIGH] [LOGIC-BUG] uploadPriceTable misclassifies invalid JSON as TOML
Why this is a problem: This block discards the JSON parse error and immediately falls back to TOML parsing:
try {
JSON.parse(content);
} catch {
const parseResult = parseCloudPriceTableToml(content);
// ...
}If a user uploads a .json file with malformed JSON, the returned error will come from parseCloudPriceTableToml, which is misleading and makes debugging harder.
Suggested fix:
const trimmed = content.trimStart();
const looksLikeJson = trimmed.startsWith("{") || trimmed.startsWith("[");
if (looksLikeJson) {
try {
JSON.parse(content);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { ok: false, error: `JSON格式不正确:${message}` };
}
return processPriceTableInternal(content, overwriteManual);
}
const parseResult = parseCloudPriceTableToml(content);
if (!parseResult.ok) return { ok: false, error: parseResult.error };
return processPriceTableInternal(JSON.stringify(parseResult.data.models), overwriteManual);|
|
||
| // 构建分页参数 | ||
| const paginationParams: PaginationParams = { | ||
| page, |
There was a problem hiding this comment.
[MEDIUM] [STANDARD-VIOLATION] Hardcoded API error string (i18n)
Why this is a problem: This returns a locale-specific display message directly from an API response:
return NextResponse.json({ ok: false, error: "source 参数无效" }, { status: 400 });Project guideline: "i18n Required - All user-facing strings must use i18n (5 languages supported). Never hardcode display text".
Suggested fix:
if (source && source !== "manual" && source !== "litellm") {
return NextResponse.json({ ok: false, error: "INVALID_SOURCE" }, { status: 400 });
}There was a problem hiding this comment.
Code Review Summary
This is an XL PR that bundles cloud price table refactor (TOML + scheduler), billing fail-open behavior changes, and Settings UI/table updates into a single change set. The main risks are edge cases in the new upload/parser flow and adding new localized display text into API error responses.
PR Size: XL
- Lines changed: 2663
- Files changed: 26
- Split suggestions:
- PR 1: Cloud price table fetch/parse + updater + scheduler (
src/lib/price-sync/*,src/instrumentation.ts, related tests) - PR 2: Billing fail-open + proxy cost tracking (
src/app/v1/_lib/proxy/*, related tests) - PR 3: Settings prices UI/filters/capabilities + i18n message updates (
src/app/[locale]/settings/prices/**,messages/**) - PR 4 (optional): Latest-price query refactor (
src/repository/model-price.ts,src/app/api/prices/route.ts)
- PR 1: Cloud price table fetch/parse + updater + scheduler (
Issues Found
| Category | Critical | High | Medium | Low |
|---|---|---|---|---|
| Logic/Bugs | 0 | 1 | 0 | 0 |
| Security | 0 | 0 | 0 | 0 |
| Error Handling | 0 | 0 | 1 | 0 |
| Types | 0 | 0 | 0 | 0 |
| Comments/Docs | 0 | 0 | 0 | 0 |
| Tests | 0 | 0 | 0 | 0 |
| Simplification | 0 | 0 | 0 | 0 |
Critical Issues (Must Fix)
None.
High Priority Issues (Should Fix)
- (90)
src/actions/model-prices.ts:uploadPriceTableJSON/TOML fallback can misclassify invalid JSON as TOML and return misleading errors. - (80)
src/app/api/prices/route.ts: new hardcoded API error stringsource 参数无效adds non-i18n display text into API responses; prefer a stable error code.
Review Coverage
- Logic and correctness
- Security (OWASP Top 10)
- Error handling
- Type safety
- Documentation accuracy
- Test coverage
- Code clarity
Automated review by Codex AI
There was a problem hiding this comment.
- Identified open PR
#580and applied the size labelsize/XL(2663 lines changed, 26 files). - Posted 2 inline review comments on diff lines:
src/actions/model-prices.ts:215—[HIGH]uploadPriceTableJSON/TOML fallback can misclassify malformed JSON as TOML and return misleading errors (suggested content sniffing + clearer error path).src/app/api/prices/route.ts:43—[MEDIUM]new hardcoded API error stringsource 参数无效adds non‑i18n display text in API responses (suggested stable error code).
- Submitted the required PR review summary (includes XL split suggestions).
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (5)
src/lib/price-sync/cloud-price-updater.ts (1)
57-104: 任务标识符命名不一致
taskId使用连字符"cloud-price-table-sync"(Line 62),而传给AsyncTaskManager.register的任务名使用下划线"cloud_price_table_sync"(Line 102)。建议统一命名风格以提高可维护性。建议修改
AsyncTaskManager.register( taskId, (async () => { // ... })(), - "cloud_price_table_sync" + "cloud-price-table-sync" );tests/unit/api/prices-route.test.ts (1)
53-75: 建议补充source和litellmProvider参数的测试覆盖当前测试验证了基本的权限和分页参数,但未覆盖本 PR 新增的
source和litellmProvider过滤参数。建议补充以下测试场景:
source=manual或source=litellm时参数正确传递source参数值无效时返回 400litellmProvider参数正确传递建议补充的测试
it("passes source filter to getModelPricesPaginated", async () => { mocks.getSession.mockResolvedValue({ user: { role: "admin" } }); mocks.getModelPricesPaginated.mockResolvedValue({ ok: true, data: { data: [], total: 0, page: 1, pageSize: 50, totalPages: 0 }, }); const { GET } = await import("@/app/api/prices/route"); await GET({ url: "http://localhost/api/prices?source=manual" } as any); expect(mocks.getModelPricesPaginated).toHaveBeenCalledWith( expect.objectContaining({ source: "manual" }) ); }); it("returns 400 for invalid source value", async () => { mocks.getSession.mockResolvedValue({ user: { role: "admin" } }); const { GET } = await import("@/app/api/prices/route"); const response = await GET({ url: "http://localhost/api/prices?source=invalid" } as any); expect(response.status).toBe(400); });tests/unit/settings/prices/price-list-zero-price-ui.test.tsx (1)
15-26:loadMessages辅助函数可考虑复用此消息加载逻辑可能在其他测试中也会用到,建议抽取到共享的测试工具模块(如
tests/utils/i18n.ts)中以避免重复。src/app/api/prices/route.ts (1)
21-21: 建议:API 响应消息考虑国际化当前 API 错误消息使用硬编码的中文字符串。虽然这可能是代码库的整体模式,但从支持多语言用户的角度考虑,API 响应也应当使用 i18n 机制。
如果项目后续需要支持国际化,可以考虑引入统一的 API 错误消息 i18n 方案。
基于编码指南:所有用户可见字符串应使用 i18n
Also applies to: 38-38, 42-42, 46-46, 64-64
tests/unit/actions/model-prices.test.ts (1)
462-491: 原型污染防护测试很好,建议添加注释新增的测试用例正确验证了价格数据比较时会过滤危险键(如
constructor),防止原型污染攻击。Line 483 的 Mock 数据包含了潜在的原型污染载荷,测试确认此类键不会影响价格比较逻辑,这是良好的安全实践。💡 可选改进:添加注释说明安全测试目的
建议在测试用例开头添加注释,说明此测试是为了防止原型污染攻击:
+ // Security test: verify that dangerous keys (e.g., constructor, __proto__) + // are ignored during price data comparison to prevent prototype pollution it("should ignore dangerous keys when comparing price data", async () => {
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to Reviews > Disable Cache setting
📒 Files selected for processing (14)
drizzle/0052_model_price_source.sqldrizzle/meta/0052_snapshot.jsondrizzle/meta/_journal.jsonsrc/actions/model-prices.tssrc/app/[locale]/settings/prices/_components/price-list.tsxsrc/app/api/prices/route.tssrc/lib/price-sync/cloud-price-table.tssrc/lib/price-sync/cloud-price-updater.tssrc/repository/model-price.tstests/unit/actions/model-prices.test.tstests/unit/api/prices-route.test.tstests/unit/price-sync/cloud-price-table.test.tstests/unit/price-sync/cloud-price-updater.test.tstests/unit/settings/prices/price-list-zero-price-ui.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- tests/unit/price-sync/cloud-price-table.test.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
No emoji characters in any code, comments, or string literals
Files:
src/repository/model-price.tstests/unit/settings/prices/price-list-zero-price-ui.test.tsxsrc/app/[locale]/settings/prices/_components/price-list.tsxtests/unit/api/prices-route.test.tstests/unit/actions/model-prices.test.tssrc/actions/model-prices.tssrc/lib/price-sync/cloud-price-table.tstests/unit/price-sync/cloud-price-updater.test.tssrc/app/api/prices/route.tssrc/lib/price-sync/cloud-price-updater.ts
**/*.{ts,tsx,jsx,js}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,jsx,js}: All user-facing strings must use i18n (5 languages supported: zh-CN, en, ja, ko, de). Never hardcode display text
Use path alias @/ to map to ./src/
Use Biome for code formatting with configuration: double quotes, trailing commas, 2-space indent, 100 character line width
Prefer named exports over default exports
Use next-intl for internationalization
Use Next.js 16 App Router with Hono for API routes
Files:
src/repository/model-price.tstests/unit/settings/prices/price-list-zero-price-ui.test.tsxsrc/app/[locale]/settings/prices/_components/price-list.tsxtests/unit/api/prices-route.test.tstests/unit/actions/model-prices.test.tssrc/actions/model-prices.tssrc/lib/price-sync/cloud-price-table.tstests/unit/price-sync/cloud-price-updater.test.tssrc/app/api/prices/route.tssrc/lib/price-sync/cloud-price-updater.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Place unit tests in tests/unit/, integration tests in tests/integration/, and source-adjacent tests in src/**/*.test.ts
Files:
tests/unit/settings/prices/price-list-zero-price-ui.test.tsxtests/unit/api/prices-route.test.tstests/unit/actions/model-prices.test.tstests/unit/price-sync/cloud-price-updater.test.ts
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use React 19, shadcn/ui, Tailwind CSS, and Recharts for the UI layer
Files:
tests/unit/settings/prices/price-list-zero-price-ui.test.tsxsrc/app/[locale]/settings/prices/_components/price-list.tsx
**/drizzle/**/*.sql
📄 CodeRabbit inference engine (CLAUDE.md)
Never create SQL migration files manually. Always generate migrations by editing src/drizzle/schema.ts, then run bun run db:generate, review the generated SQL, and run bun run db:migrate
Files:
drizzle/0052_model_price_source.sql
🧠 Learnings (12)
📓 Common learnings
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:20:04.478Z
Learning: In the `ding113/claude-code-hub` repository, Server Actions (files under `src/actions/*.ts`) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (e.g., matches DB schema or special UI logic) and undefined signifies 'inherit default'. This pattern should be consistently reflected in type definitions across related fields to preserve semantic clarity between database constraints and UI behavior.
Applied to files:
src/repository/model-price.tssrc/actions/model-prices.tssrc/lib/price-sync/cloud-price-table.tssrc/app/api/prices/route.tssrc/lib/price-sync/cloud-price-updater.ts
📚 Learning: 2026-01-07T17:05:37.171Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T17:05:37.171Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Place unit tests in tests/unit/, integration tests in tests/integration/, and source-adjacent tests in src/**/*.test.ts
Applied to files:
tests/unit/settings/prices/price-list-zero-price-ui.test.tsxtests/unit/price-sync/cloud-price-updater.test.ts
📚 Learning: 2026-01-10T06:20:13.376Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx:42-53
Timestamp: 2026-01-10T06:20:13.376Z
Learning: In the claude-code-hub project, model pricing display (in files like `src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx`) intentionally uses hardcoded USD currency symbol (`$`) and per-million-token notation (`/M`, `/img`) because the system exclusively tracks LiteLLM pricing in USD and the notation is industry standard. Configurability was deemed unnecessary complexity.
Applied to files:
tests/unit/settings/prices/price-list-zero-price-ui.test.tsxsrc/app/[locale]/settings/prices/_components/price-list.tsxtests/unit/actions/model-prices.test.tssrc/actions/model-prices.tssrc/lib/price-sync/cloud-price-table.ts
📚 Learning: 2026-01-10T06:19:56.528Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/app/[locale]/settings/prices/_components/model-price-dialog.tsx:205-257
Timestamp: 2026-01-10T06:19:56.528Z
Learning: In the pricing module (src/app/[locale]/settings/prices/_components/), currency symbols ("$") and technical unit notations ("/M" for per-million tokens, "/img" for per-image) are intentionally hardcoded. The system uses USD as the fixed currency for all pricing, and these notations are standard industry conventions. These hardcoded values are an accepted exception to the general i18n requirement.
Applied to files:
src/app/[locale]/settings/prices/_components/price-list.tsx
📚 Learning: 2026-01-07T17:05:37.171Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T17:05:37.171Z
Learning: Applies to **/*.{tsx,jsx} : Use React 19, shadcn/ui, Tailwind CSS, and Recharts for the UI layer
Applied to files:
src/app/[locale]/settings/prices/_components/price-list.tsx
📚 Learning: 2026-01-10T06:20:32.687Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx:118-125
Timestamp: 2026-01-10T06:20:32.687Z
Learning: In `src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx`, the "Cancel" button in the SyncConflictDialog is intentionally designed to call `onConfirm([])`, which triggers `doSync([])` to continue the sync while skipping (not overwriting) conflicting manual prices. This is the desired product behavior to allow users to proceed with LiteLLM sync for non-conflicting models while preserving their manual price entries.
Applied to files:
src/app/[locale]/settings/prices/_components/price-list.tsxtests/unit/actions/model-prices.test.tssrc/actions/model-prices.tssrc/lib/price-sync/cloud-price-updater.ts
📚 Learning: 2026-01-10T06:20:04.478Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:20:04.478Z
Learning: In the `ding113/claude-code-hub` repository, Server Actions (files under `src/actions/*.ts`) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.
Applied to files:
tests/unit/actions/model-prices.test.tssrc/lib/price-sync/cloud-price-table.tssrc/app/api/prices/route.ts
📚 Learning: 2026-01-10T06:19:58.167Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:19:58.167Z
Learning: Do not modify hardcoded Chinese error messages in Server Actions under src/actions/*.ts as part of piecemeal changes. This is a repo-wide architectural decision that requires a coordinated i18n refactor across all Server Action files (e.g., model-prices.ts, users.ts, system-config.ts). Treat i18n refactor as a separate unified task rather than per-PR changes, and plan a project-wide approach for replacing hardcoded strings with localized resources.
Applied to files:
src/actions/model-prices.ts
📚 Learning: 2026-01-07T17:05:37.171Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T17:05:37.171Z
Learning: Applies to **/*.{ts,tsx,jsx,js} : All user-facing strings must use i18n (5 languages supported: zh-CN, en, ja, ko, de). Never hardcode display text
Applied to files:
src/lib/price-sync/cloud-price-table.tssrc/app/api/prices/route.ts
📚 Learning: 2026-01-07T17:05:37.171Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T17:05:37.171Z
Learning: Applies to **/*.{ts,tsx,jsx,js} : Use next-intl for internationalization
Applied to files:
src/lib/price-sync/cloud-price-table.tssrc/app/api/prices/route.ts
📚 Learning: 2026-01-10T06:20:19.207Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: messages/ja/dashboard.json:1342-1345
Timestamp: 2026-01-10T06:20:19.207Z
Learning: In this project, minor i18n formatting issues (such as spacing between numbers and units in Japanese/Chinese) are accepted when fixing them would require adding template complexity. The approach prioritizes code simplicity over perfect locale-specific formatting for minor cosmetic issues.
Applied to files:
src/lib/price-sync/cloud-price-table.tssrc/app/api/prices/route.ts
🧬 Code graph analysis (9)
src/repository/model-price.ts (6)
src/drizzle/schema.ts (1)
modelPrices(365-381)src/drizzle/db.ts (1)
db(37-44)src/repository/_shared/transformers.ts (1)
toModelPrice(141-148)src/lib/logger.ts (1)
logger(168-187)src/repository/index.ts (1)
findAllLatestPrices(34-34)src/types/model-price.ts (1)
ModelPrice(63-70)
tests/unit/settings/prices/price-list-zero-price-ui.test.tsx (2)
src/types/model-price.ts (1)
ModelPrice(63-70)src/app/[locale]/settings/prices/_components/price-list.tsx (1)
PriceList(67-679)
src/app/[locale]/settings/prices/_components/price-list.tsx (6)
src/types/model-price.ts (1)
ModelPriceSource(58-58)src/components/ui/button.tsx (1)
Button(58-58)src/components/ui/table.tsx (1)
TableHead(92-92)src/components/ui/badge.tsx (1)
Badge(39-39)src/components/ui/tooltip.tsx (3)
Tooltip(57-57)TooltipTrigger(57-57)TooltipContent(57-57)src/app/[locale]/settings/prices/_components/model-price-dialog.tsx (1)
ModelPriceDialog(41-272)
tests/unit/api/prices-route.test.ts (1)
src/app/api/prices/route.ts (1)
GET(16-66)
tests/unit/actions/model-prices.test.ts (1)
src/actions/model-prices.ts (2)
checkLiteLLMSyncConflicts(339-394)processPriceTableInternal(75-195)
src/lib/price-sync/cloud-price-table.ts (1)
src/types/model-price.ts (1)
ModelPriceData(4-53)
tests/unit/price-sync/cloud-price-updater.test.ts (5)
src/lib/price-sync/cloud-price-updater.ts (2)
syncCloudPriceTableToDatabase(17-47)requestCloudPriceTableSync(57-104)src/actions/model-prices.ts (1)
processPriceTableInternal(75-195)src/lib/price-sync/cloud-price-table.ts (1)
CloudPriceTableResult(12-12)src/lib/async-task-manager.ts (1)
AsyncTaskManager(232-233)src/lib/logger.ts (1)
logger(168-187)
src/app/api/prices/route.ts (1)
src/repository/model-price.ts (1)
PaginationParams(13-19)
src/lib/price-sync/cloud-price-updater.ts (5)
src/lib/price-sync/cloud-price-table.ts (3)
CloudPriceTableResult(12-12)fetchCloudPriceTableToml(53-107)parseCloudPriceTableToml(18-51)src/types/model-price.ts (1)
PriceUpdateResult(82-89)src/actions/model-prices.ts (1)
processPriceTableInternal(75-195)src/lib/async-task-manager.ts (1)
AsyncTaskManager(232-233)src/lib/logger.ts (1)
logger(168-187)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: dev-build-deploy
🔇 Additional comments (37)
drizzle/0052_model_price_source.sql (1)
1-2: 请确认此迁移文件是由 Drizzle 工具自动生成的。根据编码规范,SQL 迁移文件不应手动创建,而应通过编辑
src/drizzle/schema.ts后运行bun run db:generate自动生成。请确认此文件是通过正确流程生成的。迁移逻辑本身是正确的:为
model_prices表添加source列(默认值'litellm')并创建索引。src/lib/price-sync/cloud-price-updater.ts (1)
17-47: LGTM!
syncCloudPriceTableToDatabase函数实现正确:
- 采用结构化错误处理,返回
ok: false而非抛出异常- 使用动态导入避免循环依赖
- 正确传递
overwriteManual参数以支持冲突覆盖src/app/[locale]/settings/prices/_components/price-list.tsx (4)
139-177: LGTM!
fetchPrices函数正确实现了带过滤参数的数据获取逻辑。错误处理通过result.ok检查和 try-catch 覆盖了主要场景。
302-375: LGTM!快捷筛选按钮实现正确,支持切换和重置功能,且所有标签都使用了 i18n。
458-486: LGTM!能力指示器的实现考虑了可访问性(
aria-label、aria-hidden),工具提示提供了清晰的状态信息。
507-509: LGTM!日期格式化正确使用了
locale变量进行本地化显示,并妥善处理了updatedAt可能为空的情况。drizzle/meta/_journal.json (1)
369-377: LGTM!Drizzle 迁移日志更新,新增
0052_model_price_source迁移条目。tests/unit/api/prices-route.test.ts (1)
1-19: LGTM!测试设置正确,使用
vi.hoisted确保 mock 在模块加载前初始化,beforeEach清理 mock 状态。drizzle/meta/0052_snapshot.json (3)
799-805: LGTM!
model_prices表的source列定义正确:varchar(20),非空,默认值'litellm'。
873-887: LGTM!
idx_model_prices_source索引定义正确,与 SQL 迁移文件一致。
1945-1951: 此列似乎不属于本 PR 的主要变更范围
enable_thinking_signature_rectifier列出现在system_settings表中,但根据 PR 描述,本次变更主要涉及价格表同步和计费逻辑。请确认此列的添加是否应该在本 PR 中,或者是否应该拆分到单独的 PR。tests/unit/settings/prices/price-list-zero-price-ui.test.tsx (1)
45-85: LGTM!测试正确验证了零价格的显示行为,确保
formatPrice(0)返回"0.0000"而非占位符"-"。测试使用固定日期和完整的 i18n 上下文,保证了可重复性。src/app/api/prices/route.ts (3)
6-15: 文档已正确更新,包含新增的查询参数JSDoc 注释清晰地列出了所有查询参数,包括新增的
source和litellmProvider,有助于 API 使用者理解接口。
27-31: 数值解析改进,提升了健壮性使用
Number.parseInt(value, 10)配合Number.isFinite()验证比隐式类型转换更明确、更安全,能正确拒绝NaN和Infinity等无效值。
45-47: 参数验证逻辑正确对
source参数的白名单验证确保了类型断言(第 54 行)的安全性,避免了无效值传递到后端。tests/unit/price-sync/cloud-price-updater.test.ts (3)
1-44: 测试 mock 设置完整且合理Mock 配置覆盖了所有必要的依赖项(logger、AsyncTaskManager、processPriceTableInternal、fetch),并且通过
asyncTasks数组捕获异步任务,便于测试异步行为的验证。
46-161: syncCloudPriceTableToDatabase 测试覆盖全面测试用例覆盖了关键路径:
- 网络错误、空响应、TOML 解析失败
- 内部处理失败的不同场景(ok=false、data 为空)
- 成功路径的完整验证
类型断言(如第 108、127、155 行)在测试代码中是可接受的权衡。
163-249: requestCloudPriceTableSync 测试逻辑严谨测试用例覆盖了:
- 任务去重逻辑
- 节流机制
- 异步任务执行流程(包括时间戳更新时机)
- 失败场景的日志记录
特别是第 192-232 行的测试通过 Promise 控制异步执行流程,精确验证了任务注册、执行和完成各阶段的行为。
src/repository/model-price.ts (5)
13-19: 接口扩展合理,支持新的过滤维度新增的
litellmProvider字段为分页查询提供了额外的过滤能力,类型和注释都很清晰。
35-67: 错误处理和查询优化改进新增的异常捕获和详细日志提升了可靠性和可观测性。查询排序逻辑正确实现了"手动配置优先,时间越新越优先"的语义。
73-92: DISTINCT ON 查询正确实现批量获取逻辑使用 PostgreSQL 的 DISTINCT ON 配合多级排序,能高效地获取每个模型的最新价格记录,且正确优先选择手动配置。
98-162: 分页查询增强功能实现正确新增的
litellmProvider过滤逻辑使用参数化查询,安全地处理 JSONB 字段查询。WHERE 条件构建和分页逻辑都正确实现。
238-263: 手动价格查询逻辑清晰使用 DISTINCT ON 配合 WHERE 过滤获取所有手动配置的最新价格,返回 Map 结构便于后续快速查找。
src/lib/price-sync/cloud-price-table.ts (3)
1-16: 类型定义和工具函数设计合理使用 Result 模式的
CloudPriceTableResult<T>提供了类型安全的成功/失败分支,isRecord类型守卫简洁有效。10 秒超时设置合理。
18-51: TOML 解析实现了完善的安全防护代码采用了多层防御措施:
- 使用
Object.create(null)创建无原型对象(第 30 行)- 显式过滤
__proto__、constructor、prototype等危险键(第 32-34 行)这些措施有效防止了原型链污染攻击,体现了良好的安全意识。
53-107: fetch 实现包含严格的安全验证关键安全措施:
- 10 秒超时防止挂起(第 64-65 行)
- 重定向目标验证,防止恶意跳转(第 76-89 行)
- 内容非空校验(第 96 行)
- 资源清理保证(finally 块,第 104-106 行)
特别是重定向验证逻辑(第 76-89 行)在 URL 解析失败时的降级处理很优雅,既不阻塞正常流程,又增强了安全性。
src/actions/model-prices.ts (7)
6-9: 新增导入支持 TOML 价格表同步引入的云端价格表工具函数为后续的 TOML 格式支持奠定了基础。
34-67: 价格数据比较函数重构实现了完善的安全防护新的
stableStringify实现包含多重安全措施:
- WeakSet 检测循环引用(第 42-45 行)
- 无原型对象创建(第 52 行)
- 显式过滤危险键名(第 54-57 行)
- 稳定的键排序(第 53 行)
这些措施确保了比较逻辑既准确又安全。
112-117: 批量查询优化消除了 N+1 问题通过一次性获取所有最新价格并构建 Map 索引,避免了在循环中逐个查询数据库(第 155 行使用 Map 查找)。这是一个显著的性能改进。
基于 PR 目标:消除价格同步中的 N+1 查询问题
180-187: revalidatePath 错误处理支持后台任务场景在后台任务或启动阶段,Next.js 请求上下文可能不存在,此处的 try-catch 降级处理避免了崩溃,同时保留调试日志,是合理的容错设计。
基于 PR 目标:定时同步任务优雅处理 revalidatePath 以避免启动日志错误
205-228: 上传接口扩展支持 JSON 和 TOML 双格式通过先尝试 JSON 解析,失败后再按 TOML 处理的策略(第 217-225 行),实现了对两种格式的透明支持,且保持了向后兼容性。
339-394: 冲突检查逻辑迁移到 TOML 数据源函数正确地从旧的 LiteLLM JSON 源切换到云端 TOML 源(第 348、356 行),保持了冲突检测逻辑的正确性。
基于 PR 目标:云端价格表从 LiteLLM JSON 切换到 TOML 格式
401-448: 同步逻辑迁移到 TOML 并增强可观测性函数完成了数据源迁移(第 414、420 行),同时引入了统一的
[PriceSync]日志前缀和结构化日志输出(第 430-440 行),显著提升了运维可观测性。tests/unit/actions/model-prices.test.ts (4)
10-10: 迁移到 TOML 的 Mock 设置正确新增的
findAllLatestPricesMock和fetchCloudPriceTableTomlMockMock 声明合理,Line 54-60 使用importOriginal模式正确保留了其他导出(如parseCloudPriceTableToml),避免了完全覆盖模块导致的问题。默认初始化逻辑也符合测试最佳实践。Also applies to: 17-17, 43-43, 54-60, 89-89
233-238: TOML Mock 数据格式合理Mock 的 TOML 格式字符串结构正确(表头 + 键值对),虽然简化但足以支持单元测试对动作逻辑的验证。这些测试专注于冲突检测逻辑而非 TOML 解析细节,符合单元测试的关注点分离原则。
Also applies to: 257-265, 284-289
309-335: 错误处理测试更新正确测试用例正确迁移到 TOML 场景:
- 网络错误 Mock 使用 "云端价格表拉取失败" 消息符合新的云端拉取逻辑
- Line 323 测试名称从 "invalid JSON" 更新为 "invalid TOML" 准确反映变更
- Line 327 的故意损坏的 TOML 字符串适合测试解析错误路径
- 断言检查错误消息包含 "云端" 和 "TOML" 关键词符合预期
346-346: 批量加载 Mock 使用正确,提升性能测试正确迁移到使用
findAllLatestPricesMock进行批量价格查询,消除了之前的 N+1 查询模式。这与 PR 目标中提到的"通过批量查找消除价格同步中的 N+1 数据库查询"一致,测试覆盖了跳过手动价格、覆盖手动价格、添加新模型等多种场景。Also applies to: 371-371, 403-403, 431-431, 448-448, 474-474
🧪 测试结果
总体结果: ✅ 所有测试通过 |
- PR ding113#580: TOML cloud price table + billing fail-open - PR ding113#578: make drizzle migrations idempotent - PR ding113#577: fix thinking enabled + tool_use first block - PR ding113#573: add manual model price management Conflict resolutions: - drizzle migrations: use upstream idempotent version - i18n messages: accept upstream additions - price-sync.ts: removed (replaced by cloud-price-table) - model-prices.ts: use upstream refactored version
Summary
This PR refactors the price table module to use a TOML-based cloud price table and implements fail-open billing to prevent 500 errors when pricing data is unavailable.
Related Issues:
revalidatePatherror during startup by adding graceful fallbackProblem
1. Price Sync Issues
2. Startup Errors
revalidatePath()called during Next.js instrumentation phase caused non-blocking errorsInvariant: static generation store missing in revalidatePath /settings/prices3. Billing Fragility
Solution
1. Cloud Price Table Migration (TOML)
https://claude-code-hub.app/config/prices-base.toml@iarna/tomlparser for robust TOML parsingsrc/lib/price-sync/cloud-price-table.ts- Fetch and parse TOMLsrc/lib/price-sync/cloud-price-updater.ts- Update logic with retry2. Fail-Open Billing
src/app/v1/_lib/proxy/response-handler.ts(lines 346-408)3. Scheduled Sync
src/instrumentation.ts(lines 62-84)revalidatePathfallback4. UI Enhancements
src/app/[locale]/settings/prices/_components/price-list.tsxdisplay_nameprioritized overmodelNamefor better readabilitymode=chatmodels (internal/duplicate entries)supports_*fields (vision, function calling, etc.)5. Performance Optimization
src/actions/model-prices.ts(lines 108-115)Changes
Core Changes
Supporting Changes
/api/pricesfor health checksBreaking Changes
None. All changes are backward compatible:
Testing
Automated Tests
tests/unit/price-sync/(21 tests)tests/integration/billing-model-source.test.ts(5 tests)tests/unit/proxy/pricing-no-price.test.ts(245 lines)Manual Testing Checklist
revalidatePatherrorsPre-commit Verification
Performance Impact
Migration Notes
No migration required. Changes are fully backward compatible:
Related PRs
Verification:
fix/prices-toml-refactordev2732f31🤖 Description enhanced by Claude AI