Skip to content

feat(prices): 添加手动模型价格管理功能#573

Merged
ding113 merged 7 commits intoding113:devfrom
NieiR:feat/manual-model-price-crud
Jan 10, 2026
Merged

feat(prices): 添加手动模型价格管理功能#573
ding113 merged 7 commits intoding113:devfrom
NieiR:feat/manual-model-price-crud

Conversation

@NieiR
Copy link
Contributor

@NieiR NieiR commented Jan 9, 2026

概述

本 PR 实现了手动模型价格管理功能,解决了自定义模型(如 gpt-5.2-codex)无法正确计费的问题。

closes #405

主要改动

1. 数据库层

  • 新增 source 字段(litellm | manual)区分价格来源
  • 添加索引优化查询性能

2. 手动价格 CRUD

  • 支持手动添加模型价格(支持 chat、image_generation、embedding 等模式)
  • 支持编辑和删除手动价格
  • 仅管理员可操作

3. LiteLLM 同步冲突处理

  • 同步时自动跳过手动价格,避免被覆盖
  • 新增冲突检测功能,同步前检查是否存在冲突
  • 添加冲突解决 UI:
    • 展示手动价格与 LiteLLM 价格的差异对比
    • 支持单个/批量选择覆盖
    • 分页展示大量冲突
Snipaste_2026-01-09_14-39-24

4. UI 组件

  • ModelPriceDialog: 创建/编辑模型价格对话框
  • DeleteModelDialog: 删除确认对话框
  • SyncConflictDialog: 冲突解决对话框

5. 国际化

  • 支持 5 种语言(zh-CN、zh-TW、en、ja、ru)

测试

  • 添加 20 个单元测试,覆盖所有核心功能
  • 测试通过率 100%

破坏性变更

无。新增字段有默认值,现有数据自动标记为 litellm 来源。

Greptile Overview

Greptile Overview

Greptile Summary

Adds manual model price management functionality to enable custom model pricing (e.g., gpt-5.2-codex) that won't be overwritten during LiteLLM sync. Implements a source field distinguishing between 'litellm' (synced) and 'manual' (user-added) prices, with conflict detection UI allowing selective overrides during sync. Includes comprehensive CRUD operations, admin authorization checks, pagination, search filtering, and 20 unit tests achieving 100% coverage.

Confidence Score: 4/5

  • Well-architected feature with proper authorization, conflict handling, and test coverage; safe to merge with minor style improvement possible
  • The implementation is thorough with strong separation of concerns: database migration is backward-compatible, repository layer uses appropriate raw SQL for complex queries, authorization is consistently enforced, conflict detection prevents accidental overwrites, and comprehensive tests validate all scenarios. One minor style suggestion regarding type safety in the checkbox indeterminate handling, but no functional issues identified.
  • No files require special attention - all critical components have proper error handling and validation

Important Files Changed

File Analysis

Filename Score Overview
src/drizzle/schema.ts 5/5 Adds source field to modelPrices table with proper typing and index; well-structured schema change
src/repository/model-price.ts 4/5 Implements repository layer with pagination and source filtering; uses raw SQL for complex queries
src/actions/model-prices.ts 4/5 Implements server actions with admin authorization, conflict detection, and manual price protection logic
src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx 5/5 Conflict resolution UI with search, pagination, and price diff comparison; well-implemented UX

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as Sync Button
    participant Action as model-prices.ts
    participant Repo as Repository
    participant CDN as LiteLLM CDN
    participant DB as Database

    User->>UI: Click Sync
    UI->>Action: checkLiteLLMSyncConflicts()
    Action->>CDN: getPriceTableJson()
    CDN-->>Action: LiteLLM prices JSON
    Action->>Repo: findAllManualPrices()
    Repo->>DB: Query manual prices
    DB-->>Repo: Manual price records
    Repo-->>Action: Manual prices map
    Action->>Action: Detect conflicts
    alt Has Conflicts
        Action-->>UI: Return conflicts list
        UI->>User: Show conflict dialog
        User->>UI: Select models to overwrite
        UI->>Action: syncLiteLLMPrices(selectedModels)
    else No Conflicts
        Action-->>UI: No conflicts
        UI->>Action: syncLiteLLMPrices()
    end
    Action->>Action: processPriceTableInternal()
    loop For each model
        alt Is manual & not in overwrite list
            Action->>Action: Skip (preserve manual)
        else Price changed
            Action->>Repo: createModelPrice(source='litellm')
            Repo->>DB: Insert new price record
        end
    end
    Action-->>UI: Sync result
    UI->>User: Show success/failure toast
Loading

Important Files Changed

File Analysis

Filename Score Overview
src/drizzle/schema.ts 5/5 Adds source field to modelPrices table with proper typing and index; well-structured schema change
src/repository/model-price.ts 4/5 Implements repository layer with pagination and source filtering; uses raw SQL for complex queries
src/actions/model-prices.ts 4/5 Implements server actions with admin authorization, conflict detection, and manual price protection logic
src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx 5/5 Conflict resolution UI with search, pagination, and price diff comparison; well-implemented UX

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as Sync Button
    participant Action as model-prices.ts
    participant Repo as Repository
    participant CDN as LiteLLM CDN
    participant DB as Database

    User->>UI: Click Sync
    UI->>Action: checkLiteLLMSyncConflicts()
    Action->>CDN: getPriceTableJson()
    CDN-->>Action: LiteLLM prices JSON
    Action->>Repo: findAllManualPrices()
    Repo->>DB: Query manual prices
    DB-->>Repo: Manual price records
    Repo-->>Action: Manual prices map
    Action->>Action: Detect conflicts
    alt Has Conflicts
        Action-->>UI: Return conflicts list
        UI->>User: Show conflict dialog
        User->>UI: Select models to overwrite
        UI->>Action: syncLiteLLMPrices(selectedModels)
    else No Conflicts
        Action-->>UI: No conflicts
        UI->>Action: syncLiteLLMPrices()
    end
    Action->>Action: processPriceTableInternal()
    loop For each model
        alt Is manual & not in overwrite list
            Action->>Action: Skip (preserve manual)
        else Price changed
            Action->>Repo: createModelPrice(source='litellm')
            Repo->>DB: Insert new price record
        end
    end
    Action-->>UI: Sync result
    UI->>User: Show success/failure toast
Loading

@coderabbitai
Copy link

coderabbitai bot commented Jan 9, 2026

📝 Walkthrough

Walkthrough

添加了模型价格源管理功能,包括数据库迁移以区分价格来源、手动价格覆盖、litellm与手动价格间的冲突检测与解决、以及对应的UI组件和国际化支持。

Changes

项目/文件 变更说明
数据库架构与迁移
drizzle/meta/_journal.json, drizzle/0052_model_price_source.sql, drizzle/meta/0052_snapshot.json, src/drizzle/schema.ts
新增 source 字段(varchar,默认'litellm')到 model_prices 表,支持区分 'litellm' 和 'manual' 来源;新增源字段的B树索引;完整数据库快照更新
类型与接口定义
src/types/model-price.ts
添加 ModelPriceSource 类型,ModelPrice 接口增加 source 字段,PriceUpdateResult 增加可选的 skippedConflicts 数组,新增 SyncConflictSyncConflictCheckResult 接口
数据访问层
src/repository/model-price.ts
扩展 PaginationParams 支持按源筛选,所有查询函数(findLatestPriceByModel, findAllLatestPrices, findAllLatestPricesPaginated)返回结果中新增 source 字段;createModelPrice 新增可选 source 参数;新增公开API:upsertModelPrice, deleteModelPriceByName, findAllManualPrices
业务逻辑与操作
src/actions/model-prices.ts, src/lib/price-sync.ts
新增 checkLiteLLMSyncConflicts 检测冲突,upsertSingleModelPricedeleteSingleModelPrice 进行单个模型价格管理,processPriceTableInternaluploadPriceTable 支持 overwriteManual 参数进行选择性覆盖;syncLiteLLMPrices 支持手动覆盖列表;修改litellm价格源URL;新增冲突处理和跳过统计
UI组件 - 价格设置
src/app/[locale]/settings/prices/_components/delete-model-dialog.tsx, model-price-dialog.tsx, sync-conflict-dialog.tsx, price-list.tsx, sync-litellm-button.tsx, upload-price-dialog.tsx, page.tsx
新增删除模型对话框、创建/编辑模型价格对话框、同步冲突解决对话框;价格列表扩展操作列;同步按钮集成冲突检测和预检查流程;页面增加创建模型对话框入口
UI组件 - 用户管理
src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx, forms/user-edit-section.tsx, hooks/use-user-translations.ts
提供者组字段现默认显示;引入新的 ProviderGroupSelect 组件替代徽章渲染;扩展翻译类型支持提供者组后缀、标签输入错误和加载失败消息
国际化翻译
messages/*/settings.json (en/ja/ru/zh-CN/zh-TW), messages/*/dashboard.json (en/ja/ru/zh-CN/zh-TW)
在settings.json中新增价格同步状态(checking)、跳过计数、冲突处理(完整UI文本)、模型管理CRUD操作、表单和toast消息;在dashboard.json中新增提供者组选择相关翻译
测试
tests/unit/actions/model-prices.test.ts
新增448行单元测试,覆盖 upsertSingleModelPrice, deleteSingleModelPrice, checkLiteLLMSyncConflicts, processPriceTableInternal 的权限控制、验证、冲突检测和错误处理场景
工具函数
src/repository/_shared/transformers.ts
toModelPrice 函数返回对象新增 source 字段(默认'litellm')

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~40 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed PR 标题准确概括了主要功能:添加手动模型价格管理,与 changeset 的核心目标一致。
Description check ✅ Passed PR 描述详细阐述了手动价格管理、冲突处理、UI 组件和国际化等改动,与 changeset 完全相关,包含 Greptile 审查和单元测试信息。
Linked Issues check ✅ Passed PR 的所有核心功能均满足 Issue #405 的需求:支持自定义模型价格 CRUD [model-prices.ts、model-price-dialog.tsx、delete-model-dialog.tsx]、防止同步覆盖 [processPriceTableInternal]、冲突检测与解决 UI [sync-conflict-dialog.tsx、sync-litellm-button.tsx]。
Out of Scope Changes check ✅ Passed 所有改动均在 Issue #405 范围内:数据库源字段、CRUD 操作、冲突处理、新增 UI 组件、5 语言国际化、20 单元测试,以及相关 dashboard 翻译补充。
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @NieiR, 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!

此拉取请求引入了一套全面的手动模型价格管理系统,允许管理员为自定义模型设置精确的计费价格。它还通过智能冲突检测和用户友好的解决界面,增强了 LiteLLM 价格同步的健壮性,确保手动配置的灵活性与自动同步的便利性并存。这些改进显著提升了系统在处理多样化模型价格方面的能力和用户体验。

Highlights

  • 手动模型价格管理: 新增了手动添加、编辑和删除模型价格的功能,支持聊天、图像生成和补全等多种模式,解决了自定义模型(如 gpt-5.2-codex)无法正确计费的问题。此功能仅限管理员操作。
  • LiteLLM 同步冲突处理: 引入了 LiteLLM 价格同步时的冲突检测机制。当 LiteLLM 价格与手动设置的价格冲突时,系统会提示管理员选择覆盖或保留手动价格,避免手动配置被意外覆盖。
  • 数据库架构更新: 在 model_prices 表中新增了 source 字段(litellm | manual),用于区分价格来源,并添加了相应的索引以优化查询性能。
  • 新增 UI 组件: 开发了 ModelPriceDialog 用于创建/编辑模型价格,DeleteModelDialog 用于删除确认,以及 SyncConflictDialog 用于展示和解决同步冲突。
  • 国际化支持: 为新增功能添加了 5 种语言(简体中文、繁体中文、英语、日语、俄语)的国际化支持。
  • 测试覆盖: 新增了 20 个单元测试,覆盖了所有核心功能,确保了新功能的稳定性和正确性。
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added enhancement New feature or request area:UI area:i18n labels Jan 9, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant enhancements to the model pricing system, primarily by adding a 'source' column to the model_prices table to distinguish between LiteLLM-synced and manually added prices. It includes new database migrations and updates to the Drizzle ORM snapshot to support this change. The UI has been updated across multiple languages to support manual model price management, including forms for adding, editing, and deleting individual model prices, and a new dialog for handling conflicts during LiteLLM price synchronization. The backend logic (src/actions/model-prices.ts) now incorporates conflict detection (checkLiteLLMSyncConflicts) and resolution mechanisms, allowing administrators to selectively overwrite manual prices with LiteLLM data or skip them during synchronization. New actions upsertSingleModelPrice and deleteSingleModelPrice are added for managing individual manual prices, and the price fetching logic (src/repository/model-price.ts) is updated to filter by source. The LiteLLM CDN URL was also updated.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, no comments

Edit Code Review Agent Settings | Greptile

@github-actions github-actions bot added the size/XL Extra Large PR (> 1000 lines) label Jan 9, 2026
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR adds manual model price CRUD functionality, which is a substantial feature addition that addresses a real business need (custom model pricing for models not in LiteLLM). The implementation is well-structured with proper separation of concerns between actions, repository, and UI components.

PR Size: XL

  • Lines changed: 4,392 (4354 additions, 38 deletions)
  • Files changed: 22

Split suggestions for this XL PR: While the PR is large, the changes are cohesive around a single feature. If splitting is desired, consider:

  1. Database migration + schema changes (2 files)
  2. Backend actions + repository (3 files)
  3. UI components (6 files)
  4. i18n + tests (7 files)

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 1 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

High Priority Issues (Should Fix)

  1. [TYPE-MISSING-VALIDATION] src/actions/model-prices.ts:416 - Price validation insufficient

    The upsertSingleModelPrice function accepts price values (inputCostPerToken, outputCostPerToken, outputCostPerImage) without validating they are non-negative finite numbers. While the form UI uses type="number" min="0", malicious actors could submit invalid data directly to the server action.

    Suggested fix:

    // Add validation after the modelName check (around line 416)
    if (input.inputCostPerToken !== undefined && 
        (input.inputCostPerToken < 0 || !Number.isFinite(input.inputCostPerToken))) {
      return { ok: false, error: "Input price must be a non-negative number" };
    }
    if (input.outputCostPerToken !== undefined && 
        (input.outputCostPerToken < 0 || !Number.isFinite(input.outputCostPerToken))) {
      return { ok: false, error: "Output price must be a non-negative number" };
    }
    if (input.outputCostPerImage !== undefined && 
        (input.outputCostPerImage < 0 || !Number.isFinite(input.outputCostPerImage))) {
      return { ok: false, error: "Image price must be a non-negative number" };
    }

Validated Items (No Issues Found)

After thorough validation, the following potential concerns were investigated and found to be properly handled:

  • Error handling in catch blocks: All catch blocks properly log errors and return user-friendly messages
  • Silent failures: No swallowed errors found - all paths return meaningful feedback
  • SQL Injection: Uses parameterized queries via Drizzle ORM's sql template literals
  • Authorization: All CRUD operations check for admin role via getSession()
  • Type safety: ModelPriceSource is properly typed as union 'litellm' | 'manual'
  • Test coverage: 20 unit tests covering core functionality including edge cases

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean (authorization checks in place)
  • Error handling - Clean (all errors logged and surfaced)
  • Type safety - One issue noted above
  • Documentation accuracy - Clean
  • Test coverage - Adequate (20 tests, covers core paths)
  • Code clarity - Good

Automated review by Claude AI

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx (1)

45-74: 将硬编码的失败模型信息国际化,并使用 toast 的 description 字段替代字符串拼接。

第 51 行的 失败模型: 是用户可见字符串,需要添加到 i18n 消息中。当前 sonner v2 支持 description 字段,建议使用结构化方式而非 \n 拼接:

  1. messages/{locale}/settings.jsonprices.sync 中添加新的 i18n 键(如 failedModels
  2. 使用 toast 的 description 参数传递失败模型列表
-        toast.error(
-          t("prices.sync.partialFailure", { failed: failed.length }) +
-            (failed.length <= 5 ? `\n失败模型: ${failed.join(", ")}` : ""),
-          {
-            duration: 5000,
-          }
-        );
+        toast.error(t("prices.sync.partialFailure", { failed: failed.length }), {
+          duration: 5000,
+          description: failed.length <= 5 ? t("prices.sync.failedModels", { models: failed.join(", ") }) : undefined,
+        });
src/repository/model-price.ts (2)

6-17: PaginationParams.source 增加得很好,但建议明确 page/pageSize 的边界约束。

当前 repository 层未防御 page <= 0 / pageSize <= 0(会导致负 OFFSET 或 totalPages 异常)。建议在 findAllLatestPricesPaginated 入口做参数归一化或直接 throw。


99-195: source 过滤存在“时间戳相同”时返回错误来源记录的风险。

你把 ${whereCondition} 只放在 latest_prices CTE(Line 123-130 / 150-157)。当 source 过滤为 manual 时,latest_prices 会计算出 manual 的 max_created_at,但 latest_recordsJOIN model_prices mp(Line 135-139 / 167-171)并未约束 mp.source,若同一 created_at 同时存在 litellm/manual 记录,会被 ROW_NUMBER() ... ORDER BY mp.id DESC 选出“id 更大”的那条,可能不是 manual。

建议:当 source 入参存在时,在 latest_records 里额外约束 mp.source = ${source}(或把 source 条件同时作用于 mp)。

src/actions/model-prices.ts (4)

33-36: isPriceDataEqualJSON.stringify 深比较不可靠,可能导致误判“价格变化”。

对象 key 顺序、undefined/缺失字段、JSONB 序列化差异都可能造成不必要的写入/更新。建议用稳定的 deepEqual(或对 key 排序后再比较)。


44-151: 同步/上传流程的“跳过手动价格 + 覆盖列表”逻辑方向正确,但存在两处高风险:硬删范围过大 + i18n。

  1. 覆盖时调用 deleteModelPriceByName(modelName)(Line 127-129)会硬删该模型所有历史(含 litellm)。建议只删除 source='manual' 的记录(或提供 repo 方法按 source 删除)。
  2. 多处直接返回中文错误文案(例如 Line 54/59/164 等)属于“用户可见字符串硬编码”,与 TS/JS 必须走 i18n 的规范冲突(应返回错误码/key,由 UI 用 next-intl 渲染)。
    此外:skippedConflicts 同时也把模型计入 unchanged(Line 111-113),容易让统计口径混淆,建议仅计入 skippedConflicts(或 UI 明确分组展示)。

157-169: uploadPriceTable 权限校验 OK,但返回错误仍需 i18n 化。

建议把 { ok: false, error: "无权限执行此操作" } 改为可国际化的错误 key(或统一的错误码)。


343-381: 移除 emoji 字符,添加 i18n 国际化支持,简化权限检查逻辑。

  • 日志字符串中的 emoji(🔄、❌)违反编码规范,需移除。建议用纯文本前缀如 [PriceSync] 替代。
  • 硬编码的中文字符串("无权限执行此操作"、"无法从 CDN 或缓存获取价格表,请检查网络连接或稍后重试"、"同步失败,请稍后重试")未使用 i18n,需统一通过 next-intl 国际化处理。
  • syncLiteLLMPrices 与 uploadPriceTable 存在重复权限检查。注释声称"避免重复检查"但未实现;建议直接调用 processPriceTableInternal 避免冗余。
🤖 Fix all issues with AI agents
In @src/actions/model-prices.ts:
- Around line 386-464: Both upsertSingleModelPrice and deleteSingleModelPrice
use hardcoded Chinese messages, lack price-field validation, and call
revalidatePath without locale; fix by loading server translations like const
tError = await getTranslations("errors") and replace all literal error strings
(e.g., "无权限执行此操作", "模型名称不能为空", "操作失败,请稍后重试", "删除失败,请稍后重试") with tError keys; in
upsertSingleModelPrice validate numeric price fields (input.inputCostPerToken,
input.outputCostPerToken, input.outputCostPerImage) are non-negative and require
at least one of them present before building priceData; on both
upsertSingleModelPrice and deleteSingleModelPrice call getLocale() (or
equivalent) and pass the full localized path to revalidatePath (e.g.,
`/${locale}/settings/prices`) so the correct localized cache is refreshed.

In @src/app/[locale]/settings/prices/_components/delete-model-dialog.tsx:
- Around line 41-55: The code currently displays result.error directly from
deleteSingleModelPrice which can be localized text; update the action to return
a stable error code/enum (e.g., DeleteModelPriceError or code string) and change
the UI here to map that code to a localized message via t(...) before calling
toast.error; specifically, modify deleteSingleModelPrice to return { ok:
boolean, code?: string } (or enum) and replace toast.error(result.error) with
toast.error(t(`errors.deleteModelPrice.${result.code}`)) while keeping the
existing fallback toast for unknown codes and preserving setOpen/onSuccess
logic.
- Around line 21-25: The interface DeleteModelDialogProps uses React.ReactNode
but the React namespace isn’t imported; fix by importing the ReactNode type from
"react" and update the interface to use ReactNode instead of React.ReactNode
(e.g., add `import { useState, ReactNode } from "react"` and change `trigger?:
React.ReactNode;` to `trigger?: ReactNode;`), leaving onSuccess and modelName
unchanged.

In @src/app/[locale]/settings/prices/_components/model-price-dialog.tsx:
- Around line 205-257: The UI currently contains hardcoded visible strings in
the model price dialog (the left "$" span, placeholder "0.00", and the
right-unit spans "/M" and "/img" used alongside the Input components bound to
inputPrice and outputPrice and the modelMode switch). Replace those literals
with i18n lookups (use t(...) keys for placeholder, currency symbol and unit
labels) and read the currency symbol from the global/system currency config
instead of hardcoding "$"; update the relevant translation keys in the locales
files for placeholder, currency symbol and units (both per-minute and per-image)
so the Input components and the unit span texts use translated values and the UI
respects configured currency.
- Around line 29-34: The interface ModelPriceDialogProps references
React.ReactNode which fails because the React namespace isn't imported; change
the property type to ReactNode and add a type import from React (e.g., add
"import type { ReactNode } from 'react';" at the top) so ModelPriceDialogProps
uses ReactNode for the trigger prop and compiles correctly.

In @src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx:
- Around line 248-257: The Cancel handler currently calls onConfirm([]) which
conflates "close" with "confirm empty selection"; change handleCancel to instead
clear any selection state (e.g. call the component's selection setter such as
setSelected([]) or equivalent) and call onOpenChange(false) to close the dialog,
removing onConfirm([]) from this path; if you need a separate action for
"continue sync but do not overwrite", add a distinct handler (e.g.
handleContinueWithoutOverwrite) that calls onConfirm([]) and use a specific
button label for that action; apply the same change to the other cancel-like
handler referenced in the same file (the one around the later occurrence noted
in the review).
- Around line 46-57: The formatPrice function (and similar formatting blocks
around lines 99-113 and 146-180) hardcodes "$" and unit suffixes like "/M" and
"/img"; change these to use the app's i18n/currency settings instead: pull the
currency symbol and unit labels from your localization or currency config (via
the i18n hook or a passed prop/context) and format the returned string using
those values rather than literals, or refactor formatPrice to accept parameters
(currencySymbol, unitLabel) and update all call sites to supply the configured
values so currency and unit text are fully localizable/configurable.
- Around line 216-233: The header checkbox indeterminate logic is wrong: change
the allCurrentPageSelected calculation to guard empty pages (e.g.,
paginatedConflicts.length > 0 && paginatedConflicts.every(...)), remove the
invalid ref hack that sets indeterminate on an HTMLButtonElement, and update
handleSelectAll to accept the full CheckedState type (boolean | "indeterminate")
from Radix/shadcn so the header checkbox can be controlled with checked={boolean
| "indeterminate"}; keep using paginatedConflicts to add/delete modelName into
the Set (selectedModels) and call setSelectedModels(newSelected) as before, but
compute someCurrentPageSelected from the new guarded allCurrentPageSelected
value and existing some logic.

In @src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx:
- Around line 118-125: The conflict dialog's "cancel" currently invokes
onConfirm([]) which calls handleConflictConfirm -> doSync([]) and
unintentionally continues sync without overwrites; change the dialog to expose a
separate onCancel handler and wire it to simply setConflictDialogOpen(false)
(leaving onConfirm used only for explicit confirms with selectedModels), or if
the product intent is to proceed without overwrites, update the cancel button
text (i18n) to a clear label like "skip overwrites and continue" and keep
current behavior; update usages around SyncConflictDialog,
handleConflictConfirm, doSync, onConfirm and add/handle onCancel accordingly
(also apply same fix at the other occurrence noted).

In @src/repository/model-price.ts:
- Around line 256-303: The query in findAllManualPrices uses latest_prices CTE
filtered by source='manual' but latest_records does not constrain mp.source, so
mp can join non-manual rows with the same created_at; update the latest_records
CTE (or the JOIN condition between latest_prices and mp) to include mp.source =
'manual' (or add WHERE mp.source = 'manual') so only manual records are
considered when computing rn and when selecting the latest record.
- Around line 238-247: The upsertModelPrice implementation is non-atomic and
currently deletes all records for a model (including litellm history); wrap the
operation in a db.transaction to ensure atomicity and either (preferred) stop
deleting altogether and just insert a new manual record via
createModelPrice(modelName, priceData, "manual") to preserve history, or if
deletion is required restrict the delete to only manual records by using
db.delete(modelPrices).where(eq(modelPrices.modelName, modelName),
eq(modelPrices.source, "manual")) inside the transaction so you don’t remove
litellm/audit entries.

In @tests/unit/actions/model-prices.test.ts:
- Around line 60-79: makeMockPrice uses Math.random() to generate id which
causes nondeterministic test data; replace the random id with a deterministic
value (e.g., a module-level incremental counter or a fixed constant) so tests
are stable. Update the makeMockPrice function to remove Math.random() and assign
id from a stable source (refer to the id property inside makeMockPrice);
initialize the counter at module scope and increment it each call or return a
constant if uniqueness across calls isn't required.
🧹 Nitpick comments (8)
src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx (1)

121-121: 建议使用更 React 化的组件通信方案

当前使用自定义 DOM 事件(window.dispatchEvent)进行跨组件通信,虽然可以工作,但存在以下问题:

  • 类型不安全:事件名称为字符串字面量,无法在编译时检查
  • 松散耦合:难以追踪哪些组件监听了此事件
  • 不符合 React 数据流:绕过了 React 的状态管理机制
推荐的替代方案

方案 1(推荐):使用 React Query/SWR 的缓存失效

import { useSWRConfig } from 'swr'

const { mutate } = useSWRConfig()

// 更新成功后
toast.success(t("dialog.updateSuccess", { count: totalUpdates }))
mutate('/api/model-prices') // 失效相关缓存,自动触发重新获取

方案 2:使用轻量状态管理(Zustand)

// stores/price-store.ts
import { create } from 'zustand'

interface PriceStore {
  lastUpdate: number
  triggerRefresh: () => void
}

export const usePriceStore = create<PriceStore>((set) => ({
  lastUpdate: Date.now(),
  triggerRefresh: () => set({ lastUpdate: Date.now() })
}))

// 使用
import { usePriceStore } from '@/stores/price-store'

const triggerRefresh = usePriceStore((state) => state.triggerRefresh)
triggerRefresh()

方案 3:通过回调通知父组件

interface UploadPriceDialogProps {
  onPriceUpdated?: () => void
}

// 更新成功后
props.onPriceUpdated?.()

如果项目中已有类似的事件驱动模式约定,当前实现可以暂时保留,但建议在后续迭代中统一重构为更类型安全的方案。

src/app/[locale]/settings/prices/_components/price-list.tsx (1)

304-304: 建议使用动态 locale 而非硬编码。

Line 304 硬编码了 "zh-CN" locale,这与项目的国际化要求不符。应该使用 Next.js 的 locale 上下文。

♻️ 建议的修复方案

next-intl 导入 useLocale hook:

-import { useTranslations } from "next-intl";
+import { useLocale, useTranslations } from "next-intl";

在组件中使用动态 locale:

 export function PriceList({
   initialPrices,
   initialTotal,
   initialPage,
   initialPageSize,
 }: PriceListProps) {
   const t = useTranslations("settings.prices");
+  const locale = useLocale();
   // ... rest of component

更新日期格式化:

                   <TableCell className="text-sm text-muted-foreground">
-                    {new Date(price.createdAt).toLocaleDateString("zh-CN")}
+                    {new Date(price.createdAt).toLocaleDateString(locale)}
                   </TableCell>
src/app/[locale]/settings/prices/_components/model-price-dialog.tsx (1)

101-119: 建议对 parseFloat(...) 的 NaN 做兜底,避免提交 NaN
type="number" 不能完全保证拿到的字符串可解析(例如清空/浏览器差异)。建议 Number.isFinite(x) 校验,不通过则置 undefined 并提示。

drizzle/meta/0050_snapshot.json (1)

777-888: model_prices.source 缺少 DB 级约束(快照里 checkConstraints 为空),建议补齐以防脏数据。
既然 source 只有 litellm|manual 两种取值,建议在数据库层加 CHECK 或改为 enum;否则未来写入任意字符串会让筛选/冲突逻辑变得不可靠。

src/repository/model-price.ts (2)

33-50: findLatestPriceByModel 仅按 createdAt 排序可能在时间戳相同场景下不稳定。

如果同一模型短时间内插入多条记录、created_at 相同(或精度不足),建议增加 id 作为 tie-break:ORDER BY created_at DESC, id DESC


249-254: deleteModelPriceByName 是硬删除:建议确认是否符合审计/回滚需求。

如果系统需要保留价格变更历史,可能更适合 soft-delete 或仅删除 manual 来源。

src/actions/model-prices.ts (1)

276-336: checkLiteLLMSyncConflicts 的冲突计算逻辑清晰,建议补充元数据字段过滤一致性。

processPriceTableInternal 会跳过 sample_spec(Line 63-73),但冲突检查直接对 priceTable[modelName] 判断(Line 312-319)。建议与同步处理保持一致:同样过滤掉非模型字段,避免误报/漏报。

messages/zh-CN/settings.json (1)

1239-1299: 冲突处理相关中文文案清晰,建议统一“统计口径”与 UI 展示。

skippedConflicts 的定义是“跳过手动模型”,UI 若同时展示 unchanged/skipped 计数,建议明确两者是否互斥,避免用户误解同步结果。

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 0d32316 and f1387d04b6953e8af30e00bd21dfbc56eb48d20d.

📒 Files selected for processing (22)
  • drizzle/0050_model_price_source.sql
  • drizzle/meta/0050_snapshot.json
  • drizzle/meta/_journal.json
  • messages/en/settings.json
  • messages/ja/settings.json
  • messages/ru/settings.json
  • messages/zh-CN/settings.json
  • messages/zh-TW/settings.json
  • src/actions/model-prices.ts
  • src/app/[locale]/settings/prices/_components/delete-model-dialog.tsx
  • src/app/[locale]/settings/prices/_components/model-price-dialog.tsx
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx
  • src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx
  • src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx
  • src/app/[locale]/settings/prices/page.tsx
  • src/drizzle/schema.ts
  • src/lib/price-sync.ts
  • src/repository/_shared/transformers.ts
  • src/repository/model-price.ts
  • src/types/model-price.ts
  • tests/unit/actions/model-prices.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/drizzle/schema.ts
  • src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx
  • src/repository/_shared/transformers.ts
  • src/types/model-price.ts
  • src/app/[locale]/settings/prices/page.tsx
  • src/lib/price-sync.ts
  • src/app/[locale]/settings/prices/_components/delete-model-dialog.tsx
  • src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx
  • src/app/[locale]/settings/prices/_components/model-price-dialog.tsx
  • src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx
  • tests/unit/actions/model-prices.test.ts
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • src/actions/model-prices.ts
  • src/repository/model-price.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/drizzle/schema.ts
  • src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx
  • src/repository/_shared/transformers.ts
  • src/types/model-price.ts
  • src/app/[locale]/settings/prices/page.tsx
  • src/lib/price-sync.ts
  • src/app/[locale]/settings/prices/_components/delete-model-dialog.tsx
  • src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx
  • src/app/[locale]/settings/prices/_components/model-price-dialog.tsx
  • src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx
  • tests/unit/actions/model-prices.test.ts
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • src/actions/model-prices.ts
  • src/repository/model-price.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.tsx
  • src/app/[locale]/settings/prices/page.tsx
  • src/app/[locale]/settings/prices/_components/delete-model-dialog.tsx
  • src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx
  • src/app/[locale]/settings/prices/_components/model-price-dialog.tsx
  • src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx
  • src/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/0050_model_price_source.sql
**/*.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/actions/model-prices.test.ts
🧠 Learnings (3)
📚 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/drizzle/schema.ts
  • src/repository/_shared/transformers.ts
  • src/types/model-price.ts
  • src/lib/price-sync.ts
  • src/actions/model-prices.ts
  • src/repository/model-price.ts
📚 Learning: 2026-01-05T03:02:06.594Z
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:06.594Z
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/ru/settings.json
  • messages/zh-CN/settings.json
📚 Learning: 2026-01-07T17:05:36.362Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T17:05:36.362Z
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
🧬 Code graph analysis (9)
src/app/[locale]/settings/prices/page.tsx (1)
src/app/[locale]/settings/prices/_components/model-price-dialog.tsx (1)
  • ModelPriceDialog (41-272)
src/app/[locale]/settings/prices/_components/delete-model-dialog.tsx (2)
src/actions/model-prices.ts (1)
  • deleteSingleModelPrice (439-464)
src/components/ui/alert-dialog.tsx (9)
  • AlertDialog (123-123)
  • AlertDialogTrigger (126-126)
  • AlertDialogContent (127-127)
  • AlertDialogHeader (128-128)
  • AlertDialogTitle (130-130)
  • AlertDialogDescription (131-131)
  • AlertDialogFooter (129-129)
  • AlertDialogCancel (133-133)
  • AlertDialogAction (132-132)
src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx (3)
src/types/model-price.ts (1)
  • SyncConflict (92-96)
src/actions/model-prices.ts (2)
  • syncLiteLLMPrices (343-381)
  • checkLiteLLMSyncConflicts (280-336)
src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx (1)
  • SyncConflictDialog (188-400)
src/app/[locale]/settings/prices/_components/model-price-dialog.tsx (2)
src/types/model-price.ts (1)
  • ModelPrice (61-68)
src/actions/model-prices.ts (1)
  • upsertSingleModelPrice (398-434)
src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx (1)
src/types/model-price.ts (2)
  • SyncConflict (92-96)
  • ModelPriceData (4-51)
tests/unit/actions/model-prices.test.ts (1)
src/types/model-price.ts (2)
  • ModelPriceData (4-51)
  • ModelPrice (61-68)
src/app/[locale]/settings/prices/_components/price-list.tsx (2)
src/app/[locale]/settings/prices/_components/model-price-dialog.tsx (1)
  • ModelPriceDialog (41-272)
src/app/[locale]/settings/prices/_components/delete-model-dialog.tsx (1)
  • DeleteModelDialog (30-92)
src/actions/model-prices.ts (4)
src/repository/model-price.ts (5)
  • findAllManualPrices (260-303)
  • findLatestPriceByModel (33-50)
  • createModelPrice (210-232)
  • deleteModelPriceByName (252-254)
  • upsertModelPrice (238-247)
src/types/model-price.ts (5)
  • SyncConflictCheckResult (101-104)
  • PriceTableJson (73-75)
  • SyncConflict (92-96)
  • ModelPriceData (4-51)
  • ModelPrice (61-68)
src/lib/auth.ts (1)
  • getSession (116-128)
src/lib/price-sync.ts (1)
  • getPriceTableJson (115-128)
src/repository/model-price.ts (3)
src/types/model-price.ts (3)
  • ModelPriceSource (56-56)
  • ModelPriceData (4-51)
  • ModelPrice (61-68)
src/drizzle/schema.ts (1)
  • modelPrices (364-380)
src/repository/_shared/transformers.ts (1)
  • toModelPrice (141-148)
⏰ 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). (4)
  • GitHub Check: pr-review
  • GitHub Check: pr-description
  • GitHub Check: 🌐 API Tests
  • GitHub Check: Docker Build Test
🔇 Additional comments (20)
messages/zh-TW/settings.json (5)

530-530: LGTM!同步狀態翻譯正確

新增的同步相關翻譯文案清晰準確,佔位符使用正確,術語一致。

Also applies to: 537-538


540-570: LGTM!衝突解決介面翻譯完整

衝突解決對話框的翻譯結構完整,涵蓋了標題、描述、表格欄位、差異對比、分頁和批量操作等所有 UI 元素。術語使用一致("手動價格" vs "LiteLLM 價格"),佔位符格式正確。


578-578: LGTM!表格操作欄翻譯正確

"操作" 是標準的繁體中文術語,與檔案中其他操作欄翻譯保持一致。


629-634: LGTM!模型操作標籤翻譯準確

新增、編輯、刪除的操作標籤及描述文案翻譯清晰,刪除確認訊息包含適當的警告提示,佔位符使用正確。


635-656: LGTM!表單欄位和提示訊息翻譯完整

表單欄位標籤翻譯準確,佔位符提供了實用的範例(如 gpt-5.2-codex,對應 issue #405 中的自訂模型)。成功和失敗的 toast 訊息簡潔明瞭,符合繁體中文的表達習慣。

messages/en/settings.json (1)

539-665: 国际化字符串结构清晰,符合编码规范

新增的价格管理相关翻译完整覆盖了以下功能:

  • 冲突检测与解决(prices.conflict.*
  • 模型 CRUD 操作(prices.addModel, prices.editModel, prices.deleteModel
  • 表格列和操作按钮(prices.table.actions, prices.actions.*
  • 表单验证和提示信息

所有用户可见字符串均已正确国际化,未使用硬编码文本或 emoji 字符,符合编码指南要求。

src/repository/_shared/transformers.ts (1)

144-144: 向后兼容处理正确

source 字段的默认值处理正确:

  • 使用 ?? 运算符提供 "litellm" 作为默认值
  • 与数据库 schema 中的 DEFAULT 'litellm' 约束保持一致
  • 确保了对现有数据(未迁移前的记录)的向后兼容性

根据 learnings,这种明确处理 null/undefined 的模式符合项目的类型安全实践。

src/lib/price-sync.ts (1)

15-16: 移除代码中的表情符号并实现国际化处理

该文件包含多处编码规范违规:

  1. 表情符号违规: 代码包含 ❌、📦、ℹ️、💾、⚠️ 等表情符号(第 51、65、67、85、89、91、105、107、126 行),违反规范 **/*.{js,ts,tsx,jsx}: 代码、注释或字符串中不允许表情符号。

  2. 硬编码字符串违规: logger 中的所有用户可见字符串均为硬编码的中文,未使用 i18n 国际化。需要实现多语言支持(zh-CN, en, ja, ko, de),规范要求:**/*.{ts,tsx,jsx,js}: 所有用户交互的字符串必须使用 i18n(使用 next-intl)。

    • "Successfully fetched LiteLLM prices from CDN"
    • "Using cached LiteLLM prices"
    • "No cached prices found"
    • "Saved prices to cache"
    • "CDN fetch failed, trying cache..."

建议将这些字符串迁移至 i18n 配置文件,并使用 getTranslationsuseTranslations 获取本地化文本。

⛔ Skipped due to learnings
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T17:05:36.362Z
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
src/app/[locale]/settings/prices/page.tsx (1)

6-6: LGTM! 组件集成正确。

ModelPriceDialog 的导入和使用遵循了现有模式,与其他操作按钮(SyncLiteLLMButton、UploadPriceDialog)保持一致。

Also applies to: 77-77

src/drizzle/schema.ts (1)

368-369: LGTM! Schema 变更设计合理。

新增的 source 字段和索引设计良好:

  • 使用默认值 'litellm' 确保向后兼容性,不会破坏现有数据
  • 类型约束正确限制为 'litellm' | 'manual'
  • 索引命名遵循现有约定,将优化按来源过滤的查询性能

Also applies to: 378-379

drizzle/0050_model_price_source.sql (1)

1-2: LGTM! 迁移文件正确。

SQL 迁移与 schema.ts 的变更保持一致。根据编码指南,请确认此文件是通过 bun run db:generate 自动生成的,而非手动创建。

src/types/model-price.ts (1)

53-104: LGTM! 类型定义清晰且完整。

新增的类型定义设计合理:

  • ModelPriceSource 与数据库 schema 保持一致
  • skippedConflicts 使用可选字段,适配不同场景
  • 冲突相关的接口(SyncConflictSyncConflictCheckResult)结构清晰,便于冲突检测和处理
src/app/[locale]/settings/prices/_components/price-list.tsx (3)

3-12: LGTM! 导入声明正确。

新增的图标和组件导入符合项目规范,所有依赖都已正确声明。

Also applies to: 17-22, 41-42


74-83: LGTM! 事件监听器实现正确。

全局事件监听器的添加和清理符合 React 最佳实践:

  • 正确使用 useEffect 钩子
  • 在清理函数中移除监听器,避免内存泄漏
  • eslint-disable 注释合理(依赖项由 fetchPrices 间接使用)

260-260: LGTM! 表格操作列集成正确。

操作列的添加实现良好:

  • 表头新增 actions 列
  • 下拉菜单集成 Edit 和 Delete 操作
  • Loading 和空状态的 colspan 正确更新为 7
  • 操作成功后正确触发数据刷新

Also applies to: 266-266, 306-340, 345-345

messages/ru/settings.json (1)

519-656: 新增价格/冲突/CRUD 文案覆盖看起来完整,但请核对“支持语言列表”与各 locale 的同步情况。
当前只看到 ru 增量;而规范里要求的语言集合与 PR 描述的语言集合不一致,容易导致部分语言缺 key 而运行时报错。建议确认并补齐所有目标语言的同名 key。

src/repository/model-price.ts (1)

210-232: createModelPrice 默认 source=litellm OK,但建议避免冗余字段赋值。

可选:.values({ modelName, priceData, source }) 简化可读性。

messages/zh-CN/settings.json (1)

1311-1368: CRUD 文案在现有语言文件中已齐全且一致,但缺少 ko (韩文) 和 de (德文) 的完整翻译文件。

验证显示 en、ja、ru、zh-CN、zh-TW 中均包含相同的 CRUD 相关 key(addModel、editModel、deleteModel、outputPriceImage 等),不存在某个 locale 缺失的情况。但根据已有的语言支持计划,应补齐 ko 和 de 的完整翻译。

messages/ja/settings.json (2)

628-656: 所有现有 locale 的 CRUD 翻译 key 已完全对齐,无需调整。

已验证 messages 目录中的 5 个 locale 文件(en、ja、ru、zh-CN、zh-TW)均包含完整的 addModel、editModel、deleteModel 及其 form/toast 子结构,key 命名与层级一致,不存在缺失导致运行时回退的情况。


527-570: 关键路径已确认正确,所有支持的语言文件中均有完整且一致的定义。

已验证:

  • prices.sync.*prices.conflict.*prices.table.* 的路径与 UI 代码使用完全一致
  • 5 个语言文件(en、ja、zh-CN、zh-TW、ru)中的 prices 结构完全相同,无缺失
  • 不存在 prices.dialog.* 的路径问题

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/actions/model-prices.ts (4)

43-59: 错误消息必须使用 i18n

根据编码规范,所有用户可见的字符串必须使用 i18n(支持 zh-CN、en、ja、ko、de 五种语言)。此处的错误消息通过 ActionResult 返回给 UI,属于用户可见字符串。

受影响的消息:

  • Line 53: "JSON格式不正确,请检查文件内容"
  • Line 58: "价格表必须是一个JSON对象"
推荐使用 next-intl 的示例
+"use server";
+
+import { getTranslations } from 'next-intl/server';
+
 export async function processPriceTableInternal(
   jsonContent: string,
   overwriteManual?: string[]
 ): Promise<ActionResult<PriceUpdateResult>> {
+  const t = await getTranslations('modelPrices.errors');
   try {
     let priceTable: PriceTableJson;
     try {
       priceTable = JSON.parse(jsonContent);
     } catch {
-      return { ok: false, error: "JSON格式不正确,请检查文件内容" };
+      return { ok: false, error: t('invalidJson') };
     }

     if (typeof priceTable !== "object" || priceTable === null) {
-      return { ok: false, error: "价格表必须是一个JSON对象" };
+      return { ok: false, error: t('mustBeObject') };
     }

需要在对应的语言文件中添加翻译键值。

基于编码规范要求。


156-168: uploadPriceTable 签名正确更新

函数作为公共入口,正确执行权限检查后委托给内部处理函数。overwriteManual 参数传递清晰。

但 Line 163 的错误消息"无权限执行此操作"需要使用 i18n。此问题在整个文件中重复出现。

基于编码规范要求,建议创建统一的权限错误消息键(如 t('errors.unauthorized'))在所有权限检查处复用。


342-380: syncLiteLLMPrices 正确集成 overwriteManual 参数

函数流程清晰:

  1. 权限检查
  2. 从 CDN/缓存获取价格表
  3. 调用 uploadPriceTable 并传递 overwriteManual

参数传递正确,支持冲突解决流程。

错误消息需要使用 i18n:

  • Line 349: "无权限执行此操作"
  • Line 361: "无法从 CDN 或缓存获取价格表,请检查网络连接或稍后重试"
  • Line 377: "同步失败,请稍后重试"

基于编码规范要求。


43-483: 将所有硬编码的错误消息转换为使用 i18n

本文件中所有错误返回均为硬编码中文字符串(共 31 处),违反编码规范要求。应使用 getTranslations()"next-intl/server" 导入翻译函数,参考 src/actions/users.ts 中的实现模式:

  1. 在函数中获取翻译: const t = await getTranslations("errors")(通用错误)或 const t = await getTranslations("modelPrices.errors")(模型价格特定错误)
  2. 错误消息分类
    • 通用验证错误(如权限、参数):使用 errors 命名空间(如 UNAUTHORIZEDINVALID_FORMAT
    • 特定操作错误:在 modelPrices 命名空间下定义特定键
  3. 替换所有硬编码字符串:如 "无权限执行此操作"t("UNAUTHORIZED")"JSON格式不正确,请检查文件内容"t("modelPrices.errors.invalidJsonFormat")

modelPrices 命名空间已在 messages/en/settings.json 中存在,需确保对应中文翻译在 messages/zh-CN/settings.json 中完整。

🤖 Fix all issues with AI agents
In @src/actions/model-prices.ts:
- Around line 458-483: Replace the three hardcoded Chinese error strings in
deleteSingleModelPrice with i18n lookups: instead of "无权限执行此操作", "模型名称不能为空", and
"删除失败,请稍后重试" call the project's translation function (e.g., i18n.t or t) with
descriptive keys like errors.no_permission, errors.model_name_required, and
errors.delete_failed; ensure you import or access the same i18n helper used
elsewhere in the codebase and pass the translated message into the returned
error values (and keep logger.error as-is or log the raw Error for debugging).
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between f1387d04b6953e8af30e00bd21dfbc56eb48d20d and b8196bb0849a8599a6c3199522fd6b839a12b317.

📒 Files selected for processing (4)
  • src/actions/model-prices.ts
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx
  • tests/unit/actions/model-prices.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

No emoji characters in any code, comments, or string literals

Files:

  • tests/unit/actions/model-prices.test.ts
  • src/actions/model-prices.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/actions/model-prices.test.ts
  • src/actions/model-prices.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/actions/model-prices.test.ts
🧠 Learnings (2)
📚 Learning: 2026-01-07T17:05:36.362Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T17:05:36.362Z
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:

  • tests/unit/actions/model-prices.test.ts
  • src/actions/model-prices.ts
📚 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/actions/model-prices.ts
🧬 Code graph analysis (2)
tests/unit/actions/model-prices.test.ts (2)
src/types/model-price.ts (2)
  • ModelPriceData (4-51)
  • ModelPrice (61-68)
src/actions/model-prices.ts (4)
  • upsertSingleModelPrice (397-453)
  • deleteSingleModelPrice (458-483)
  • checkLiteLLMSyncConflicts (279-335)
  • processPriceTableInternal (43-150)
src/actions/model-prices.ts (4)
src/repository/model-price.ts (5)
  • findAllManualPrices (260-303)
  • findLatestPriceByModel (33-50)
  • createModelPrice (210-232)
  • deleteModelPriceByName (252-254)
  • upsertModelPrice (238-247)
src/types/model-price.ts (6)
  • PriceUpdateResult (80-87)
  • SyncConflictCheckResult (101-104)
  • PriceTableJson (73-75)
  • SyncConflict (92-96)
  • ModelPriceData (4-51)
  • ModelPrice (61-68)
src/lib/auth.ts (1)
  • getSession (116-128)
src/lib/price-sync.ts (1)
  • getPriceTableJson (115-128)
⏰ 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). (2)
  • GitHub Check: 🌐 API Tests
  • GitHub Check: Docker Build Test
🔇 Additional comments (8)
tests/unit/actions/model-prices.test.ts (3)

1-77: 测试基础设施设置良好

Mock 设置遵循 Vitest 最佳实践,依赖注入清晰。makeMockPrice 辅助函数简化了测试数据创建。


79-222: CRUD 操作测试覆盖全面

测试用例覆盖了以下关键场景:

  • 管理员权限验证
  • 输入验证(空名称)
  • 不同模式(chat、image_generation)
  • 数据库错误处理

测试结构清晰,断言准确。


224-448: 冲突检测与价格表处理测试设计优秀

测试覆盖了新增功能的核心逻辑:

  • 冲突检测算法(手动价格 vs LiteLLM 价格)
  • 覆盖策略(默认跳过 vs 显式覆盖)
  • Source 字段正确性(manual vs litellm)
  • 边缘情况(元数据字段、缺失 mode 字段)

测试场景全面,验证了功能的正确性和健壮性。

src/actions/model-prices.ts (5)

7-27: 依赖导入正确对齐新功能

新增的 repository 函数导入(deleteModelPriceByNamefindAllManualPricesupsertModelPrice)和类型导入(SyncConflictSyncConflictCheckResult)支持手动价格管理和冲突检测功能。


382-392: SingleModelPriceInput 接口定义清晰

接口设计合理,支持不同模式(chat、image_generation、completion)的可选价格字段,符合 ModelPriceData 的结构。


74-114: 冲突检测逻辑实现正确

使用 Set 进行快速查找(O(1)),逻辑清晰:

  • 默认跳过手动价格(记录到 skippedConflicts
  • 仅覆盖显式指定的模型

实现符合 PR 目标,确保手动价格不会被意外覆盖。


119-130: Source 字段处理逻辑正确

当覆盖手动价格时:

  1. 先删除旧记录(Line 127)
  2. 创建新的 litellm 来源记录(Line 129)

确保同步操作的价格始终标记为 litellm 来源,逻辑清晰。


412-430: 价格验证逻辑严谨

使用 Number.isFinite() 检查非负数并排除 NaNInfinity,防御性编程做得很好。涵盖三种价格类型:

  • inputCostPerToken
  • outputCostPerToken
  • outputCostPerImage

验证逻辑全面且正确。

@NieiR
Copy link
Contributor Author

NieiR commented Jan 9, 2026

关于 CodeRabbit 代码审查的回复

感谢详细的代码审查!

已修复的问题

  • 价格字段非负数校验 - 已在 commit b8196bb0 中添加 Number.isFinite()>= 0 检查

关于其他建议

i18n 相关问题(硬编码中文错误消息、货币符号等)
这些问题在项目其他 actions 文件中同样存在(如 users.tssystem-config.ts 等),建议作为后续统一的 i18n 重构任务处理,而非在本 PR 中引入不一致的写法。

React.ReactNode 类型
TypeScript 编译(bun run typecheck)已通过。在 JSX 环境下 React 命名空间会自动导入,但确实显式导入更规范,可以后续优化。

revalidatePath 缺少 locale
项目其他地方(如 request-filters.tssensitive-words.ts)也使用了相同的写法,实际测试中页面刷新正常工作。建议作为独立的基础设施优化任务处理。

取消按钮语义
当前设计是有意的:"取消" = "跳过冲突模型,继续同步其他模型"。如果产品需要调整,可以后续更新。

Checkbox indeterminate 状态
当前 ref hack 实际可以工作,建议的 Radix 原生方式更规范,可以后续优化。

NieiR added 2 commits January 9, 2026 15:24
- Always show providerGroup field in edit mode (was hidden when user had no providerGroup)
- Replace read-only Badge display with editable ProviderGroupSelect component
- Move modelSuggestions hook after form declaration to support dynamic updates

Regression from ding113#539
…ialog

Pass full translations object to ProviderGroupSelect including:
- tagInputErrors for validation messages (empty, duplicate, too_long, etc.)
- errors.loadFailed for API error handling
- providersSuffix for provider count display

This fixes untranslated error messages when users input invalid provider group tags.
@ding113
Copy link
Owner

ding113 commented Jan 9, 2026

LGTM.
Could you resolve these branch conflicts before merge?

@NieiR
Copy link
Contributor Author

NieiR commented Jan 9, 2026

Sure! I will rebase onto the latest dev branch and resolve the conflicts.

NieiR added 2 commits January 9, 2026 21:21
- 新增 source 字段区分 litellm/manual 来源
- 支持手动添加、编辑、删除模型价格
- LiteLLM 同步时自动跳过手动价格,避免覆盖
- 添加冲突检测和解决 UI,支持批量处理
- 完整的单元测试覆盖

closes ding113#405
- 移除未使用的 ModelPriceSource 导入
- 修复 useEffect 依赖数组 (fetchPrices)
- 修复 fetchPrices 声明前使用问题
- 添加价格非负数验证
- 格式化代码
@NieiR NieiR force-pushed the feat/manual-model-price-crud branch from b8196bb to b72ba62 Compare January 9, 2026 13:23
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @src/repository/model-price.ts:
- Around line 238-247: upsertModelPrice currently deletes then inserts without a
transaction which can leave data lost if the insert fails; wrap the delete and
the subsequent insert in a single DB transaction (use the project's transaction
API, e.g., db.transaction or similar) so both operations commit or roll back
together, and ensure the insert uses the same transaction context (modify
createModelPrice to accept an optional transaction/connection parameter or
perform the insert inside the transaction using tx.insert into modelPrices) so
the delete and create execute atomically.
🧹 Nitpick comments (10)
messages/en/settings.json (2)

547-556: 同步状态文案 OK;建议统一单位表述并留意占位符一致性

partialFailureskippedConflicts 的占位符看起来合理;建议把 Input/Output Price 的单位($/M vs $/M tokens)统一成同一种表达,减少 UI 认知差异。


595-673: 新增 CRUD 文案齐全;硬编码 $ 建议后续与“货币显示单位”配置打通

actions/toast/form 的 key 设计清晰;但多处包含 $(如 $/M tokens, $/image),如果系统支持切换货币符号,后续可考虑改为可配置/不写死符号。

messages/zh-TW/settings.json (2)

538-547: 同步文案可用;“跳過 {count} 個手動模型”建议更精确到“手動價格”

当前表达可能被理解为“模型是手动的”;建议改为“跳過 {count} 個手動價格模型 / 手動維護的模型價格”,更贴近功能语义。


548-578: 冲突/CRUD 文案结构与占位符齐全;建议统一单位写法

整体 key 与结构和 en 对齐,便于维护;建议统一 $/M vs $/M tokens 的写法,避免同页出现不同单位描述。

Also applies to: 586-664

messages/zh-CN/settings.json (2)

1250-1290: 价格同步/冲突文案 OK;建议把“手动模型”明确为“手动价格(配置)”

例如可考虑“跳过 {count} 个手动价格模型/配置”,并在冲突描述里强调“手动维护的价格”以减少误解。


1298-1376: CRUD 文案齐全;建议统一单位并评估 $ 是否应随货币配置变化

新增 actions/toast/form 覆盖完整;若系统支持货币符号切换,建议后续把 $ 抽象掉或动态渲染。

src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx (1)

48-55: Toast 消息中存在硬编码中文字符串。

Line 51 的 失败模型: 违反了 i18n 规范。根据 PR 评论中作者的说明,此类问题在项目其他 actions 文件中普遍存在,建议作为统一的 i18n 重构任务在后续处理。

建议的修复方案

messages/*/settings.json 中添加翻译键:

"prices.sync.failedModels": "失败模型: {models}"

然后修改代码:

       if (failed.length > 0) {
         toast.error(
           t("prices.sync.partialFailure", { failed: failed.length }) +
-            (failed.length <= 5 ? `\n失败模型: ${failed.join(", ")}` : ""),
+            (failed.length <= 5 ? `\n${t("prices.sync.failedModels", { models: failed.join(", ") })}` : ""),
           {
             duration: 5000,
           }
         );
       }
src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx (1)

281-290: 建议:考虑使用 Radix UI 原生的 indeterminate 支持

当前通过 ref 回调手动设置 indeterminate 属性是一个可行的变通方案,但 Radix UI 的 Checkbox 组件可能支持更规范的 API。建议后续优化时查阅最新的 Radix UI 文档,使用官方推荐的方式处理半选状态,以提高代码的可维护性和类型安全性。

参考:更符合类型安全的实现方式

如果 Radix UI 支持,可以考虑:

 <Checkbox
   checked={allCurrentPageSelected}
-  ref={(el) => {
-    if (el) {
-      (el as HTMLButtonElement & { indeterminate?: boolean }).indeterminate =
-        someCurrentPageSelected;
-    }
-  }}
+  indeterminate={someCurrentPageSelected}
   onCheckedChange={handleSelectAll}
 />

注:需要验证当前版本的 Radix UI Checkbox 是否支持 indeterminate 属性。

src/actions/model-prices.ts (2)

53-53: 用户可见的错误消息硬编码中文字符串

代码中多处错误消息使用了硬编码的中文字符串,违反了项目的 i18n 规范(应支持 zh-CN、en、ja、ko、de 五种语言)。虽然根据 PR 讨论,这个问题在项目的其他 actions 文件中也普遍存在,作者建议作为独立的基础设施优化任务统一处理,但仍需标记以便后续改进。

基于 PR 讨论的共识,建议:

  1. 创建独立的 issue 追踪整个项目的 action 层 i18n 改造
  2. 统一定义 action 错误消息的 i18n key 规范
  3. 在后续 PR 中批量重构,避免与本 PR 的功能变更混淆

受影响的错误消息包括但不限于:

  • 权限相关:"无权限执行此操作"
  • 验证相关:"模型名称不能为空"、"输入价格必须为非负数" 等
  • 业务逻辑相关:"JSON格式不正确"、"价格表必须是一个JSON对象" 等

Also applies to: 58-58, 163-163, 284-284, 349-349, 409-409, 417-417, 423-423, 429-429, 463-463, 468-468


142-142: revalidatePath 缺少 locale 参数

revalidatePath 调用未包含 locale 参数,可能影响多语言环境下的缓存失效精确性。根据 PR 讨论,这个模式在项目其他地方也存在,实际刷新功能正常,建议作为独立的基础设施优化任务处理。

参考 Next.js 15 的 i18n 最佳实践,后续可以考虑:

// 获取当前 locale
const locale = await getLocale();
// 精确刷新特定语言版本的路径
revalidatePath(`/${locale}/settings/prices`);
// 或者刷新所有语言版本
['zh-CN', 'en', 'ja', 'ko', 'de'].forEach(loc => {
  revalidatePath(`/${loc}/settings/prices`);
});

Also applies to: 445-445, 475-475

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between b8196bb0849a8599a6c3199522fd6b839a12b317 and b72ba62.

📒 Files selected for processing (22)
  • drizzle/0051_model_price_source.sql
  • drizzle/meta/0051_snapshot.json
  • drizzle/meta/_journal.json
  • messages/en/settings.json
  • messages/ja/settings.json
  • messages/ru/settings.json
  • messages/zh-CN/settings.json
  • messages/zh-TW/settings.json
  • src/actions/model-prices.ts
  • src/app/[locale]/settings/prices/_components/delete-model-dialog.tsx
  • src/app/[locale]/settings/prices/_components/model-price-dialog.tsx
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx
  • src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx
  • src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx
  • src/app/[locale]/settings/prices/page.tsx
  • src/drizzle/schema.ts
  • src/lib/price-sync.ts
  • src/repository/_shared/transformers.ts
  • src/repository/model-price.ts
  • src/types/model-price.ts
  • tests/unit/actions/model-prices.test.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/lib/price-sync.ts
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • src/app/[locale]/settings/prices/_components/delete-model-dialog.tsx
  • src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx
  • src/app/[locale]/settings/prices/_components/model-price-dialog.tsx
  • src/repository/_shared/transformers.ts
  • src/types/model-price.ts
  • tests/unit/actions/model-prices.test.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:

  • src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx
  • src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx
  • src/drizzle/schema.ts
  • src/actions/model-prices.ts
  • src/repository/model-price.ts
  • src/app/[locale]/settings/prices/page.tsx
**/*.{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/app/[locale]/settings/prices/_components/sync-litellm-button.tsx
  • src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx
  • src/drizzle/schema.ts
  • src/actions/model-prices.ts
  • src/repository/model-price.ts
  • src/app/[locale]/settings/prices/page.tsx
**/*.{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/sync-litellm-button.tsx
  • src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx
  • src/app/[locale]/settings/prices/page.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/0051_model_price_source.sql
🧠 Learnings (3)
📚 Learning: 2026-01-07T17:05:36.362Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T17:05:36.362Z
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/sync-conflict-dialog.tsx
  • src/actions/model-prices.ts
📚 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/drizzle/schema.ts
  • src/actions/model-prices.ts
  • src/repository/model-price.ts
📚 Learning: 2026-01-05T03:02:06.594Z
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:06.594Z
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/ru/settings.json
🧬 Code graph analysis (4)
src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx (3)
src/types/model-price.ts (1)
  • SyncConflict (92-96)
src/actions/model-prices.ts (2)
  • syncLiteLLMPrices (342-380)
  • checkLiteLLMSyncConflicts (279-335)
src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx (1)
  • SyncConflictDialog (184-391)
src/app/[locale]/settings/prices/_components/sync-conflict-dialog.tsx (1)
src/types/model-price.ts (2)
  • SyncConflict (92-96)
  • ModelPriceData (4-51)
src/repository/model-price.ts (3)
src/types/model-price.ts (3)
  • ModelPriceSource (56-56)
  • ModelPriceData (4-51)
  • ModelPrice (61-68)
src/drizzle/schema.ts (1)
  • modelPrices (365-381)
src/repository/_shared/transformers.ts (1)
  • toModelPrice (141-148)
src/app/[locale]/settings/prices/page.tsx (1)
src/app/[locale]/settings/prices/_components/model-price-dialog.tsx (1)
  • ModelPriceDialog (41-272)
⏰ 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). (2)
  • GitHub Check: 🌐 API Tests
  • GitHub Check: Docker Build Test
🔇 Additional comments (16)
drizzle/meta/_journal.json (1)

361-368: 新增 migration journal 记录看起来正确,注意 idx/tag/时间戳与实际迁移文件一致性

idx 连续且 tag 命名合理;建议合并前确认对应的 0051_model_price_source 迁移与 snapshot 已在同一 PR 中提交并且可重放。

messages/en/settings.json (1)

557-587: 冲突对话框文案结构完整;注意 “manual models” 语义与字段命名一致

manualPrice/litellmPrice、diff 字段等覆盖面很好;建议确认 UI/类型里字段名是否是 imagePrice 还是 outputPriceImage(避免后续做字段映射/展示时出现歧义)。

messages/ja/settings.json (1)

538-664: 新增的价格管理相关翻译键结构良好。

新增的翻译键覆盖了冲突检测、模型 CRUD 操作、表单字段和 toast 消息,与 PR 中新增的 UI 组件对应。日语翻译在语法上看起来是正确的。

drizzle/0051_model_price_source.sql (1)

1-2: 迁移脚本符合预期,对现有数据安全。

新增的 source 列使用 NOT NULL 配合 DEFAULT 'litellm',确保现有记录自动标记为 litellm,无破坏性变更。索引有助于按来源筛选价格数据。

src/app/[locale]/settings/prices/page.tsx (2)

6-6: 导入新增的 ModelPriceDialog 组件。

导入路径正确使用了 @/ 别名,符合项目规范。


77-77: 在操作区域添加手动创建模型价格的入口。

ModelPriceDialog 放置在 SyncLiteLLMButton 之前,用户可以先手动添加模型,再同步 LiteLLM 价格,符合业务逻辑。

src/drizzle/schema.ts (2)

369-370: Schema 定义正确,与迁移脚本一致。

source 字段的类型约束 'litellm' | 'manual'src/types/model-price.ts 中的 ModelPriceSource 类型保持一致,确保类型安全。


379-380: 按来源过滤的索引。

索引有助于在同步时高效查询手动价格(source = 'manual')。

drizzle/meta/0051_snapshot.json (1)

813-819: Schema 快照由 Drizzle 自动生成。

source 列和 idx_model_prices_source 索引的定义与 schema.ts 保持同步,无需手动修改。

Also applies to: 873-887

src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx (5)

8-11: 导入冲突检测功能和对话框组件。

新增的导入支持同步前的冲突检测流程,类型导入和组件导入清晰分离。


20-24: 新增状态管理冲突检测流程。

checking 状态用于显示检查中的 UI 反馈,conflicts 存储检测到的冲突列表,状态设计合理。


87-116: 冲突检测流程实现正确。

同步前先调用 checkLiteLLMSyncConflicts 检查冲突,若存在冲突则显示对话框让用户选择要覆盖的模型,否则直接同步。这个设计确保手动价格不会被意外覆盖,符合 PR 目标。


118-125: 冲突确认处理逻辑。

当用户在对话框中确认后,关闭对话框并执行同步,传入选中要覆盖的模型列表。如果用户取消(selectedModels 为空数组),则同步时跳过所有手动模型,符合预期行为。


129-147: UI 更新支持检查和同步两种加载状态。

按钮文本根据 checkingsyncing 状态显示不同提示,用户体验良好。SyncConflictDialog 正确接收所需的 props。

src/actions/model-prices.ts (2)

413-430: 价格验证逻辑正确实现

价格字段的非负数验证实现得很好,使用 Number.isFinite() 可以同时拒绝 NaNInfinity,比简单的 >= 0 检查更严格,符合业务需求。


75-114: 冲突处理逻辑实现正确

手动价格的冲突处理逻辑设计合理:

  1. 通过 overwriteSet 快速查找需要覆盖的模型
  2. 未在覆盖列表中的手动价格会被跳过并记录到 skippedConflicts
  3. 同时标记为 unchanged,保持统计准确性

这种设计既保护了手动维护的价格数据,又提供了明确的覆盖机制,符合 PR 目标。

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/repository/model-price.ts (1)

131-143: 边界情况:JOIN 条件未包含 source 过滤。

latest_records CTE 中,JOIN 仅使用 model_namecreated_at,未重新应用 source 过滤。如果存在两条记录具有相同的 model_namecreated_at 但不同的 source,可能返回非预期来源的记录。

虽然此场景发生概率极低(需要毫秒级时间戳完全相同),但若需严格保证,可在 JOIN 中追加 source 条件。

♻️ 可选优化:在 JOIN 中追加 source 条件
     latest_records AS (
       SELECT
         mp.id,
         ROW_NUMBER() OVER (PARTITION BY mp.model_name ORDER BY mp.id DESC) as rn
       FROM model_prices mp
       INNER JOIN latest_prices lp
         ON mp.model_name = lp.model_name
         AND mp.created_at = lp.max_created_at
+        ${source ? sql`AND mp.source = ${source}` : sql``}
     )
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between b72ba62 and 8fa0fb6.

📒 Files selected for processing (1)
  • src/repository/model-price.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

No emoji characters in any code, comments, or string literals

Files:

  • src/repository/model-price.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.ts
🧠 Learnings (1)
📚 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.ts
🧬 Code graph analysis (1)
src/repository/model-price.ts (3)
src/types/model-price.ts (3)
  • ModelPriceSource (56-56)
  • ModelPriceData (4-51)
  • ModelPrice (61-68)
src/drizzle/schema.ts (1)
  • modelPrices (365-381)
src/repository/_shared/transformers.ts (1)
  • toModelPrice (141-148)
⏰ 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). (2)
  • GitHub Check: 🌐 API Tests
  • GitHub Check: Docker Build Test
🔇 Additional comments (6)
src/repository/model-price.ts (6)

6-6: LGTM!

类型导入和 PaginationParams 扩展正确,使用了 @/ 路径别名,可选的 source 过滤参数设计合理。

Also applies to: 16-16


105-119: LGTM!

buildWhereCondition 函数逻辑清晰,使用 Drizzle 的 sql 模板字面量正确处理了参数化,避免了 SQL 注入风险。条件组合逻辑(0/1/多个条件)处理得当。


238-258: LGTM!

使用事务确保删除旧记录和插入新记录的原子性,避免了并发问题和数据不一致。source 固定为 "manual" 符合手动价格管理的设计意图。


260-265: LGTM!

硬删除实现简洁明了,符合预期的清理所有历史记录行为。


271-314: LGTM!

使用 Map<string, ModelPrice> 作为返回类型便于后续冲突检测时的快速查找。SQL 结构与其他查询保持一致。

注意:此处 JOIN 同样存在前述的理论边界情况(未在 JOIN 中重新过滤 source),但由于 source = 'manual' 是硬编码的,实际影响更小。


208-232: LGTM!

source 参数默认值为 "litellm" 保持了向后兼容性,显式在 returning() 中包含 source 字段确保返回值完整。JSDoc 注释清晰说明了参数用途。

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (1)
src/app/[locale]/dashboard/_components/user/forms/user-edit-section.tsx (1)

323-399: 阻塞:存在硬编码用户可见英文文案(违反仓库 i18n 规范)

此文件内多处 || "Enable Status" / "Cancel" / "Processing..." / \Are you sure...`等 fallback 属于用户可见文本硬编码;按规范需要全部走 i18n(或通过translations` 完整覆盖,移除硬编码 fallback)。

🤖 Fix all issues with AI agents
In @src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx:
- Around line 75-77: The model suggestion hook is receiving raw
currentUserDraft.providerGroup which can be unnormalized (extra spaces,
unordered comma lists) and causes inconsistent API calls; before calling
useModelSuggestions, normalize the value by calling
normalizeProviderGroup(currentUserDraft.providerGroup) and pass that normalized
string into useModelSuggestions (handle null/undefined by converting to empty
string if needed) so suggestions and submit logic use the same canonical
providerGroup.
🧹 Nitpick comments (2)
messages/ru/dashboard.json (1)

1348-1351: 俄语 providersSuffix: "провайдеров" 可能导致数量词语法不正确,建议做复数规则支持
如果 UI 直接拼接数字 + suffix,俄语通常需要至少 3 种形式(1 / 2-4 / 5+)。建议后续在 i18n 层支持 plural(ICU message 或多 key 方案),否则会长期显示不自然。

messages/en/dashboard.json (1)

1375-1378: 英文可能出现 “1 providers”,可考虑后续做复数处理(可选)
目前与既有 providerCount: "{count} providers" 一致;如果要提升体验,建议后续引入 plural(或在代码侧按 count 选择 provider/providers)。

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8fa0fb6 and b8967c2.

📒 Files selected for processing (8)
  • messages/en/dashboard.json
  • messages/ja/dashboard.json
  • messages/ru/dashboard.json
  • messages/zh-CN/dashboard.json
  • messages/zh-TW/dashboard.json
  • src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx
  • src/app/[locale]/dashboard/_components/user/forms/user-edit-section.tsx
  • src/app/[locale]/dashboard/_components/user/hooks/use-user-translations.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

No emoji characters in any code, comments, or string literals

Files:

  • src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx
  • src/app/[locale]/dashboard/_components/user/hooks/use-user-translations.ts
  • src/app/[locale]/dashboard/_components/user/forms/user-edit-section.tsx
**/*.{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/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx
  • src/app/[locale]/dashboard/_components/user/hooks/use-user-translations.ts
  • src/app/[locale]/dashboard/_components/user/forms/user-edit-section.tsx
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use React 19, shadcn/ui, Tailwind CSS, and Recharts for the UI layer

Files:

  • src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx
  • src/app/[locale]/dashboard/_components/user/forms/user-edit-section.tsx
🧠 Learnings (4)
📚 Learning: 2026-01-05T03:02:06.594Z
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:06.594Z
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/zh-TW/dashboard.json
  • src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx
  • src/app/[locale]/dashboard/_components/user/hooks/use-user-translations.ts
  • messages/en/dashboard.json
  • messages/ru/dashboard.json
📚 Learning: 2026-01-07T17:05:36.362Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T17:05:36.362Z
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]/dashboard/_components/user/hooks/use-user-translations.ts
📚 Learning: 2026-01-07T17:05:36.362Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T17:05:36.362Z
Learning: Applies to **/*.{ts,tsx,jsx,js} : Use next-intl for internationalization

Applied to files:

  • src/app/[locale]/dashboard/_components/user/hooks/use-user-translations.ts
📚 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/[locale]/dashboard/_components/user/hooks/use-user-translations.ts
🧬 Code graph analysis (3)
src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx (2)
src/app/[locale]/dashboard/_components/user/hooks/use-user-translations.ts (1)
  • useUserTranslations (107-214)
src/app/[locale]/dashboard/_components/user/hooks/use-model-suggestions.ts (1)
  • useModelSuggestions (11-32)
src/app/[locale]/dashboard/_components/user/hooks/use-user-translations.ts (1)
scripts/sync-settings-keys.js (1)
  • t (72-72)
src/app/[locale]/dashboard/_components/user/forms/user-edit-section.tsx (2)
src/app/[locale]/dashboard/_components/user/forms/provider-group-select.tsx (1)
  • ProviderGroupSelect (38-141)
src/lib/constants/provider.constants.ts (1)
  • PROVIDER_GROUP (25-30)
🔇 Additional comments (4)
messages/zh-CN/dashboard.json (1)

1376-1379: 新增 providerGroupSelect 文案一致且清晰
suffix/loadFailed 与现有 providerCount 风格一致。

messages/zh-TW/dashboard.json (1)

1346-1349: 新增 providerGroupSelect 文案一致且可直接使用

src/app/[locale]/dashboard/_components/user/hooks/use-user-translations.ts (1)

26-40: useTranslations("ui.tagInput") 调用是安全的,key 映射正确,但注意缺失的 locale

tUi 虽然无条件初始化,但其实际调用(emptyTag、duplicateTag、tooLong、invalidFormat、maxTags)仅在 showProviderGroup=true 时执行,next-intl 会延迟验证 namespace 直到函数被调用,因此不存在运行时报错风险。所有活跃 locale(zh-CN, zh-TW, en, ja, ru)的 ui.tagInput 都已包含这些 key,且与 TagInputField 的 reason 值完全一致。

注意:项目编码指南声称支持 5 种语言(zh-CN, en, ja, ko, de),但实际只有 5 个 locale(zh-CN, zh-TW, en, ja, ru),缺少 ko 和 de。若后续要添加这两种语言,必须确保 messages/{ko,de}/ui.json 中包含完整的 tagInput namespace。

src/app/[locale]/dashboard/_components/user/forms/user-edit-section.tsx (1)

25-26: 确认:ProviderGroupSelect 集成中存在哨兵值持久化和国际化问题

ProviderGroupSelect 的接入逻辑本身正确,但存在数据完整性和国际化两个问题:

  1. 哨兵值泄漏到数据库(关键)

    • normalizeProviderGroup() 会将 null/undefined 转换为字符串 "default"(PROVIDER_GROUP.DEFAULT)
    • edit-user-dialog.tsx 第 91 行在提交前调用了 normalizeProviderGroup(data.providerGroup)
    • 该规范化值随后被 editUser 操作(src/actions/users.ts 第 1165 行)直接持久化到数据库
    • 读取时没有逆向规范化,导致字符串 "default" 永久存储在数据库中
    • 结果:用户从未选择提供商组的情况下,providerGroup 字段会记录 "default" 而不是 null/undefined

    需要修复:在 editUser 提交时添加逆向规范化逻辑,将 PROVIDER_GROUP.DEFAULT 转换回 null 后再写入数据库

  2. 国际化硬编码字符串(主要)

    • 启用/禁用状态区块(第 341、345-346、361-369、374、391、394、396 行)包含英文硬编码后备文本
    • 违反编码规范:所有用户可见字符串必须通过国际化传入,不应硬编码后备文本
    • 涉及文本:"Enable Status"、"Currently enabled"、"Currently disabled"、"Disable User"、"Enable User"、"Cancel"、"Processing..."、"Disable"、"Enable" 等
⛔ Skipped due to learnings
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T17:05:36.362Z
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

- Rename 0051_model_price_source.sql to 0052_model_price_source.sql
- Update snapshot prevId to chain after upstream's 0051_silent_maelstrom
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
messages/ru/settings.json (1)

540-666: prices.sync.failedError 未传递 {error} 参数,导致占位符直出

JSON 定义了 "failedError": "Ошибка синхронизации: {error}",但 sync-litellm-button.tsx 的两处 catch 块(第 79 和 111 行)调用 t("prices.sync.failedError") 时未传递任何参数,导致 {error} 会直接显示给用户。应修改为 t("prices.sync.failedError", { error: error.message })

次要问题:第 71 行 t("prices.sync.successNoChanges", { unchanged }) 传递了未使用的参数,因为对应的 JSON 字符串中不存在 {unchanged} 占位符。

🤖 Fix all issues with AI agents
In @drizzle/meta/_journal.json:
- Around line 368-375: Do not manually edit drizzle/meta/_journal.json; the
entry with idx 52 and tag "0052_model_price_source" has an out-of-order "when"
timestamp (1767924921400) caused by a manual merge — revert that manual change
and instead update your schema in src/drizzle/schema.ts as needed, then run "bun
run db:generate" to regenerate the migration metadata so the journal entries
(idx 51/52) are produced in correct order.

In @messages/en/settings.json:
- Around line 546-558: The string for the "skippedConflicts" key is ambiguous;
update messages/en/settings.json -> sync.skippedConflicts to more precise
wording to reflect that entries with a manual source were not overwritten by
LiteLLM (e.g., "Skipped {count} manual price entries" or "Skipped {count}
manually-priced models"); change only the message value for the
"skippedConflicts" key (preserve the placeholder {count}) and ensure
punctuation/capitalization follows the surrounding strings.

In @messages/zh-CN/settings.json:
- Around line 1265-1277: 更新 messages/zh-CN/settings.json 中 "skippedConflicts"
的翻译措辞以更准确表达“跳过手动价格来源的条目/不覆盖手动价格”的含义;将当前值 "跳过 {count} 个手动模型" 改为更清晰的例如 "跳过 {count}
个手动价格项"(或 "保留 {count} 个手动模型"),确保保持占位符 {count} 不变且不修改键名 skippedConflicts。
🧹 Nitpick comments (7)
drizzle/0052_model_price_source.sql (1)

1-2: 建议为 source 增加 DB 级约束(可选但更稳)
当前仅用 varchar(20) + 默认值,DB 侧不会阻止写入其它值;如果业务只允许 litellm/manual,建议在 schema 中加 CHECK/enum 约束并重新生成迁移(避免未来脏数据)。

messages/ru/settings.json (1)

548-579: 俄语复数/格变化(可选优化)
例如 skippedConflicts: "Пропущено {count} ручных моделей" 在部分数字下语感不自然;如果项目支持 ICU plural rules,可后续改成复数规则以提升本地化质量。

messages/zh-TW/settings.json (1)

550-579: zh-TW 术语一致性(后续可选)
新增价格冲突相关翻译整体偏一致;不过文件内长期存在用词/繁简混用现象的话,建议单独开一个 i18n 清理任务集中处理(避免在功能 PR 里扩大范围)。

messages/zh-CN/settings.json (2)

1278-1308: 冲突对话框文案/字段命名整体 OK;建议核对“imagePrice/outputPriceImage”字段映射
diff.imagePrice(图片价格)与表单里的 outputPriceImage(输出价格 $/image)命名不完全一致,建议确认 UI/后端字段映射不会把 “image generation price” / “output per image” 混用。


1366-1394: CRUD 相关 keys 完整;建议统一金额单位表述
prices.table.inputPrice/outputPrice 用的是 ($/M),而表单用 ($/M tokens),建议统一口径(要么都强调 tokens,要么都简写),避免同页出现两套单位描述。

messages/en/settings.json (1)

648-675: CRUD 文案齐全;建议统一价格单位写法
表格是 ($/M),表单是 ($/M tokens),建议统一,减少认知切换。

messages/ja/settings.json (1)

639-666: 表単位の英語(tokens/image)を日本語化するかは方針次第
現状でも意味は通りますが、UI の統一感を重視するなら “トークン/画像” 表記に寄せるのもありです。

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between b8967c2 and 94959ee.

📒 Files selected for processing (15)
  • drizzle/0052_model_price_source.sql
  • drizzle/meta/0052_snapshot.json
  • drizzle/meta/_journal.json
  • messages/en/dashboard.json
  • messages/en/settings.json
  • messages/ja/dashboard.json
  • messages/ja/settings.json
  • messages/ru/dashboard.json
  • messages/ru/settings.json
  • messages/zh-CN/dashboard.json
  • messages/zh-CN/settings.json
  • messages/zh-TW/dashboard.json
  • messages/zh-TW/settings.json
  • src/drizzle/schema.ts
  • src/repository/_shared/transformers.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/drizzle/schema.ts
  • src/repository/_shared/transformers.ts
  • messages/zh-TW/dashboard.json
  • messages/en/dashboard.json
  • messages/ru/dashboard.json
  • messages/ja/dashboard.json
🧰 Additional context used
📓 Path-based instructions (1)
**/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 (1)
📚 Learning: 2026-01-05T03:02:06.594Z
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:06.594Z
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/zh-TW/settings.json
  • messages/ja/settings.json
⏰ 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). (2)
  • GitHub Check: Docker Build Test
  • GitHub Check: 🌐 API Tests
🔇 Additional comments (10)
messages/zh-CN/dashboard.json (1)

1381-1384: 多语言翻译已正确同步,无需调整

providerGroupSelect 翻译块的中文翻译准确,JSON 结构正确。已确认所有语言文件(en、ja、ru、zh-TW)均已完整同步相应的翻译,多语言支持完整性符合要求。

drizzle/meta/0052_snapshot.json (1)

1-2374: 确认 snapshot 为自动生成且与迁移一致
该文件通常是 drizzle 自动生成产物;建议确认它与 0052_model_price_source.sql / drizzle/schema.ts 同步更新,避免迁移漂移。

drizzle/0052_model_price_source.sql (1)

1-2: 确保迁移文件由 drizzle 自动生成

根据仓库规范,drizzle/0052_model_price_source.sql 不应手动编写或修改。请验证该文件是否通过编辑 src/drizzle/schema.ts 后执行 bun run db:generate 自动生成,并与 schema 定义完全一致。

messages/zh-TW/settings.json (1)

540-666: 占位符参数名与代码调用完全一致(已验证)

已验证 zh-TW 文件中的所有占位符参数名与前端代码调用完全匹配:

  • prices.sync: {added}、{updated}、{unchanged}、{failed}、{count} ✓
  • prices.conflict: {count}、{total}、{from}、{to} ✓
  • deleteConfirm: {name} ✓
  • dialog.results: {total}、{success}、{failed}、{skipped} ✓
  • stats: {count} ✓

无占位符替换风险。

messages/zh-CN/settings.json (1)

1309-1325: 新增“操作”列翻译 OK
和 CRUD 功能配套,增加 prices.table.actions 很合理。

messages/en/settings.json (2)

559-589: 冲突解决区块翻译覆盖全面,LGTM
字段/表头/分页/批量操作等文案齐全,和冲突解决 UI 的信息架构一致。


590-606: 新增 Actions 列 OK
为行内 Edit/Delete 提供明确列名。

messages/ja/settings.json (3)

537-549: 同期状態追加はOK;skippedConflictsの語感も自然
“{count}件の手動モデルをスキップしました” は意図が伝わります。


550-580: 競合解決 UI の翻訳一式が揃っていて良い
テーブル/差分/ページング/一括適用まで抜けなく追加されています。


581-597: actions 列追加 OK
行操作(編集/削除)との整合が取れています。

@NieiR
Copy link
Contributor Author

NieiR commented Jan 10, 2026

第二次冲突已解决

@ding113 ding113 merged commit ecce1f5 into ding113:dev Jan 10, 2026
69 of 70 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Jan 10, 2026
NieiR added a commit to NieiR/claude-code-hub that referenced this pull request Jan 10, 2026
- 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
@coderabbitai coderabbitai bot mentioned this pull request Jan 21, 2026
15 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:i18n area:UI enhancement New feature or request size/XL Extra Large PR (> 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants

Comments