Skip to content

feat: 价格表 UI 支持自定义模型/缓存价格#583

Merged
ding113 merged 3 commits intodevfrom
feat/prices-ui-custom-model
Jan 10, 2026
Merged

feat: 价格表 UI 支持自定义模型/缓存价格#583
ding113 merged 3 commits intodevfrom
feat/prices-ui-custom-model

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Jan 10, 2026

  • 价格表列表 UI:合并输入/输出价格列,新增缓存读取/缓存创建列;移除提供商列并在模型名处用 badge 展示;移除模型类型 badge\n- 自定义模型:支持配置是否支持缓存、缓存读取/创建价格、按次调用价格;新增/编辑改为右侧抽屉并支持搜索现有模型预填充\n- 模型 id 支持点击复制\n- 修复上传/更新价格表弹窗文案:异步统计云端模型数量\n\n验证:bun run build / lint / typecheck / test:coverage

Greptile Overview

Greptile Summary

Overview

This PR enhances the pricing table UI to support custom model configuration with cache pricing and per-request pricing. The changes include:

  1. UI Improvements: Merged input/output price columns, added cache read/creation columns, moved provider display to badges
  2. Custom Model Support: New drawer for creating/editing models with search-based prefill functionality
  3. Per-Request Pricing: Added input_cost_per_request field for models that charge per API call
  4. Cache Pricing Configuration: Support for configuring cache read/write prices and supports_prompt_caching flag
  5. Async Cloud Model Count: Upload dialog now asynchronously fetches and displays cloud model count

Key Changes

Backend (Actions & Repository)

  • upsertSingleModelPrice(): Added comprehensive validation for cache pricing fields and input_cost_per_request (all must be non-negative finite numbers)
  • Cost calculation properly handles the new input_cost_per_request field with validation and multiplier support
  • hasValidPriceData() updated to recognize models with only per-request pricing as valid

Frontend (UI Components)

  • ModelPriceDrawer: New component with create/edit modes, prefill search, and comprehensive form fields for all pricing options
  • PriceList: Restructured table columns per requirements - merged price displays, added cache columns, provider shown as badges
  • UploadPriceDialog: Async cloud model count fetch with loading/error states

Test Coverage

Excellent test coverage for:

  • Cost calculation with input_cost_per_request
  • Price data validation
  • UI requirements (column structure, badge display)
  • Drawer prefill functionality
  • Cloud model count async loading
  • Zero price handling

Issues Found

🔴 Critical Issues

None

🟡 Moderate Issues

  1. Performance Issue - Unbounded API Requests (model-price-drawer.tsx, line 172-219)

    • Prefill search triggers fetch on every keystroke without debouncing
    • Can overwhelm API with rapid requests when user types quickly
    • Recommendation: Use useDebounce hook (already used successfully in price-list.tsx)
  2. UX Bug - Empty Page After Deletion (price-list.tsx, line 638-648)

    • Deleting the last item on page > 1 results in empty table display
    • Delete callback stays on same page number instead of navigating to previous page
    • Affects user experience when managing paginated data

Architecture & Code Quality

Strengths:

  • ✅ Comprehensive input validation at multiple layers (UI parsing, action validation, cost calculation)
  • ✅ Proper SQL parameterization using drizzle-orm template tags
  • ✅ Good error handling with proper cancellation logic in async operations
  • ✅ Consistent patterns across similar components
  • ✅ Excellent test coverage including edge cases (zero prices, invalid inputs)
  • ✅ Type safety with TypeScript interfaces
  • ✅ i18n support with translations in all 5 languages

Areas for Improvement:

  • The two issues identified above should be addressed to improve performance and UX
  • Consider adding indices on model_name and source columns if not already present for query performance

Verification Recommendations

Before merging:

  1. Test the prefill search with rapid typing to confirm API request volume
  2. Verify pagination behavior when deleting the last item on page 2+
  3. Confirm cache pricing displays correctly for models with/without cache support
  4. Test per-request pricing calculation in cost reports

Confidence Score: 4/5

  • This PR is mostly safe to merge with two moderate issues that should be addressed to improve performance and user experience
  • Score of 4/5 reflects: (1) Two moderate issues found - unbounded API requests in prefill search and pagination bug on deletion, (2) Otherwise excellent code quality with comprehensive validation, proper error handling, and strong test coverage, (3) No critical bugs or security vulnerabilities, (4) Well-structured architecture following existing patterns
  • Pay close attention to model-price-drawer.tsx (prefill search debouncing) and price-list.tsx (deletion pagination logic)

Important Files Changed

File Analysis

Filename Score Overview
src/app/[locale]/settings/prices/_components/model-price-drawer.tsx 4/5 New drawer component for creating/editing model prices with prefill search functionality. Has a performance issue with un-debounced search (line 172-219) that could overwhelm the API.
src/app/[locale]/settings/prices/_components/price-list.tsx 4/5 Price table with pagination, search, and filtering. Has a pagination bug on deletion (line 638-648) where deleting the last item on a page > 1 results in an empty table instead of navigating to previous page.
src/lib/utils/cost-calculation.ts 5/5 Added input_cost_per_request support with proper validation (lines 116-127). Logic correctly validates non-negative finite numbers and applies multiplier appropriately.
src/actions/model-prices.ts 5/5 Added comprehensive validation for cache pricing fields and input_cost_per_request (lines 506-529). All new fields properly validated as non-negative finite numbers.
src/app/api/prices/cloud-model-count/route.ts 5/5 New API endpoint for asynchronously fetching cloud model count. Proper auth check and error handling.

Sequence Diagram

sequenceDiagram
    participant User
    participant PriceList
    participant ModelPriceDrawer
    participant API
    participant Actions
    participant Repository
    participant DB

    Note over User,DB: Create/Edit Custom Model Price Flow
    
    User->>PriceList: Click "Add Model"
    PriceList->>ModelPriceDrawer: Open drawer (create mode)
    
    User->>ModelPriceDrawer: Type search query
    Note over ModelPriceDrawer: ⚠️ No debounce - triggers fetch per keystroke
    ModelPriceDrawer->>API: GET /api/prices?search={query}
    API->>Actions: getModelPricesPaginated()
    Actions->>Repository: findAllLatestPricesPaginated()
    Repository->>DB: SELECT with pagination & filters
    DB-->>Repository: Return matching models
    Repository-->>Actions: Return paginated results
    Actions-->>API: Return results
    API-->>ModelPriceDrawer: Return search results
    ModelPriceDrawer->>ModelPriceDrawer: Display prefill options
    
    User->>ModelPriceDrawer: Select model to prefill
    ModelPriceDrawer->>ModelPriceDrawer: Populate form fields
    Note over ModelPriceDrawer: Display name, prices,<br/>cache settings, per-request cost
    
    User->>ModelPriceDrawer: Edit/enter price data
    User->>ModelPriceDrawer: Toggle cache support
    User->>ModelPriceDrawer: Enter per-request price
    User->>ModelPriceDrawer: Click submit
    
    ModelPriceDrawer->>Actions: upsertSingleModelPrice()
    Note over Actions: Validate all prices >= 0
    Actions->>Repository: upsertModelPrice()
    Repository->>DB: DELETE old records + INSERT new
    DB-->>Repository: Success
    Repository-->>Actions: Return new record
    Actions->>Actions: revalidatePath("/settings/prices")
    Actions-->>ModelPriceDrawer: Success
    
    ModelPriceDrawer->>ModelPriceDrawer: Dispatch "price-data-updated" event
    ModelPriceDrawer->>PriceList: Close drawer
    PriceList->>PriceList: Listen to event
    PriceList->>API: Fetch updated prices
    API->>Actions: getModelPricesPaginated()
    Actions->>Repository: findAllLatestPricesPaginated()
    Repository->>DB: SELECT with DISTINCT ON
    DB-->>Repository: Return latest prices
    Repository-->>Actions: Return results
    Actions-->>API: Return results
    API-->>PriceList: Return updated data
    PriceList->>PriceList: Update display
    
    Note over User,DB: Delete Model Flow with Pagination Issue
    
    User->>PriceList: Click delete on last item (page > 1)
    PriceList->>Actions: deleteSingleModelPrice()
    Actions->>Repository: deleteModelPriceByName()
    Repository->>DB: DELETE WHERE model_name = ?
    DB-->>Repository: Success
    Repository-->>Actions: Success
    Actions-->>PriceList: Success
    
    Note over PriceList: ⚠️ Stays on same page number
    PriceList->>API: fetchPrices(page=current)
    API->>Actions: getModelPricesPaginated(page=current)
    Actions->>Repository: findAllLatestPricesPaginated()
    Repository->>DB: SELECT with pagination
    DB-->>Repository: Empty array (page out of bounds)
    Repository-->>Actions: Return empty results
    Actions-->>API: Return empty results
    API-->>PriceList: Return empty data
    PriceList->>User: Display empty table
    Note over PriceList: Should navigate to page-1 instead
    
    Note over User,DB: Upload Price Table Flow
    
    User->>PriceList: Click "Update Price Table"
    PriceList->>UploadDialog: Open dialog
    UploadDialog->>API: GET /api/prices/cloud-model-count
    Note over API: Async fetch model count
    API->>CloudPriceAPI: fetchCloudPriceTableToml()
    CloudPriceAPI-->>API: Return TOML data
    API->>API: parseCloudPriceTableToml()
    API->>API: Count models
    API-->>UploadDialog: Return count
    UploadDialog->>UploadDialog: Show "Currently supports N models"
    
    User->>UploadDialog: Upload JSON/TOML file
    UploadDialog->>Actions: uploadPriceTable(content)
    Actions->>Actions: Parse JSON/TOML
    Actions->>Actions: processPriceTableInternal()
    loop For each model in file
        Actions->>Repository: Check existing price
        alt Model doesn't exist
            Actions->>Repository: createModelPrice()
        else Model exists and changed
            Actions->>Repository: createModelPrice()
        else Manual model (skip)
            Actions->>Actions: Add to skippedConflicts
        end
    end
    Actions->>Actions: revalidatePath()
    Actions-->>UploadDialog: Return result summary
    UploadDialog->>UploadDialog: Display results
    UploadDialog->>PriceList: Dispatch event
    PriceList->>API: Refresh data
Loading

@coderabbitai
Copy link

coderabbitai bot commented Jan 10, 2026

📝 Walkthrough

Walkthrough

该变更引入每次请求定价与提示缓存相关字段,扩展前端定价管理界面(由对话框改为抽屉式编辑器),增加云模型计数 API,更新多语言本地化字符串,并辅以相关类型、校验、成本计算调整及大量单元测试覆盖。

Changes

内聚群 / 文件(s) 更改摘要
翻译消息更新
messages/en/settings.json, messages/ja/settings.json, messages/ru/settings.json, messages/zh-CN/settings.json, messages/zh-TW/settings.json
新增/更新定价相关本地化键:price, priceInput, priceOutput, pricePerRequest, cacheReadPrice, cacheCreationPrice, cache5m, cache1h, copyModelId;将 modelName 改为 Model ID 文案并新增 displayNamedisplayNamePlaceholderrequestPrice;新增抽屉(prefill/缓存)与云模型计数提示文本。
后端动作与类型
src/actions/model-prices.ts, src/types/model-price.ts
扩展 SingleModelPriceInput 与上游 upsert 逻辑,新增可选字段 displayNamesupportsPromptCachinginputCostPerRequestcacheReadInputTokenCostcacheCreationInputTokenCostcacheCreationInputTokenCostAbove1hr,并在服务端校验这些数值;ModelPriceData 添加 input_cost_per_request 字段。
UI:编辑器与列表
src/app/[locale]/settings/prices/_components/model-price-drawer.tsx, src/app/[locale]/settings/prices/_components/price-list.tsx, src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx, src/app/[locale]/settings/prices/page.tsx
新增 ModelPriceDrawer(替代旧 Dialog)实现创建/编辑、预填充搜索、缓存定价输入;PriceList 改为使用 Drawer、增加复制模型 ID 功能、多行价格/缓存展示与新表头;UploadPriceDialog 增加云模型计数加载状态显示;page.tsx 改为使用 Drawer。
API 路由与工具函数
src/app/api/prices/cloud-model-count/route.ts, src/lib/utils/cost-calculation.ts, src/lib/utils/price-data.ts
新增云模型计数 GET 路由(管理员权限);在成本计算中加入 input_cost_per_request 处理以参与请求成本计算;hasValidPriceData 包含对 input_cost_per_request 的有效性检查。
单元测试
tests/unit/lib/cost-calculation-input-cost-per-request.test.ts, tests/unit/lib/price-data-input-cost-per-request.test.ts, tests/unit/settings/prices/*
添加覆盖输入每次请求成本、price-data 验证、DeleteModelDialog、ModelPriceDrawer(prefill/submit)、PriceList 交互与 UI 要求、UploadPriceDialog 云模型计数及上传流程的一系列单元/集成测试。

代码审查难度估算

🎯 4 (复杂) | ⏱️ ~45 分钟

可能相关的PR

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 标题清晰准确地反映了主要变更:支持在价格表 UI 中自定义模型和缓存价格配置。
Description check ✅ Passed PR 描述详细说明了所有主要改动:UI 优化、自定义模型支持、缓存价格配置、按次调用价格、模型 ID 复制、以及异步统计云端模型数量。

✏️ 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
  • Commit unit tests in branch feat/prices-ui-custom-model

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

@gemini-code-assist
Copy link
Contributor

Summary of Changes

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

此拉取请求旨在全面提升价格表的用户界面和自定义模型管理功能。通过重新组织价格信息的显示方式,引入缓存相关成本的配置,并优化模型编辑流程,使得用户能够更直观、高效地管理和配置模型价格。这些改进不仅美化了界面,也增强了系统的灵活性和实用性。

Highlights

  • 价格表UI优化: 价格表列表UI进行了重大调整,合并了输入/输出价格列,并新增了缓存读取和缓存创建价格列。提供商信息现在以徽章形式展示在模型名称旁,同时移除了模型类型徽章,使界面更简洁。
  • 自定义模型管理增强: 自定义模型现在支持配置是否启用缓存、缓存读取/创建价格以及按次调用价格。新增和编辑模型的功能已改为右侧抽屉式界面,并支持搜索现有模型进行预填充,提升了用户体验。
  • 模型ID复制功能: 现在可以点击模型ID进行复制,方便用户快速获取和使用模型标识符。
  • 上传/更新价格表弹窗改进: 修复了上传/更新价格表弹窗的文案,并实现了异步统计云端模型数量的功能,提供更准确和实时的信息。
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 size/XL Extra Large PR (> 1000 lines) labels Jan 10, 2026
@github-actions
Copy link
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

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 management feature, primarily by refactoring the model price creation/editing UI from a dialog to a new ModelPriceDrawer component. This new drawer supports prefilling model details via a search function and allows for the configuration of new pricing metrics, including per-request costs and detailed prompt caching prices (read, 5-minute creation, and 1-hour+ creation costs). Corresponding backend changes were made to src/actions/model-prices.ts to handle these new price inputs and validations, and the ModelPriceData type was updated to include these fields. The PriceList component was also updated to reflect these new pricing categories in the table display, replacing separate input/output price columns with a consolidated 'Price' column and adding dedicated columns for cache pricing. Additionally, a new API endpoint (/api/prices/cloud-model-count) was added to fetch the count of cloud models, which is now displayed in the UploadPriceDialog. Review comments highlighted the need to implement debouncing for the prefill search input in ModelPriceDrawer to optimize API requests and to internationalize hardcoded Chinese error messages in the new cloud-model-count API route by returning error keys instead of literal strings.

Comment on lines 173 to 219
useEffect(() => {
if (!open || mode !== "create") {
return;
}

const query = prefillQuery.trim();
if (!query) {
setPrefillResults([]);
setPrefillStatus("idle");
return;
}

let cancelled = false;
const fetchPrefillResults = async () => {
setPrefillStatus("loading");
setPrefillResults([]);
try {
const params = new URLSearchParams();
params.set("page", "1");
params.set("pageSize", "10");
params.set("search", query);
const response = await fetch(`/api/prices?${params.toString()}`, { cache: "no-store" });
const payload = await response.json();
if (!payload?.ok) {
throw new Error(payload?.error || "unknown error");
}

const data: ModelPrice[] = payload.data?.data ?? [];
if (!cancelled) {
setPrefillResults(data);
setPrefillStatus("loaded");
}
} catch (error) {
console.error("搜索模型失败:", error);
if (!cancelled) {
setPrefillResults([]);
setPrefillStatus("error");
}
}
};

fetchPrefillResults();

return () => {
cancelled = true;
};
}, [mode, open, prefillQuery]);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

在创建新模型时,预填充搜索功能会在用户每次按键时都触发一次 API 请求。这可能会导致不必要的网络请求和服务器负载,尤其是在用户快速输入时。这也会影响用户体验。

建议对搜索输入 prefillQuery 使用防抖(debounce)处理。项目中已存在 useDebounce hook(例如在 price-list.tsx 中使用),可以复用它来优化此处的搜索功能,在用户停止输入一段时间后再发起请求。

修改建议:

  1. 在组件顶部导入 useDebounce hook: import { useDebounce } from "@/lib/hooks/use-debounce";
  2. prefillQuery 状态下方添加:const debouncedPrefillQuery = useDebounce(prefillQuery, 300);
  3. 在此 useEffect 中,将 prefillQuery 替换为 debouncedPrefillQuery

// 权限检查:只有管理员可以访问
const session = await getSession();
if (!session || session.user.role !== "admin") {
return NextResponse.json({ ok: false, error: "无权限访问此资源" }, { status: 403 });
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

此 API 路由中的错误信息是硬编码的中文字符串。这将导致在其他语言环境下(如英语、日语)显示中文错误,影响国际化体验。

建议返回错误码或错误键(error key),由前端根据当前语言环境来渲染对应的翻译文本。例如,可以将错误从 "无权限访问此资源" 改为 "error.unauthorized",然后在 i18n JSON 文件中定义此键的翻译。

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.

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 172 to 219

useEffect(() => {
if (!open || mode !== "create") {
return;
}

const query = prefillQuery.trim();
if (!query) {
setPrefillResults([]);
setPrefillStatus("idle");
return;
}

let cancelled = false;
const fetchPrefillResults = async () => {
setPrefillStatus("loading");
setPrefillResults([]);
try {
const params = new URLSearchParams();
params.set("page", "1");
params.set("pageSize", "10");
params.set("search", query);
const response = await fetch(`/api/prices?${params.toString()}`, { cache: "no-store" });
const payload = await response.json();
if (!payload?.ok) {
throw new Error(payload?.error || "unknown error");
}

const data: ModelPrice[] = payload.data?.data ?? [];
if (!cancelled) {
setPrefillResults(data);
setPrefillStatus("loaded");
}
} catch (error) {
console.error("搜索模型失败:", error);
if (!cancelled) {
setPrefillResults([]);
setPrefillStatus("error");
}
}
};

fetchPrefillResults();

return () => {
cancelled = true;
};
}, [mode, open, prefillQuery]);
Copy link

Choose a reason for hiding this comment

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

The prefill search triggers a fetch request on every keystroke without debouncing, which can overwhelm the API with requests and degrade performance.

When a user types quickly (e.g., "gpt-4"), this creates 5 separate fetch requests in rapid succession, where only the last one is needed.

Recommendation: Use the useDebounce hook (already imported in price-list.tsx) to debounce the prefillQuery value before triggering the fetch. For example:

Suggested change
useEffect(() => {
if (!open || mode !== "create") {
return;
}
const query = prefillQuery.trim();
if (!query) {
setPrefillResults([]);
setPrefillStatus("idle");
return;
}
let cancelled = false;
const fetchPrefillResults = async () => {
setPrefillStatus("loading");
setPrefillResults([]);
try {
const params = new URLSearchParams();
params.set("page", "1");
params.set("pageSize", "10");
params.set("search", query);
const response = await fetch(`/api/prices?${params.toString()}`, { cache: "no-store" });
const payload = await response.json();
if (!payload?.ok) {
throw new Error(payload?.error || "unknown error");
}
const data: ModelPrice[] = payload.data?.data ?? [];
if (!cancelled) {
setPrefillResults(data);
setPrefillStatus("loaded");
}
} catch (error) {
console.error("搜索模型失败:", error);
if (!cancelled) {
setPrefillResults([]);
setPrefillStatus("error");
}
}
};
fetchPrefillResults();
return () => {
cancelled = true;
};
}, [mode, open, prefillQuery]);
const debouncedPrefillQuery = useDebounce(prefillQuery, 300);
useEffect(() => {
if (!open || mode !== "create") {
return;
}
const query = debouncedPrefillQuery.trim();
if (!query) {
setPrefillResults([]);
setPrefillStatus("idle");
return;
}
let cancelled = false;
const fetchPrefillResults = async () => {
// ... rest of the code
};
fetchPrefillResults();
return () => {
cancelled = true;
};
}, [mode, open, debouncedPrefillQuery]);

This pattern is already used successfully in price-list.tsx (line 91) for the main search functionality.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/prices/_components/model-price-drawer.tsx
Line: 172:219

Comment:
The prefill search triggers a fetch request on every keystroke without debouncing, which can overwhelm the API with requests and degrade performance. 

When a user types quickly (e.g., "gpt-4"), this creates 5 separate fetch requests in rapid succession, where only the last one is needed.

**Recommendation**: Use the `useDebounce` hook (already imported in price-list.tsx) to debounce the `prefillQuery` value before triggering the fetch. For example:

```suggestion
  const debouncedPrefillQuery = useDebounce(prefillQuery, 300);

  useEffect(() => {
    if (!open || mode !== "create") {
      return;
    }

    const query = debouncedPrefillQuery.trim();
    if (!query) {
      setPrefillResults([]);
      setPrefillStatus("idle");
      return;
    }

    let cancelled = false;
    const fetchPrefillResults = async () => {
      // ... rest of the code
    };

    fetchPrefillResults();

    return () => {
      cancelled = true;
    };
  }, [mode, open, debouncedPrefillQuery]);
```

This pattern is already used successfully in price-list.tsx (line 91) for the main search functionality.

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link

greptile-apps bot commented Jan 10, 2026

Additional Comments (1)

src/app/[locale]/settings/prices/_components/price-list.tsx
When deleting the last item on a page (especially when page > 1), the delete callback stays on the same page number, which results in displaying an empty table.

Example scenario:

  • User is on page 3 with 1 item remaining
  • User deletes that item
  • The UI fetches page 3 again, which now has 0 items
  • User sees an empty table instead of being navigated to page 2

Recommendation: After deletion, check if the current page would be empty and navigate to the previous page:

                          onSuccess={() => {
                            // Calculate if current page will be empty after deletion
                            const itemsOnCurrentPage = filteredPrices.length;
                            const willBeEmpty = itemsOnCurrentPage <= 1 && page > 1;
                            const targetPage = willBeEmpty ? page - 1 : page;
                            
                            fetchPrices(
                              targetPage,
                              pageSize,
                              debouncedSearchTerm,
                              sourceFilter,
                              litellmProviderFilter
                            );
                          }}

This same logic should also be applied to the edit dialog's onSuccess callback (lines 620-630).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/prices/_components/price-list.tsx
Line: 638:648

Comment:
When deleting the last item on a page (especially when page > 1), the delete callback stays on the same page number, which results in displaying an empty table. 

**Example scenario**:
- User is on page 3 with 1 item remaining
- User deletes that item  
- The UI fetches page 3 again, which now has 0 items
- User sees an empty table instead of being navigated to page 2

**Recommendation**: After deletion, check if the current page would be empty and navigate to the previous page:

```suggestion
                          onSuccess={() => {
                            // Calculate if current page will be empty after deletion
                            const itemsOnCurrentPage = filteredPrices.length;
                            const willBeEmpty = itemsOnCurrentPage <= 1 && page > 1;
                            const targetPage = willBeEmpty ? page - 1 : page;
                            
                            fetchPrices(
                              targetPage,
                              pageSize,
                              debouncedSearchTerm,
                              sourceFilter,
                              litellmProviderFilter
                            );
                          }}
```

This same logic should also be applied to the edit dialog's onSuccess callback (lines 620-630).

How can I resolve this? If you propose a fix, please make it concise.

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 @messages/zh-TW/settings.json:
- Around line 610-620: The translation for the 1-hour cache tier is
inconsistent: the key cache1h is "1h+" but related form labels like
cacheCreationPrice1h (and the same pattern at the other mentioned ranges) show
"1h" — update the displayed strings so meaning is consistent (use "1h+"
everywhere if the tier means "1 hour or more"); locate and change the values for
cache1h, cacheCreationPrice1h and the duplicated entries at the referenced
ranges (lines ~663-664, 684-699, 701-706) to use the same "1h+" wording so the
UI and form labels match.
🧹 Nitpick comments (4)
tests/unit/lib/price-data-input-cost-per-request.test.ts (1)

1-20: 建议补一条边界用例:0 / NaN / Infinity

当前两条用例能覆盖主要分支;为了避免“可选数值字段”在边界值上回归,建议再补:

  • input_cost_per_request: 0 应为 true
  • NaN/Infinity 应为 false(与 Number.isFinite 规则对齐)
src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx (2)

103-103: 控制台错误消息应使用英文或考虑移除中文硬编码

控制台日志中包含硬编码的中文字符串 "获取云端模型数量失败:"。虽然控制台消息通常不面向最终用户,但为了代码库的一致性和国际化团队的可维护性,建议使用英文或将其完全移除。

建议的修复
-        console.error("获取云端模型数量失败:", error);
+        console.error("Failed to fetch cloud model count:", error);

260-269: 考虑简化嵌套三元运算符以提高可读性

当前的嵌套三元运算符虽然功能正确,但可读性略显复杂。可以考虑提取为独立函数或使用早期返回模式来提高代码的可维护性。

可选的重构方案
+                const getModelCountText = () => {
+                  if (cloudModelCountStatus === "loading") {
+                    return t("dialog.cloudModelCountLoading");
+                  }
+                  if (cloudModelCountStatus === "loaded") {
+                    return t("dialog.supportedModels", { count: cloudModelCount ?? 0 });
+                  }
+                  if (cloudModelCountStatus === "error") {
+                    return t("dialog.cloudModelCountFailed");
+                  }
+                  return t("dialog.supportedModels", { count: "-" });
+                };
+
                 <p>
                   •{" "}
-                  {cloudModelCountStatus === "loading"
-                    ? t("dialog.cloudModelCountLoading")
-                    : cloudModelCountStatus === "loaded"
-                      ? t("dialog.supportedModels", { count: cloudModelCount ?? 0 })
-                      : cloudModelCountStatus === "error"
-                        ? t("dialog.cloudModelCountFailed")
-                        : t("dialog.supportedModels", { count: "-" })}
+                  {getModelCountText()}
                 </p>
src/app/[locale]/settings/prices/_components/model-price-drawer.tsx (1)

520-591: 建议:缓存价格输入区域在 supportsPromptCaching 为 false 时可考虑折叠或隐藏。

当前实现中,即使 supportsPromptCaching 为 false,缓存价格输入区域仍然可见(只是被禁用)。从 UX 角度,可以考虑完全隐藏该区域以减少视觉干扰,或者保持当前实现以便用户了解该功能的存在。

📜 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 373220d and cf6b2fd.

📒 Files selected for processing (22)
  • 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/model-price-drawer.tsx
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx
  • src/app/[locale]/settings/prices/page.tsx
  • src/app/api/prices/cloud-model-count/route.ts
  • src/lib/utils/cost-calculation.ts
  • src/lib/utils/price-data.ts
  • src/types/model-price.ts
  • tests/unit/lib/cost-calculation-input-cost-per-request.test.ts
  • tests/unit/lib/price-data-input-cost-per-request.test.ts
  • tests/unit/settings/prices/delete-model-dialog.test.tsx
  • tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx
  • tests/unit/settings/prices/price-list-interactions.test.tsx
  • tests/unit/settings/prices/price-list-ui-requirements.test.tsx
  • tests/unit/settings/prices/upload-price-dialog-cloud-model-count.test.tsx
  • tests/unit/settings/prices/upload-price-dialog-upload-flow.test.tsx
🧰 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/lib/price-data-input-cost-per-request.test.ts
  • src/lib/utils/price-data.ts
  • src/types/model-price.ts
  • tests/unit/settings/prices/price-list-ui-requirements.test.tsx
  • tests/unit/settings/prices/upload-price-dialog-upload-flow.test.tsx
  • tests/unit/settings/prices/price-list-interactions.test.tsx
  • tests/unit/lib/cost-calculation-input-cost-per-request.test.ts
  • tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx
  • tests/unit/settings/prices/upload-price-dialog-cloud-model-count.test.tsx
  • src/app/[locale]/settings/prices/_components/model-price-drawer.tsx
  • src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx
  • src/app/api/prices/cloud-model-count/route.ts
  • src/lib/utils/cost-calculation.ts
  • tests/unit/settings/prices/delete-model-dialog.test.tsx
  • src/actions/model-prices.ts
  • src/app/[locale]/settings/prices/page.tsx
  • src/app/[locale]/settings/prices/_components/price-list.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:

  • tests/unit/lib/price-data-input-cost-per-request.test.ts
  • src/lib/utils/price-data.ts
  • src/types/model-price.ts
  • tests/unit/settings/prices/price-list-ui-requirements.test.tsx
  • tests/unit/settings/prices/upload-price-dialog-upload-flow.test.tsx
  • tests/unit/settings/prices/price-list-interactions.test.tsx
  • tests/unit/lib/cost-calculation-input-cost-per-request.test.ts
  • tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx
  • tests/unit/settings/prices/upload-price-dialog-cloud-model-count.test.tsx
  • src/app/[locale]/settings/prices/_components/model-price-drawer.tsx
  • src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx
  • src/app/api/prices/cloud-model-count/route.ts
  • src/lib/utils/cost-calculation.ts
  • tests/unit/settings/prices/delete-model-dialog.test.tsx
  • src/actions/model-prices.ts
  • src/app/[locale]/settings/prices/page.tsx
  • src/app/[locale]/settings/prices/_components/price-list.tsx
**/*.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/lib/price-data-input-cost-per-request.test.ts
  • tests/unit/settings/prices/price-list-ui-requirements.test.tsx
  • tests/unit/settings/prices/upload-price-dialog-upload-flow.test.tsx
  • tests/unit/settings/prices/price-list-interactions.test.tsx
  • tests/unit/lib/cost-calculation-input-cost-per-request.test.ts
  • tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx
  • tests/unit/settings/prices/upload-price-dialog-cloud-model-count.test.tsx
  • tests/unit/settings/prices/delete-model-dialog.test.tsx
**/*.{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-ui-requirements.test.tsx
  • tests/unit/settings/prices/upload-price-dialog-upload-flow.test.tsx
  • tests/unit/settings/prices/price-list-interactions.test.tsx
  • tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx
  • tests/unit/settings/prices/upload-price-dialog-cloud-model-count.test.tsx
  • src/app/[locale]/settings/prices/_components/model-price-drawer.tsx
  • src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx
  • tests/unit/settings/prices/delete-model-dialog.test.tsx
  • src/app/[locale]/settings/prices/page.tsx
  • src/app/[locale]/settings/prices/_components/price-list.tsx
🧠 Learnings (9)
📚 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/lib/utils/price-data.ts
  • src/types/model-price.ts
  • src/app/api/prices/cloud-model-count/route.ts
  • src/lib/utils/cost-calculation.ts
  • 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 **/*.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/upload-price-dialog-upload-flow.test.tsx
  • tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.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:

  • tests/unit/settings/prices/upload-price-dialog-upload-flow.test.tsx
  • src/app/[locale]/settings/prices/_components/model-price-drawer.tsx
  • src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx
  • tests/unit/settings/prices/delete-model-dialog.test.tsx
  • src/app/[locale]/settings/prices/page.tsx
  • src/app/[locale]/settings/prices/_components/price-list.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:

  • tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx
  • src/app/[locale]/settings/prices/_components/model-price-drawer.tsx
  • src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx
  • src/actions/model-prices.ts
  • messages/zh-CN/settings.json
  • src/app/[locale]/settings/prices/page.tsx
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • messages/ja/settings.json
  • messages/en/settings.json
  • messages/zh-TW/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:

  • tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx
  • src/app/[locale]/settings/prices/_components/model-price-drawer.tsx
  • tests/unit/settings/prices/delete-model-dialog.test.tsx
  • messages/zh-CN/settings.json
  • src/app/[locale]/settings/prices/page.tsx
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • messages/ja/settings.json
  • messages/zh-TW/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
📚 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:

  • messages/zh-CN/settings.json
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • messages/ja/settings.json
  • messages/en/settings.json
  • messages/zh-TW/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 **/*.{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-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.json
  • messages/ru/settings.json
🧬 Code graph analysis (13)
tests/unit/lib/price-data-input-cost-per-request.test.ts (1)
src/lib/utils/price-data.ts (1)
  • hasValidPriceData (7-41)
tests/unit/settings/prices/price-list-ui-requirements.test.tsx (2)
src/types/model-price.ts (1)
  • ModelPrice (64-71)
src/app/[locale]/settings/prices/_components/price-list.tsx (1)
  • PriceList (69-775)
tests/unit/lib/cost-calculation-input-cost-per-request.test.ts (1)
src/lib/utils/cost-calculation.ts (1)
  • calculateRequestCost (106-293)
tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx (1)
src/types/model-price.ts (1)
  • ModelPrice (64-71)
tests/unit/settings/prices/upload-price-dialog-cloud-model-count.test.tsx (1)
src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx (1)
  • UploadPriceDialog (56-350)
src/app/[locale]/settings/prices/_components/model-price-drawer.tsx (2)
src/types/model-price.ts (1)
  • ModelPrice (64-71)
src/actions/model-prices.ts (1)
  • upsertSingleModelPrice (471-565)
src/app/[locale]/settings/prices/_components/upload-price-dialog.tsx (1)
scripts/sync-settings-keys.js (2)
  • p (102-102)
  • t (72-72)
src/app/api/prices/cloud-model-count/route.ts (2)
src/lib/auth.ts (1)
  • getSession (116-128)
src/lib/price-sync/cloud-price-table.ts (2)
  • fetchCloudPriceTableToml (53-107)
  • parseCloudPriceTableToml (18-51)
src/lib/utils/cost-calculation.ts (2)
src/lib/utils/index.ts (1)
  • toDecimal (20-20)
src/lib/utils/currency.ts (1)
  • toDecimal (46-65)
tests/unit/settings/prices/delete-model-dialog.test.tsx (1)
src/app/[locale]/settings/prices/_components/delete-model-dialog.tsx (1)
  • DeleteModelDialog (30-92)
src/actions/model-prices.ts (1)
src/types/model-price.ts (1)
  • ModelPriceData (4-54)
src/app/[locale]/settings/prices/page.tsx (1)
src/app/[locale]/settings/prices/_components/model-price-drawer.tsx (1)
  • ModelPriceDrawer (76-609)
src/app/[locale]/settings/prices/_components/price-list.tsx (2)
src/lib/utils/clipboard.ts (1)
  • copyToClipboard (18-30)
src/app/[locale]/settings/prices/_components/model-price-drawer.tsx (1)
  • ModelPriceDrawer (76-609)
⏰ 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). (5)
  • GitHub Check: 🌐 API Tests
  • GitHub Check: pr-review
  • GitHub Check: pr-description
  • GitHub Check: check-codex-status
  • GitHub Check: Docker Build Test
🔇 Additional comments (34)
src/types/model-price.ts (1)

4-9: 类型与注释 OK;确认是否需要接纳 null(取决于数据源)

input_cost_per_request?: number 的语义和命名与现有字段保持一致,注释也清楚。唯一需要确认的是:如果该字段来自 DB/外部 JSON 可能出现 null,是否要显式建模为 number | null 以区分 “未配置/空值”。(当前其它 cost 字段也未包含 null,所以若全局约定是不使用 null,则保持现状即可。)

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

1338-1348: 建议把“缓存创建价格(1h,...)”改为“1h+”与表格列一致

表格列 cache1h: "1h+" 已明确“1h 以上”,但表单 cacheCreationPrice1h: "缓存创建价格(1h,$/M tokens)" 少了 “+”,可能导致用户按“恰好 1h”理解。

Also applies to: 1391-1392, 1412-1427, 1429-1434

⛔ Skipped due to 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.
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.
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.
messages/en/settings.json (1)

619-629: 表格列的 cache1h 和表单字段的 cacheCreationPrice1h 在时间范围表示上需保持一致

  • 表格显示 cache1h: "1h+" 但表单显示 cacheCreationPrice1h: "Cache Creation Price (1h, $/M tokens)"(缺少加号)。建议统一使用 "1h+" 以准确表示"1小时及以上"的语义,或调整表格移除加号保持一致。

  • 缩写标签 priceInput: "In"priceOutput: "Out"pricePerRequest: "Req" 较为简短,若 UI 未提供额外说明(tooltip/aria-label),建议确保在组件层面添加辅助标签,这里先不改文案。

  • 已验证:各语言(en、zh-CN、ja)的 prices.tableprices.form key 集合保持一致,JSON 格式合法。

⛔ Skipped due to learnings
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.
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.
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: 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.
src/lib/utils/price-data.ts (1)

11-11: 代码变更正确。

input_cost_per_request 添加到数值成本验证数组中,与其他成本字段的处理方式保持一致,确保了新增的按次调用价格字段能够被正确验证。

src/lib/utils/cost-calculation.ts (1)

116-127: 实现正确且符合最佳实践。

按次调用价格的处理逻辑:

  • 正确验证了字段类型、有限性和非负性
  • 使用 toDecimal 转换并处理可能的 null 返回值
  • 将成本段添加到总费用计算中

代码逻辑清晰,与现有的成本计算模式保持一致。

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

6-6: UI 组件替换正确。

将价格创建界面从对话框(Dialog)改为抽屉式(Drawer),符合 PR 的 UI 改进目标。组件导入和使用方式正确,mode="create" 属性传递正确。

Also applies to: 89-89

tests/unit/lib/cost-calculation-input-cost-per-request.test.ts (1)

1-62: 测试覆盖全面且正确。

测试用例涵盖了以下场景:

  • 仅配置按次调用价格
  • 按次调用价格与 token 计费叠加
  • 倍率同时作用于所有费用类型
  • 无效值(负数、NaN)被正确忽略

测试断言准确,符合预期行为。

src/app/api/prices/cloud-model-count/route.ts (2)

8-27: 实现逻辑正确,错误处理得当。

API 路由实现:

  • 正确进行管理员权限校验
  • 错误处理使用了合适的 HTTP 状态码(403/502/500)
  • 模型数量统计逻辑简洁明了

12-12: 验证 API 路由的错误消息是否需要国际化。

Line 12 使用了硬编码的中文错误消息 "无权限访问此资源"。根据代码规范,所有面向用户的字符串应使用 i18n(支持 5 种语言)。

虽然学习记录显示 Server Actions 中故意使用硬编码中文消息作为架构决策,但该文件是 API 路由而非 Server Action。请确认 API 路由是否也遵循相同的模式,或者是否应该使用国际化。

基于代码规范的要求。

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

83-116: 取消令牌模式实现良好

useEffect 中的清理逻辑使用 cancelled 标志正确处理了组件卸载时的竞态条件,避免了在组件已卸载后更新状态。这是处理异步操作的最佳实践。

tests/unit/settings/prices/delete-model-dialog.test.tsx (1)

1-161: 测试结构和覆盖率良好

此测试文件展示了良好的测试实践:

  • 正确的模拟策略(使用 hoisted mocks)
  • 完整的成功和失败路径覆盖
  • 适当的清理逻辑(beforeEach 中清除模拟和重置 DOM)
  • 验证了副作用(toast 调用、onSuccess 回调、price-data-updated 事件)

测试全面验证了 DeleteModelDialog 组件的删除流程。

tests/unit/settings/prices/price-list-ui-requirements.test.tsx (3)

65-121: UI 需求验证全面,与 PR 目标一致

测试正确验证了 PR 中描述的 UI 变更:

  • 确认存在合并后的"Price"列以及新增的"Cache Read"和"Cache Create"列
  • 确认移除了独立的"Provider"、"Input Price"和"Output Price"列

这与 PR 目标中提到的"合并输入/输出价格列,新增缓存读取/缓存创建列;移除提供商列"完全对应。


123-168: Provider badge 和模型类型 badge 显示逻辑验证正确

测试准确验证了 UI 变更:

  • 提供商以 badge 形式显示在模型名称列中
  • 不再显示模型类型 badge(Completion/Image/Unknown)

这与 PR 目标"在模型名处用 badge 展示;移除模型类型 badge"一致。


170-271: 剪贴板交互测试覆盖全面

两个测试用例完整覆盖了模型 ID 复制功能的成功和失败场景:

  • 成功时调用 copyToClipboard 并显示成功 toast
  • 失败时显示错误 toast

验证了 PR 目标中的"模型 id 支持点击复制"功能。

tests/unit/settings/prices/upload-price-dialog-cloud-model-count.test.tsx (1)

67-107: 异步加载状态测试实现优秀

测试使用手动控制的 Promise 来精确验证加载状态的转换过程,这是测试异步 UI 行为的良好实践:

  1. 首先断言加载状态文本的显示
  2. 手动触发 fetch 响应的解析
  3. 等待 Promise 队列刷新后断言最终的加载完成状态

此测试有效覆盖了 PR 目标中的"增加异步统计云端模型数量"功能。

tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx (2)

76-151: 搜索预填充功能测试完整

测试全面验证了新的自定义模型编辑器的搜索和预填充功能:

  • 模拟用户在搜索框输入
  • 验证 fetch 调用
  • 模拟选择搜索结果
  • 断言字段正确预填充(modelName、displayName、provider)

这对应 PR 目标中的"新增/编辑界面改为右侧抽屉并支持搜索现有模型以预填充"。


153-257: 价格转换逻辑验证严谨

测试精确验证了提交时的价格字段转换:

  • $/M 到每 token 的转换(lines 245-246)
  • 按次价格不进行转换(line 249)
  • 缓存价格的 $/M 到每 token 转换(lines 252-254)

使用 toBeCloseTo 进行浮点数比较是正确的做法,避免了精度问题。测试覆盖了 PR 中提到的"支持配置是否支持缓存、缓存读取/创建价格、按次调用价格"功能。

tests/unit/settings/prices/price-list-interactions.test.tsx (1)

1-385: 测试文件结构良好,覆盖了关键交互场景。

测试用例涵盖了筛选、分页、页面大小切换、防抖搜索和事件监听等核心功能,模拟和清理逻辑也很完善。

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

453-466: 新增字段定义清晰,类型安全。

SingleModelPriceInput 接口扩展合理,新增的缓存价格和按次调用价格字段与 ModelPriceData 类型保持一致。


505-529: 验证逻辑完整且一致。

新增字段的验证模式与现有验证保持一致,正确检查了 undefined、负数和非有限数值。


531-544: 字段映射正确。

新增字段正确映射到 priceData 对象,字段命名符合 snake_case 约定。

tests/unit/settings/prices/upload-price-dialog-upload-flow.test.tsx (1)

1-427: 上传流程测试覆盖全面。

测试用例覆盖了文件类型校验、大小校验、成功/失败流程、上传中状态阻止关闭以及导航跳转等场景,边界情况处理得当。

src/app/[locale]/settings/prices/_components/model-price-drawer.tsx (2)

50-71: 价格转换辅助函数实现正确。

parsePricePerMillionToPerTokenparsePriceformatPerTokenPriceToPerMillion 正确处理了空值、负数和非有限数值的边界情况。


173-219: 预填充搜索逻辑健壮。

使用 cancelled 标志防止组件卸载后的状态更新,避免了 React 状态更新警告。cache: "no-store" 确保实时获取最新数据。

messages/ja/settings.json (1)

610-706: 日语翻译完整,与其他语言文件结构一致。

新增的价格相关翻译键(table.pricedrawer 区块等)已正确添加,文案专业准确。

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

249-278: 价格格式化辅助函数实现合理。

formatScalarPrice 正确处理了 undefined、非有限数和负数的边界情况。formatPerMillionTokenPriceLabelformatPerImagePriceLabelformatPerRequestPriceLabel 保持了一致的格式化模式。


280-290: 复制模型 ID 功能实现正确。

使用 copyToClipboard 工具函数并通过 toast 提供用户反馈,符合 UX 最佳实践。useCallback 的依赖项 [tCommon] 正确。


484-498: 模型 ID 复制按钮的可访问性良好。

使用 <button> 元素配合 aria-label 和 Tooltip 提供了良好的可访问性支持。focus-visible 样式确保了键盘导航的可见性。


529-564: 价格显示布局清晰。

输入/输出价格的分行显示提高了可读性,按次调用价格的条件渲染逻辑正确(仅在有效时显示)。


619-637: 成功切换到 ModelPriceDrawer。

ModelPriceDialog 切换到 ModelPriceDrawer 的实现正确,onSuccess 回调保持了数据刷新逻辑。

messages/ru/settings.json (4)

610-620: 价格表列标签翻译正确。

新增的表格列标签翻译准确,使用了适合紧凑显示的简短形式。Lines 613-615("Ввод"、"Вывод"、"Запрос")和 lines 618-619("5m"、"1h+")的极简标签在表格上下文中应该足够清晰。缓存相关术语使用一致("кэш")。


663-665: 云端模型数量加载状态翻译准确。

新增的加载和错误状态翻译语法正确,清晰表达了异步操作的状态反馈。


684-698: 表单字段翻译准确且语义清晰。

关键变更包括:

  1. modelName 标签改为"ID 模型",与新增的可选 displayName 字段形成清晰对比
  2. 新增的按次调用价格和缓存价格字段翻译准确
  3. 缓存价格字段正确区分不同 TTL(5m, 1h)
  4. 图像输出价格使用恰当的单位"$/изображение"

翻译与 PR 目标完全一致,术语使用规范。


700-706: 抽屉组件翻译完整且用户友好。

新增的抽屉组件翻译涵盖了预填充和缓存配置功能:

  • 预填充相关文本清晰表达了搜索和自动填充的功能
  • promptCachingHint 提供了有价值的使用指导,提醒用户仅在模型支持时启用
  • 所有状态消息(空状态、错误状态)表达准确

翻译质量良好,符合用户体验最佳实践。

Comment on lines +610 to +620
"price": "價格",
"inputPrice": "輸入價格 ($/M)",
"outputPrice": "輸出價格 ($/M)",
"priceInput": "輸入",
"priceOutput": "輸出",
"pricePerRequest": "按次",
"cacheReadPrice": "快取讀取 ($/M)",
"cacheCreationPrice": "快取建立 ($/M)",
"cache5m": "5m",
"cache1h": "1h+",
"copyModelId": "複製模型 ID",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

建议与英文保持相同的“1h+”语义标注

当前表格为 cache1h: "1h+",但表单字段 cacheCreationPrice1h 文案是“1h”;若确实表示 “1h 以上”阶梯价,建议同步改成“1h+”以对齐。

Also applies to: 663-664, 684-699, 701-706

🤖 Prompt for AI Agents
In @messages/zh-TW/settings.json around lines 610 - 620, The translation for the
1-hour cache tier is inconsistent: the key cache1h is "1h+" but related form
labels like cacheCreationPrice1h (and the same pattern at the other mentioned
ranges) show "1h" — update the displayed strings so meaning is consistent (use
"1h+" everywhere if the tier means "1 hour or more"); locate and change the
values for cache1h, cacheCreationPrice1h and the duplicated entries at the
referenced ranges (lines ~663-664, 684-699, 701-706) to use the same "1h+"
wording so the UI and form labels match.

return () => {
cancelled = true;
};
}, [mode, open, prefillQuery]);
Copy link
Contributor

Choose a reason for hiding this comment

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

[HIGH] [PERFORMANCE-ISSUE] Prefill search triggers API fetch on every keystroke without debouncing

Why this is a problem: The prefill search field (lines 173-219) triggers a fetch request immediately on every character typed. When a user types quickly (e.g., "gpt-4-turbo"), this creates 11 sequential API requests, overwhelming the server and degrading UX with unnecessary loading states.

Evidence of inconsistency: The main price list component already uses debouncing correctly:

  • price-list.tsx:52 - const debouncedSearchTerm = useDebounce(searchTerm, 500);
  • This same pattern should be applied to the prefill search

Suggested fix:

// Add at top of component with other imports
import { useDebounce } from "@/lib/hooks/use-debounce";

// In ModelPriceDrawer component, after line 105:
const debouncedPrefillQuery = useDebounce(prefillQuery, 500);

// Then change the useEffect dependency on line 219 from:
}, [mode, open, prefillQuery]);

// To:
}, [mode, open, debouncedPrefillQuery]);

// And update the query variable on line 178:
const query = debouncedPrefillQuery.trim();

This will batch rapid keystrokes into a single request after 500ms of inactivity, matching the proven pattern already used in this codebase.

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

Comprehensive multi-perspective review of PR #583 completed. This PR adds custom model pricing configuration UI with cache pricing support and per-request pricing.

PR Size: XL

  • Lines changed: 2,769 (2,707 additions + 62 deletions)
  • Files changed: 22
  • Recommendation: This XL-sized PR could benefit from being split in future similar changes:
    • Part 1: Backend actions + types + cost calculation logic
    • Part 2: UI components (ModelPriceDrawer + PriceList updates)
    • Part 3: i18n translations + tests
    • This would make reviews more manageable and reduce risk of merge conflicts

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
Performance 0 1 0 0

High Priority Issue (Should Fix)

1. Missing Debouncing on Prefill Search (Confidence: 95/100)

  • Location: model-price-drawer.tsx:173-219
  • Impact: Triggers API fetch on every keystroke, creating unnecessary load and poor UX
  • Evidence: price-list.tsx:52 already uses useDebounce(searchTerm, 500) for similar functionality
  • Recommendation: Apply the same debouncing pattern to maintain consistency and prevent API spam
  • See inline comment for detailed fix

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Excellent (proper cancellation, validation at multiple layers)
  • Type safety - Clean (comprehensive TypeScript interfaces)
  • Documentation accuracy - Clean
  • Test coverage - Excellent (80%+ coverage with edge cases)
  • Code clarity - Good (clear naming, proper separation of concerns)

Validation Notes

What was checked:

  1. Comment Analyzer: All comments match code behavior, no outdated documentation
  2. Test Analyzer: Comprehensive test coverage including:
    • Cost calculation with input_cost_per_request
    • Cache pricing validation
    • UI requirements (column structure, badge display)
    • Drawer prefill functionality
    • Zero price handling
  3. Silent Failure Hunter:
    • All errors properly logged (console.error)
    • Fetch cancellation properly implemented (line 216-218)
    • User feedback via toast notifications
  4. Type Design Auditor:
    • No any usage
    • Proper validation in upsertSingleModelPrice (lines 502-529)
    • Strong type safety with TypeScript interfaces
  5. General Code Reviewer:
    • SQL injection safe (using drizzle-orm parameterization)
    • No hardcoded secrets
    • Proper input validation (non-negative, finite numbers)
    • i18n compliance (all 5 languages supported)
  6. Code Simplifier: Code is clear and readable, no unnecessary complexity

False Positives Filtered:

  • Initial concern about missing error handling in cloud model count API → Dismissed: Proper error handling exists at lines 16-18 and 20-23
  • Initial concern about hardcoded "Claude + OpenAI" in upload dialog → Dismissed: This is intentional fallback text pending async fetch, not a bug

Architecture Strengths

  • ✅ Comprehensive validation at multiple layers (UI parsing, action validation, cost calculation)
  • ✅ Proper SQL parameterization using drizzle-orm
  • ✅ Async cloud model count fetch with proper cancellation
  • ✅ Consistent error handling patterns
  • ✅ Strong test coverage (80%+ with edge cases)
  • ✅ i18n compliance across all 5 languages
  • ✅ Type safety throughout

Recommendation

Approve with minor fix: This PR is well-architected and thoroughly tested. The single performance issue identified (missing debouncing) is a minor optimization that should be addressed but does not block merge. All critical aspects (security, error handling, type safety, test coverage) are excellent.


Automated review by Claude AI - Reviewed through 6 specialized perspectives with 80+ confidence threshold

const trimmed = value.trim();
if (!trimmed) return undefined;
const parsed = Number.parseFloat(trimmed);
if (!Number.isFinite(parsed) || parsed < 0) return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

[HIGH] [LOGIC-BUG] Invalid numeric input can be silently dropped and clear saved prices

Why this is a problem: In src/app/[locale]/settings/prices/_components/model-price-drawer.tsx:54 you currently treat negative/NaN as empty:

  • if (!Number.isFinite(parsed) || parsed < 0) return undefined;

In handleSubmit, those undefined values are sent to upsertSingleModelPrice, and the stored priceData is replaced. This means an invalid entry (e.g. -1) can remove an existing saved price while still showing a success toast.

Suggested fix:

function parsePricePerMillionToPerToken(value: string): number | undefined {
  const trimmed = value.trim();
  if (!trimmed) return undefined;
  return Number.parseFloat(trimmed) / 1000000;
}

function parsePrice(value: string): number | undefined {
  const trimmed = value.trim();
  if (!trimmed) return undefined;
  return Number.parseFloat(trimmed);
}

This way, negative/NaN values reach the server-side validation (and can be rejected) instead of being treated as “empty”.

});

if (!result.ok) {
toast.error(result.error);
Copy link
Contributor

Choose a reason for hiding this comment

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

[HIGH] [STANDARD-VIOLATION] User-facing error messages bypass i18n

Why this is a problem: In src/app/[locale]/settings/prices/_components/model-price-drawer.tsx:265 the UI renders the server-provided string directly:

  • toast.error(result.error);

This violates CLAUDE.md:14:

  • **i18n Required** - All user-facing strings must use i18n (5 languages supported). Never hardcode display text

Because upsertSingleModelPrice currently returns non-localized strings, this will leak e.g. Chinese validation messages into non-Chinese locales.

Suggested fix:

import { getErrorMessage } from "@/lib/utils/error-messages";

const tErrors = useTranslations("errors");

// ...
if (!result.ok) {
  const message = result.errorCode
    ? getErrorMessage(tErrors, result.errorCode, result.errorParams)
    : t("toast.saveFailed");

  toast.error(message);
  return;
}

Follow-up: for new validations in src/actions/model-prices.ts, return errorCode/errorParams so the client can render a localized message.

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 is XL and mixes pricing data model changes, new pricing logic (per-request + caching), UI refactors, and extensive tests in one diff. Two high-impact issues remain in the new model price drawer: invalid numeric input can silently clear existing prices, and server-provided error messages are displayed without i18n.

PR Size: XL

  • Lines changed: 2769
  • Files changed: 22
  • Split suggestion: separate into (1) data/types + cost/price utils, (2) settings/prices UI refactor, (3) i18n message updates + tests.

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 1 0 0
Security 0 0 0 0
Error Handling 0 1 0 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)

  • src/app/[locale]/settings/prices/_components/model-price-drawer.tsx:54 (confidence 85) - [LOGIC-BUG] parsing drops invalid values to undefined, which can clear saved prices without user-visible error.
  • src/app/[locale]/settings/prices/_components/model-price-drawer.tsx:265 + CLAUDE.md:14 (confidence 90) - [STANDARD-VIOLATION] toast.error(result.error) displays non-localized server strings, violating i18n requirements.

Review Coverage

  • Logic and correctness
  • Security (OWASP Top 10)
  • Error handling
  • Type safety
  • Documentation accuracy
  • Test coverage
  • Code clarity

Automated review by Codex AI

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.

  • Identified PR #583 and applied label size/XL (2769 lines changed across 22 files).
  • Posted 2 inline review comments on the diff:
    • src/app/[locale]/settings/prices/_components/model-price-drawer.tsx:54[HIGH][LOGIC-BUG] numeric parsing drops invalid values to undefined, which can silently clear saved prices.
    • src/app/[locale]/settings/prices/_components/model-price-drawer.tsx:265 + CLAUDE.md:14[HIGH][STANDARD-VIOLATION] toast.error(result.error) bypasses i18n for user-facing errors.
  • Submitted the required PR review summary via gh pr review.

@github-actions
Copy link
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

@ding113 ding113 merged commit 55dfd21 into dev Jan 10, 2026
8 of 9 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Jan 10, 2026
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 (2)
messages/zh-TW/settings.json (1)

606-621: prices.table 新增列文案覆盖完整,建议统一 zh-TW 术语风格(繁体/用词)
新增 key 本身 OK;不过该文件中存在“用戶/用户、啟用/启用”等繁简混用的历史痕迹,建议后续集中做一次术语/繁简一致性校对,避免同一页面出现两套风格。

messages/ja/settings.json (1)

606-621: 建议改进日文"回"的表述,使其更清晰直观

表价格表中的 pricePerRequest 字段当前翻译为"回"(意为"次数")。虽然在定价上下文中能理解,但相比中文的"按次"(更明确)和英文的"Req"(更熟悉的缩写),日文"回"较为简洁且略显抽象。

根据代码检查,UI 中该标签旁边没有"/request"等额外的单位说明文本,仅以标签-价格对的形式呈现。建议考虑以下改进:

  • 改为"リクエスト当たり"(per request,更明确)或其他更清晰的表述
  • 或确认 UI 其他位置有足够的上下文说明这一字段含义
🤖 Fix all issues with AI agents
In
@tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx:
- Around line 67-73: The helper setReactInputValue currently assumes
Object.getOwnPropertyDescriptor(prototype, "value") exists and silently does
nothing if descriptor is missing; update setReactInputValue to fall back to
directly assigning input.value = value when descriptor?.set is undefined (still
dispatching the "input" and "change" events afterward) and ensure it handles
readonly descriptors safely (e.g., try/catch around the assignment) so the value
change always takes effect in tests; reference the setReactInputValue function
and keep the existing event dispatches.
🧹 Nitpick comments (6)
tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx (1)

279-289: 提交按钮定位不要依赖可见文本(对 i18n/文案变更敏感)
建议用更稳定的选择器(例如 data-testid / aria-label / role+name(来自 messages)),否则改一下英文文案就会让测试无意义失败。

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

51-51: 验证 copyToClipboard 工具函数的错误日志。

根据提供的代码片段,copyToClipboard 函数在 src/lib/utils/clipboard.ts 中包含硬编码的中文错误日志(console.error("复制失败:", err))。虽然这是客户端控制台日志而非用户界面文本,但为了代码库的一致性和国际化,建议考虑使用英文或移除这些调试日志。


175-175: 建议使用英文错误日志以保持一致性。

虽然 console.error 不是面向用户的界面文本,但为了代码库的国际化和可维护性,建议将错误日志改为英文,例如 "Failed to fetch price data:"


241-255: formatPrice 缺少健壮的输入验证。

formatScalarPrice(Line 261-262)对输入值进行了严格验证(typeof value !== "number" || !Number.isFinite(value) || value < 0),但 formatPrice 只进行了宽松的 null 检查。为了一致性和健壮性,建议为 formatPrice 添加类似的验证。

♻️ 建议的改进
 const formatPrice = (value?: number): string => {
-  if (value === undefined || value === null) return "-";
+  if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
+    return "-";
+  }
   // 将每token的价格转换为每百万token的价格
   const pricePerMillion = value * 1000000;

241-286: 考虑将价格格式化函数提取到工具模块。

formatPrice、formatScalarPrice、formatPerMillionTokenPriceLabel、formatPerImagePriceLabel 和 formatPerRequestPriceLabel 这些函数不依赖于组件的 props 或 state,可以提取到 src/lib/utils/ 下的独立工具模块中(例如 price-formatting.ts),以提高代码复用性和可测试性。


300-323: 考虑将 capabilityItems 移到组件外部。

capabilityItems 数组不依赖任何组件内部状态或 props,可以移到组件外部作为常量定义,避免每次渲染时重新创建。虽然性能影响很小,但这是更好的实践。

注意:i18n 标签需要在组件内部调用,所以只能移动键和图标的映射,标签文本仍需在组件内生成。

📜 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 cf6b2fd and 2f7ab7d.

📒 Files selected for processing (9)
  • messages/en/settings.json
  • messages/ja/settings.json
  • messages/ru/settings.json
  • messages/zh-CN/settings.json
  • messages/zh-TW/settings.json
  • src/app/[locale]/settings/prices/_components/model-price-drawer.tsx
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • tests/unit/lib/price-data-input-cost-per-request.test.ts
  • tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • messages/en/settings.json
  • messages/zh-CN/settings.json
  • tests/unit/lib/price-data-input-cost-per-request.test.ts
  • src/app/[locale]/settings/prices/_components/model-price-drawer.tsx
🧰 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/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx
  • src/app/[locale]/settings/prices/_components/price-list.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:

  • tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx
  • src/app/[locale]/settings/prices/_components/price-list.tsx
**/*.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/model-price-drawer-prefill-and-submit-ui.test.tsx
**/*.{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/model-price-drawer-prefill-and-submit-ui.test.tsx
  • src/app/[locale]/settings/prices/_components/price-list.tsx
🧠 Learnings (9)
📓 Common learnings
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.
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-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:

  • messages/ru/settings.json
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • messages/zh-TW/settings.json
  • messages/ja/settings.json
📚 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/ru/settings.json
  • messages/ja/settings.json
📚 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:

  • messages/ru/settings.json
  • tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • messages/zh-TW/settings.json
  • messages/ja/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:

  • tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • messages/zh-TW/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 **/*.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/model-price-drawer-prefill-and-submit-ui.test.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.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: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:

  • messages/zh-TW/settings.json
🧬 Code graph analysis (2)
tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx (1)
src/types/model-price.ts (1)
  • ModelPrice (64-71)
src/app/[locale]/settings/prices/_components/price-list.tsx (2)
src/lib/utils/clipboard.ts (1)
  • copyToClipboard (18-30)
src/app/[locale]/settings/prices/_components/model-price-drawer.tsx (1)
  • ModelPriceDrawer (77-620)
⏰ 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). (3)
  • GitHub Check: 🌐 API Tests
  • GitHub Check: Docker Build Test
  • GitHub Check: dev-build-deploy
🔇 Additional comments (18)
messages/zh-TW/settings.json (3)

663-665: cloud model count 状态文案齐全
loading/failed 两个状态都有覆盖,符合 UploadPriceDialog 异步加载场景。


684-699: prices.form 字段文案与新增定价模型匹配
模型 ID、显示名称、按次价格、cache 读/建价的文案补齐,整体一致。


700-706: drawer 的 prefill/caching 引导文案齐全
可直接支撑 create 模式的搜索预填充与 prompt caching 配置引导。

messages/ja/settings.json (3)

663-665: cloud model count 状态文案覆盖 OK


684-699: form 文案与新增字段一致


700-706: drawer 预填充/缓存引导文案 OK

messages/ru/settings.json (4)

606-621: prices.table 新增列文案覆盖完整


663-665: cloud model count 状态文案覆盖 OK


684-699: form 新增字段文案 OK


700-706: drawer 预填充/缓存引导文案 OK

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

184-198: LGTM!

pendingRefreshPage 机制很好地解决了删除操作后的分页同步问题。当删除最后一页的最后一项时,会先设置目标页码并更新 URL,然后在事件监听器中使用该页码进行刷新。实现逻辑清晰正确。


288-298: LGTM!

handleCopyModelId 实现正确,使用了 i18n 翻译键提供用户反馈,并正确处理了成功/失败两种情况。依赖数组也配置正确。


452-460: LGTM!

表头结构符合 PR 目标,成功合并了输入/输出价格列并添加了缓存相关列。所有文本都正确使用了 i18n。


478-507: LGTM!

模型名称单元格的实现优秀:

  • provider badge 正确地移入了模型名称列
  • 模型 ID 的复制功能实现完善,包含了无障碍标签和工具提示
  • 按钮样式提供了良好的悬停和焦点状态

537-573: LGTM!

价格单元格的重构实现了 PR 目标:

  • 成功合并输入/输出价格到单列
  • 正确处理了不同模式(image_generation vs token-based)
  • 条件性显示 per-request 价格
  • 布局清晰,标签使用 i18n

574-610: LGTM!

缓存价格单元格的实现完善:

  • 正确基于 supports_prompt_caching 标志进行条件渲染
  • Cache creation 列正确显示了两个时间范围(5分钟和1小时)的价格
  • 格式化和布局一致

639-653: LGTM!

删除成功处理器正确实现了分页修复:

  • 检测删除最后一项是否会导致当前页为空(filteredPrices.length <= 1 && page > 1
  • 如果会为空,导航到前一页并设置 pendingRefreshPage
  • 更新 URL 以保持状态同步
  • 通过 pendingRefreshPage 机制确保后续刷新使用正确的页码

这解决了 PR 中提到的分页 UX 问题。


627-636: LGTM!

ModelPriceDrawer 正确替换了 ModelPriceDialog:

  • 传入了 mode="edit" 和 initialData
  • 使用 DropdownMenuItem 作为触发器
  • 编辑操作不需要分页调整,组件会自动发出 "price-data-updated" 事件来刷新当前页

Comment on lines +67 to +73
function setReactInputValue(input: HTMLInputElement, value: string) {
const prototype = Object.getPrototypeOf(input) as HTMLInputElement;
const descriptor = Object.getOwnPropertyDescriptor(prototype, "value");
descriptor?.set?.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

setReactInputValue 建议加兜底赋值,避免 descriptor 缺失时静默不生效

建议修改
 function setReactInputValue(input: HTMLInputElement, value: string) {
   const prototype = Object.getPrototypeOf(input) as HTMLInputElement;
   const descriptor = Object.getOwnPropertyDescriptor(prototype, "value");
-  descriptor?.set?.call(input, value);
+  if (descriptor?.set) {
+    descriptor.set.call(input, value);
+  } else {
+    input.value = value;
+  }
   input.dispatchEvent(new Event("input", { bubbles: true }));
   input.dispatchEvent(new Event("change", { bubbles: true }));
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function setReactInputValue(input: HTMLInputElement, value: string) {
const prototype = Object.getPrototypeOf(input) as HTMLInputElement;
const descriptor = Object.getOwnPropertyDescriptor(prototype, "value");
descriptor?.set?.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
}
function setReactInputValue(input: HTMLInputElement, value: string) {
const prototype = Object.getPrototypeOf(input) as HTMLInputElement;
const descriptor = Object.getOwnPropertyDescriptor(prototype, "value");
if (descriptor?.set) {
descriptor.set.call(input, value);
} else {
input.value = value;
}
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
}
🤖 Prompt for AI Agents
In @tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx
around lines 67 - 73, The helper setReactInputValue currently assumes
Object.getOwnPropertyDescriptor(prototype, "value") exists and silently does
nothing if descriptor is missing; update setReactInputValue to fall back to
directly assigning input.value = value when descriptor?.set is undefined (still
dispatching the "input" and "change" events afterward) and ensure it handles
readonly descriptors safely (e.g., try/catch around the assignment) so the value
change always takes effect in tests; reference the setReactInputValue function
and keep the existing event dispatches.

Comment on lines +85 to +209
test("create 模式应支持搜索现有模型并预填充字段", async () => {
vi.useFakeTimers();
const messages = loadMessages();
const originalFetch = globalThis.fetch;

const now = new Date("2026-01-01T00:00:00.000Z");
const prefillModel: ModelPrice = {
id: 100,
modelName: "openai/gpt-test",
priceData: {
mode: "chat",
display_name: "GPT Test",
litellm_provider: "openai",
supports_prompt_caching: true,
input_cost_per_token: 0.000001,
output_cost_per_token: 0.000002,
input_cost_per_request: 0.005,
cache_read_input_token_cost: 0.0000001,
cache_creation_input_token_cost: 0.00000125,
cache_creation_input_token_cost_above_1hr: 0.000002,
},
source: "litellm",
createdAt: now,
updatedAt: now,
};

const fetchMock = vi.fn().mockResolvedValue({
json: async () => ({
ok: true,
data: { data: [prefillModel], total: 1, page: 1, pageSize: 10, totalPages: 1 },
}),
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
globalThis.fetch = fetchMock as any;

const { unmount } = render(
<NextIntlClientProvider locale="en" messages={messages}>
<ModelPriceDrawer mode="create" defaultOpen />
</NextIntlClientProvider>
);

const searchInput = document.getElementById("prefill-search") as HTMLInputElement | null;
expect(searchInput).toBeTruthy();

await act(async () => {
setReactInputValue(searchInput!, "gpt");
});

await act(async () => {
vi.advanceTimersByTime(350);
});

await act(async () => {
await flushMicrotasks();
});

expect(fetchMock).toHaveBeenCalled();
expect(document.body.textContent).toContain("openai/gpt-test");

const resultItem = Array.from(document.querySelectorAll('[data-slot="command-item"]')).find(
(el) => el.textContent?.includes("openai/gpt-test")
);
expect(resultItem).toBeTruthy();

await act(async () => {
resultItem?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
await flushMicrotasks();
});

const modelIdInput = document.getElementById("modelName") as HTMLInputElement | null;
const displayNameInput = document.getElementById("displayName") as HTMLInputElement | null;
const providerInput = document.getElementById("provider") as HTMLInputElement | null;

expect(modelIdInput?.value).toBe("openai/gpt-test");
expect(displayNameInput?.value).toBe("GPT Test");
expect(providerInput?.value).toBe("openai");

globalThis.fetch = originalFetch;
unmount();
});

test("预填充搜索应防抖:快速输入只触发一次请求", async () => {
vi.useFakeTimers();
const messages = loadMessages();
const originalFetch = globalThis.fetch;

const fetchMock = vi.fn().mockResolvedValue({
json: async () => ({
ok: true,
data: { data: [], total: 0, page: 1, pageSize: 10, totalPages: 0 },
}),
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
globalThis.fetch = fetchMock as any;

const { unmount } = render(
<NextIntlClientProvider locale="en" messages={messages}>
<ModelPriceDrawer mode="create" defaultOpen />
</NextIntlClientProvider>
);

const searchInput = document.getElementById("prefill-search") as HTMLInputElement | null;
expect(searchInput).toBeTruthy();

await act(async () => {
setReactInputValue(searchInput!, "g");
setReactInputValue(searchInput!, "gp");
setReactInputValue(searchInput!, "gpt");
});

await act(async () => {
vi.advanceTimersByTime(350);
});

await act(async () => {
await flushMicrotasks();
});

expect(fetchMock).toHaveBeenCalledTimes(1);
const url = fetchMock.mock.calls[0][0] as string;
expect(url).toContain("search=gpt");

globalThis.fetch = originalFetch;
unmount();
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the test file exists and examine the full context
fd "model-price-drawer-prefill-and-submit-ui.test.tsx" --type f

# Then examine the file to see if there are afterEach hooks or other cleanup patterns
head -n 250 "tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx" | cat -n

Repository: ding113/claude-code-hub

Length of output: 9590


🏁 Script executed:

# Check if there are other test files in the same directory to see how they handle mocking
fd "\.test\.tsx?$" "tests/unit/settings/prices/" -type f

# Look for patterns of how fetch or globalThis is used in other tests
rg "globalThis\." tests/unit/settings/prices/ -A 2 -B 2

Repository: ding113/claude-code-hub

Length of output: 21095


🏁 Script executed:

# Check if there's a setup file or beforeEach/afterEach patterns in Vitest config
fd "vitest" "." -type f | head -20
rg "afterEach|beforeEach" tests/unit/settings/prices/ -B 2 -A 2

Repository: ding113/claude-code-hub

Length of output: 7695


将 globalThis.fetch 恢复移至 afterEach() 钩子,而非手动恢复

目前在测试末尾手动恢复 fetch,若中途断言失败,恢复代码不执行,导致后续测试串扰。相比 try/finally,应采用 afterEach() 钩子恢复(与 price-list-interactions.test.tsx 保持一致),确保无条件执行:

建议修改
  describe("ModelPriceDrawer: 预填充与提交", () => {
+   const originalFetch = globalThis.fetch;
+
    beforeEach(() => {
      vi.clearAllMocks();
      document.body.innerHTML = "";
    });

    afterEach(() => {
+     // eslint-disable-next-line @typescript-eslint/no-explicit-any
+     globalThis.fetch = originalFetch as any;
      vi.useRealTimers();
    });

    test("create 模式应支持搜索现有模型并预填充字段", async () => {
      vi.useFakeTimers();
      const messages = loadMessages();
-     const originalFetch = globalThis.fetch;
      const now = new Date("2026-01-01T00:00:00.000Z");
      // ... 其他代码 ...
      globalThis.fetch = fetchMock as any;
      // ... 测试逻辑 ...
-     globalThis.fetch = originalFetch;
      unmount();
    });

    test("预填充搜索应防抖:快速输入只触发一次请求", async () => {
      vi.useFakeTimers();
      const messages = loadMessages();
-     const originalFetch = globalThis.fetch;
      // ... 其他代码 ...
      globalThis.fetch = fetchMock as any;
      // ... 测试逻辑 ...
-     globalThis.fetch = originalFetch;
      unmount();
    });

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.

1 participant