Conversation
📝 WalkthroughWalkthrough该变更引入每次请求定价与提示缓存相关字段,扩展前端定价管理界面(由对话框改为抽屉式编辑器),增加云模型计数 API,更新多语言本地化字符串,并辅以相关类型、校验、成本计算调整及大量单元测试覆盖。 Changes
代码审查难度估算🎯 4 (复杂) | ⏱️ ~45 分钟 可能相关的PR
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
Summary of ChangesHello @ding113, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 此拉取请求旨在全面提升价格表的用户界面和自定义模型管理功能。通过重新组织价格信息的显示方式,引入缓存相关成本的配置,并优化模型编辑流程,使得用户能够更直观、高效地管理和配置模型价格。这些改进不仅美化了界面,也增强了系统的灵活性和实用性。 Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
🧪 测试结果
总体结果: ✅ 所有测试通过 |
There was a problem hiding this comment.
Code Review
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.
| 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]); |
There was a problem hiding this comment.
在创建新模型时,预填充搜索功能会在用户每次按键时都触发一次 API 请求。这可能会导致不必要的网络请求和服务器负载,尤其是在用户快速输入时。这也会影响用户体验。
建议对搜索输入 prefillQuery 使用防抖(debounce)处理。项目中已存在 useDebounce hook(例如在 price-list.tsx 中使用),可以复用它来优化此处的搜索功能,在用户停止输入一段时间后再发起请求。
修改建议:
- 在组件顶部导入
useDebouncehook:import { useDebounce } from "@/lib/hooks/use-debounce"; - 在
prefillQuery状态下方添加:const debouncedPrefillQuery = useDebounce(prefillQuery, 300); - 在此
useEffect中,将prefillQuery替换为debouncedPrefillQuery。
| // 权限检查:只有管理员可以访问 | ||
| const session = await getSession(); | ||
| if (!session || session.user.role !== "admin") { | ||
| return NextResponse.json({ ok: false, error: "无权限访问此资源" }, { status: 403 }); |
|
|
||
| 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]); |
There was a problem hiding this 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:
| 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.
Additional Comments (1)
Example scenario:
Recommendation: After deletion, check if the current page would be empty and navigate to the previous page: This same logic should also be applied to the edit dialog's onSuccess callback (lines 620-630). Prompt To Fix With AIThis 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. |
There was a problem hiding this comment.
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应为trueNaN/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
📒 Files selected for processing (22)
messages/en/settings.jsonmessages/ja/settings.jsonmessages/ru/settings.jsonmessages/zh-CN/settings.jsonmessages/zh-TW/settings.jsonsrc/actions/model-prices.tssrc/app/[locale]/settings/prices/_components/model-price-drawer.tsxsrc/app/[locale]/settings/prices/_components/price-list.tsxsrc/app/[locale]/settings/prices/_components/upload-price-dialog.tsxsrc/app/[locale]/settings/prices/page.tsxsrc/app/api/prices/cloud-model-count/route.tssrc/lib/utils/cost-calculation.tssrc/lib/utils/price-data.tssrc/types/model-price.tstests/unit/lib/cost-calculation-input-cost-per-request.test.tstests/unit/lib/price-data-input-cost-per-request.test.tstests/unit/settings/prices/delete-model-dialog.test.tsxtests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsxtests/unit/settings/prices/price-list-interactions.test.tsxtests/unit/settings/prices/price-list-ui-requirements.test.tsxtests/unit/settings/prices/upload-price-dialog-cloud-model-count.test.tsxtests/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.tssrc/lib/utils/price-data.tssrc/types/model-price.tstests/unit/settings/prices/price-list-ui-requirements.test.tsxtests/unit/settings/prices/upload-price-dialog-upload-flow.test.tsxtests/unit/settings/prices/price-list-interactions.test.tsxtests/unit/lib/cost-calculation-input-cost-per-request.test.tstests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsxtests/unit/settings/prices/upload-price-dialog-cloud-model-count.test.tsxsrc/app/[locale]/settings/prices/_components/model-price-drawer.tsxsrc/app/[locale]/settings/prices/_components/upload-price-dialog.tsxsrc/app/api/prices/cloud-model-count/route.tssrc/lib/utils/cost-calculation.tstests/unit/settings/prices/delete-model-dialog.test.tsxsrc/actions/model-prices.tssrc/app/[locale]/settings/prices/page.tsxsrc/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.tssrc/lib/utils/price-data.tssrc/types/model-price.tstests/unit/settings/prices/price-list-ui-requirements.test.tsxtests/unit/settings/prices/upload-price-dialog-upload-flow.test.tsxtests/unit/settings/prices/price-list-interactions.test.tsxtests/unit/lib/cost-calculation-input-cost-per-request.test.tstests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsxtests/unit/settings/prices/upload-price-dialog-cloud-model-count.test.tsxsrc/app/[locale]/settings/prices/_components/model-price-drawer.tsxsrc/app/[locale]/settings/prices/_components/upload-price-dialog.tsxsrc/app/api/prices/cloud-model-count/route.tssrc/lib/utils/cost-calculation.tstests/unit/settings/prices/delete-model-dialog.test.tsxsrc/actions/model-prices.tssrc/app/[locale]/settings/prices/page.tsxsrc/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.tstests/unit/settings/prices/price-list-ui-requirements.test.tsxtests/unit/settings/prices/upload-price-dialog-upload-flow.test.tsxtests/unit/settings/prices/price-list-interactions.test.tsxtests/unit/lib/cost-calculation-input-cost-per-request.test.tstests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsxtests/unit/settings/prices/upload-price-dialog-cloud-model-count.test.tsxtests/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.tsxtests/unit/settings/prices/upload-price-dialog-upload-flow.test.tsxtests/unit/settings/prices/price-list-interactions.test.tsxtests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsxtests/unit/settings/prices/upload-price-dialog-cloud-model-count.test.tsxsrc/app/[locale]/settings/prices/_components/model-price-drawer.tsxsrc/app/[locale]/settings/prices/_components/upload-price-dialog.tsxtests/unit/settings/prices/delete-model-dialog.test.tsxsrc/app/[locale]/settings/prices/page.tsxsrc/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.tssrc/types/model-price.tssrc/app/api/prices/cloud-model-count/route.tssrc/lib/utils/cost-calculation.tssrc/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.tsxtests/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.tsxsrc/app/[locale]/settings/prices/_components/model-price-drawer.tsxsrc/app/[locale]/settings/prices/_components/upload-price-dialog.tsxtests/unit/settings/prices/delete-model-dialog.test.tsxsrc/app/[locale]/settings/prices/page.tsxsrc/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.tsxsrc/app/[locale]/settings/prices/_components/model-price-drawer.tsxsrc/app/[locale]/settings/prices/_components/upload-price-dialog.tsxsrc/actions/model-prices.tsmessages/zh-CN/settings.jsonsrc/app/[locale]/settings/prices/page.tsxsrc/app/[locale]/settings/prices/_components/price-list.tsxmessages/ja/settings.jsonmessages/en/settings.jsonmessages/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.tsxsrc/app/[locale]/settings/prices/_components/model-price-drawer.tsxtests/unit/settings/prices/delete-model-dialog.test.tsxmessages/zh-CN/settings.jsonsrc/app/[locale]/settings/prices/page.tsxsrc/app/[locale]/settings/prices/_components/price-list.tsxmessages/ja/settings.jsonmessages/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.jsonsrc/app/[locale]/settings/prices/_components/price-list.tsxmessages/ja/settings.jsonmessages/en/settings.jsonmessages/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.jsonmessages/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.table和prices.formkey 集合保持一致,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 行为的良好实践:
- 首先断言加载状态文本的显示
- 手动触发 fetch 响应的解析
- 等待 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: 价格转换辅助函数实现正确。
parsePricePerMillionToPerToken、parsePrice和formatPerTokenPriceToPerMillion正确处理了空值、负数和非有限数值的边界情况。
173-219: 预填充搜索逻辑健壮。使用
cancelled标志防止组件卸载后的状态更新,避免了 React 状态更新警告。cache: "no-store"确保实时获取最新数据。messages/ja/settings.json (1)
610-706: 日语翻译完整,与其他语言文件结构一致。新增的价格相关翻译键(
table.price、drawer区块等)已正确添加,文案专业准确。src/app/[locale]/settings/prices/_components/price-list.tsx (5)
249-278: 价格格式化辅助函数实现合理。
formatScalarPrice正确处理了undefined、非有限数和负数的边界情况。formatPerMillionTokenPriceLabel、formatPerImagePriceLabel和formatPerRequestPriceLabel保持了一致的格式化模式。
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: 表单字段翻译准确且语义清晰。关键变更包括:
modelName标签改为"ID 模型",与新增的可选displayName字段形成清晰对比- 新增的按次调用价格和缓存价格字段翻译准确
- 缓存价格字段正确区分不同 TTL(5m, 1h)
- 图像输出价格使用恰当的单位"$/изображение"
翻译与 PR 目标完全一致,术语使用规范。
700-706: 抽屉组件翻译完整且用户友好。新增的抽屉组件翻译涵盖了预填充和缓存配置功能:
- 预填充相关文本清晰表达了搜索和自动填充的功能
promptCachingHint提供了有价值的使用指导,提醒用户仅在模型支持时启用- 所有状态消息(空状态、错误状态)表达准确
翻译质量良好,符合用户体验最佳实践。
| "price": "價格", | ||
| "inputPrice": "輸入價格 ($/M)", | ||
| "outputPrice": "輸出價格 ($/M)", | ||
| "priceInput": "輸入", | ||
| "priceOutput": "輸出", | ||
| "pricePerRequest": "按次", | ||
| "cacheReadPrice": "快取讀取 ($/M)", | ||
| "cacheCreationPrice": "快取建立 ($/M)", | ||
| "cache5m": "5m", | ||
| "cache1h": "1h+", | ||
| "copyModelId": "複製模型 ID", |
There was a problem hiding this comment.
建议与英文保持相同的“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]); |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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:52already usesuseDebounce(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:
- Comment Analyzer: All comments match code behavior, no outdated documentation
- 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
- Cost calculation with
- Silent Failure Hunter:
- All errors properly logged (
console.error) - Fetch cancellation properly implemented (line 216-218)
- User feedback via toast notifications
- All errors properly logged (
- Type Design Auditor:
- No
anyusage - Proper validation in
upsertSingleModelPrice(lines 502-529) - Strong type safety with TypeScript interfaces
- No
- 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)
- 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; |
There was a problem hiding this comment.
[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); |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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 toundefined, 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
There was a problem hiding this comment.
- Identified PR
#583and applied labelsize/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 toundefined, 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.
🧪 测试结果
总体结果: ✅ 所有测试通过 |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (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
📒 Files selected for processing (9)
messages/en/settings.jsonmessages/ja/settings.jsonmessages/ru/settings.jsonmessages/zh-CN/settings.jsonmessages/zh-TW/settings.jsonsrc/app/[locale]/settings/prices/_components/model-price-drawer.tsxsrc/app/[locale]/settings/prices/_components/price-list.tsxtests/unit/lib/price-data-input-cost-per-request.test.tstests/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.tsxsrc/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.tsxsrc/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.tsxsrc/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.jsonsrc/app/[locale]/settings/prices/_components/price-list.tsxmessages/zh-TW/settings.jsonmessages/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.jsonmessages/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.jsontests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsxsrc/app/[locale]/settings/prices/_components/price-list.tsxmessages/zh-TW/settings.jsonmessages/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.tsxsrc/app/[locale]/settings/prices/_components/price-list.tsxmessages/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 预填充/缓存引导文案 OKmessages/ru/settings.json (4)
606-621: prices.table 新增列文案覆盖完整
663-665: cloud model count 状态文案覆盖 OK
684-699: form 新增字段文案 OK
700-706: drawer 预填充/缓存引导文案 OKsrc/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" 事件来刷新当前页
| 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 })); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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(); | ||
| }); |
There was a problem hiding this comment.
🧩 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 -nRepository: 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 2Repository: 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 2Repository: 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();
});
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:
input_cost_per_requestfield for models that charge per API callsupports_prompt_cachingflagKey Changes
Backend (Actions & Repository)
upsertSingleModelPrice(): Added comprehensive validation for cache pricing fields andinput_cost_per_request(all must be non-negative finite numbers)input_cost_per_requestfield with validation and multiplier supporthasValidPriceData()updated to recognize models with only per-request pricing as validFrontend (UI Components)
Test Coverage
Excellent test coverage for:
input_cost_per_requestIssues Found
🔴 Critical Issues
None
🟡 Moderate Issues
Performance Issue - Unbounded API Requests (model-price-drawer.tsx, line 172-219)
useDebouncehook (already used successfully in price-list.tsx)UX Bug - Empty Page After Deletion (price-list.tsx, line 638-648)
Architecture & Code Quality
Strengths:
Areas for Improvement:
model_nameandsourcecolumns if not already present for query performanceVerification Recommendations
Before merging:
Confidence Score: 4/5
Important Files Changed
File Analysis
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