Skip to content

fix: preserve extra pricing fields and expose semantic pricing details#1049

Merged
ding113 merged 3 commits into
devfrom
fix/pricing-billing-pr-20260420
Apr 20, 2026
Merged

fix: preserve extra pricing fields and expose semantic pricing details#1049
ding113 merged 3 commits into
devfrom
fix/pricing-billing-pr-20260420

Conversation

@ding113

@ding113 ding113 commented Apr 20, 2026

Copy link
Copy Markdown
Owner

Summary

Preserve extra price-like fields during manual price edits instead of dropping them to the drawer-managed subset, align billing-model-source integration tests with breakdown-backed DB cost writes, add a semantic pricing details dialog and advanced extra-fields JSON editing for local model prices, and recognize generic price-like fields as valid pricing data.

Problem

When editing model prices through the drawer UI, only the explicitly managed fields (input/output/cache costs) were preserved. Any extra pricing fields from cloud price tables (e.g. input_cost_per_second, file_search_cost_per_1k_calls, code_interpreter_cost_per_session) were silently dropped on save. Additionally, there was no way to inspect the full pricing record to see what fields exist, and the integration test suite still referenced the old updateMessageRequestCost function that was renamed to updateMessageRequestCostWithBreakdown in PR #1025.

Related PRs:

Solution

  1. New model-price-fields utility (src/lib/utils/model-price-fields.ts) — Classifies every field in a price record as "supported" (billing keys the app knows), "unsupported" (generic price-like keys by regex), or "display" (metadata). Provides getEditableExtraPriceData() to extract non-managed fields for round-tripping, and collectAdditionalPriceLikeNumbers() for price validity checks.
  2. Extra fields JSON in drawer — The model price drawer now pre-fills and submits an extraFieldsJson textarea so users can view/edit arbitrary extra pricing fields that aren't covered by the basic form. These are spread into the priceData object on upsert.
  3. Pricing details dialog (model-price-details-dialog.tsx) — A new "View details" action in the price list dropdown opens a dialog showing all fields organized into Core fields, Additional billable fields, Additional metadata, and Provider pricing nodes (grouped by provider key).
  4. Price validity enhancement (price-data.ts) — hasValidPriceData now also considers generic price-like numeric fields (matching cost|price|rate|multiplier|per_|second|session|query|page|pixel|character|dbu) as valid pricing data, so models priced by non-token metrics are recognized as having valid prices.
  5. Integration test alignment — Renamed all updateMessageRequestCost mock references to updateMessageRequestCostWithBreakdown in billing-model-source.test.ts to match the current repository API.

Changes

Core Changes

  • src/lib/utils/model-price-fields.ts (new) — Field classification, editable extras extraction, and price-like key detection utilities
  • src/actions/model-prices.ts — Accept extraFieldsJson, parse and spread into priceData on upsert
  • src/lib/utils/price-data.ts — Include generic price-like numbers in hasValidPriceData check
  • src/app/[locale]/settings/prices/_components/model-price-details-dialog.tsx (new) — Semantic details dialog with field categories
  • src/app/[locale]/settings/prices/_components/model-price-drawer.tsx — Extra fields JSON textarea with pre-fill on edit
  • src/app/[locale]/settings/prices/_components/price-list.tsx — "View details" menu item

Supporting Changes

  • messages/*/settings/prices.json (all 5 locales) — i18n strings for advanced fields and details dialog
  • tests/unit/lib/model-price-fields.test.ts (new) — Tests for field classification, editable extras, and price-like detection
  • tests/unit/lib/price-data-price-like-fields.test.ts (new) — Tests for generic price-like field recognition in hasValidPriceData
  • tests/unit/settings/prices/model-price-details-dialog.test.tsx (new) — Dialog rendering tests
  • tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx — Tests for extra fields pre-fill and submit round-trip
  • tests/unit/actions/model-prices.test.ts — Test for extra JSON fields merge on upsert
  • tests/unit/price-sync/cloud-price-table.test.ts — Test for preserving generic pricing fields from TOML
  • tests/integration/billing-model-source.test.ts — Align mocks with renamed updateMessageRequestCostWithBreakdown

Breaking Changes

None. All changes are additive — existing price records and workflows continue to work unchanged.

Verification

bunx vitest run tests/unit/price-sync/cloud-price-table.test.ts tests/unit/actions/model-prices.test.ts tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx tests/unit/settings/prices/model-price-details-dialog.test.tsx tests/unit/lib/model-price-fields.test.ts tests/unit/lib/price-data-price-like-fields.test.ts tests/integration/billing-model-source.test.ts --reporter=dot
bun run typecheck
bun run build
bun run test
bun run lint -- --max-diagnostics 200

Screenshot


Description enhanced by Claude AI

Greptile Summary

This PR preserves extra pricing fields during manual edits by introducing a model-price-fields utility for field classification, adds an extraFieldsJson textarea to the price drawer for round-tripping non-managed fields, exposes a semantic "View details" dialog for inspecting all price record fields, broadens hasValidPriceData to recognise generic price-like numerics, and aligns integration test mocks with the updateMessageRequestCostWithBreakdown rename from PR #1025.

Confidence Score: 5/5

Safe to merge; all remaining findings are minor style/consistency P2s that do not affect correctness or data integrity.

Prior P0/P1 concerns (negative-value bypass, server-side managed-field stripping, regex false positives) were surfaced in earlier review rounds and the current state either addresses them or intentionally defers them. The two new findings — duplicate classifyField call and unconditional core-section rendering — are cosmetic and do not impact runtime behaviour or data correctness.

No files require special attention; src/lib/utils/model-price-fields.ts has the minor duplicate-call nit and model-price-details-dialog.tsx has the inconsistent section guard.

Important Files Changed

Filename Overview
src/lib/utils/model-price-fields.ts New utility providing field classification, editable-extra extraction, and price-like key detection; minor duplicate classifyField call per entry in pushEntries
src/actions/model-prices.ts Accepts extraFieldsJson, sanitizes via sanitizeExtraPriceData (prototype-pollution-safe, validates price-like leaf values), spreads before managed fields; server-side does not strip NON_EDITABLE_MANAGED_FIELDS keys such as pricing (noted in prior review)
src/lib/utils/price-data.ts Extends hasValidPriceData to delegate to collectAdditionalPriceLikeNumbers; known cost fields are double-counted with collectNumericCosts but harmlessly so due to .some() semantics
src/app/[locale]/settings/prices/_components/model-price-details-dialog.tsx New dialog displaying price fields organised into core/billable/metadata/provider sections; core section renders unconditionally unlike the other three guarded sections
src/app/[locale]/settings/prices/_components/model-price-drawer.tsx Adds extraFieldsJson textarea with pre-fill via getEditableExtraPriceData on edit/prefill; correctly round-trips extra fields through the action
src/app/[locale]/settings/prices/_components/price-list.tsx Adds "View details" dropdown item wired to ModelPriceDetailsDialog via standard onSelect-prevents-close + asChild pattern
tests/integration/billing-model-source.test.ts Aligns mock references from renamed updateMessageRequestCostupdateMessageRequestCostWithBreakdown; straightforward rename alignment

Comments Outside Diff (1)

  1. src/actions/model-prices.ts, line 661-694 (link)

    P2 Server-side does not strip NON_EDITABLE_MANAGED_FIELDS from extraPriceData before the spread

    getEditableExtraPriceData filters out NON_EDITABLE_MANAGED_FIELDS (including pricing) only on the client to pre-populate the textarea. The server never applies the same filter. A user who manually types {"pricing": {"openai": {...}}} in the advanced JSON textarea will persist a pricing node even though the form treats pricing as a managed field it doesn't control. The pricing key is not set explicitly later in the spread, so it won't be overridden. Consider stripping managed fields server-side before the merge, or reusing getEditableExtraPriceData on the server path.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/actions/model-prices.ts
    Line: 661-694
    
    Comment:
    **Server-side does not strip `NON_EDITABLE_MANAGED_FIELDS` from `extraPriceData` before the spread**
    
    `getEditableExtraPriceData` filters out `NON_EDITABLE_MANAGED_FIELDS` (including `pricing`) only on the client to pre-populate the textarea. The server never applies the same filter. A user who manually types `{"pricing": {"openai": {...}}}` in the advanced JSON textarea will persist a `pricing` node even though the form treats `pricing` as a managed field it doesn't control. The `pricing` key is not set explicitly later in the spread, so it won't be overridden. Consider stripping managed fields server-side before the merge, or reusing `getEditableExtraPriceData` on the server path.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/lib/utils/model-price-fields.ts
Line: 163-184

Comment:
**`classifyField` computed twice per entry**

`classifyField(path, key)` is called once to populate `kind` and again inside the `isCoreField` call. Since it's a pure function, extracting it to a local variable avoids the duplicate computation on every iteration.

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

---

This is a comment left during a code review.
Path: src/app/[locale]/settings/prices/_components/model-price-details-dialog.tsx
Line: 180-187

Comment:
**Core fields section renders unconditionally**

The "Core fields" `<section>` is always rendered even when `coreEntries` is empty, producing an orphaned heading. The other three sections (`additionalBillable`, `additionalMetadata`, `providerGroups`) all guard with a length check — wrapping this section in a `{coreEntries.length > 0 && (...)}` guard would make the rendering logic consistent.

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

Reviews (3): Last reviewed commit: "fix(pricing): address second-round revie..." | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

新增“高级字段”本地化条目、前后端对额外 JSON 字段的采集/校验/合并、详情对话框与抽屉编辑支持,扩展价格字段收集/验证工具并补充相应单元与集成测试。无公共 API 签名变更。

Changes

Cohort / File(s) Summary
Localization Strings
messages/en/settings/prices.json, messages/ja/settings/prices.json, messages/ru/settings/prices.json, messages/zh-CN/settings/prices.json, messages/zh-TW/settings/prices.json
新增“高级字段”相关本地化条目(标题/JSON 标签/示例 placeholder/提示),新增 actions.viewDetails 文案并添加顶层 details 对象及其字段标签。
Price Field Utilities
src/lib/utils/model-price-fields.ts, src/lib/utils/price-data.ts
新增字段分类与采集工具(字段种类、来源、扁平化条目收集、额外可编辑字段提取与价格类数值采集),并在 hasValidPriceData 中纳入额外“price-like”数值检测。
Action & Data Layer
src/actions/model-prices.ts
SingleModelPriceInput 添加可选 extraFieldsJson?: string;解析并严格校验 JSON(要求非数组对象)、防护 prototype 污染、对“price-like”字段做数值正规化,并将额外字段与受管字段合并后持久化;解析错误返回失败并包含 JSON 错误信息。
UI Components
src/app/[locale]/settings/prices/_components/model-price-details-dialog.tsx, src/app/[locale]/settings/prices/_components/model-price-drawer.tsx, src/app/[locale]/settings/prices/_components/price-list.tsx
新增 ModelPriceDetailsDialog 组件展示分组字段;在编辑抽屉中添加 extraFieldsJson 文本区及预填/重置逻辑并随提交上传;在列表行操作菜单中新增“查看详情”入口。
Tests
tests/unit/actions/model-prices.test.ts, tests/unit/lib/model-price-fields.test.ts, tests/unit/lib/price-data-price-like-fields.test.ts, tests/unit/price-sync/cloud-price-table.test.ts, tests/unit/settings/prices/model-price-details-dialog.test.tsx, tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx, tests/integration/billing-model-source.test.ts
新增/修改单元与集成测试:覆盖 extraFieldsJson 的解析/合并/冲突/失败路径、字段识别与采集、hasValidPriceData 对“price-like”字段的判断、抽屉预填与提交、详情对话框渲染,以及集成测试中消息成本接口的 mock 调整(updateMessageRequestCost -> updateMessageRequestCostWithBreakdown)。

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.81% 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标题清晰准确地反映了核心变更:保留额外定价字段并暴露语义定价详情对话框。
Description check ✅ Passed PR描述详细全面,涵盖了问题陈述、解决方案、具体改动和验证步骤,与变更内容高度相关。

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pricing-billing-pr-20260420
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/pricing-billing-pr-20260420

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added bug Something isn't working area:UI area:i18n area:core size/L Large PR (< 1000 lines) labels Apr 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

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

661-694: ⚠️ Potential issue | 🟠 Major

校验额外计费字段,避免保存负价格。

基础价格字段都做了非负校验,但 extraFieldsJson 中的 *_cost_* / *_price_* 等额外计费字段会直接写入 priceData。这会允许 input_cost_per_second: -1 之类的数据进入价格表,后续计费或详情展示可能被污染。

建议修改
         if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
           return { ok: false, error: "高级字段必须是 JSON 对象" };
         }
+
+        for (const [key, value] of Object.entries(parsed)) {
+          const isPriceLikeField = /(?:^|_)(?:cost|price)(?:_|$)/.test(key);
+          if (
+            isPriceLikeField &&
+            (typeof value !== "number" || !Number.isFinite(value) || value < 0)
+          ) {
+            return { ok: false, error: `高级字段 ${key} 必须是非负数` };
+          }
+        }
+
         extraPriceData = parsed as Record<string, unknown>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/model-prices.ts` around lines 661 - 694, The parsed
extraFieldsJson (extraPriceData) can contain arbitrary cost/price fields which
are currently merged into priceData without validation; update the flow after
JSON.parse and before merging by scanning extraPriceData for keys that indicate
monetary/usage rates (e.g., keys matching
/(cost|price|per_|per[A-Za-z0-9_]*|_cost_|_price_|per_second|per_token|per_image|per_request)/i),
ensure each corresponding value is a number and clamp or reject negative values
(either remove those keys, set them to undefined, or return a validation error),
and coerce valid numeric strings to numbers; keep this validation localized to
the extraFieldsJson parsing block that produces extraPriceData so priceData and
ModelPriceData only receive non-negative numeric cost/price fields.
🧹 Nitpick comments (5)
tests/unit/actions/model-prices.test.ts (1)

177-206: 建议补充负路径用例。

当前仅覆盖了合法 JSON 合并路径。建议再加至少两条:1) extraFieldsJson 为非法 JSON 时应返回 ok=false 且不调用仓库;2) 合法 JSON 但其中与表单字段(如 input_cost_per_token)冲突时的优先级(表单 vs extra)行为,以锁定合并顺序。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/actions/model-prices.test.ts` around lines 177 - 206, Add two
negative-path unit tests for upsertSingleModelPrice: 1) when extraFieldsJson is
invalid JSON, assert the returned result.ok is false and that upsertModelPrice
(upsertModelPriceMock) is not called; 2) when extraFieldsJson is valid but
contains keys that conflict with form-provided fields (e.g.,
input_cost_per_token or mode), assert and lock the merge priority by testing the
expected precedence (choose and assert either that form fields override extra
JSON or vice versa) — reference upsertSingleModelPrice, extraFieldsJson, and
upsertModelPriceMock in the new assertions to ensure behavior is deterministic.
src/app/[locale]/settings/prices/_components/model-price-details-dialog.tsx (3)

92-109: providerGroupsuseMemo 实际上每次都会重算,缓存失效。

providerEntries 在每次渲染时都由 entries.filter(...) 产生一个新的数组引用(Line 99),而 providerGroupsuseMemo 依赖项就是这个新引用(Line 109),因此 Object.is 比较永远不等,memo 起不到缓存作用。同理,coreEntries / additionalBillableEntries / additionalMetadataEntries 也在每次渲染时重新过滤,属于小的冗余计算。

建议将所有分组逻辑合并到一个以 entries(或 price.priceData)为依赖的 useMemo 中一次性完成:

♻️ 建议的重构
-  const entries = useMemo(() => collectModelPriceFieldEntries(price.priceData), [price.priceData]);
-  const coreEntries = entries.filter((entry) => entry.isCore && entry.source === "top_level");
-  const additionalBillableEntries = entries.filter(
-    (entry) => !entry.isCore && entry.kind !== "display" && entry.source === "top_level"
-  );
-  const additionalMetadataEntries = entries.filter(
-    (entry) => !entry.isCore && entry.kind === "display" && entry.source === "top_level"
-  );
-  const providerEntries = entries.filter((entry) => entry.source === "provider_pricing");
-  const providerGroups = useMemo(() => {
-    const groups = new Map<string, ModelPriceFieldEntry[]>();
-    for (const entry of providerEntries) {
-      const key = entry.providerKey ?? "unknown";
-      const bucket = groups.get(key) ?? [];
-      bucket.push(entry);
-      groups.set(key, bucket);
-    }
-    return Array.from(groups.entries()).sort((a, b) => a[0].localeCompare(b[0]));
-  }, [providerEntries]);
+  const { coreEntries, additionalBillableEntries, additionalMetadataEntries, providerGroups } =
+    useMemo(() => {
+      const entries = collectModelPriceFieldEntries(price.priceData);
+      const core: ModelPriceFieldEntry[] = [];
+      const addBillable: ModelPriceFieldEntry[] = [];
+      const addMetadata: ModelPriceFieldEntry[] = [];
+      const providerGroupsMap = new Map<string, ModelPriceFieldEntry[]>();
+      for (const entry of entries) {
+        if (entry.source === "provider_pricing") {
+          const key = entry.providerKey ?? "unknown";
+          const bucket = providerGroupsMap.get(key) ?? [];
+          bucket.push(entry);
+          providerGroupsMap.set(key, bucket);
+        } else if (entry.source === "top_level") {
+          if (entry.isCore) core.push(entry);
+          else if (entry.kind === "display") addMetadata.push(entry);
+          else addBillable.push(entry);
+        }
+      }
+      return {
+        coreEntries: core,
+        additionalBillableEntries: addBillable,
+        additionalMetadataEntries: addMetadata,
+        providerGroups: Array.from(providerGroupsMap.entries()).sort((a, b) =>
+          a[0].localeCompare(b[0])
+        ),
+      };
+    }, [price.priceData]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/prices/_components/model-price-details-dialog.tsx
around lines 92 - 109, providerGroups is recomputed every render because
providerEntries (and the other filtered arrays coreEntries,
additionalBillableEntries, additionalMetadataEntries) are recreated by
entries.filter(...) each render and used as dependencies in separate useMemo
calls; consolidate the filtering into a single useMemo that depends on entries
(or price.priceData) and inside it compute coreEntries,
additionalBillableEntries, additionalMetadataEntries, providerEntries and
providerGroups in one pass (grouping by providerKey and sorting as required) and
return the computed values so subsequent renders use the stable memoized results
instead of re-filtering on new array references.

120-120: trigger || defaultTrigger 在传入合法 falsy 节点时会意外回退。

React.ReactNode 允许 nullfalse0"" 作为有效值。当前 trigger || defaultTrigger 的写法意味着若调用方显式传入 null(例如想要隐藏默认触发器、使用外部控制弹窗时),组件仍会渲染默认 "View details" 按钮。建议使用 trigger ?? defaultTrigger,仅在 triggerundefined 时回退,更贴合语义。

♻️ 建议修改
-      <DialogTrigger asChild>{trigger || defaultTrigger}</DialogTrigger>
+      <DialogTrigger asChild>{trigger ?? defaultTrigger}</DialogTrigger>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/prices/_components/model-price-details-dialog.tsx
at line 120, The DialogTrigger fallback logic uses "trigger || defaultTrigger",
which will incorrectly replace valid falsy React nodes (null/false/0/""); update
the component (the DialogTrigger usage in ModelPriceDetailsDialog) to use the
nullish coalescing operator "trigger ?? defaultTrigger" so it only falls back
when trigger is undefined, preserving explicit falsy values passed by callers.

31-33: 数值格式化会在非常小/大的数值上显示为科学计数法。

String(value) 会对例如 0.0000025 这样的定价值输出 "0.0000025"(OK),但对更小的数(如 1e-7)或更大的数会变为科学计数法,对定价展示可能不直观。考虑使用 Number.prototype.toLocaleString 或自定义的定价格式化(例如根据字段语义选择 /M tokens/img 等)以与同模块中其他价格展示保持一致。

如果当前业务场景下所有字段都已用原始精度存储且预期按原样显示,则可忽略此建议。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/prices/_components/model-price-details-dialog.tsx
around lines 31 - 33, The numeric-to-string conversion currently uses
String(value) in the number branch (where you check typeof value === "number"
and Number.isFinite(value)), which can produce scientific notation for very
small/large prices; replace that conversion with a consistent price formatter
(e.g., Number.prototype.toLocaleString with appropriate
minimumFractionDigits/maximumFractionDigits or a shared price-format helper used
elsewhere in this module) so values like 1e-7 render as a readable decimal and
follow the same `/M tokens` or `/img` semantics used by the rest of the
component; update the code around the value check (the Number.isFinite(value)
branch) to call the chosen formatter instead of String(value).
tests/unit/settings/prices/model-price-details-dialog.test.tsx (1)

66-75: 断言依赖英文硬编码文案,建议改用更稳定的定位方式。

测试通过 textContent?.includes("View details") 以及后续的 "Core fields""Additional billable fields" 字符串查找触发器和验证渲染。这些字面量直接耦合到 en 语言包中的具体翻译文案,未来一旦文案措辞调整(例如改为 "Show details"),测试就会失败,即便组件行为完全正确。

建议改用 data-testidaria-label 或通过 messages 对象读取相应的翻译键(如 messages.settings.prices.actions.viewDetails)来定位元素与断言内容,降低与具体文案的耦合。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/settings/prices/model-price-details-dialog.test.tsx` around lines
66 - 75, The test is brittle because it finds the trigger by matching
textContent?.includes("View details"); update the test to locate the button more
stably (e.g., use a data-testid or aria-label on the trigger button) instead of
literal English copy: replace the
Array.from(document.querySelectorAll("button")).find(...) lookup (the trigger
variable) with a query that uses getByTestId / queryByTestId or getByLabelText,
or alternatively read the translation key from the messages object
(messages.settings.prices.actions.viewDetails) and use that value for lookup;
ensure the same approach is used for subsequent assertions that check "Core
fields" and "Additional billable fields" so the test no longer depends on
hard-coded English strings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@messages/en/settings/prices.json`:
- Around line 197-199: The placeholder for the advancedFields JSON is not valid
JSON (uses key = value); update the "advancedFieldsPlaceholder" value to a valid
JSON example (e.g. use an object with quoted keys and numeric values) so users
can copy/paste without JSON.parse errors; locate the keys "advancedFieldsJson",
"advancedFieldsPlaceholder", and "advancedFieldsHint" in the diff and replace
the placeholder string accordingly.

In `@messages/ja/settings/prices.json`:
- Around line 197-199: The placeholder string for advancedFieldsPlaceholder is
not valid JSON; update the value to a legal JSON example so users can copy/paste
successfully (change the placeholder key "advancedFieldsPlaceholder" and ensure
the example uses double-quoted keys, colon separators and proper JSON value
types, e.g. a quoted key like "input_cost_per_second" with a numeric value like
0.5), leaving advancedFieldsJson and advancedFieldsHint as-is.

In `@messages/ru/settings/prices.json`:
- Around line 197-199: The placeholder value for advancedFieldsPlaceholder is
not valid JSON (it uses "key = value"), so replace it with a legal JSON example
(e.g., an object with a quoted key and numeric value) so users copying it won't
trigger JSON.parse errors; update the string referenced by
"advancedFieldsPlaceholder" (and ensure consistency with
"advancedFieldsJson"/"advancedFieldsHint" wording) to something like a valid
JSON object with quoted keys and proper separators.

In `@messages/zh-CN/settings/prices.json`:
- Around line 197-199: The placeholder for advancedFieldsPlaceholder is not
valid JSON (it uses "input_cost_per_second = 0.5") so users copying it will
trigger JSON.parse errors; update the value of advancedFieldsPlaceholder to a
valid JSON example (e.g., an object with a numeric field or string) so it
matches the advancedFieldsJson/advancedFieldsHint context and will parse
correctly when saved, referring to the keys advancedFieldsJson,
advancedFieldsPlaceholder, and advancedFieldsHint to locate the text to change.

In `@messages/zh-TW/settings/prices.json`:
- Around line 197-199: The placeholder "advancedFieldsPlaceholder" currently
shows an invalid example using "key = value"; update that string to present a
valid JSON object example (a JSON object with a quoted key and colon-separated
value, e.g., a single-property object with the key in double quotes and a
numeric value) so users can copy/paste without JSON.parse failing; keep
"advancedFieldsJson" and "advancedFieldsHint" unchanged but ensure the
placeholder clearly indicates it's a JSON object.

In `@src/lib/utils/model-price-fields.ts`:
- Around line 115-119: The isPriceLikeFieldKey function's regex is too
permissive and causes false positives (e.g., "generate", "session_id",
"seconds_since_*"); tighten it by changing the matching strategy in
isPriceLikeFieldKey to require explicit suffixes/prefixes or boundaries — for
example match keys that end with _cost, _price, _rate, _multiplier (use $
anchors), or start with cost_per_, price_per_, price_per or contain explicit
patterns like cost_per_* / price_per_*; use word boundaries (\b) or anchors
instead of substring matches so collectAdditionalPriceLikeNumbers /
hasValidPriceData only treat clearly labeled price fields as price-like.
- Around line 91-113: The LABEL_OVERRIDES map and humanizeKey() currently
produce hardcoded English labels; move localization into the UI by removing or
ignoring English values from LABEL_OVERRIDES in rendering and instead use
useTranslations("settings.prices") inside ModelPriceDetailsDialog to resolve
labels for known keys (e.g., display_name, litellm_provider,
selected_pricing_provider, max_input_tokens, etc.), falling back to
humanizeKey(key) only when the translation lookup returns undefined; update
ModelPriceDetailsDialog to call the translation function for each field label
and reserve humanizeKey/ LABEL_OVERRIDES only as a last-resort fallback.

In `@src/lib/utils/price-data.ts`:
- Around line 55-62: 当前逻辑把 collectAdditionalPriceLikeNumbers 的所有结果并入
hasValidNumericPrice,导致上游
isPriceLikeFieldKey/正则误判时仅含元数据的记录也会被认定为“有效价格”;请修改该判断:仅当
collectNumericCosts(priceData) 返回至少一个数值时才直接返回 true,或者在将
collectAdditionalPriceLikeNumbers 的结果并入前用 isPriceLikeFieldKey(来自
model-price-fields.ts)对字段做一次严格过滤/验证;定位并修改使用
hasValidNumericPrice、collectNumericCosts、collectAdditionalPriceLikeNumbers
的代码路径以实现上述改动。

---

Outside diff comments:
In `@src/actions/model-prices.ts`:
- Around line 661-694: The parsed extraFieldsJson (extraPriceData) can contain
arbitrary cost/price fields which are currently merged into priceData without
validation; update the flow after JSON.parse and before merging by scanning
extraPriceData for keys that indicate monetary/usage rates (e.g., keys matching
/(cost|price|per_|per[A-Za-z0-9_]*|_cost_|_price_|per_second|per_token|per_image|per_request)/i),
ensure each corresponding value is a number and clamp or reject negative values
(either remove those keys, set them to undefined, or return a validation error),
and coerce valid numeric strings to numbers; keep this validation localized to
the extraFieldsJson parsing block that produces extraPriceData so priceData and
ModelPriceData only receive non-negative numeric cost/price fields.

---

Nitpick comments:
In `@src/app/`[locale]/settings/prices/_components/model-price-details-dialog.tsx:
- Around line 92-109: providerGroups is recomputed every render because
providerEntries (and the other filtered arrays coreEntries,
additionalBillableEntries, additionalMetadataEntries) are recreated by
entries.filter(...) each render and used as dependencies in separate useMemo
calls; consolidate the filtering into a single useMemo that depends on entries
(or price.priceData) and inside it compute coreEntries,
additionalBillableEntries, additionalMetadataEntries, providerEntries and
providerGroups in one pass (grouping by providerKey and sorting as required) and
return the computed values so subsequent renders use the stable memoized results
instead of re-filtering on new array references.
- Line 120: The DialogTrigger fallback logic uses "trigger || defaultTrigger",
which will incorrectly replace valid falsy React nodes (null/false/0/""); update
the component (the DialogTrigger usage in ModelPriceDetailsDialog) to use the
nullish coalescing operator "trigger ?? defaultTrigger" so it only falls back
when trigger is undefined, preserving explicit falsy values passed by callers.
- Around line 31-33: The numeric-to-string conversion currently uses
String(value) in the number branch (where you check typeof value === "number"
and Number.isFinite(value)), which can produce scientific notation for very
small/large prices; replace that conversion with a consistent price formatter
(e.g., Number.prototype.toLocaleString with appropriate
minimumFractionDigits/maximumFractionDigits or a shared price-format helper used
elsewhere in this module) so values like 1e-7 render as a readable decimal and
follow the same `/M tokens` or `/img` semantics used by the rest of the
component; update the code around the value check (the Number.isFinite(value)
branch) to call the chosen formatter instead of String(value).

In `@tests/unit/actions/model-prices.test.ts`:
- Around line 177-206: Add two negative-path unit tests for
upsertSingleModelPrice: 1) when extraFieldsJson is invalid JSON, assert the
returned result.ok is false and that upsertModelPrice (upsertModelPriceMock) is
not called; 2) when extraFieldsJson is valid but contains keys that conflict
with form-provided fields (e.g., input_cost_per_token or mode), assert and lock
the merge priority by testing the expected precedence (choose and assert either
that form fields override extra JSON or vice versa) — reference
upsertSingleModelPrice, extraFieldsJson, and upsertModelPriceMock in the new
assertions to ensure behavior is deterministic.

In `@tests/unit/settings/prices/model-price-details-dialog.test.tsx`:
- Around line 66-75: The test is brittle because it finds the trigger by
matching textContent?.includes("View details"); update the test to locate the
button more stably (e.g., use a data-testid or aria-label on the trigger button)
instead of literal English copy: replace the
Array.from(document.querySelectorAll("button")).find(...) lookup (the trigger
variable) with a query that uses getByTestId / queryByTestId or getByLabelText,
or alternatively read the translation key from the messages object
(messages.settings.prices.actions.viewDetails) and use that value for lookup;
ensure the same approach is used for subsequent assertions that check "Core
fields" and "Additional billable fields" so the test no longer depends on
hard-coded English strings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1fae5692-f47a-48cb-9e24-1729c014fb68

📥 Commits

Reviewing files that changed from the base of the PR and between 70e3d1f and 128c0c3.

📒 Files selected for processing (18)
  • messages/en/settings/prices.json
  • messages/ja/settings/prices.json
  • messages/ru/settings/prices.json
  • messages/zh-CN/settings/prices.json
  • messages/zh-TW/settings/prices.json
  • src/actions/model-prices.ts
  • src/app/[locale]/settings/prices/_components/model-price-details-dialog.tsx
  • src/app/[locale]/settings/prices/_components/model-price-drawer.tsx
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • src/lib/utils/model-price-fields.ts
  • src/lib/utils/price-data.ts
  • tests/integration/billing-model-source.test.ts
  • tests/unit/actions/model-prices.test.ts
  • tests/unit/lib/model-price-fields.test.ts
  • tests/unit/lib/price-data-price-like-fields.test.ts
  • tests/unit/price-sync/cloud-price-table.test.ts
  • tests/unit/settings/prices/model-price-details-dialog.test.tsx
  • tests/unit/settings/prices/model-price-drawer-prefill-and-submit-ui.test.tsx

Comment thread messages/en/settings/prices.json
Comment thread messages/ja/settings/prices.json
Comment thread messages/ru/settings/prices.json
Comment thread messages/zh-CN/settings/prices.json
Comment thread messages/zh-TW/settings/prices.json
Comment on lines +91 to +113
const LABEL_OVERRIDES: Record<string, string> = {
display_name: "Display name",
litellm_provider: "LiteLLM provider",
selected_pricing_provider: "Selected pricing provider",
selected_pricing_source_model: "Selected source model",
selected_pricing_resolution: "Selected resolution",
max_input_tokens: "Max input tokens",
max_output_tokens: "Max output tokens",
max_tokens: "Max tokens",
output_vector_size: "Output vector size",
};

function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

function humanizeKey(key: string): string {
if (LABEL_OVERRIDES[key]) {
return LABEL_OVERRIDES[key];
}

return key.replace(/_/g, " ").replace(/\b([a-z])/g, (match) => match.toUpperCase());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 确认 label 是否直接渲染到 UI
fd -e tsx 'model-price-details-dialog' | xargs rg -n -C2 '\.label|humanizeKey|LABEL_OVERRIDES'

Repository: ding113/claude-code-hub

Length of output: 751


🏁 Script executed:

rg -n 'entry\.|label' src/app/\[locale\]/settings/prices/_components/model-price-details-dialog.tsx -B5 -A5

Repository: ding113/claude-code-hub

Length of output: 3935


🏁 Script executed:

rg -n 'import.*humanizeKey|from.*model-price-fields' src/app/\[locale\]/settings/prices/_components/model-price-details-dialog.tsx

Repository: ding113/claude-code-hub

Length of output: 110


🏁 Script executed:

cat -n src/app/\[locale\]/settings/prices/_components/model-price-details-dialog.tsx | head -100

Repository: ding113/claude-code-hub

Length of output: 3772


🏁 Script executed:

cat -n src/lib/utils/model-price-fields.ts | head -150

Repository: ding113/claude-code-hub

Length of output: 5583


🏁 Script executed:

rg -n 'collectModelPriceFieldEntries|entry\.label|humanizeKey' src/lib/utils/model-price-fields.ts -B3 -A3

Repository: ding113/claude-code-hub

Length of output: 673


修复硬编码字段标签,应用 i18n。

humanizeKey() 生成的字段标签在 ModelPriceDetailsDialog 中直接渲染给用户(line 74),但这些标签为硬编码英文:

  • LABEL_OVERRIDES 包含英文字符串("Display name", "LiteLLM provider" 等)
  • 缺失覆盖的字段由函数生成英文格式标签(例如 "Input Cost Per Token")

应将这些标签的本地化处理移至对话框组件内:在 ModelPriceDetailsDialog 中使用 useTranslations("settings.prices") 为已知字段提供翻译查询,仅在无译文时才降级使用 humanizeKey()

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/model-price-fields.ts` around lines 91 - 113, The
LABEL_OVERRIDES map and humanizeKey() currently produce hardcoded English
labels; move localization into the UI by removing or ignoring English values
from LABEL_OVERRIDES in rendering and instead use
useTranslations("settings.prices") inside ModelPriceDetailsDialog to resolve
labels for known keys (e.g., display_name, litellm_provider,
selected_pricing_provider, max_input_tokens, etc.), falling back to
humanizeKey(key) only when the translation lookup returns undefined; update
ModelPriceDetailsDialog to call the translation function for each field label
and reserve humanizeKey/ LABEL_OVERRIDES only as a last-resort fallback.

Comment thread src/lib/utils/model-price-fields.ts
Comment on lines +55 to 62
if (
hasValidNumericPrice([
...collectNumericCosts(priceData),
...collectAdditionalPriceLikeNumbers(priceData),
])
) {
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

逻辑变更依赖 isPriceLikeFieldKey 的准确性。

此处把 collectAdditionalPriceLikeNumbers 的结果并入 hasValidNumericPrice 输入。一旦上游正则误判(见 model-price-fields.ts 的建议),仅含元数据的记录也可能被判定为“有效价格”,从而影响订阅/计费前置校验。修正上游正则后本处即可收敛。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/price-data.ts` around lines 55 - 62, 当前逻辑把
collectAdditionalPriceLikeNumbers 的所有结果并入 hasValidNumericPrice,导致上游
isPriceLikeFieldKey/正则误判时仅含元数据的记录也会被认定为“有效价格”;请修改该判断:仅当
collectNumericCosts(priceData) 返回至少一个数值时才直接返回 true,或者在将
collectAdditionalPriceLikeNumbers 的结果并入前用 isPriceLikeFieldKey(来自
model-price-fields.ts)对字段做一次严格过滤/验证;定位并修改使用
hasValidNumericPrice、collectNumericCosts、collectAdditionalPriceLikeNumbers
的代码路径以实现上述改动。

Comment on lines +115 to +119
export function isPriceLikeFieldKey(key: string): boolean {
return /(cost|price|rate|multiplier|per_|second|session|query|page|pixel|character|dbu)/i.test(
key
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Regex matches "second", "session", "query", "page", "character" as bare substrings — false positives

The pattern tests whether any of those tokens appear anywhere in the key. Fields like response_time_in_seconds, session_timeout, max_queries, total_pages, or character_limit all satisfy the test, so hasValidPriceData can return true for models that only carry metadata numerics, and the details dialog will badge them as "unsupported billing field". Consider anchoring the ambiguous tokens to their pricing-specific compound forms (e.g., per_second, per_session, per_query, per_page) rather than accepting them as bare substrings.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/utils/model-price-fields.ts
Line: 115-119

Comment:
**Regex matches "second", "session", "query", "page", "character" as bare substrings — false positives**

The pattern tests whether any of those tokens appear *anywhere* in the key. Fields like `response_time_in_seconds`, `session_timeout`, `max_queries`, `total_pages`, or `character_limit` all satisfy the test, so `hasValidPriceData` can return `true` for models that only carry metadata numerics, and the details dialog will badge them as "unsupported billing field". Consider anchoring the ambiguous tokens to their pricing-specific compound forms (e.g., `per_second`, `per_session`, `per_query`, `per_page`) rather than accepting them as bare substrings.

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

Comment on lines +661 to +678
let extraPriceData: Record<string, unknown> = {};
if (input.extraFieldsJson?.trim()) {
try {
const parsed = JSON.parse(input.extraFieldsJson);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return { ok: false, error: "高级字段必须是 JSON 对象" };
}
extraPriceData = parsed as Record<string, unknown>;
} catch (error) {
return {
ok: false,
error:
error instanceof Error
? `高级字段 JSON 解析失败: ${error.message}`
: "高级字段 JSON 解析失败",
};
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Numeric values inside extraFieldsJson bypass non-negative validation

The action validates each explicit price field (inputCostPerToken, outputCostPerToken, etc.) for non-negative finiteness, but the parsed extraPriceData receives no equivalent check. A well-formed JSON object like {"output_cost_per_token_priority": -0.5} passes the type guard and gets written to the DB. Applying a shallow check over the parsed object's numeric values, or at least documenting the intentional gap, would align it with the rest of the validation logic.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/model-prices.ts
Line: 661-678

Comment:
**Numeric values inside `extraFieldsJson` bypass non-negative validation**

The action validates each explicit price field (`inputCostPerToken`, `outputCostPerToken`, etc.) for non-negative finiteness, but the parsed `extraPriceData` receives no equivalent check. A well-formed JSON object like `{"output_cost_per_token_priority": -0.5}` passes the type guard and gets written to the DB. Applying a shallow check over the parsed object's numeric values, or at least documenting the intentional gap, would align it with the rest of the validation logic.

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

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

Copy link
Copy Markdown
Contributor

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 an "Advanced fields" feature for model pricing, enabling the storage and editing of additional price-related fields through a JSON object. Key additions include a detailed model price information dialog, a JSON editor within the price management drawer, and utility logic for classifying and extracting dynamic pricing fields. Feedback identifies a critical issue where existing provider-specific pricing data is lost during manual edits because it is excluded from the editable fields. Additionally, a refinement to the regex for identifying price-like fields was suggested to prevent false positives on metadata like timeouts.

Comment thread src/lib/utils/model-price-fields.ts Outdated
"cache_read_input_token_cost",
"cache_creation_input_token_cost",
"cache_creation_input_token_cost_above_1hr",
"pricing",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Including pricing in NON_EDITABLE_MANAGED_FIELDS causes it to be stripped from the editable JSON in the UI. Since the upsertSingleModelPrice server action builds the new price data object from scratch using only the form fields and the JSON input, any existing provider-specific pricing (the pricing object) will be permanently lost when a model is manually edited. If the intention is to preserve all extra fields, pricing should be allowed in the JSON editor or handled explicitly on the server to merge with existing data.

Comment on lines +116 to +118
return /(cost|price|rate|multiplier|per_|second|session|query|page|pixel|character|dbu)/i.test(
key
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The regex for identifying price-like fields is too broad and will flag common metadata fields for review. For example, timeout_seconds or max_seconds will match second, and max_characters will match character. Consider using word boundaries or more specific patterns to reduce false positives.

Suggested change
return /(cost|price|rate|multiplier|per_|second|session|query|page|pixel|character|dbu)/i.test(
key
);
return /(cost|price|rate|multiplier|per_|(_|^)second$|(_|^)session$|(_|^)query$|(_|^)page$|(_|^)pixel$|(_|^)character$|(_|^)dbu$)/i.test(
key
);

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 128c0c3b5e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +56 to +59
hasValidNumericPrice([
...collectNumericCosts(priceData),
...collectAdditionalPriceLikeNumbers(priceData),
])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Stop marking unsupported price-like fields as billable

Including collectAdditionalPriceLikeNumbers in hasValidPriceData makes fields like input_cost_per_second/file_search_cost_per_1k_calls count as “valid pricing”, but calculateRequestCost still ignores those keys and only bills token/request/image/cache fields. Because resolvePricingForModelRecords prioritizes manual records whenever hasValidPriceData is true, a manual override that only has these unsupported fields can now suppress a valid cloud pricing record and produce $0 charges instead of the expected billed amount.

Useful? React with 👍 / 👎.

Comment thread messages/en/settings/prices.json Outdated
"cachePricingTitle": "Cache Pricing",
"advancedFieldsTitle": "Advanced fields",
"advancedFieldsJson": "Additional price fields JSON",
"advancedFieldsPlaceholder": "Example: input_cost_per_second = 0.5",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] [COMMENT-INACCURATE] The placeholder shows invalid syntax for a JSON-only field

Why this is a problem: upsertSingleModelPrice() only accepts a JSON object in extraFieldsJson, but this example uses assignment syntax (input_cost_per_second = 0.5). If a user follows the placeholder literally, save fails with a JSON parse error.

Suggested fix:

"advancedFieldsPlaceholder": "{\"input_cost_per_second\": 0.5}"

Please mirror the same fix in the other locale files because the same placeholder was added there too.

return typeof value === "object" && value !== null && !Array.isArray(value);
}

function humanizeKey(key: string): string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] [STANDARD-VIOLATION] The new details dialog hardcodes English labels instead of using i18n

Why this is a problem: CLAUDE.md says "All user-facing strings must use i18n (5 languages supported). Never hardcode display text". LABEL_OVERRIDES and humanizeKey() always produce English ("Display name", "Max input tokens", title-cased snake_case, etc.), so the new dialog is partially untranslated in every non-English locale.

Suggested fix:

// model-price-fields.ts
entries.push({
  key,
  label: key,
  // ...
});

// model-price-details-dialog.tsx
const fieldLabelMap: Partial<Record<string, string>> = {
  display_name: t("details.fieldLabels.display_name"),
  litellm_provider: t("details.fieldLabels.litellm_provider"),
};
const label = fieldLabelMap[entry.key] ?? entry.path;

That keeps the fallback technical path, while moving user-facing labels into messages/*/settings/prices.json.

Comment thread src/lib/utils/model-price-fields.ts Outdated
return "supported";
}

if (isPriceLikeFieldKey(key)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] [LOGIC-BUG] Nested price objects are misclassified as metadata because only the leaf key is inspected

Why this is a problem: classifyField() checks isPriceLikeFieldKey(key), but some billable structures only expose the pricing hint in the parent path. For example, search_context_cost_per_query.search_context_size_high ends up with key === "search_context_size_high", so it is tagged as display and rendered under Additional metadata even though it is a pricing field.

Suggested fix:

function classifyField(path: string, key: string): ModelPriceFieldKind {
  if (
    SUPPORTED_TOP_LEVEL_BILLING_KEYS.has(key) ||
    (path.startsWith("long_context_pricing.") && SUPPORTED_LONG_CONTEXT_KEYS.has(key))
  ) {
    return "supported";
  }

  if (isPriceLikeFieldKey(key) || isPriceLikeFieldKey(path)) {
    return "unsupported";
  }

  return "display";
}

Using the full path here keeps nested pricing nodes in the billable section instead of hiding them as metadata.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Summary

I found 3 high-priority issues in the new pricing details/editor work. The main problems are a broken JSON example in the new advanced-fields UI, hardcoded English labels in a localized dialog, and classification logic that hides some nested pricing fields under metadata.

PR Size: L

  • Lines changed: 952
  • Files changed: 18
  • Split suggestion: Separate the server/data-shape changes (src/actions, src/lib/utils, billing tests) from the UI/i18n additions (messages/*, drawer/details dialog) so review and rollback stay smaller.

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 2 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 1 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

  • None.

High Priority Issues (Should Fix)

  • messages/*/settings/prices.json: the new advanced-fields placeholder shows assignment syntax instead of a JSON object, so users following the example will hit a parse error on save.
  • src/lib/utils/model-price-fields.ts: the new details dialog hardcodes English field labels, which violates the repo i18n rule and leaves non-English locales partially untranslated.
  • src/lib/utils/model-price-fields.ts: nested pricing paths like search_context_cost_per_query.* are classified as metadata because only the leaf key is inspected.

Review Coverage

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

Automated review by Codex AI

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review Complete

  • Reviewed PR #1049, applied the size/L label, and submitted the required summary review.
  • Posted 3 inline comments on validated new-code issues:
    • Invalid JSON placeholder in messages/en/settings/prices.json:198 (same problem exists in the other locale files).
    • Hardcoded English field labels bypassing i18n in src/lib/utils/model-price-fields.ts:107.
    • Nested pricing paths being classified as metadata in src/lib/utils/model-price-fields.ts:129.
  • Summary also includes an L-PR split suggestion: separate server/data-shape changes from UI/i18n work next time.

Want me to do a follow-up review after the author pushes fixes?

@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
messages/zh-TW/settings/prices.json (1)

198-198: ⚠️ Potential issue | 🟡 Minor

将 JSON 占位示例改为可复制的合法 JSON。

这里标注为 JSON,但示例使用 key = value,用户照抄后会被 JSON.parse 拒绝。

建议修改
-    "advancedFieldsPlaceholder": "範例:input_cost_per_second = 0.5",
+    "advancedFieldsPlaceholder": "範例:{\"input_cost_per_second\": 0.5}",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@messages/zh-TW/settings/prices.json` at line 198, 字段
advancedFieldsPlaceholder 当前给出的示例使用了 key = value 语法,会导致 JSON.parse
失败;将该占位示例改成一个合法的 JSON 对象格式:用双引号包住键名、用冒号分隔键和值、数值不加引号,并用大括号包裹整个对象(例如键名为
input_cost_per_second 且值为 0.5 的合法 JSON 表示),以确保用户复制粘贴后能被 JSON.parse 正常解析。
🧹 Nitpick comments (1)
src/actions/model-prices.ts (1)

717-731: extraPriceData 会被表单字段的 undefined 覆盖,确认行为符合预期。

priceData 使用 { ...extraPriceData, ...formFields } 展开;当用户未在抽屉表单里填写 displayName / inputCostPerToken 等字段时,对应键会以 undefined 值被显式写入,从而覆盖 extraFieldsJson 中的同名键(例如用户在 JSON 里写了 display_name: "Foo",但 UI 文本框清空,最终 display_name 会变为 undefined)。

鉴于 NON_EDITABLE_MANAGED_FIELDSsrc/lib/utils/model-price-fields.ts:76-88)已把这些字段从"可编辑 extras"中剔除,正常流程下 JSON 编辑器不会再提交这些键,这个覆盖策略是可接受的;但在用户手工在 JSON 里补这些键、又清空对应 UI 字段的边缘路径下会出现"静默丢失"。请确认这是期望行为,或考虑用条件展开避免 undefined 覆盖:

♻️ 可选:避免 undefined 覆盖 extras
-    const priceData: ModelPriceData = {
-      ...extraPriceData,
-      mode: input.mode,
-      display_name: input.displayName?.trim() || undefined,
-      litellm_provider: input.litellmProvider || undefined,
-      supports_prompt_caching: input.supportsPromptCaching,
-      input_cost_per_token: input.inputCostPerToken,
-      ...
-    };
+    const priceData: ModelPriceData = {
+      ...extraPriceData,
+      mode: input.mode,
+      ...(input.displayName?.trim() ? { display_name: input.displayName.trim() } : {}),
+      ...(input.litellmProvider ? { litellm_provider: input.litellmProvider } : {}),
+      ...(input.supportsPromptCaching !== undefined
+        ? { supports_prompt_caching: input.supportsPromptCaching }
+        : {}),
+      ...(input.inputCostPerToken !== undefined
+        ? { input_cost_per_token: input.inputCostPerToken }
+        : {}),
+      // ...同理处理剩余字段
+    };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/model-prices.ts` around lines 717 - 731, The current priceData
construction spreads extraPriceData then writes form fields directly, so
undefined form values (e.g., input.displayName, input.inputCostPerToken) will
overwrite extras and silently drop JSON-provided values; update the construction
of priceData (the block that creates priceData using ...extraPriceData and the
listed input.* fields) to only assign each form-derived key when its input value
is not undefined (or empty after trim for strings), e.g. build priceData from
...extraPriceData and conditionally spread each field only if input.<field> !==
undefined/empty; reference NON_EDITABLE_MANAGED_FIELDS to confirm which keys
must remain editable via extras vs UI, or keep current behavior if silent
overwrites are acceptable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/actions/model-prices.ts`:
- Around line 598-633: The sanitizeExtraPriceData function duplicates a
price-like field regex that is inconsistent with the exported utility; replace
the inline regex check in sanitizeExtraPriceData with a call to the shared
predicate (isPriceLikeFieldPath or isPriceLikeFieldKey as exported from
src/lib/utils/model-price-fields) by importing it, so price-like detection
(including "second") is centralized; keep the existing numeric/string conversion
and non-negative validation logic intact and only swap the regex condition to
use the imported function (ensure the same path argument you pass into
sanitizeExtraPriceData is fed to the utility).
- Around line 602-633: sanitizeExtraPriceData currently iterates
Object.entries(value) and will copy dangerous keys like "__proto__",
"constructor", and "prototype" into the returned object (and later merged into
priceData in upsertSingleModelPrice), opening the door to prototype pollution;
update sanitizeExtraPriceData to explicitly skip those three keys when mapping
object entries (i.e., if key is "__proto__" || key === "constructor" || key ===
"prototype" then omit it) so they are not recursed into or returned, preserving
the same defensive behavior as isPriceDataEqual and preventing unsafe merging in
upsertSingleModelPrice.

---

Duplicate comments:
In `@messages/zh-TW/settings/prices.json`:
- Line 198: 字段 advancedFieldsPlaceholder 当前给出的示例使用了 key = value 语法,会导致
JSON.parse 失败;将该占位示例改成一个合法的 JSON 对象格式:用双引号包住键名、用冒号分隔键和值、数值不加引号,并用大括号包裹整个对象(例如键名为
input_cost_per_second 且值为 0.5 的合法 JSON 表示),以确保用户复制粘贴后能被 JSON.parse 正常解析。

---

Nitpick comments:
In `@src/actions/model-prices.ts`:
- Around line 717-731: The current priceData construction spreads extraPriceData
then writes form fields directly, so undefined form values (e.g.,
input.displayName, input.inputCostPerToken) will overwrite extras and silently
drop JSON-provided values; update the construction of priceData (the block that
creates priceData using ...extraPriceData and the listed input.* fields) to only
assign each form-derived key when its input value is not undefined (or empty
after trim for strings), e.g. build priceData from ...extraPriceData and
conditionally spread each field only if input.<field> !== undefined/empty;
reference NON_EDITABLE_MANAGED_FIELDS to confirm which keys must remain editable
via extras vs UI, or keep current behavior if silent overwrites are acceptable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2237dbcd-138c-4109-a99c-49dbbbc999a3

📥 Commits

Reviewing files that changed from the base of the PR and between 128c0c3 and e6a4333.

📒 Files selected for processing (11)
  • messages/en/settings/prices.json
  • messages/ja/settings/prices.json
  • messages/ru/settings/prices.json
  • messages/zh-CN/settings/prices.json
  • messages/zh-TW/settings/prices.json
  • src/actions/model-prices.ts
  • src/app/[locale]/settings/prices/_components/model-price-details-dialog.tsx
  • src/app/[locale]/settings/prices/_components/model-price-drawer.tsx
  • src/lib/utils/model-price-fields.ts
  • tests/unit/actions/model-prices.test.ts
  • tests/unit/settings/prices/model-price-details-dialog.test.tsx
✅ Files skipped from review due to trivial changes (3)
  • messages/zh-CN/settings/prices.json
  • messages/en/settings/prices.json
  • tests/unit/settings/prices/model-price-details-dialog.test.tsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • tests/unit/actions/model-prices.test.ts
  • messages/ja/settings/prices.json
  • messages/ru/settings/prices.json
  • src/app/[locale]/settings/prices/_components/model-price-details-dialog.tsx
  • src/app/[locale]/settings/prices/_components/model-price-drawer.tsx

Comment thread src/actions/model-prices.ts
Comment thread src/actions/model-prices.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e6a4333a44

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +239 to +242
if (NON_EDITABLE_MANAGED_FIELDS.has(key) || value === undefined) {
continue;
}
result[key] = value;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exclude provider pricing map from editable extras

getEditableExtraPriceData currently copies every non-managed top-level key, which includes pricing, and upsertSingleModelPrice persists that blob back into a new manual record. For models whose actual rates are only under priceData.pricing (the resolver already supports this shape), this produces a manual record with no top-level billable fields; resolveManualPricing still prefers it because hasValidPriceData sees nested numbers, but calculateRequestCost does not bill from nested pricing nodes, so requests can be recorded as $0.

Useful? React with 👍 / 👎.

Comment on lines +115 to +116
return /(cost|price|rate|multiplier|per_|second|session|query|page|pixel|character|dbu)/i.test(
key

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Tighten price-like matcher to avoid metadata misclassification

The matcher treats bare words such as rate, session, query, and page as price indicators, so non-billing metadata keys (for example request_rate_limit or session_timeout) are classified as price-like. Because these keys now feed collectAdditionalPriceLikeNumbers, a metadata-only manual record can pass hasValidPriceData and override cloud pricing even though the billing path cannot charge from those fields.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@messages/zh-TW/settings/prices.json`:
- Line 198: The placeholder string advancedFieldsPlaceholder currently contains
outer single quotes that will cause JSON.parse to fail when users paste it;
update the value for "advancedFieldsPlaceholder" to remove the surrounding
single quotes so it contains a plain JSON object example like
{"input_cost_per_second": 0.5} (ensure the file remains valid JSON string value
and escape quotes as needed in the JSON file).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 92d8860a-5ff6-4244-9062-351f37ccfdc2

📥 Commits

Reviewing files that changed from the base of the PR and between e6a4333 and 19aa55e.

📒 Files selected for processing (6)
  • messages/en/settings/prices.json
  • messages/ja/settings/prices.json
  • messages/ru/settings/prices.json
  • messages/zh-CN/settings/prices.json
  • messages/zh-TW/settings/prices.json
  • src/actions/model-prices.ts
✅ Files skipped from review due to trivial changes (2)
  • messages/zh-CN/settings/prices.json
  • messages/en/settings/prices.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • messages/ja/settings/prices.json
  • messages/ru/settings/prices.json
  • src/actions/model-prices.ts

"cachePricingTitle": "快取價格",
"advancedFieldsTitle": "進階欄位",
"advancedFieldsJson": "額外價格欄位 JSON",
"advancedFieldsPlaceholder": "範例:'{\"input_cost_per_second\": 0.5}'",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

占位示例外层的单引号会导致复制即失败。

当前 placeholder 为 '{"input_cost_per_second": 0.5}',用户按示例原样粘贴到 textarea 时,外层单引号会使 JSON.parse 失败。建议去掉外层单引号,仅保留纯 JSON 对象。

建议修改
-    "advancedFieldsPlaceholder": "範例:'{\"input_cost_per_second\": 0.5}'",
+    "advancedFieldsPlaceholder": "範例:{\"input_cost_per_second\": 0.5}",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@messages/zh-TW/settings/prices.json` at line 198, The placeholder string
advancedFieldsPlaceholder currently contains outer single quotes that will cause
JSON.parse to fail when users paste it; update the value for
"advancedFieldsPlaceholder" to remove the surrounding single quotes so it
contains a plain JSON object example like {"input_cost_per_second": 0.5} (ensure
the file remains valid JSON string value and escape quotes as needed in the JSON
file).

@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

@ding113
ding113 merged commit 7210c0f into dev Apr 20, 2026
12 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap Apr 20, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 19aa55e3e4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +609 to +610
if (path && isPriceLikeFieldPath(path)) {
if (typeof value === "string" && value.trim()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Narrow extra-field numeric validation to billing keys

The new sanitizeExtraPriceData path check uses isPriceLikeFieldPath for validation, so any extra field whose key contains broad tokens like session, rate, or page is forced to be a non-negative number. This causes legitimate metadata (for example {"session_timeout":"30s"} or {"request_rate_limit":"1000rpm"}) to be rejected with a validation error, which breaks the stated goal of preserving arbitrary extra fields during manual edits. Validation should be limited to explicit billable key patterns (or a maintained allowlist) rather than all loosely “price-like” names.

Useful? React with 👍 / 👎.

@ding113
ding113 deleted the fix/pricing-billing-pr-20260420 branch May 13, 2026 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core area:i18n area:UI bug Something isn't working size/L Large PR (< 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant