Skip to content

feat(pricing): resolve provider-aware billing for multi-provider models#873

Merged
ding113 merged 9 commits intodevfrom
feat/codex-1m
Mar 6, 2026
Merged

feat(pricing): resolve provider-aware billing for multi-provider models#873
ding113 merged 9 commits intodevfrom
feat/codex-1m

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Mar 6, 2026

Summary

This PR implements provider-aware billing resolution for multi-provider models (gpt-5.4/gpt-5.4-pro), fixes the long-context tiered pricing calculation bug, and adds comprehensive pricing source visibility throughout the system.

Problem

Related Issues:

Issues Addressed

  1. Multi-provider model billing ambiguity - Models available through multiple providers (gpt-5.4 via OpenAI, Azure, or other aggregators) had unclear pricing attribution.

  2. Incorrect long-context tiered pricing - The calculateTieredCost() function used a split calculation (base for first 200K, premium for excess) when providers require all tokens at premium rate when threshold is exceeded.

  3. Lack of pricing transparency - Users couldn't see which provider's pricing was used or the source of price data in logs or settings.

  4. Provider price override limitations - No way to pin specific provider prices to local manual prices.

Solution

Core Changes

  1. New Pricing Resolution System (src/lib/utils/pricing-resolution.ts)

    • Implements provider-aware price resolution with 5 pricing sources:
      • local_manual - User-defined manual pricing
      • cloud_exact - Exact match from cloud provider pricing
      • cloud_model_fallback - Fallback to model-level pricing
      • priority_fallback - Priority provider fallback
      • single_provider_top_level - Single provider top-level pricing
    • Handles multi-provider model resolution with fallback chains
  2. Fixed Long-Context Pricing (src/lib/utils/cost-calculation.ts)

    • Changed calculateTieredCost() to charge ALL tokens at premium rate when threshold exceeded
    • Same fix applied to calculateTieredCostWithSeparatePrices()
    • Cost impact example: For 300K tokens at $3/MTok base with 2x premium:
      • Before (incorrect): $1.20 (200K * $3 + 100K * $6)
      • After (correct): $1.80 (300K * $6) - 33% increase, matching provider billing
  3. Pricing Source Visibility in Logs (src/app/[locale]/dashboard/logs/)

    • Added pricing provider and source display in log detail views
    • Metadata tab shows resolved pricing source
    • Summary tab displays pricing provider badge
  4. Settings Price Management Enhancement (src/app/[locale]/settings/prices/)

    • Added provider pricing dialog for viewing cloud provider prices
    • Implemented provider-price pinning to local manual prices
    • Added pricing source indicators in price list
  5. Model Price Repository Updates (src/repository/model-price.ts)

    • Added functions for provider-aware price queries
    • Enhanced price data retrieval with source tracking

Supporting Changes

  • i18n Updates - Added translations for pricing source labels in 5 languages (en, zh-CN, zh-TW, ja, ru)
  • Type Definitions - Added ResolvedPricingSource type and updated ModelPrice types
  • Test Coverage - Added comprehensive tests:
    • pricing-resolution.test.ts - Unit tests for pricing resolution logic
    • cost-calculation-long-context.test.ts - Tests for fixed tiered pricing
    • billing-model-source.test.ts - Integration tests for billing with model source
    • model-prices.test.ts - Tests for model price actions
    • price-list-multi-provider-ui.test.tsx - UI tests for multi-provider price list

Changes Summary

Category Count Details
New files 1 src/lib/utils/pricing-resolution.ts (408 lines)
Modified files 22 Core logic, UI components, i18n, tests
i18n languages 5 en, zh-CN, zh-TW, ja, ru
Test files added 6 Unit and integration tests

Breaking Changes

None. This PR maintains backward compatibility:

  • Existing price calculations continue to work
  • New pricing source fields are additive only
  • Database schema changes are nullable/optional

Testing

Automated Tests

  • Unit tests added for pricing resolution (pricing-resolution.test.ts)
  • Unit tests added for fixed long-context tiered pricing (cost-calculation-long-context.test.ts)
  • Integration tests for billing with model source (billing-model-source.test.ts)
  • Unit tests for model price actions (model-prices.test.ts)
  • UI tests for multi-provider price list (price-list-multi-provider-ui.test.tsx)

Validation Commands

bun run typecheck  # TypeScript type checking
bun run build      # Production build
bun run lint       # Biome linting
bun run test       # Run all tests

Manual Testing Areas

  1. Pricing Resolution: Verify correct pricing source is shown for multi-provider models
  2. Long-context Pricing: Test that 1M context models charge all tokens at premium rate when >200K tokens
  3. Settings UI: Verify provider pricing dialog and pinning functionality
  4. Logs UI: Check pricing provider display in usage log details

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally
  • TypeScript type checking passes
  • i18n strings added for all 5 languages
  • Breaking changes assessed (none found)

Description enhanced by Claude AI

Greptile Summary

This PR implements provider-aware billing resolution for multi-provider models, fixes long-context tiered pricing (charging all tokens at the premium rate when the threshold is exceeded rather than only the excess), and adds pricing source visibility in logs and settings UI across 37 files.

Key changes:

  • New pricing-resolution.ts module with a 5-step fallback chain (local_manualcloud_exactcloud_model_fallbackofficial_fallbackpriority_fallbacksingle_provider_top_level) for resolving which provider's pricing to use
  • calculateTieredCost and calculateTieredCostWithSeparatePrices renamed (prefixed with _/__) and effectively replaced by inline multiplyCost logic that applies the premium rate to all tokens when the threshold is exceeded — a ~33%+ cost increase for long-context requests that were previously using split-tier billing
  • Session-level pricing resolution now uses a resolvedPricingCache Map to avoid redundant DB queries per request
  • Pricing source and provider key are surfaced in log tooltips, metadata tabs, and settings UI

Issues found:

  • The cost tooltip in usage-logs-table.tsx still displays (2x >200k) / (1.5x >200k) annotations that describe the old tiered behavior. With the new all-tokens-at-premium approach, these labels are misleading and the guard condition only checks inputTokens rather than total input context (which includes cache tokens)
  • In resolveLongContextThreshold, GPT-family models (model_family === "gpt") are assigned the 272k threshold even when the price record only contains 200k pricing fields — this creates an asymmetry between when the premium activates (272k) and what rate is used (200k-tier rate), potentially under-billing requests in the 200k–272k range
  • resolvePricingKeyCandidates uses host.includes("openai.com") which also matches Azure OpenAI endpoints (*.openai.azure.com), causing Azure-hosted GPT deployments to resolve against the "openai" pricing node rather than an Azure-specific one

Confidence Score: 2/5

  • This PR has a high-impact billing behavior change and contains a known unfixed UI bug, a potential threshold/rate mismatch for GPT models, and a provider misclassification that could affect Azure customers.
  • The core billing change (all tokens at premium) is a significant behavioral change with real financial impact on users. The virtualized-logs-table.tsx getPricingSourceLabel bug (missing ${source} interpolation) is still present and unaddressed from prior review threads. The GPT-family threshold mismatch in resolveLongContextThreshold could cause under-billing in the 200k–272k token range for models that only have 200k pricing data. The Azure/OpenAI provider URL overlap is a potential pricing correctness issue for a provider type the PR explicitly aims to support. Combined, these issues warrant careful attention before merging.
  • Pay close attention to src/lib/utils/cost-calculation.ts (threshold/rate mismatch for GPT models), src/lib/utils/pricing-resolution.ts (Azure provider URL matching), src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx (broken translation key interpolation), and src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx (stale billing annotations).

Important Files Changed

Filename Overview
src/lib/utils/cost-calculation.ts Revised long-context billing to charge all tokens at premium rate when threshold exceeded. The hasRealCacheCreationBase guard is correctly applied to the 1h cache branch. However, a threshold/rate mismatch exists for GPT-family models that only have 200k pricing fields but are now evaluated against the 272k threshold. The two renamed-but-unused helper functions (_calculateTieredCost, __calculateTieredCostWithSeparatePrices) are dead code (previously flagged).
src/lib/utils/pricing-resolution.ts New file implementing provider-aware pricing resolution with a 5-step fallback chain. The official_fallback source is now correctly included in ResolvedPricingSource and is produced by resolveFromPricingMap. The resolveManualPricing function correctly passes resolvedPricingProviderKey to mergePriceData. Azure OpenAI endpoints may be incorrectly matched against the OpenAI pricing node due to the host.includes("openai.com") check.
src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx Added pricing provider and source display in cost tooltip. However, the hardcoded "(2x >200k)" / "(1.5x >200k)" tooltip annotations are now stale — they describe the old split-tiered behavior, while the new billing charges all tokens at the premium rate when the threshold is exceeded. The guard condition also only checks inputTokens, missing cache tokens that contribute to the threshold check.
src/app/v1/_lib/proxy/session.ts Replaced getCachedPriceDataByBillingSource with getResolvedPricingByBillingSource, now backed by a resolvedPricingCache Map to avoid redundant DB queries per request. Cache key correctly includes provider identity. Logic correctly implements 2-stage resolution (primary then fallback model).
src/app/v1/_lib/proxy/response-handler.ts Integrated resolvePricingForModelRecords for billing resolution. The updateRequestCostFromUsage function performs its own DB queries independent of the session cache, but this is acceptable as it runs in a standalone context. The early return when usageMetrics is null (missing updateMessageRequestDetails) was previously flagged.
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx Added pricing provider/source display in cost tooltip. The getPricingSourceLabel function on line 78 has a bug where the source parameter is not interpolated into the translation key (previously flagged). Every pricing-source label will render as a broken key string.
src/types/special-settings.ts Added PricingResolutionSpecialSetting and AnthropicContext1mHeaderOverrideSpecialSetting types. The "official_fallback" source is now correctly present in both PricingResolutionSpecialSetting.source and ResolvedPricingSource (previously flagged as mismatch). New CodexServiceTierResultSpecialSetting type added cleanly.
src/repository/model-price.ts Added findPricesByModelFamily and findProviderPricesByModel functions for provider-aware pricing queries. Functions are well-structured and include proper null handling.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Request with model + provider] --> B[getResolvedPricingByBillingSource]
    B --> C{Check billingModelSource setting}
    C -->|original| D[primaryModel = originalModel]
    C -->|redirected| E[primaryModel = redirectedModel]
    D --> F[findLatestPriceByModel primary]
    E --> F
    F --> G[resolvePricingForModelRecords]

    G --> H{Manual source?}
    H -->|yes| I[resolveManualPricing → local_manual]
    H -->|no| J{Exact provider key match?}

    J -->|yes| K[resolveFromPricingMap exact → cloud_exact / cloud_model_fallback]
    J -->|no| L{Official provider key match?}

    L -->|yes| M[resolveFromPricingMap official → official_fallback]
    L -->|no| N{Detailed score fallback?}

    N -->|yes| O[resolveDetailedFallback → priority_fallback]
    N -->|no| P[resolveTopLevel → single_provider_top_level / cloud_model_fallback]

    I --> Q[ResolvedPricing priceData + source]
    K --> Q
    M --> Q
    O --> Q
    P --> Q

    Q --> R{longContextThresholdExceeded?}
    R -->|yes| S[ALL tokens × premium rate]
    R -->|no| T[tokens × base rate]
    S --> U[Final cost]
    T --> U
Loading

Comments Outside Diff (1)

  1. src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx, line 435-449 (link)

    Stale tooltip annotation — describes old tiered behavior

    The (2x >200k) / (1.5x >200k) tooltip annotations imply that only tokens above the threshold are billed at the premium rate. However, this PR changed calculateRequestCost and calculateRequestCostBreakdown to bill all tokens at the premium rate the moment the threshold is exceeded (not just the excess).

    For example, a 300K-token request is now billed at 2× on all 300K tokens, not 200K×1x + 100K×2x. The annotation says ">200k" which is now misleading for users trying to reconcile costs from the tooltip.

    Additionally, the guard condition (log.inputTokens ?? 0) > 200000 only checks inputTokens, while the actual long-context premium is triggered by total input context (including cache creation and cache read tokens). A request with 150K prompt tokens + 100K cache tokens would exceed the 200K threshold and be billed at premium rates, yet this annotation would not appear.

    Consider updating the annotation to reflect the new behavior:

    and similarly for the output annotation:

    (1.5x all tokens)
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
    Line: 435-449
    
    Comment:
    **Stale tooltip annotation — describes old tiered behavior**
    
    The `(2x >200k)` / `(1.5x >200k)` tooltip annotations imply that *only tokens above the threshold* are billed at the premium rate. However, this PR changed `calculateRequestCost` and `calculateRequestCostBreakdown` to bill **all** tokens at the premium rate the moment the threshold is exceeded (not just the excess).
    
    For example, a 300K-token request is now billed at 2× on all 300K tokens, not `200K×1x + 100K×2x`. The annotation says ">200k" which is now misleading for users trying to reconcile costs from the tooltip.
    
    Additionally, the guard condition `(log.inputTokens ?? 0) > 200000` only checks `inputTokens`, while the actual long-context premium is triggered by total input context (including cache creation and cache read tokens). A request with 150K prompt tokens + 100K cache tokens would exceed the 200K threshold and be billed at premium rates, yet this annotation would not appear.
    
    Consider updating the annotation to reflect the new behavior:
    
    
    and similarly for the output annotation:
    ```ts
    (1.5x all tokens)
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: cffef3b

Greptile also left 2 inline comments on this PR.

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

📝 Walkthrough

Walkthrough

引入多供应商定价解析与固化功能:新增 pricing-resolution 模块、扩展定价数据模型与相关仓库/动作、在代理/响应路径中使用解析结果并更新成本计算,同时添加多语言字符串与前端比较/固化 UI 及相应测试覆盖。

Changes

Cohort / File(s) Summary
本地化 - 仪表板
messages/en/dashboard.json, messages/ja/dashboard.json, messages/ru/dashboard.json, messages/zh-CN/dashboard.json, messages/zh-TW/dashboard.json
为 billingDetails 添加 pricingProviderpricingSourceLabelpricingSource(包含 local_manual、cloud_exact、cloud_model_fallback、priority_fallback、single_provider_top_level、official_fallback);调整 statusCodes.not200 位置并在 quickValues 中加入 unlimited
本地化 - 价格设置
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
新增 providerPricing 翻译块(title/description/input/output/cacheRead/pinAction/pinSuccess/pinFailed/pinned/priority),添加 actions.comparePricing 与 badges 文本更新。
定价解析核心模块
src/lib/utils/pricing-resolution.ts, src/types/special-settings.ts
新增多阶段定价解析实现 resolvePricingForModelRecords,导出 ResolvedPricingResolvedPricingSource(含官方回退等源)及相关类型和候选解析函数。
特殊设置工具
src/lib/utils/special-settings.ts
支持新 special-setting 类型(pricing_resolution、codex_service_tier_result),新增序列化与检测逻辑,导出 getPricingResolutionSpecialSettinggetPriorityServiceTierSpecialSetting,并调整 anthropic 1M 覆盖的插入逻辑。
类型与数据结构
src/types/model-price.ts
扩展 ModelPriceData:新增 272k/200k/1hr 分层字段、priority 定价字段、pricingselected_pricing_providerselected_pricing_source_modelselected_pricing_resolution 等元数据;mode 支持 responses
仓库与动作
src/repository/model-price.ts, src/actions/model-prices.ts
仓库新增 findLatestPriceByModelAndSourcefindLatestPricesByModels 批量查询;新增动作 pinModelPricingProviderAsManual 与辅助构建函数,实现将云端节点固化为本地手动价格并 upsert。
代理/会话/响应处理
src/app/v1/_lib/proxy/forwarder.ts, src/app/v1/_lib/proxy/response-handler.ts, src/app/v1/_lib/proxy/session.ts
将成本计算与持久化改为基于 ResolvedPricing(含 provider/source/model),在会话中引入 resolvedPricingCache 与 getResolvedPricingByBillingSource,并在 response-handler 中传播并持久化 pricing_resolution 与 service-tier 结果,修改若干签名(如 updateRequestCostFromUsage)。
成本计算与价格验证
src/lib/utils/cost-calculation.ts, src/lib/utils/price-data.ts
重构成本计算以支持长上下文阈值与 priority 覆盖(新增阈值常量、长上下文路径、priority 参数),并增强价格数据数值验证(新增 hasValidNumericPrice / collectNumericCosts)。
仪表盘日志 UI
src/app/[locale]/dashboard/logs/_components/.../MetadataTab.tsx, .../SummaryTab.tsx, .../usage-logs-table.tsx, .../virtualized-logs-table.tsx
从 special-settings 读取 pricing_resolution 与 priority 指示,条件渲染 pricingProviderpricingSourceLabel 与优先级徽章;VirtualizedLogsTableProps 添加可选 serverTimeZone
价格设置 UI 与对话框
src/app/[locale]/settings/prices/_components/price-list.tsx, .../provider-pricing-dialog.tsx
price-list 使用 resolvePricingForModelRecords 计算 displayPricing,显示多供应商/本地徽章并在操作内嵌入 ProviderPricingDialog;新增 ProviderPricingDialog 用于比较供应商价格并触发后端 pin 操作。
后端计费/流/追踪改动
src/app/v1/_lib/proxy/response-handler.ts, 相关 repo/trace 调用
成本计算、Redis 与 Langfuse 跟踪现在携带 pricing-resolution 元数据(provider/source/model)和 priorityServiceTier,使用 resolved priceData 计算并在 finalize 流程中持久化。
测试
tests/integration/..., tests/unit/...
新增/更新集成与单元测试:pricing-resolution、长上下文与 priority 成本计算、pin 动作、UI 多供应商显示、special-settings 行为、代理/会话与无 usageMetrics 的 finalize 场景等。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 分钟

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning PR未完全实现#869的全部要求:长上下文定价修复部分实现,但缺少对Anthropic模型前缀的更新。 在src/lib/special-attributes/index.ts中添加缺失的Anthropic模型前缀(claude-opus-4-6, claude-sonnet-4-6, claude-opus-4-5)以完成#869的次要需求。
Out of Scope Changes check ⚠️ Warning 检测到超范围变更:成本计算中的长上下文阈值与费率不匹配问题未修复,虚拟化日志表中的i18n关键字插值bug仍存在。 修复src/lib/utils/cost-calculation.ts中GPT模型的272k阈值与200k费率不匹配问题,以及virtualized-logs-table.tsx中getPricingSourceLabel的缺失参数插值。
Docstring Coverage ⚠️ Warning Docstring coverage is 26.23% 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 (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/codex-1m

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 the size/XL Extra Large PR (> 1000 lines) label Mar 6, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

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

This pull request significantly upgrades the model pricing and billing system to handle complex scenarios involving multi-provider models and tiered long-context pricing. It introduces a robust pricing resolution pipeline, enhances the data structure for model prices, and provides new user interface features for better visibility and control over how costs are determined and applied.

Highlights

  • Multi-Provider Pricing Resolution: Implemented a comprehensive system to resolve model pricing, prioritizing local manual prices, exact cloud provider matches, official provider fallbacks, and detailed fallbacks based on data completeness for multi-provider models.
  • Long-Context Pricing Logic: Enhanced cost calculation to accurately apply tiered pricing for long contexts (200k and 272k tokens) by considering request input-context thresholds and explicit provider pricing.
  • Pricing Visibility in UI: Introduced new UI elements in dashboard logs and settings to display the resolved pricing provider and its source, offering greater transparency into billing decisions.
  • Provider Price Pinning: Added functionality to compare provider-specific prices for a model and pin a chosen provider's pricing as the local manual price, giving users more control over their billing.
  • Refactored Pricing Data and Logic: Updated the ModelPriceData structure to support nested provider-specific pricing and refactored core billing logic to leverage the new pricing resolution mechanism.
Changelog
  • messages/en/dashboard.json
    • Added new translation keys for pricing provider, pricing source label, and various pricing source types.
    • Reordered some status code translations and quick value options.
  • messages/en/settings/prices.json
    • Added 'multi' badge translation.
    • Added 'Compare pricing' action translation.
    • Introduced new translation keys for the provider pricing dialog, including title, description, input/output/cache read labels, and pinning actions.
  • messages/ja/dashboard.json
    • Added new Japanese translation keys for pricing provider, pricing source label, and various pricing source types.
    • Reordered some status code translations and quick value options.
  • messages/ja/settings/prices.json
    • Added 'Compare pricing' action translation in Japanese.
    • Introduced new Japanese translation keys for the provider pricing dialog.
  • messages/ru/dashboard.json
    • Added new Russian translation keys for pricing provider, pricing source label, and various pricing source types.
    • Reordered some status code translations and quick value options.
  • messages/ru/settings/prices.json
    • Added 'Compare pricing' action translation in Russian.
    • Introduced new Russian translation keys for the provider pricing dialog.
  • messages/zh-CN/dashboard.json
    • Added new Simplified Chinese translation keys for pricing provider, pricing source label, and various pricing source types.
    • Reordered some status code translations and quick value options.
  • messages/zh-CN/settings/prices.json
    • Added 'multi' badge translation in Simplified Chinese.
    • Added 'Compare pricing' action translation in Simplified Chinese.
    • Introduced new Simplified Chinese translation keys for the provider pricing dialog.
  • messages/zh-TW/dashboard.json
    • Added new Traditional Chinese translation keys for pricing provider, pricing source label, and various pricing source types.
    • Reordered some status code translations and quick value options.
  • messages/zh-TW/settings/prices.json
    • Added 'multi' badge translation in Traditional Chinese.
    • Added 'Compare pricing' action translation in Traditional Chinese.
    • Introduced new Traditional Chinese translation keys for the provider pricing dialog.
  • src/actions/model-prices.ts
    • Imported findLatestPriceByModelAndSource for specific price lookups.
    • Added buildManualPriceDataFromProviderPricing to create manual price data from provider pricing nodes.
    • Removed a redundant comment in deleteSingleModelPrice.
    • Implemented pinModelPricingProviderAsManual action to allow administrators to pin a cloud provider's pricing as a local manual price.
  • src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx
    • Imported getPricingResolutionSpecialSetting to retrieve pricing resolution details.
    • Added UI to display the resolved pricing provider and source in the metadata tab.
  • src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx
    • Imported getPricingResolutionSpecialSetting to retrieve pricing resolution details.
    • Added UI to display the resolved pricing provider and source in the summary tab.
  • src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx
    • Added a new test suite to verify the rendering of pricing provider and source details in the usage logs table.
  • src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
    • Imported getPricingResolutionSpecialSetting for pricing details.
    • Added a helper function getPricingSourceLabel for localized pricing source names.
    • Modified the table to display pricing provider and source information for each log entry.
  • src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
    • Imported getPricingResolutionSpecialSetting for pricing details.
    • Added a helper function getPricingSourceLabel for localized pricing source names.
    • Modified the table to display pricing provider and source information for each log entry.
    • Added serverTimeZone prop to the component.
  • src/app/[locale]/settings/prices/_components/price-list.tsx
    • Imported ArrowRightLeft icon and resolvePricingForModelRecords.
    • Integrated ProviderPricingDialog for comparing and pinning prices.
    • Updated price rendering logic to use resolved pricing data, displaying the active pricing provider and a 'Multi' badge for models with multiple pricing nodes.
    • Added a 'Compare pricing' option to the dropdown menu for models with multi-provider pricing.
  • src/app/[locale]/settings/prices/_components/provider-pricing-dialog.tsx
    • Added a new component ProviderPricingDialog to display and compare provider-specific pricing for a model.
    • Implemented functionality to pin a selected provider's pricing as the local manual price.
  • src/app/v1/_lib/proxy/forwarder.ts
    • Modified the context1mApplied logic to specifically apply the Anthropic 1M context header override only for Claude providers.
  • src/app/v1/_lib/proxy/response-handler.ts
    • Imported resolvePricingForModelRecords for advanced pricing determination.
    • Added ensurePricingResolutionSpecialSetting to record the resolved pricing details as a special setting.
    • Updated various functions (finalizeDeferredStreamingFinalizationIfNeeded, finalizeRequestStats, updateRequestCostFromUsage, trackCostToRedis, persistRequestFailure) to use the new pricing resolution logic and persist special settings.
    • Corrected a log message from 'stream' to 'stream finalized'.
    • Adjusted Gemini usage metrics priority from promptTokenCount to candidatesTokenCount.
    • Refactored updateRequestCostFromUsage to simplify billing model source fallback logic using resolvePricingForModelRecords.
  • src/app/v1/_lib/proxy/session.ts
    • Imported resolvePricingForModelRecords and ResolvedPricing type.
    • Removed cachedBillingPriceData property.
    • Refactored getCachedPriceDataByBillingSource into a new getResolvedPricingByBillingSource method that encapsulates the complex pricing resolution logic.
  • src/lib/utils/cost-calculation.ts
    • Added OPENAI_LONG_CONTEXT_TOKEN_THRESHOLD constant (272k).
    • Refactored tiered cost calculation functions (_calculateTieredCost, __calculateTieredCostWithSeparatePrices) for clarity and consistency.
    • Introduced resolveLongContextThreshold to dynamically determine the long-context threshold (200k or 272k) based on model family or explicit price data.
    • Added getRequestInputContextTokens to calculate total input context for threshold checks.
    • Updated calculateRequestCostBreakdown and calculateRequestCost to apply long-context tiered pricing based on the resolved threshold and new price data fields.
  • src/lib/utils/price-data.ts
    • Refactored hasValidPriceData to use helper functions for numeric price validation.
    • Expanded numeric cost collection to include new 272k, 1hr cache, and priority pricing fields.
    • Modified hasValidPriceData to also check for valid numeric prices within the nested pricing object, supporting multi-provider pricing.
  • src/lib/utils/pricing-resolution.ts
    • Added a new utility file pricing-resolution.ts to centralize and manage the complex logic for resolving model pricing.
    • Defined types for ResolvedPricingSource and ResolvedPricing.
    • Implemented functions to identify pricing key candidates, extract official provider keys, merge price data, and resolve pricing based on various criteria and fallbacks.
  • src/lib/utils/special-settings.ts
    • Removed CONTEXT_1M_BETA_HEADER import.
    • Updated comment for context1mApplied to clarify its role.
    • Added pricing_resolution case to buildSettingKey for consistent serialization.
    • Removed automatic derivation of anthropic_context_1m_header_override from context1mApplied.
    • Added getPricingResolutionSpecialSetting helper to easily retrieve pricing resolution details.
  • src/repository/model-price.ts
    • Imported inArray for more efficient queries.
    • Added findLatestPriceByModelAndSource to fetch price records by model name and source.
    • Added findLatestPricesByModels to retrieve latest prices for multiple models using DISTINCT ON.
  • src/types/model-price.ts
    • Extended ModelPriceData with new tiered pricing fields for 272k tokens and priority service tiers.
    • Added a pricing field to store nested provider-specific pricing data.
    • Introduced selected_pricing_provider, selected_pricing_source_model, and selected_pricing_resolution fields to track how pricing was resolved.
    • Expanded the mode type to include 'responses'.
  • src/types/special-settings.ts
    • Added PricingResolutionSpecialSetting type to define the structure for tracking pricing resolution information.
  • tests/integration/billing-model-source.test.ts
    • Modified createSession to accept providerOverrides for more flexible testing.
    • Added a new integration test case to validate nested pricing resolution for alias models with specific providers.
  • tests/unit/actions/model-prices.test.ts
    • Mocked findLatestPriceByModelAndSource for unit testing.
    • Added a unit test for pinModelPricingProviderAsManual to ensure correct pinning of cloud provider pricing.
  • tests/unit/lib/cost-calculation-long-context.test.ts
    • Added a new unit test file to specifically verify long-context pricing calculations.
  • tests/unit/lib/utils/pricing-resolution.test.ts
    • Added a new unit test file with comprehensive tests for resolvePricingForModelRecords, covering various fallback and prioritization scenarios.
  • tests/unit/lib/utils/special-settings.test.ts
    • Updated a test to reflect that context1mApplied no longer automatically derives Anthropic 1M context header overrides.
  • tests/unit/settings/prices/price-list-multi-provider-ui.test.tsx
    • Added a new UI test file to ensure the 'Multi' badge is correctly rendered for models with multiple provider pricing nodes in the price list.
Activity
  • The pull request introduces a new feature for provider-aware billing and long-context pricing.
  • It includes significant refactoring of pricing resolution logic and data structures.
  • New UI components and translations have been added to support the new pricing features.
  • Comprehensive unit and integration tests have been added for the new pricing resolution and UI components.
  • The author has validated the changes through typecheck, build, and targeted vitest suites.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

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

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

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

Copy link
Contributor

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

Choose a reason for hiding this comment

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

Code Review

This is a substantial and well-executed pull request that introduces a sophisticated provider-aware billing mechanism. The new pricing resolution logic is well-structured, and the changes are consistently applied across the backend, UI, and tests. The addition of provider price pinning and visibility in logs are excellent features that will greatly improve usability. I've identified a couple of issues, one in the UI and a more subtle one in the backend stream handling logic, which I've detailed in the comments.

Comment on lines +78 to 79
const getPricingSourceLabel = (source: string) => t(`logs.billingDetails.pricingSource.`);
const tChain = useTranslations("provider-chain");
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The implementation of getPricingSourceLabel is incorrect. The source parameter is not being used within the template literal, which will cause translation lookups to fail. It should be included in the key path.

Suggested change
const getPricingSourceLabel = (source: string) => t(`logs.billingDetails.pricingSource.`);
const tChain = useTranslations("provider-chain");
t(`logs.billingDetails.pricingSource.${source}`);

Comment on lines +1221 to +1225

pushChunk(headPart, remainingHeadBytes);

pushToTail();
pushChunk(tailPart, bytes - remainingHeadBytes);
Copy link
Contributor

Choose a reason for hiding this comment

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

high

There's a logical issue in how a chunk that spans the head/tail boundary is processed. The current implementation calls pushToTail() which uses the original, full-sized chunk from its closure scope, not the tailPart. Additionally, it then recursively calls pushChunk(tailPart, ...) which will process the tail part again. This will lead to duplicated and incorrect data in the buffered response for statistics.

A simpler and more correct approach would be to recursively call pushChunk for the head and tail parts, and set inTailMode to true after the head is filled.

Suggested change
pushChunk(headPart, remainingHeadBytes);
pushToTail();
pushChunk(tailPart, bytes - remainingHeadBytes);
pushChunk(headPart, remainingHeadBytes);
inTailMode = true;
pushChunk(tailPart, bytes - remainingHeadBytes);

Comment on lines 2898 to 2901
const providerIdForPersistence = providerIdOverride ?? session.provider?.id;

// 1. 结束请求状态追踪
ProxyStatusTracker.getInstance().endRequest(messageContext.user.id, messageContext.id);

// 2. 更新请求时长
await updateMessageRequestDuration(messageContext.id, duration);

// 3. 解析 usage metrics
const { usageMetrics } = parseUsageFromResponseText(responseText, provider.providerType);

if (!usageMetrics) {
// 即使没有 usageMetrics,也需要更新状态码和 provider chain
await updateMessageRequestDetails(messageContext.id, {
statusCode: statusCode,
...(errorMessage ? { errorMessage } : {}),
ttfbMs: session.ttfbMs ?? duration,
providerChain: session.getProviderChain(),
model: session.getCurrentModel() ?? undefined,
providerId: providerIdForPersistence, // 更新最终供应商ID(重试切换后)
context1mApplied: session.getContext1mApplied(),
swapCacheTtlApplied: provider.swapCacheTtlBilling ?? false,
});
return null;
Copy link

Choose a reason for hiding this comment

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

Missing request-details update when usageMetrics is null

The old finalizeRequestStats called updateMessageRequestDetails before the early return when usageMetrics was null, ensuring that the status code, error message, provider chain, model, and providerId were always written to the database for every request. That call was removed — now the function simply returns null without persisting anything.

For all code paths that delegate to finalizeRequestStats (Gemini non-stream passthrough, streaming passthrough error branches, etc.), requests whose response body cannot be parsed for usage metrics — including many error responses — will never have their status code, error details, or redirected model written to the message record. This means the log entry will remain in an incomplete state.

Suggested change
const providerIdForPersistence = providerIdOverride ?? session.provider?.id;
// 1. 结束请求状态追踪
ProxyStatusTracker.getInstance().endRequest(messageContext.user.id, messageContext.id);
// 2. 更新请求时长
await updateMessageRequestDuration(messageContext.id, duration);
// 3. 解析 usage metrics
const { usageMetrics } = parseUsageFromResponseText(responseText, provider.providerType);
if (!usageMetrics) {
// 即使没有 usageMetrics,也需要更新状态码和 provider chain
await updateMessageRequestDetails(messageContext.id, {
statusCode: statusCode,
...(errorMessage ? { errorMessage } : {}),
ttfbMs: session.ttfbMs ?? duration,
providerChain: session.getProviderChain(),
model: session.getCurrentModel() ?? undefined,
providerId: providerIdForPersistence, // 更新最终供应商ID(重试切换后)
context1mApplied: session.getContext1mApplied(),
swapCacheTtlApplied: provider.swapCacheTtlBilling ?? false,
});
return null;
if (!usageMetrics) {
await updateMessageRequestDetails(messageContext.id, {
statusCode: statusCode,
...(errorMessage ? { errorMessage } : {}),
ttfbMs: session.ttfbMs ?? duration,
providerChain: session.getProviderChain(),
model: session.getCurrentModel() ?? undefined,
providerId: providerIdForPersistence,
context1mApplied: session.getContext1mApplied(),
swapCacheTtlApplied: session.provider?.swapCacheTtlBilling ?? false,
specialSettings: session.getSpecialSettings() ?? undefined,
});
return null;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/response-handler.ts
Line: 2898-2901

Comment:
**Missing request-details update when usageMetrics is null**

The old `finalizeRequestStats` called `updateMessageRequestDetails` before the early return when `usageMetrics` was null, ensuring that the status code, error message, provider chain, model, and `providerId` were always written to the database for every request. That call was removed — now the function simply returns `null` without persisting anything.

For all code paths that delegate to `finalizeRequestStats` (Gemini non-stream passthrough, streaming passthrough error branches, etc.), requests whose response body cannot be parsed for usage metrics — including many error responses — will never have their status code, error details, or redirected model written to the message record. This means the log entry will remain in an incomplete state.

```suggestion
  if (!usageMetrics) {
    await updateMessageRequestDetails(messageContext.id, {
      statusCode: statusCode,
      ...(errorMessage ? { errorMessage } : {}),
      ttfbMs: session.ttfbMs ?? duration,
      providerChain: session.getProviderChain(),
      model: session.getCurrentModel() ?? undefined,
      providerId: providerIdForPersistence,
      context1mApplied: session.getContext1mApplied(),
      swapCacheTtlApplied: session.provider?.swapCacheTtlBilling ?? false,
      specialSettings: session.getSpecialSettings() ?? undefined,
    });
    return null;
  }
```

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

Comment on lines +221 to 236
if (longContextThresholdExceeded && inputAboveThreshold != null && usage.input_tokens != null) {
inputBucket = inputBucket.add(multiplyCost(usage.input_tokens, inputAboveThreshold));
} else if (
longContextThresholdExceeded &&
context1mApplied &&
inputCostPerToken != null &&
usage.input_tokens != null
) {
inputBucket = inputBucket.add(
calculateTieredCostWithSeparatePrices(usage.input_tokens, inputCostPerToken, inputAbove200k)
multiplyCost(usage.input_tokens, inputCostPerToken * CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER)
);
} else {
inputBucket = inputBucket.add(multiplyCost(usage.input_tokens, inputCostPerToken));
}

// Output tokens -> output bucket
Copy link

Choose a reason for hiding this comment

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

All tokens billed at premium rate when threshold is exceeded

The previous implementation applied a tiered calculation: tokens up to the threshold were billed at the base rate; only the excess above the threshold was billed at the premium/above-threshold rate. The new implementation applies the inputAboveThreshold price to all input tokens the moment the threshold is crossed.

For example, with a Gemini model using 300K input tokens, a base rate of $1/M, and an above-200K rate of $2/M:

  • Old behavior: 200K × $1 + 100K × $2 = $0.40
  • New behavior: 300K × $2 = $0.60 (50% higher)

Similarly, for Claude 1M-context requests (context1mApplied), the CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER is now applied to the full token count rather than only to tokens above the threshold.

If the intent is to match a provider's "all tokens at the high tier" billing model, that should be documented explicitly. If this is unintentional, the tiered logic needs to be restored. The same pattern is repeated for outputBucket, all cache-creation buckets, and cache-read buckets in both calculateRequestCostBreakdown and calculateRequestCost.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/utils/cost-calculation.ts
Line: 221-236

Comment:
**All tokens billed at premium rate when threshold is exceeded**

The previous implementation applied a tiered calculation: tokens up to the threshold were billed at the base rate; only the excess above the threshold was billed at the premium/above-threshold rate. The new implementation applies the `inputAboveThreshold` price to **all** input tokens the moment the threshold is crossed.

For example, with a Gemini model using 300K input tokens, a base rate of $1/M, and an above-200K rate of $2/M:
- **Old behavior:** 200K × $1 + 100K × $2 = $0.40  
- **New behavior:** 300K × $2 = $0.60 (50% higher)

Similarly, for Claude 1M-context requests (`context1mApplied`), the `CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER` is now applied to the full token count rather than only to tokens above the threshold.

If the intent is to match a provider's "all tokens at the high tier" billing model, that should be documented explicitly. If this is unintentional, the tiered logic needs to be restored. The same pattern is repeated for `outputBucket`, all cache-creation buckets, and cache-read buckets in both `calculateRequestCostBreakdown` and `calculateRequestCost`.

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

Comment on lines 704 to +768
@@ -755,26 +740,37 @@ export class ProxySession {
const primaryModel = useOriginal ? originalModel : redirectedModel;
const fallbackModel = useOriginal ? redirectedModel : originalModel;

const findValidPriceDataByModel = async (modelName: string): Promise<ModelPriceData | null> => {
const result = await findLatestPriceByModel(modelName);
const data = result?.priceData;
if (!data || !hasValidPriceData(data)) {
return null;
}
return data;
};

let priceData: ModelPriceData | null = null;
if (primaryModel) {
priceData = await findValidPriceDataByModel(primaryModel);
}

if (!priceData && fallbackModel && fallbackModel !== primaryModel) {
priceData = await findValidPriceDataByModel(fallbackModel);
}
const primaryRecord = primaryModel ? await findLatestPriceByModel(primaryModel) : null;
const fallbackRecord =
fallbackModel && fallbackModel !== primaryModel
? await findLatestPriceByModel(fallbackModel)
: null;

return resolvePricingForModelRecords({
provider: provider ?? this.provider,
primaryModelName: primaryModel,
fallbackModelName: fallbackModel,
primaryRecord,
fallbackRecord,
});
}

this.cachedBillingPriceData = priceData;
return this.cachedBillingPriceData;
/**
* 根据系统配置的计费模型来源获取价格数据(带缓存)
*
* billingModelSource:
* - "original": 优先使用重定向前模型(getOriginalModel)
* - "redirected": 优先使用重定向后模型(request.model)
*
* Fallback:主模型无价格时尝试备选模型。
*
* @returns 价格数据;无模型或无价格时返回 null
*/
Copy link

Choose a reason for hiding this comment

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

getResolvedPricingByBillingSource has no per-request cache

The old getCachedPriceDataByBillingSource stored its result in this.cachedBillingPriceData and returned the cached value on subsequent calls. The new getResolvedPricingByBillingSource always performs fresh DB queries (up to two: one for the primary model, one for the fallback).

In response-handler.ts this method is now called in multiple independent places during a single request lifecycle:

  1. Inside updateRequestCostFromUsage
  2. In the streaming response handler (ensurePricingResolutionSpecialSetting path)
  3. In trackCostToRedis
  4. In finalizeRequestStats

This can result in 6–8 findLatestPriceByModel DB queries per request where there was previously 1. Consider adding a request-level cache (e.g. private resolvedPricingCache?: ResolvedPricing | null) similar to the removed cachedBillingPriceData.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/session.ts
Line: 704-768

Comment:
**`getResolvedPricingByBillingSource` has no per-request cache**

The old `getCachedPriceDataByBillingSource` stored its result in `this.cachedBillingPriceData` and returned the cached value on subsequent calls. The new `getResolvedPricingByBillingSource` always performs fresh DB queries (up to two: one for the primary model, one for the fallback).

In `response-handler.ts` this method is now called in multiple independent places during a single request lifecycle:
1. Inside `updateRequestCostFromUsage`
2. In the streaming response handler (`ensurePricingResolutionSpecialSetting` path)
3. In `trackCostToRedis`
4. In `finalizeRequestStats`

This can result in 6–8 `findLatestPriceByModel` DB queries per request where there was previously 1. Consider adding a request-level cache (e.g. `private resolvedPricingCache?: ResolvedPricing | null`) similar to the removed `cachedBillingPriceData`.

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

Comment on lines +207 to +217
modelName: string;
resolvedModelName: string;
resolvedPricingProviderKey: string;
source:
| "local_manual"
| "cloud_exact"
| "cloud_model_fallback"
| "priority_fallback"
| "single_provider_top_level"
| "official_fallback";
};
Copy link

Choose a reason for hiding this comment

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

"official_fallback" source value is defined but never produced

PricingResolutionSpecialSetting.source includes "official_fallback", but ResolvedPricingSource (in pricing-resolution.ts) does not — the resolution logic never returns this value. The i18n messages also include a translation key for "official_fallback".

Since ensurePricingResolutionSpecialSetting always derives source from a ResolvedPricing (which is typed as ResolvedPricingSource), "official_fallback" can never appear in a stored PricingResolutionSpecialSetting. Consider either:

  • Removing "official_fallback" from PricingResolutionSpecialSetting.source (and the i18n files) until it is implemented, or
  • Adding "official_fallback" to ResolvedPricingSource if it is intentionally reserved for future use and document accordingly.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/types/special-settings.ts
Line: 207-217

Comment:
**`"official_fallback"` source value is defined but never produced**

`PricingResolutionSpecialSetting.source` includes `"official_fallback"`, but `ResolvedPricingSource` (in `pricing-resolution.ts`) does not — the resolution logic never returns this value. The i18n messages also include a translation key for `"official_fallback"`.

Since `ensurePricingResolutionSpecialSetting` always derives `source` from a `ResolvedPricing` (which is typed as `ResolvedPricingSource`), `"official_fallback"` can never appear in a stored `PricingResolutionSpecialSetting`. Consider either:
- Removing `"official_fallback"` from `PricingResolutionSpecialSetting.source` (and the i18n files) until it is implemented, or  
- Adding `"official_fallback"` to `ResolvedPricingSource` if it is intentionally reserved for future use and document accordingly.

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

Comment on lines +198 to +214
pricing: base.pricing,
selected_pricing_provider: pricingProviderKey,
};
}

function getDetailScore(pricingNode: Record<string, unknown>): number {
return DETAIL_FIELDS.reduce((score, field) => {
const value = pricingNode[field];
return typeof value === "number" && Number.isFinite(value) ? score + 1 : score;
}, 0);
}

function compareDetailKeys(
a: string,
b: string,
pricingMap: Record<string, Record<string, unknown>>
): number {
Copy link

Choose a reason for hiding this comment

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

mergePriceData third argument may be undefined in resolveManualPricing

In resolveManualPricing, mergePriceData is called with record.priceData.selected_pricing_provider as string as the pricingProviderKey parameter. If selected_pricing_provider is undefined (which is the common case for legacy records), the cast silences TypeScript but the runtime value of pricingProviderKey inside mergePriceData will be undefined. When base.selected_pricing_provider is also not a string, mergePriceData will then set selected_pricing_provider: undefined in the returned priceData, which is benign but inconsistent with the resolvedPricingProviderKey that was computed with a "manual" fallback.

Consider passing the already-computed resolvedPricingProviderKey as the third argument instead:

Suggested change
pricing: base.pricing,
selected_pricing_provider: pricingProviderKey,
};
}
function getDetailScore(pricingNode: Record<string, unknown>): number {
return DETAIL_FIELDS.reduce((score, field) => {
const value = pricingNode[field];
return typeof value === "number" && Number.isFinite(value) ? score + 1 : score;
}, 0);
}
function compareDetailKeys(
a: string,
b: string,
pricingMap: Record<string, Record<string, unknown>>
): number {
return {
resolvedModelName: modelName ?? record.modelName,
resolvedPricingProviderKey:
(typeof record.priceData.selected_pricing_provider === "string" &&
record.priceData.selected_pricing_provider.trim()) ||
(typeof record.priceData.litellm_provider === "string" && record.priceData.litellm_provider.trim()) ||
"manual",
source: "local_manual",
priceData: mergePriceData(
record.priceData,
null,
(typeof record.priceData.selected_pricing_provider === "string" &&
record.priceData.selected_pricing_provider.trim()) ||
(typeof record.priceData.litellm_provider === "string" && record.priceData.litellm_provider.trim()) ||
"manual"
),
pricingNode: null,
};
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/utils/pricing-resolution.ts
Line: 198-214

Comment:
**`mergePriceData` third argument may be `undefined` in `resolveManualPricing`**

In `resolveManualPricing`, `mergePriceData` is called with `record.priceData.selected_pricing_provider as string` as the `pricingProviderKey` parameter. If `selected_pricing_provider` is `undefined` (which is the common case for legacy records), the cast silences TypeScript but the runtime value of `pricingProviderKey` inside `mergePriceData` will be `undefined`. When `base.selected_pricing_provider` is also not a string, `mergePriceData` will then set `selected_pricing_provider: undefined` in the returned `priceData`, which is benign but inconsistent with the `resolvedPricingProviderKey` that was computed with a `"manual"` fallback.

Consider passing the already-computed `resolvedPricingProviderKey` as the third argument instead:

```suggestion
  return {
    resolvedModelName: modelName ?? record.modelName,
    resolvedPricingProviderKey:
      (typeof record.priceData.selected_pricing_provider === "string" &&
        record.priceData.selected_pricing_provider.trim()) ||
      (typeof record.priceData.litellm_provider === "string" && record.priceData.litellm_provider.trim()) ||
      "manual",
    source: "local_manual",
    priceData: mergePriceData(
      record.priceData,
      null,
      (typeof record.priceData.selected_pricing_provider === "string" &&
        record.priceData.selected_pricing_provider.trim()) ||
        (typeof record.priceData.litellm_provider === "string" && record.priceData.litellm_provider.trim()) ||
        "manual"
    ),
    pricingNode: null,
  };
```

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

Fixed:
- Import ordering (alphabetical) in MetadataTab.tsx, SummaryTab.tsx, price-list.tsx, response-handler.ts, session.ts
- Changed 'let' to 'const' for never-reassigned variable in response-handler.ts

CI Run: https://github.com/ding113/claude-code-hub/actions/runs/22762234077

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

This PR implements provider-aware billing resolution for multi-provider models with a clean pricing fallback chain architecture. The core pricing resolution logic is well-structured with comprehensive test coverage.

PR Size: XL

  • Lines changed: 2,618 (2061 additions + 557 deletions)
  • Files changed: 34

The PR is appropriately sized for its scope (new pricing resolution system + UI enhancements + i18n + tests). Consider splitting future enhancements into smaller PRs for faster review cycles.

Issues Found

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

Medium Priority Issues (Should Fix)

1. [TYPE-INCONSISTENCY] official_fallback defined in i18n but not in ResolvedPricingSource type

Location: src/lib/utils/pricing-resolution.ts:19-26, messages/en/dashboard.json:296

Why this is a problem: All 5 i18n language files define pricingSource.official_fallback label, but the ResolvedPricingSource type only includes:

export type ResolvedPricingSource =
  | "local_manual"
  | "cloud_exact"
  | "cloud_model_fallback"
  | "priority_fallback"
  | "single_provider_top_level";

The official_fallback value is never produced by the resolution logic. This creates dead translation strings that will never be displayed.

Suggested fix: Either add official_fallback to the type and update the resolution logic to produce it when appropriate, or remove the unused translation keys from all i18n files.


2. [LOGIC-BUG] pinModelPricingProviderAsManual hardcodes litellm source

Location: src/actions/model-prices.ts:649

Why this is a problem: The function queries cloud prices with hardcoded source "litellm":

const latestCloudPrice = await findLatestPriceByModelAndSource(modelName, "litellm");

If a model's cloud price was ingested from a different source (e.g., openrouter, manual), the pinning feature will fail with the error "未找到云端模型价格" (cloud model price not found), even though the price exists in the database.

Suggested fix: Query for the most recent cloud price regardless of source, or try multiple sources in fallback order:

// Option 1: Query latest cloud price from any non-manual source
const latestCloudPrice = await findLatestPriceByModel(modelName);

// Option 2: Try litellm first, then fall back to other sources
const latestCloudPrice = 
  (await findLatestPriceByModelAndSource(modelName, "litellm")) ??
  (await findLatestPriceByModelAndSource(modelName, "openrouter"));

Low Priority Issues

3. [DEAD-CODE] findLatestPricesByModels defined but unused

Location: src/repository/model-price.ts:93-124

This batch query function is added but never called in this PR. Consider removing it if not planned for immediate use, or document its intended purpose.


Review Coverage

  • Logic and correctness - Reviewed
  • Security (OWASP Top 10) - No security concerns
  • Error handling - Adequate error handling with user feedback
  • Type safety - Minor inconsistency noted above
  • Documentation accuracy - PR description accurately reflects changes
  • Test coverage - Good coverage for new pricing resolution logic
  • Code clarity - Clean implementation with clear separation of concerns

Positive Observations

  • Well-structured pricing resolution with clear fallback chain (manual -> exact provider -> official -> detailed -> top-level)
  • Comprehensive i18n coverage for all 5 supported languages
  • New tests adequately cover pricing resolution logic and cost calculation changes
  • Clean separation between ResolvedPricing and ModelPriceData types

Automated review by Claude AI

Comment on lines 44 to +89
@@ -55,13 +57,7 @@ function calculateTieredCost(
return new Decimal(tokens).mul(baseCostDecimal);
}

// 阈值内的token按基础费率计算
const baseCost = new Decimal(threshold).mul(baseCostDecimal);
// 超出阈值的token按溢价费率计算
const premiumTokens = tokens - threshold;
const premiumCost = new Decimal(premiumTokens).mul(baseCostDecimal).mul(premiumMultiplier);

return baseCost.add(premiumCost);
return new Decimal(tokens).mul(baseCostDecimal).mul(premiumMultiplier);
}

/**
@@ -72,7 +68,7 @@ function calculateTieredCost(
* @param threshold - 阈值(默认 200K)
* @returns 费用
*/
function calculateTieredCostWithSeparatePrices(
function __calculateTieredCostWithSeparatePrices(
tokens: number,
baseCostPerToken: number,
premiumCostPerToken: number,
@@ -89,13 +85,41 @@ function calculateTieredCostWithSeparatePrices(
return new Decimal(tokens).mul(baseCostDecimal);
}

// 阈值内的 token 按基础费率计算
const baseCost = new Decimal(threshold).mul(baseCostDecimal);
// 超出阈值的 token 按溢价费率计算
const premiumTokens = tokens - threshold;
const premiumCost = new Decimal(premiumTokens).mul(premiumCostDecimal);
return new Decimal(tokens).mul(premiumCostDecimal);
}
Copy link

Choose a reason for hiding this comment

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

Dead code: unused functions should be removed

_calculateTieredCost (lines 44–61) and __calculateTieredCostWithSeparatePrices (lines 71–89) were refactored away and are never called anywhere in the codebase. The underscore prefix does not denote private functions in TypeScript convention. These should be deleted entirely to avoid confusion and potential noUnusedLocals type-check failures.

Suggested change
// Remove both _calculateTieredCost and __calculateTieredCostWithSeparatePrices functions
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/utils/cost-calculation.ts
Line: 44-89

Comment:
## Dead code: unused functions should be removed

`_calculateTieredCost` (lines 44–61) and `__calculateTieredCostWithSeparatePrices` (lines 71–89) were refactored away and are never called anywhere in the codebase. The underscore prefix does not denote private functions in TypeScript convention. These should be deleted entirely to avoid confusion and potential `noUnusedLocals` type-check failures.

```suggestion
// Remove both _calculateTieredCost and __calculateTieredCostWithSeparatePrices functions
```

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

Comment on lines +278 to 285
if (
longContextThresholdExceeded &&
cacheCreation1hAboveThreshold != null &&
cache1hTokens != null
) {
cacheCreationBucket = cacheCreationBucket.add(
calculateTieredCost(cache1hTokens, cacheCreation1hCost, CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER)
multiplyCost(cache1hTokens, cacheCreation1hAboveThreshold)
);
Copy link

Choose a reason for hiding this comment

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

Missing hasRealCacheCreationBase guard for 1-hour cache long-context billing in calculateRequestCostBreakdown

The 5-minute TTL long-context cache branch (lines 255–263) correctly gates on hasRealCacheCreationBase before applying the above-threshold rate. The analogous 1-hour TTL branch here does not:

  • 5m path (line 255–260): longContextThresholdExceeded && hasRealCacheCreationBase && cacheCreationAboveThreshold != null
  • 1h path (line 279–282): longContextThresholdExceeded && cacheCreation1hAboveThreshold != null ← missing guard

If a model record has cache_creation_input_token_cost_above_1hr_above_272k_tokens set but no base cache_creation_input_token_cost, the 1-hour premium will incorrectly apply even when the model should not use tiered pricing.

Suggested change
if (
longContextThresholdExceeded &&
cacheCreation1hAboveThreshold != null &&
cache1hTokens != null
) {
cacheCreationBucket = cacheCreationBucket.add(
calculateTieredCost(cache1hTokens, cacheCreation1hCost, CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER)
multiplyCost(cache1hTokens, cacheCreation1hAboveThreshold)
);
if (
longContextThresholdExceeded &&
hasRealCacheCreationBase &&
cacheCreation1hAboveThreshold != null &&
cache1hTokens != null
) {
cacheCreationBucket = cacheCreationBucket.add(
multiplyCost(cache1hTokens, cacheCreation1hAboveThreshold)
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/utils/cost-calculation.ts
Line: 278-285

Comment:
## Missing `hasRealCacheCreationBase` guard for 1-hour cache long-context billing in `calculateRequestCostBreakdown`

The 5-minute TTL long-context cache branch (lines 255–263) correctly gates on `hasRealCacheCreationBase` before applying the above-threshold rate. The analogous 1-hour TTL branch here does not:

- **5m path** (line 255–260): `longContextThresholdExceeded && hasRealCacheCreationBase && cacheCreationAboveThreshold != null`
- **1h path** (line 279–282): `longContextThresholdExceeded && cacheCreation1hAboveThreshold != null` ← missing guard

If a model record has `cache_creation_input_token_cost_above_1hr_above_272k_tokens` set but no base `cache_creation_input_token_cost`, the 1-hour premium will incorrectly apply even when the model should not use tiered pricing.

```suggestion
  if (
    longContextThresholdExceeded &&
    hasRealCacheCreationBase &&
    cacheCreation1hAboveThreshold != null &&
    cache1hTokens != null
  ) {
    cacheCreationBucket = cacheCreationBucket.add(
      multiplyCost(cache1hTokens, cacheCreation1hAboveThreshold)
    );
```

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

Comment on lines +483 to +488
if (
longContextThresholdExceeded &&
cacheCreation1hAboveThreshold != null &&
cache1hTokens != null
) {
segments.push(multiplyCost(cache1hTokens, cacheCreation1hAboveThreshold));
Copy link

Choose a reason for hiding this comment

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

Missing hasRealCacheCreationBase guard in calculateRequestCost 1-hour cache branch

Same issue as in calculateRequestCostBreakdown: the 5-minute cache branch (lines 462–468) correctly checks hasRealCacheCreationBase, but the 1-hour branch here does not. This creates an asymmetry where models without a base cache creation price can still be charged above-threshold rates for 1-hour TTL caching.

Suggested change
if (
longContextThresholdExceeded &&
cacheCreation1hAboveThreshold != null &&
cache1hTokens != null
) {
segments.push(multiplyCost(cache1hTokens, cacheCreation1hAboveThreshold));
if (
longContextThresholdExceeded &&
hasRealCacheCreationBase &&
cacheCreation1hAboveThreshold != null &&
cache1hTokens != null
) {
segments.push(multiplyCost(cache1hTokens, cacheCreation1hAboveThreshold));
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/utils/cost-calculation.ts
Line: 483-488

Comment:
## Missing `hasRealCacheCreationBase` guard in `calculateRequestCost` 1-hour cache branch

Same issue as in `calculateRequestCostBreakdown`: the 5-minute cache branch (lines 462–468) correctly checks `hasRealCacheCreationBase`, but the 1-hour branch here does not. This creates an asymmetry where models without a base cache creation price can still be charged above-threshold rates for 1-hour TTL caching.

```suggestion
  if (
    longContextThresholdExceeded &&
    hasRealCacheCreationBase &&
    cacheCreation1hAboveThreshold != null &&
    cache1hTokens != null
  ) {
    segments.push(multiplyCost(cache1hTokens, cacheCreation1hAboveThreshold));
```

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

Comment on lines +210 to +216
source:
| "local_manual"
| "cloud_exact"
| "cloud_model_fallback"
| "priority_fallback"
| "single_provider_top_level"
| "official_fallback";
Copy link

Choose a reason for hiding this comment

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

"official_fallback" source value is defined but never produced

PricingResolutionSpecialSetting.source includes "official_fallback" at line 216, but ResolvedPricingSource in src/lib/utils/pricing-resolution.ts does not include this value. The pricing resolution logic never returns "official_fallback" — it only returns "local_manual", "cloud_exact", "cloud_model_fallback", "priority_fallback", or "single_provider_top_level".

Since ensurePricingResolutionSpecialSetting always derives source from a ResolvedPricing, the "official_fallback" value can never appear in a stored PricingResolutionSpecialSetting.

Either remove "official_fallback" from the union (lines 210–216) and from i18n files, or add it to ResolvedPricingSource if it is reserved for future use and document accordingly.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/types/special-settings.ts
Line: 210-216

Comment:
## `"official_fallback"` source value is defined but never produced

`PricingResolutionSpecialSetting.source` includes `"official_fallback"` at line 216, but `ResolvedPricingSource` in `src/lib/utils/pricing-resolution.ts` does not include this value. The pricing resolution logic never returns `"official_fallback"` — it only returns `"local_manual"`, `"cloud_exact"`, `"cloud_model_fallback"`, `"priority_fallback"`, or `"single_provider_top_level"`.

Since `ensurePricingResolutionSpecialSetting` always derives `source` from a `ResolvedPricing`, the `"official_fallback"` value can never appear in a stored `PricingResolutionSpecialSetting`. 

Either remove `"official_fallback"` from the union (lines 210–216) and from i18n files, or add it to `ResolvedPricingSource` if it is reserved for future use and document accordingly.

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

Comment on lines +247 to +251
priceData: mergePriceData(
record.priceData,
null,
record.priceData.selected_pricing_provider as string
),
Copy link

Choose a reason for hiding this comment

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

mergePriceData may receive undefined as third argument

In resolveManualPricing, mergePriceData is called with record.priceData.selected_pricing_provider as string as the pricingProviderKey parameter (line 250). If selected_pricing_provider is undefined (common for legacy records), the type assertion silences TypeScript but the runtime value becomes undefined.

When mergePriceData receives undefined as pricingProviderKey and base.selected_pricing_provider is also not a string, the function will set selected_pricing_provider: undefined in the returned priceData, which is inconsistent with the resolvedPricingProviderKey computed above (line 245) with a "manual" fallback.

Consider passing the already-computed resolvedPricingProviderKey as the third argument instead:

Suggested change
priceData: mergePriceData(
record.priceData,
null,
record.priceData.selected_pricing_provider as string
),
priceData: mergePriceData(
record.priceData,
null,
resolvedPricingProviderKey
),
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/utils/pricing-resolution.ts
Line: 247-251

Comment:
## `mergePriceData` may receive `undefined` as third argument

In `resolveManualPricing`, `mergePriceData` is called with `record.priceData.selected_pricing_provider as string` as the `pricingProviderKey` parameter (line 250). If `selected_pricing_provider` is `undefined` (common for legacy records), the type assertion silences TypeScript but the runtime value becomes `undefined`. 

When `mergePriceData` receives `undefined` as `pricingProviderKey` and `base.selected_pricing_provider` is also not a string, the function will set `selected_pricing_provider: undefined` in the returned `priceData`, which is inconsistent with the `resolvedPricingProviderKey` computed above (line 245) with a `"manual"` fallback.

Consider passing the already-computed `resolvedPricingProviderKey` as the third argument instead:

```suggestion
    priceData: mergePriceData(
      record.priceData,
      null,
      resolvedPricingProviderKey
    ),
```

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

Comment on lines 2899 to 2902
const { usageMetrics } = parseUsageFromResponseText(responseText, provider.providerType);

if (!usageMetrics) {
// 即使没有 usageMetrics,也需要更新状态码和 provider chain
await updateMessageRequestDetails(messageContext.id, {
statusCode: statusCode,
...(errorMessage ? { errorMessage } : {}),
ttfbMs: session.ttfbMs ?? duration,
providerChain: session.getProviderChain(),
model: session.getCurrentModel() ?? undefined,
providerId: providerIdForPersistence, // 更新最终供应商ID(重试切换后)
context1mApplied: session.getContext1mApplied(),
swapCacheTtlApplied: provider.swapCacheTtlBilling ?? false,
});
return null;
}
Copy link

Choose a reason for hiding this comment

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

Request details never persisted when usage metrics cannot be parsed

When parseUsageFromResponseText returns null for usageMetrics (line 2900–2901), the function returns immediately without calling updateMessageRequestDetails. This means requests whose response body cannot be parsed for usage metrics — including error responses and malformed JSON — will never have their status code, error message, provider chain, model, or provider ID written to the database.

The resulting log entry will be incomplete and missing critical metadata needed for debugging. The request status, error reason, and model information should be recorded even when token usage cannot be extracted.

Move the updateMessageRequestDetails call (currently at line 2966) earlier in the function, before the early return, to ensure request metadata is always persisted:

Suggested change
const { usageMetrics } = parseUsageFromResponseText(responseText, provider.providerType);
if (!usageMetrics) {
// 即使没有 usageMetrics,也需要更新状态码和 provider chain
await updateMessageRequestDetails(messageContext.id, {
statusCode: statusCode,
...(errorMessage ? { errorMessage } : {}),
ttfbMs: session.ttfbMs ?? duration,
providerChain: session.getProviderChain(),
model: session.getCurrentModel() ?? undefined,
providerId: providerIdForPersistence, // 更新最终供应商ID(重试切换后)
context1mApplied: session.getContext1mApplied(),
swapCacheTtlApplied: provider.swapCacheTtlBilling ?? false,
});
return null;
}
const { usageMetrics } = parseUsageFromResponseText(responseText, provider.providerType);
// Always record request metadata before early return
await updateMessageRequestDetails(messageContext.id, {
statusCode: statusCode,
...(errorMessage ? { errorMessage } : {}),
model: session.getCurrentModel() ?? undefined,
providerId: providerIdForPersistence,
providerChain: session.getProviderChain(),
});
if (!usageMetrics) {
return null;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/response-handler.ts
Line: 2899-2902

Comment:
## Request details never persisted when usage metrics cannot be parsed

When `parseUsageFromResponseText` returns null for `usageMetrics` (line 2900–2901), the function returns immediately without calling `updateMessageRequestDetails`. This means requests whose response body cannot be parsed for usage metrics — including error responses and malformed JSON — will never have their status code, error message, provider chain, model, or provider ID written to the database.

The resulting log entry will be incomplete and missing critical metadata needed for debugging. The request status, error reason, and model information should be recorded even when token usage cannot be extracted.

Move the `updateMessageRequestDetails` call (currently at line 2966) earlier in the function, before the early return, to ensure request metadata is always persisted:

```suggestion
  const { usageMetrics } = parseUsageFromResponseText(responseText, provider.providerType);
  
  // Always record request metadata before early return
  await updateMessageRequestDetails(messageContext.id, {
    statusCode: statusCode,
    ...(errorMessage ? { errorMessage } : {}),
    model: session.getCurrentModel() ?? undefined,
    providerId: providerIdForPersistence,
    providerChain: session.getProviderChain(),
  });

  if (!usageMetrics) {
    return null;
  }
```

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

interface ModelRecordCandidate {
modelName: string | null;
record: ModelPrice | null;
isPrimary: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

**[Medium] [TYPE-INCONSISTENCY] official_fallback defined in i18n but not in ResolvedPricingSource type

Why this is a problem: All 5 i18n language files define pricingSource.official_fallback label, but this type only includes 5 values and never produces official_fallback. This creates dead translation strings that will never be displayed to users.

Suggested fix:

export type ResolvedPricingSource =
  | "local_manual"
  | "cloud_exact"
  | "cloud_model_fallback"
  | "priority_fallback"
  | "single_provider_top_level"
  | "official_fallback";  // Add this if planned for future use, or remove from i18n

return { ok: false, error: "价格提供商不能为空" };
}

const latestCloudPrice = await findLatestPriceByModelAndSource(modelName, "litellm");
Copy link
Contributor

Choose a reason for hiding this comment

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

**[Medium] [LOGIC-BUG] Hardcoded litellm source limits pin functionality

Why this is a problem: This query hardcodes source = \"litellm\". If a model's cloud price was ingested from a different source (e.g., openrouter, manual), the pin feature will fail with "未找到云端模型价格" even though the price exists in the database.

Suggested fix:

// Option 1: Try litellm first, then fall back to other sources
const latestCloudPrice = 
  (await findLatestPriceByModelAndSource(modelName, \"litellm\")) ??
  (await findLatestPriceByModelAndSource(modelName, \"openrouter\"));

// Option 2: Query latest non-manual price
const latestCloudPrice = await findLatestPriceByModelExcludingSource(modelName, \"manual\");

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

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

⚠️ Outside diff range comments (1)
src/app/v1/_lib/proxy/response-handler.ts (1)

2791-2843: ⚠️ Potential issue | 🟠 Major

这里再次独立解析价表,会让同一请求的账单、限流和日志落到不同价格版本。

updateRequestCostFromUsage() 现在自己走一遍 getSystemSettings() / findLatestPriceByModel() / resolvePricingForModelRecords(),而本文件后面的 trackCostToRedis()、session usage 和 Langfuse 成本路径又会通过 session.getResolvedPricingByBillingSource(provider) 再解析一遍。长流期间如果价表同步落在这两个 await 之间,同一请求就可能把 DB 账单、限流成本和日志成本写成不同数值。建议把 ResolvedPricing 在请求级解析一次并复用到这些路径。

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

In `@src/app/v1/_lib/proxy/response-handler.ts` around lines 2791 - 2843, 当前实现中
updateRequestCostFromUsage() 会再次调用
getSystemSettings()/findLatestPriceByModel()/resolvePricingForModelRecords()
导致同一请求在不同位置(DB 账单、限流、日志)可能使用不同价表版本;请改为在请求级只解析一次 ResolvedPricing 并复用:在处理请求的入口(调用
updateRequestCostFromUsage 的上游)调用 getSystemSettings(), findLatestPriceByModel(),
resolvePricingForModelRecords() 得到 ResolvedPricing(引用
resolvedPricing/priceData),并把它作为参数传入 updateRequestCostFromUsage(provider, usage,
costMultiplier, context1mApplied, resolvedPricing) 或存入请求上下文;随后确保
trackCostToRedis(), session.getResolvedPricingByBillingSource(provider)
的使用路径改为直接读取传入或上下文中的 ResolvedPricing,移除 updateRequestCostFromUsage
内部重复的系统设置/价表查找逻辑(相关标识符:updateRequestCostFromUsage, getSystemSettings,
findLatestPriceByModel, resolvePricingForModelRecords, trackCostToRedis,
session.getResolvedPricingByBillingSource, ResolvedPricing)。
🧹 Nitpick comments (3)
src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx (1)

71-74: 把 pricing resolution 展示抽成共享片段。

getPricingResolutionSpecialSetting、source 文案解析和这两行 JSX 在 SummaryTab.tsxMetadataTab.tsx 已经重复了一遍,后面新增 source 类型或调整显示格式时很容易只改一处。抽成一个小 helper/子组件会更稳。

Also applies to: 361-378

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

In
`@src/app/`[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx
around lines 71 - 74, 将重复的 pricing resolution 逻辑抽成共享的 helper 或子组件:把
getPricingResolutionSpecialSetting 调用、pricingResolution 的判空、基于
pricingResolution.source 的多语言文本解析(目前在 SummaryTab.tsx 与 MetadataTab.tsx
重复)抽到一个单一导出的函数或小组件(例如 usePricingSourceLabel 或 PricingSourceLabel 组件),然后在
SummaryTab.tsx 和 MetadataTab.tsx 中替换掉原有的 pricingResolution/pricingSourceLabel
两行逻辑并统一使用该共享符号,从而后续新增 source 类型或修改显示格式只需改一处。
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx (1)

341-342: 存在未使用的变量。

_isWarmupSkipped 被计算但未使用。如果是预留变量,可以保留;否则建议移除以保持代码整洁。

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

In `@src/app/`[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
around lines 341 - 342, The variable _isWarmupSkipped is computed from
log.blockedBy but never used; remove the unused declaration (the const
_isWarmupSkipped = log.blockedBy === "warmup";) to clean up the component, or if
it was intended to affect rendering or logic, wire it into the relevant
conditional or prop handling where virtualized-logs-table uses
pricingResolution/getPricingResolutionSpecialSetting so the value is actually
consumed; update references accordingly and ensure no linter warnings remain.
tests/unit/actions/model-prices.test.ts (1)

497-551: 测试覆盖可进一步完善。

当前测试仅覆盖了成功路径。建议补充以下边界情况的测试:

  • modelNamepricingProviderKey 应返回错误
  • 非管理员用户应被拒绝
  • 未找到云端价格时的错误处理
  • 指定的 pricingProviderKeypricing 映射中不存在时的错误处理
💡 建议补充的测试用例
it("should reject empty modelName", async () => {
  const { pinModelPricingProviderAsManual } = await import("@/actions/model-prices");
  const result = await pinModelPricingProviderAsManual({
    modelName: "",
    pricingProviderKey: "openrouter",
  });
  expect(result.ok).toBe(false);
  expect(result.error).toContain("模型名称");
});

it("should reject non-admin users", async () => {
  getSessionMock.mockResolvedValue({ user: { id: 2, role: "user" } });
  const { pinModelPricingProviderAsManual } = await import("@/actions/model-prices");
  const result = await pinModelPricingProviderAsManual({
    modelName: "gpt-5.4",
    pricingProviderKey: "openrouter",
  });
  expect(result.ok).toBe(false);
  expect(result.error).toContain("无权限");
});

it("should return error when cloud price not found", async () => {
  findLatestPriceByModelAndSourceMock.mockResolvedValue(null);
  const { pinModelPricingProviderAsManual } = await import("@/actions/model-prices");
  const result = await pinModelPricingProviderAsManual({
    modelName: "gpt-5.4",
    pricingProviderKey: "openrouter",
  });
  expect(result.ok).toBe(false);
  expect(result.error).toContain("云端");
});
🤖 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 497 - 551, Add unit
tests for pinModelPricingProviderAsManual to cover failure/edge cases: validate
empty inputs (call pinModelPricingProviderAsManual with modelName: "" and expect
ok=false and an error message about model name), test authorization by mocking
getSessionMock to return a non-admin user and expect rejection, test
cloud-price-not-found by mocking findLatestPriceByModelAndSourceMock to resolve
null and expect an error, and test missing pricing provider key by returning a
price whose pricing map does not include the requested pricingProviderKey and
expect an error; use the existing mocks (getSessionMock,
findLatestPriceByModelAndSourceMock, upsertModelPriceMock) and import
pinModelPricingProviderAsManual from "@/actions/model-prices" as in the existing
test.
🤖 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/dashboard.json`:
- Around line 376-377: 更新 "not200" 文案以覆盖所有非 200/非成功响应而不是仅指“errors/blocked”: 在
messages/en/dashboard.json 中找到键名 "not200" 并将其值从 "Non-200 (errors/blocked)"
修改为更广义的表述(例如 "Non-200 (non-success responses)" 或 "Non-200 (non-successful
responses)"),确保语义覆盖所有非 200 状态码。

In `@messages/ja/dashboard.json`:
- Around line 376-377: The Japanese copy for the "not200" key is too
narrow—update the value for the "not200" key in messages/ja/dashboard.json (the
entry with key "not200") to a broader phrase that covers all non-200 /
non-success responses (e.g., replace "非 200(エラー/ブロック)" with something like
"200以外(非成功ステータス)" or "200以外(失敗/非成功リクエスト)") so it matches the key semantics and
other language versions.

In `@messages/ja/settings/prices.json`:
- Around line 196-218: Add the missing JSON key "badges.multi" (e.g., "badges":
{ "multi": "複数プロバイダー" }) to the same translations object (near
"actions"/"toast"/"providerPricing") so the multi-vendor badge has a Japanese
label; ensure the key name is exactly "badges.multi" and the value is the
Japanese string (for example "複数プロバイダー") consistent with other badge entries.

In `@messages/ru/dashboard.json`:
- Around line 289-299: The Russian translation for the key context1mPricing is
inconsistent between its two occurrences (one reads "Вход 2x >200k, Выход 1.5x
>200k" and the other "Вход >200k 2x..."); make both entries identical by
choosing the correct phrasing and updating the other occurrence so both
context1mPricing values match exactly; locate the key context1mPricing and the
duplicate occurrence referenced in the diff (also check the block around the
pricingSource object) and apply the same string to both places to ensure UI
consistency.

In `@messages/ru/settings/prices.json`:
- Around line 196-218: Add the missing localization key badges.multi to this
locale: under the same top-level object that contains providerPricing, add
"badges": { "multi": "Несколько провайдеров" } (or appropriate Russian phrasing)
so the multi-badge used for multi-provider models is present and prevents
missing-string errors when rendering the multi badge.

In `@messages/zh-CN/dashboard.json`:
- Around line 290-298: The JSON keys pricingProvider and the pricingSource
values (local_manual, cloud_exact, cloud_model_fallback, priority_fallback,
single_provider_top_level, official_fallback) use the term "提供商" while the rest
of the file uses "供应商"; update these strings to use "供应商" to ensure consistent
terminology across the page (replace "提供商" with "供应商" in the value for
pricingProvider and in each affected pricingSource child value).

In `@messages/zh-TW/settings/prices.json`:
- Around line 196-218: The zh-TW locale JSON is missing the badges.multi key
used by the price list; add a badges object with the multi key (e.g., "badges":
{ "multi": "多供應商" }) alongside the existing providerPricing block so the UI can
find the translation for badges.multi when rendering the multi badge.

In `@src/app/`[locale]/dashboard/logs/_components/virtualized-logs-table.tsx:
- Line 78: getPricingSourceLabel currently calls t with the incomplete key
"logs.billingDetails.pricingSource." which will cause a runtime lookup failure;
update getPricingSourceLabel so it interpolates the source variable into the
translation key (e.g., produce "logs.billingDetails.pricingSource.<source>")
when calling the t function, ensuring the correct translation key is requested
for each source.

In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Around line 2894-2904: The added anthropic 1M audit setting is only added to
session via session.addSpecialSetting({... type:
"anthropic_context_1m_header_override", ...}) but not persisted, so it can be
lost; modify the same code path to call persistSpecialSettings(session)
immediately after adding the special setting (or ensure doForward() calls
persistSpecialSettings(session) unconditionally before returning) and keep
buildUnifiedSpecialSettings() unchanged; locate the session.addSpecialSetting
call in forwarder.ts and add the persistSpecialSettings(session) invocation (or
a single unified persist in doForward) so the 1M audit entry is guaranteed to be
written to storage.

In `@src/app/v1/_lib/proxy/response-handler.ts`:
- Around line 1216-1232: The branch that exceeds MAX_STATS_HEAD_BYTES never sets
inTailMode so tail content is never considered by joinChunks(); modify the logic
in the block using inTailMode, headBufferedBytes, MAX_STATS_HEAD_BYTES,
pushChunk, pushToTail and headChunks so that when you transition from head to
tail you set inTailMode = true before calling pushToTail (and avoid calling
pushToTail twice causing duplication), push only the correct headPart and
tailPart once (use pushChunk for each), and ensure joinChunks() combines
headChunks and the tail buffer so final output includes both head and tail for
correct usageMetadata and error detection.

In `@src/lib/utils/pricing-resolution.ts`:
- Around line 5-10: The code currently classifies hits from the official
provider as "priority_fallback" but the UI/types expect a distinct
"official_fallback"; update the ResolvedPricingSource union to include
"official_fallback" and change resolveFromPricingMap (and any branch/return that
checks for provider === "official" or maps official provider hits) to return
"official_fallback" instead of "priority_fallback" (ensure any places
referencing "priority_fallback" for other flows remain unchanged).

In `@src/lib/utils/special-settings.ts`:
- Around line 25-27: The PR removed the fallback derivation for context1mApplied
which causes historical or non-explicit requests to lose 1M context audit data;
restore the existing fallback behavior in the special settings logic so that
when specialSettings.anthropic_context_1m_header_override is absent you still
derive its value from context1mApplied (the property used by
src/repository/usage-logs.ts when passing row.context1mApplied), ensuring
backward compatibility until all write paths persist explicit specialSettings;
update the code paths around specialSettings and the logic referencing
context1mApplied/anthropic_context_1m_header_override to read the fallback if
the explicit override is missing.
- Around line 110-117: The deduplication key for "pricing_resolution" omits the
boolean hit flag, causing entries with hit: true and hit: false to collide;
update the dedupe key generation in the switch case for "pricing_resolution"
inside buildUnifiedSpecialSettings (or the function handling the switch in
special-settings.ts) to include setting.hit in the JSON.stringify array
alongside setting.type, setting.modelName, setting.resolvedModelName,
setting.resolvedPricingProviderKey, and setting.source so each distinct hit
value produces a distinct key.

In `@tests/integration/billing-model-source.test.ts`:
- Around line 377-436: The new nested-pricing test only covers non-stream and
omits asserting the rate-limit billing path; add a parallel stream-case and
include assertions for all three billing chains. Duplicate the setup used in
this test (mocks for findLatestPriceByModel, updateMessageRequestCost,
SessionManager.updateSessionUsage, and RateLimitService.trackUserDailyCost) but
send a streaming response via the streaming helper (e.g., createStreamResponse
or the project’s stream-response factory) and call ProxyResponseHandler.dispatch
with that session, then await drainAsyncTasks; assert the DB cost captured by
updateMessageRequestCost, the session cost captured by
SessionManager.updateSessionUsage, and that RateLimitService.trackUserDailyCost
was called with the expected cost (use the same expected value "50" as in the
non-stream case). Ensure you reference and reuse the same symbols:
ProxyResponseHandler.dispatch, createSession,
createNonStreamResponse/createStreamResponse, updateMessageRequestCost,
SessionManager.updateSessionUsage, and RateLimitService.trackUserDailyCost.

In `@tests/unit/lib/cost-calculation-long-context.test.ts`:
- Around line 5-23: 当前测试只覆盖了 calculateRequestCost 在 claude-sonnet 且 input_tokens
>= 200k 时触发长上下文分支;请补两个用例:一是对同样的 model_family "claude-sonnet" 增加一个反例(input_tokens
< 200000 但 input_tokens+output_tokens > 200000),断言应使用短上下文定价(即不触发
input_cost_per_token_above_200k_tokens /
output_cost_per_token_above_200k_tokens);二是新增一个 model_family "gpt" 的场景,使用靠近 GPT
长上下文阈值 272k 的 token 组合(一个命中 272k 分支的正例和/或一个不命中的反例),分别断言使用/不使用对应的 272k
分支费率以确保阈值逻辑被覆盖;都在 tests/unit/lib/cost-calculation-long-context.test.ts 中与现有
calculateRequestCost 调用保持相同参数结构并显式断言期望的费率分支。

---

Outside diff comments:
In `@src/app/v1/_lib/proxy/response-handler.ts`:
- Around line 2791-2843: 当前实现中 updateRequestCostFromUsage() 会再次调用
getSystemSettings()/findLatestPriceByModel()/resolvePricingForModelRecords()
导致同一请求在不同位置(DB 账单、限流、日志)可能使用不同价表版本;请改为在请求级只解析一次 ResolvedPricing 并复用:在处理请求的入口(调用
updateRequestCostFromUsage 的上游)调用 getSystemSettings(), findLatestPriceByModel(),
resolvePricingForModelRecords() 得到 ResolvedPricing(引用
resolvedPricing/priceData),并把它作为参数传入 updateRequestCostFromUsage(provider, usage,
costMultiplier, context1mApplied, resolvedPricing) 或存入请求上下文;随后确保
trackCostToRedis(), session.getResolvedPricingByBillingSource(provider)
的使用路径改为直接读取传入或上下文中的 ResolvedPricing,移除 updateRequestCostFromUsage
内部重复的系统设置/价表查找逻辑(相关标识符:updateRequestCostFromUsage, getSystemSettings,
findLatestPriceByModel, resolvePricingForModelRecords, trackCostToRedis,
session.getResolvedPricingByBillingSource, ResolvedPricing)。

---

Nitpick comments:
In
`@src/app/`[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx:
- Around line 71-74: 将重复的 pricing resolution 逻辑抽成共享的 helper 或子组件:把
getPricingResolutionSpecialSetting 调用、pricingResolution 的判空、基于
pricingResolution.source 的多语言文本解析(目前在 SummaryTab.tsx 与 MetadataTab.tsx
重复)抽到一个单一导出的函数或小组件(例如 usePricingSourceLabel 或 PricingSourceLabel 组件),然后在
SummaryTab.tsx 和 MetadataTab.tsx 中替换掉原有的 pricingResolution/pricingSourceLabel
两行逻辑并统一使用该共享符号,从而后续新增 source 类型或修改显示格式只需改一处。

In `@src/app/`[locale]/dashboard/logs/_components/virtualized-logs-table.tsx:
- Around line 341-342: The variable _isWarmupSkipped is computed from
log.blockedBy but never used; remove the unused declaration (the const
_isWarmupSkipped = log.blockedBy === "warmup";) to clean up the component, or if
it was intended to affect rendering or logic, wire it into the relevant
conditional or prop handling where virtualized-logs-table uses
pricingResolution/getPricingResolutionSpecialSetting so the value is actually
consumed; update references accordingly and ensure no linter warnings remain.

In `@tests/unit/actions/model-prices.test.ts`:
- Around line 497-551: Add unit tests for pinModelPricingProviderAsManual to
cover failure/edge cases: validate empty inputs (call
pinModelPricingProviderAsManual with modelName: "" and expect ok=false and an
error message about model name), test authorization by mocking getSessionMock to
return a non-admin user and expect rejection, test cloud-price-not-found by
mocking findLatestPriceByModelAndSourceMock to resolve null and expect an error,
and test missing pricing provider key by returning a price whose pricing map
does not include the requested pricingProviderKey and expect an error; use the
existing mocks (getSessionMock, findLatestPriceByModelAndSourceMock,
upsertModelPriceMock) and import pinModelPricingProviderAsManual from
"@/actions/model-prices" as in the existing test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e6441f36-7056-45a8-b27a-eaf7a32b278c

📥 Commits

Reviewing files that changed from the base of the PR and between f566dd8 and 543ae83.

📒 Files selected for processing (34)
  • messages/en/dashboard.json
  • messages/en/settings/prices.json
  • messages/ja/dashboard.json
  • messages/ja/settings/prices.json
  • messages/ru/dashboard.json
  • messages/ru/settings/prices.json
  • messages/zh-CN/dashboard.json
  • messages/zh-CN/settings/prices.json
  • messages/zh-TW/dashboard.json
  • messages/zh-TW/settings/prices.json
  • src/actions/model-prices.ts
  • src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx
  • src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx
  • src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx
  • src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
  • src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • src/app/[locale]/settings/prices/_components/provider-pricing-dialog.tsx
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/lib/utils/cost-calculation.ts
  • src/lib/utils/price-data.ts
  • src/lib/utils/pricing-resolution.ts
  • src/lib/utils/special-settings.ts
  • src/repository/model-price.ts
  • src/types/model-price.ts
  • src/types/special-settings.ts
  • tests/integration/billing-model-source.test.ts
  • tests/unit/actions/model-prices.test.ts
  • tests/unit/lib/cost-calculation-long-context.test.ts
  • tests/unit/lib/utils/pricing-resolution.test.ts
  • tests/unit/lib/utils/special-settings.test.ts
  • tests/unit/settings/prices/price-list-multi-provider-ui.test.tsx

Comment on lines +376 to +377
"500": "500 (Server Error)",
"not200": "Non-200 (errors/blocked)"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

not200 文案把范围写窄了。

not200 这个键名表达的是“所有非 200 状态”。现在的 Non-200 (errors/blocked) 会让用户理解成只包含错误和拦截,不包含其他非 200 结果。建议改成覆盖所有非 200 / 非成功请求的表述。

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

In `@messages/en/dashboard.json` around lines 376 - 377, 更新 "not200" 文案以覆盖所有非
200/非成功响应而不是仅指“errors/blocked”: 在 messages/en/dashboard.json 中找到键名 "not200"
并将其值从 "Non-200 (errors/blocked)" 修改为更广义的表述(例如 "Non-200 (non-success responses)"
或 "Non-200 (non-successful responses)"),确保语义覆盖所有非 200 状态码。

Comment on lines +376 to +377
"500": "500 (サーバーエラー)",
"not200": "非 200(エラー/ブロック)"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

not200 的日文文案也把范围写窄了。

非 200(エラー/ブロック) 会让用户理解成只包含错误和拦截;但这个键名表达的是所有非 200 状态。建议改成覆盖全部非 200 / 非成功请求的表述,避免和其他语言版本的语义漂移。

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

In `@messages/ja/dashboard.json` around lines 376 - 377, The Japanese copy for the
"not200" key is too narrow—update the value for the "not200" key in
messages/ja/dashboard.json (the entry with key "not200") to a broader phrase
that covers all non-200 / non-success responses (e.g., replace "非 200(エラー/ブロック)"
with something like "200以外(非成功ステータス)" or "200以外(失敗/非成功リクエスト)") so it matches the
key semantics and other language versions.

Comment on lines +196 to +218
"actions": {
"edit": "編集",
"more": "その他の操作",
"delete": "削除"
"delete": "削除",
"comparePricing": "価格を比較"
},
"toast": {
"createSuccess": "モデルを追加しました",
"updateSuccess": "モデルを更新しました",
"deleteSuccess": "モデルを削除しました",
"saveFailed": "保存に失敗しました",
"deleteFailed": "削除に失敗しました"
},
"providerPricing": {
"title": "プロバイダー価格 · {model}",
"description": "プロバイダー別のクラウド価格を比較し、そのうち一つをローカルモデル価格として固定します。",
"input": "入力",
"output": "出力",
"cacheRead": "キャッシュ読取",
"pinAction": "ローカル価格として固定",
"pinSuccess": "{provider} の価格をローカルモデル価格として固定しました",
"pinFailed": "プロバイダー価格の固定に失敗しました",
"pinned": "固定済み"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

日语资源同样漏了 badges.multi

这次已经补了多供应商价格相关文案,但多供应商徽标对应的 key 只在英文里新增了,ja 这里还没有;如果列表展示 multi badge,这个语言会出现缺失翻译。请同步补上。

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

In `@messages/ja/settings/prices.json` around lines 196 - 218, Add the missing
JSON key "badges.multi" (e.g., "badges": { "multi": "複数プロバイダー" }) to the same
translations object (near "actions"/"toast"/"providerPricing") so the
multi-vendor badge has a Japanese label; ensure the key name is exactly
"badges.multi" and the value is the Japanese string (for example "複数プロバイダー")
consistent with other badge entries.

Comment on lines +289 to +299
"context1mPricing": "Вход 2x >200k, Выход 1.5x >200k",
"pricingProvider": "Провайдер тарифа",
"pricingSourceLabel": "Источник тарифа",
"pricingSource": {
"local_manual": "Локальная ручная цена",
"cloud_exact": "Точная облачная цена провайдера",
"cloud_model_fallback": "Цена провайдера из резервной модели",
"priority_fallback": "Приоритетная резервная цена",
"single_provider_top_level": "Цена верхнего уровня для одного провайдера",
"official_fallback": "Официальная резервная цена"
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

统一两处 context1mPricing 的俄文文案。

Line 289 和 Line 392 现在是两种顺序(Вход 2x >200k... vs Вход >200k 2x...)。这两个 key 会在不同日志视图里展示同一含义,建议统一成同一说法,避免前后 UI 不一致。

Also applies to: 392-402

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

In `@messages/ru/dashboard.json` around lines 289 - 299, The Russian translation
for the key context1mPricing is inconsistent between its two occurrences (one
reads "Вход 2x >200k, Выход 1.5x >200k" and the other "Вход >200k 2x..."); make
both entries identical by choosing the correct phrasing and updating the other
occurrence so both context1mPricing values match exactly; locate the key
context1mPricing and the duplicate occurrence referenced in the diff (also check
the block around the pricingSource object) and apply the same string to both
places to ensure UI consistency.

Comment on lines +196 to +218
"actions": {
"edit": "Редактировать",
"more": "Больше действий",
"delete": "Удалить"
"delete": "Удалить",
"comparePricing": "Сравнить цены"
},
"toast": {
"createSuccess": "Модель добавлена",
"updateSuccess": "Модель обновлена",
"deleteSuccess": "Модель удалена",
"saveFailed": "Ошибка сохранения",
"deleteFailed": "Ошибка удаления"
},
"providerPricing": {
"title": "Цены провайдеров · {model}",
"description": "Сравните облачные цены по провайдерам и закрепите одну из них как локальную цену модели.",
"input": "Вход",
"output": "Выход",
"cacheRead": "Чтение кэша",
"pinAction": "Закрепить как локальную цену",
"pinSuccess": "Цена {provider} закреплена как локальная цена модели",
"pinFailed": "Не удалось закрепить цену провайдера",
"pinned": "Закреплено"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

俄语资源也漏了 badges.multi

本次功能除了“比较价格/供应商价格”外,还引入了 multi badge;英文已经有对应 key,但这个 locale 还没同步,运行到多供应商模型时会缺少文案。请把 badges.multi 补齐。

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

In `@messages/ru/settings/prices.json` around lines 196 - 218, Add the missing
localization key badges.multi to this locale: under the same top-level object
that contains providerPricing, add "badges": { "multi": "Несколько провайдеров"
} (or appropriate Russian phrasing) so the multi-badge used for multi-provider
models is present and prevents missing-string errors when rendering the multi
badge.

Comment on lines +2894 to +2904
if (
session.getContext1mApplied?.() &&
(provider.providerType === "claude" || provider.providerType === "claude-auth")
) {
session.addSpecialSetting({
type: "anthropic_context_1m_header_override",
scope: "request_header",
hit: true,
header: "anthropic-beta",
flag: CONTEXT_1M_BETA_HEADER,
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

这里新增的 1M 审计没有保证落库。

现在 anthropic_context_1m_header_override 只是在这里写进 session,但这条路径本身没有持久化;而 buildUnifiedSpecialSettings() 又已经不再从 context1mApplied 自动补派生。这样一来,只要后续没命中其他 persistSpecialSettings(session) 分支(例如关闭 enableClaudeMetadataUserIdInjection),日志/详情页就会直接丢掉这条 1M 审计记录。建议把持久化放到与这次写入同一条路径,或者在 doForward() 里统一持久化一次。

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

In `@src/app/v1/_lib/proxy/forwarder.ts` around lines 2894 - 2904, The added
anthropic 1M audit setting is only added to session via
session.addSpecialSetting({... type: "anthropic_context_1m_header_override",
...}) but not persisted, so it can be lost; modify the same code path to call
persistSpecialSettings(session) immediately after adding the special setting (or
ensure doForward() calls persistSpecialSettings(session) unconditionally before
returning) and keep buildUnifiedSpecialSettings() unchanged; locate the
session.addSpecialSetting call in forwarder.ts and add the
persistSpecialSettings(session) invocation (or a single unified persist in
doForward) so the 1M audit entry is guaranteed to be written to storage.

Comment on lines +377 to +436
it("nested pricing: gpt-5.4 alias model should bill from pricing.openai when provider is chatgpt", async () => {
vi.mocked(getSystemSettings).mockResolvedValue(makeSystemSettings("redirected"));
vi.mocked(updateMessageRequestDetails).mockResolvedValue(undefined);
vi.mocked(updateMessageRequestDuration).mockResolvedValue(undefined);
vi.mocked(SessionManager.storeSessionResponse).mockResolvedValue(undefined);
vi.mocked(RateLimitService.trackUserDailyCost).mockResolvedValue(undefined);
vi.mocked(SessionTracker.refreshSession).mockResolvedValue(undefined);

vi.mocked(findLatestPriceByModel).mockImplementation(async (modelName: string) => {
if (modelName === "gpt-5.4") {
return makePriceRecord(modelName, {
mode: "responses",
model_family: "gpt",
litellm_provider: "chatgpt",
pricing: {
openai: {
input_cost_per_token: 2.5,
output_cost_per_token: 15,
},
},
});
}
return null;
});

const dbCosts: string[] = [];
vi.mocked(updateMessageRequestCost).mockImplementation(
async (_id: number, costUsd: unknown) => {
dbCosts.push(String(costUsd));
}
);

const sessionCosts: string[] = [];
vi.mocked(SessionManager.updateSessionUsage).mockImplementation(
async (_sessionId: string, payload: Record<string, unknown>) => {
if (typeof payload.costUsd === "string") {
sessionCosts.push(payload.costUsd);
}
}
);

const session = createSession({
originalModel: "gpt-5.4",
redirectedModel: "gpt-5.4",
sessionId: "sess-gpt54-chatgpt",
messageId: 3100,
providerOverrides: {
name: "ChatGPT",
url: "https://chatgpt.com/backend-api/codex",
providerType: "codex",
},
});

const response = createNonStreamResponse({ input_tokens: 2, output_tokens: 3 });
await ProxyResponseHandler.dispatch(session, response);
await drainAsyncTasks();

expect(dbCosts[0]).toBe("50");
expect(sessionCosts[0]).toBe("50");
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

把这个 nested pricing 场景补成和前面一致的覆盖矩阵。

前面的 billing source 用例都分别覆盖了 stream/non-stream,并且校验了 DB、Session、RateLimitService.trackCost 三条计费链路;这个新场景目前只测了非流式,且没断言 trackCost。如果 provider-aware resolution 只在某一条链路接上,这里会漏报。

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

In `@tests/integration/billing-model-source.test.ts` around lines 377 - 436, The
new nested-pricing test only covers non-stream and omits asserting the
rate-limit billing path; add a parallel stream-case and include assertions for
all three billing chains. Duplicate the setup used in this test (mocks for
findLatestPriceByModel, updateMessageRequestCost,
SessionManager.updateSessionUsage, and RateLimitService.trackUserDailyCost) but
send a streaming response via the streaming helper (e.g., createStreamResponse
or the project’s stream-response factory) and call ProxyResponseHandler.dispatch
with that session, then await drainAsyncTasks; assert the DB cost captured by
updateMessageRequestCost, the session cost captured by
SessionManager.updateSessionUsage, and that RateLimitService.trackUserDailyCost
was called with the expected cost (use the same expected value "50" as in the
non-stream case). Ensure you reference and reuse the same symbols:
ProxyResponseHandler.dispatch, createSession,
createNonStreamResponse/createStreamResponse, updateMessageRequestCost,
SessionManager.updateSessionUsage, and RateLimitService.trackUserDailyCost.

Comment on lines +5 to +23
test("uses long-context output pricing when total input context exceeds threshold", () => {
const cost = calculateRequestCost(
{
input_tokens: 250000,
output_tokens: 100000,
},
{
mode: "chat",
model_family: "claude-sonnet",
input_cost_per_token: 0.000003,
input_cost_per_token_above_200k_tokens: 0.000006,
output_cost_per_token: 0.000015,
output_cost_per_token_above_200k_tokens: 0.0000225,
},
1,
false
);

expect(Number(cost.toString())).toBe(3.75);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

这个用例还没把这次阈值回归真正钉住。

现在这组数据只覆盖了“200k 分支正向命中”。如果实现误把总 tokens 当阈值输入,或者 GPT 的 272k 分支回归,这个测试也会继续通过。建议至少再补一个 input_tokens < threshold 但总量更高的反例,以及一个 model_family: "gpt" 的 272k 场景。

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

In `@tests/unit/lib/cost-calculation-long-context.test.ts` around lines 5 - 23,
当前测试只覆盖了 calculateRequestCost 在 claude-sonnet 且 input_tokens >= 200k
时触发长上下文分支;请补两个用例:一是对同样的 model_family "claude-sonnet" 增加一个反例(input_tokens <
200000 但 input_tokens+output_tokens > 200000),断言应使用短上下文定价(即不触发
input_cost_per_token_above_200k_tokens /
output_cost_per_token_above_200k_tokens);二是新增一个 model_family "gpt" 的场景,使用靠近 GPT
长上下文阈值 272k 的 token 组合(一个命中 272k 分支的正例和/或一个不命中的反例),分别断言使用/不使用对应的 272k
分支费率以确保阈值逻辑被覆盖;都在 tests/unit/lib/cost-calculation-long-context.test.ts 中与现有
calculateRequestCost 调用保持相同参数结构并显式断言期望的费率分支。

// 3. 解析 usage metrics
const { usageMetrics } = parseUsageFromResponseText(responseText, provider.providerType);

if (!usageMetrics) {
Copy link
Contributor

Choose a reason for hiding this comment

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

[CRITICAL] [LOGIC-BUG] Missing-usage path in finalizeRequestStats no longer persists status/providerChain for Gemini passthrough

Evidence (src/app/v1/_lib/proxy/response-handler.ts:2899-2902):

const { usageMetrics } = parseUsageFromResponseText(responseText, provider.providerType);

if (!usageMetrics) {
  return null;
}

Why this is a problem: For Gemini passthrough (stream + non-stream), finalizeRequestStats(...) is the stats task that persists the final request snapshot. When usage is missing (common on upstream error payloads), this now returns before writing statusCode / errorMessage / providerChain (and also no longer ends the ProxyStatusTracker / writes duration in this function).

Suggested fix: Always persist the final request snapshot (and end tracking) even if usage is missing; only skip billing calculations.

const { usageMetrics } = parseUsageFromResponseText(responseText, provider.providerType);

ProxyStatusTracker.getInstance().endRequest(messageContext.user.id, messageContext.id);
await updateMessageRequestDuration(messageContext.id, duration);

if (!usageMetrics) {
  await updateMessageRequestDetails(messageContext.id, {
    statusCode,
    ...(errorMessage ? { errorMessage } : {}),
    ttfbMs: session.ttfbMs ?? duration,
    providerChain: session.getProviderChain(),
    model: session.getCurrentModel() ?? undefined,
    providerId: providerIdForPersistence,
    context1mApplied: session.getContext1mApplied(),
    swapCacheTtlApplied: provider.swapCacheTtlBilling ?? false,
    specialSettings: session.getSpecialSettings() ?? undefined,
  });
  return null;
}

// existing normalizedUsage/cost path continues...

serverTimeZone: _serverTimeZone,
}: VirtualizedLogsTableProps) {
const t = useTranslations("dashboard");
const getPricingSourceLabel = (source: string) => t(`logs.billingDetails.pricingSource.`);
Copy link
Contributor

Choose a reason for hiding this comment

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

[HIGH] [LOGIC-BUG] getPricingSourceLabel uses an incomplete i18n key (missing ${source})

Evidence (src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx:78):

const getPricingSourceLabel = (source: string) => t(`logs.billingDetails.pricingSource.`);

Why this is a problem: source is never interpolated, so the UI always looks up logs.billingDetails.pricingSource. instead of logs.billingDetails.pricingSource.{source}. That renders the wrong label (or can throw a missing-message error depending on next-intl config) whenever pricing_resolution is present.

Suggested fix:

const getPricingSourceLabel = (source: string) =>
  t(`logs.billingDetails.pricingSource.${source}`);

) : null}
{price.priceData.pricing &&
Object.keys(price.priceData.pricing).length > 1 ? (
<Badge variant="outline">{t("badges.multi")}</Badge>
Copy link
Contributor

Choose a reason for hiding this comment

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

[HIGH] [STANDARD-VIOLATION] badges.multi is referenced but missing in 3 locale message files

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

<Badge variant="outline">{t("badges.multi")}</Badge>

Guideline (CLAUDE.md):

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

Why this is a problem: badges.multi exists in messages/en/settings/prices.json and messages/zh-CN/settings/prices.json, but is missing from:

  • messages/ja/settings/prices.json
  • messages/ru/settings/prices.json
  • messages/zh-TW/settings/prices.json

This can trigger a next-intl missing-message error when the Multi badge renders in those locales.

Suggested fix: Add badges.multi to the missing locale files.

// messages/ja/settings/prices.json
"badges": {
  "local": "ローカル",
  "multi": "複数"
}

// messages/ru/settings/prices.json
"badges": {
  "local": "Локальная",
  "multi": "Мульти"
}

// messages/zh-TW/settings/prices.json
"badges": {
  "local": "本機",
  "multi": "多供應商"
}

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

Choose a reason for hiding this comment

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

[MEDIUM] [STANDARD-VIOLATION] Pin-provider errors are not localized (toast.error(result.error))

Evidence (src/app/[locale]/settings/prices/_components/provider-pricing-dialog.tsx:68):

toast.error(result.error);

Guideline (CLAUDE.md):

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

Why this is a problem: pinModelPricingProviderAsManual returns hardcoded Chinese error strings (e.g. src/actions/model-prices.ts:636-661), and the dialog shows them verbatim. This breaks localization for ja/en/ru/zh-TW users.

Suggested fix: Return errorCode/errorParams from the action and translate on the client via useTranslations("errors") + getErrorMessage.

// src/app/[locale]/settings/prices/_components/provider-pricing-dialog.tsx
import { getErrorMessage } from "@/lib/utils/error-messages";

const tErrors = useTranslations("errors");

if (!result.ok) {
  const message = result.errorCode
    ? getErrorMessage(tErrors, result.errorCode, result.errorParams)
    : result.error;
  toast.error(message);
  return;
}
// src/actions/model-prices.ts
import { ERROR_CODES } from "@/lib/utils/error-messages";

if (!session || session.user.role !== "admin") {
  return { ok: false, error: ERROR_CODES.PERMISSION_DENIED, errorCode: ERROR_CODES.PERMISSION_DENIED };
}

if (!modelName) {
  return {
    ok: false,
    error: ERROR_CODES.REQUIRED_FIELD,
    errorCode: ERROR_CODES.REQUIRED_FIELD,
    errorParams: { field: "model" },
  };
}

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

This PR introduces provider-aware pricing resolution across billing + UI, but it currently has a critical regression in the Gemini passthrough stats finalization path and multiple i18n issues that can break non-en locales. These should be addressed before merge to avoid missing request records and runtime translation failures.

PR Size: XL

  • Lines changed: 2618
  • Files changed: 34
  • Split suggestion: (1) core pricing-resolution + types + cost-calculation, (2) proxy pipeline integration (session/response-handler/forwarder), (3) dashboard/settings UI + i18n/messages

Issues Found

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

Critical Issues (Must Fix)

  • src/app/v1/_lib/proxy/response-handler.ts:2900 - finalizeRequestStats returns on missing usageMetrics without persisting statusCode/providerChain for Gemini passthrough (Confidence: 98)

High Priority Issues (Should Fix)

  • src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx:78 - pricing source label uses incomplete i18n key (missing ${source}) (Confidence: 92)
  • src/app/[locale]/settings/prices/_components/price-list.tsx:489 - badges.multi missing in ja/ru/zh-TW messages can trigger next-intl missing-message errors (Confidence: 90)
  • src/app/[locale]/settings/prices/_components/provider-pricing-dialog.tsx:68 - pin-provider errors are shown via raw result.error (action returns hardcoded Chinese strings); use errorCode + errors i18n (Confidence: 85)

Review Coverage

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

Automated review by Codex AI

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Applied PR size label size/XL, left 4 inline review comments, and submitted a PR review summary.

Inline comments posted:

  • [CRITICAL] src/app/v1/_lib/proxy/response-handler.ts:2900 — Gemini passthrough stats can skip persistence when usageMetrics is missing.
  • [HIGH] src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx:78 — pricing source label i18n key missing ${source}.
  • [HIGH] src/app/[locale]/settings/prices/_components/price-list.tsx:489badges.multi used but missing in ja/ru/zh-TW messages.
  • [MEDIUM] src/app/[locale]/settings/prices/_components/provider-pricing-dialog.tsx:68 — pin-provider errors shown via raw result.error (not localized); recommend errorCode + errors i18n.

serverTimeZone: _serverTimeZone,
}: VirtualizedLogsTableProps) {
const t = useTranslations("dashboard");
const getPricingSourceLabel = (source: string) => t(`logs.billingDetails.pricingSource.`);
Copy link

Choose a reason for hiding this comment

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

Missing ${source} interpolation in getPricingSourceLabel

The source parameter is never inserted into the translation key — the string ends with a trailing dot, so t(...) always receives the key "logs.billingDetails.pricingSource." (empty suffix). This means every pricing-source label in the virtualized table will be a raw/broken translation key rather than the correct human-readable string (e.g. "Local manual pricing").

Compare with the correct implementation in usage-logs-table.tsx (line 65-66):

const getPricingSourceLabel = (source: string) =>
  t(`logs.billingDetails.pricingSource.${source}`);
Suggested change
const getPricingSourceLabel = (source: string) => t(`logs.billingDetails.pricingSource.`);
const getPricingSourceLabel = (source: string) => t(`logs.billingDetails.pricingSource.${source}`);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
Line: 78

Comment:
**Missing `${source}` interpolation in `getPricingSourceLabel`**

The `source` parameter is never inserted into the translation key — the string ends with a trailing dot, so `t(...)` always receives the key `"logs.billingDetails.pricingSource."` (empty suffix). This means every pricing-source label in the virtualized table will be a raw/broken translation key rather than the correct human-readable string (e.g. `"Local manual pricing"`).

Compare with the correct implementation in `usage-logs-table.tsx` (line 65-66):
```ts
const getPricingSourceLabel = (source: string) =>
  t(`logs.billingDetails.pricingSource.${source}`);
```

```suggestion
const getPricingSourceLabel = (source: string) => t(`logs.billingDetails.pricingSource.${source}`);
```

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

Comment on lines 288 to +296
);
} else if (
hasRealCacheCreationBase &&
cacheCreationAbove200k != null &&
longContextThresholdExceeded &&
context1mApplied &&
cacheCreation1hCost != null &&
cache1hTokens != null
) {
cacheCreationBucket = cacheCreationBucket.add(
calculateTieredCostWithSeparatePrices(
cache1hTokens,
cacheCreation1hCost,
cacheCreationAbove200k
)
multiplyCost(cache1hTokens, cacheCreation1hCost * CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER)
Copy link

Choose a reason for hiding this comment

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

Missing hasRealCacheCreationBase guard for 1-hour cache long-context billing

The 5-minute cache branch (lines 266–273) correctly gates on hasRealCacheCreationBase before applying the above-threshold rate:

if (
  longContextThresholdExceeded &&
  hasRealCacheCreationBase &&
  cacheCreationAboveThreshold != null &&
  cache5mTokens != null
)

However, the 1-hour cache branch here does not include this guard:

if (
  longContextThresholdExceeded &&
  cacheCreation1hAboveThreshold != null &&
  cache1hTokens != null
)

If a model record has cache_creation_input_token_cost_above_1hr_above_272k_tokens set but no base cache_creation_input_token_cost, the 1-hour premium will incorrectly apply even when the model should not use tiered pricing. The same issue exists in calculateRequestCost around line 505–510.

Add the guard:

Suggested change
);
} else if (
hasRealCacheCreationBase &&
cacheCreationAbove200k != null &&
longContextThresholdExceeded &&
context1mApplied &&
cacheCreation1hCost != null &&
cache1hTokens != null
) {
cacheCreationBucket = cacheCreationBucket.add(
calculateTieredCostWithSeparatePrices(
cache1hTokens,
cacheCreation1hCost,
cacheCreationAbove200k
)
multiplyCost(cache1hTokens, cacheCreation1hCost * CONTEXT_1M_INPUT_PREMIUM_MULTIPLIER)
// Cache creation 1h -> cache_creation bucket
if (
longContextThresholdExceeded &&
hasRealCacheCreationBase &&
cacheCreation1hAboveThreshold != null &&
cache1hTokens != null
) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/utils/cost-calculation.ts
Line: 288-296

Comment:
**Missing `hasRealCacheCreationBase` guard for 1-hour cache long-context billing**

The 5-minute cache branch (lines 266–273) correctly gates on `hasRealCacheCreationBase` before applying the above-threshold rate:
```ts
if (
  longContextThresholdExceeded &&
  hasRealCacheCreationBase &&
  cacheCreationAboveThreshold != null &&
  cache5mTokens != null
)
```

However, the 1-hour cache branch here does not include this guard:
```ts
if (
  longContextThresholdExceeded &&
  cacheCreation1hAboveThreshold != null &&
  cache1hTokens != null
)
```

If a model record has `cache_creation_input_token_cost_above_1hr_above_272k_tokens` set but no base `cache_creation_input_token_cost`, the 1-hour premium will incorrectly apply even when the model should not use tiered pricing. The same issue exists in `calculateRequestCost` around line 505–510.

Add the guard:
```suggestion
  // Cache creation 1h -> cache_creation bucket
  if (
    longContextThresholdExceeded &&
    hasRealCacheCreationBase &&
    cacheCreation1hAboveThreshold != null &&
    cache1hTokens != null
  ) {
```

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

Comment on lines 505 to +510
segments.push(multiplyCost(cache1hTokens, cacheCreation1hCost));
}

// 缓存读取费用
if (
longContextThresholdExceeded &&
Copy link

Choose a reason for hiding this comment

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

Missing hasRealCacheCreationBase guard in calculateRequestCost 1-hour cache branch

Same issue as in calculateRequestCostBreakdown: the 5-minute cache branch (lines 484–490) correctly checks hasRealCacheCreationBase, but the 1-hour branch here does not. This creates an asymmetry where models without a base cache creation price can still be charged above-threshold rates for 1-hour TTL caching.

Add the guard:

Suggested change
segments.push(multiplyCost(cache1hTokens, cacheCreation1hCost));
}
// 缓存读取费用
if (
longContextThresholdExceeded &&
// 缓存创建费用(1小时 TTL):优先级 explicit long-context > context1m fallback > 普通
if (
longContextThresholdExceeded &&
hasRealCacheCreationBase &&
cacheCreation1hAboveThreshold != null &&
cache1hTokens != null
) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/utils/cost-calculation.ts
Line: 505-510

Comment:
**Missing `hasRealCacheCreationBase` guard in `calculateRequestCost` 1-hour cache branch**

Same issue as in `calculateRequestCostBreakdown`: the 5-minute cache branch (lines 484–490) correctly checks `hasRealCacheCreationBase`, but the 1-hour branch here does not. This creates an asymmetry where models without a base cache creation price can still be charged above-threshold rates for 1-hour TTL caching.

Add the guard:
```suggestion
  // 缓存创建费用(1小时 TTL):优先级 explicit long-context > context1m fallback > 普通
  if (
    longContextThresholdExceeded &&
    hasRealCacheCreationBase &&
    cacheCreation1hAboveThreshold != null &&
    cache1hTokens != null
  ) {
```

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (4)
src/lib/utils/special-settings.ts (1)

25-27: ⚠️ Potential issue | 🟠 Major

不要现在就去掉 context1mApplied 的兼容派生。

buildUnifiedSpecialSettings() 现在完全不再使用这个参数了,但 src/repository/usage-logs.ts 仍会把历史记录里的 context1mApplied 传进来。这样旧日志或未显式写入 anthropic_context_1m_header_override 的请求,会直接丢掉 1M 上下文审计。至少应保留从 context1mApplied 派生 special setting 的回退,直到所有写路径都稳定持久化显式 special setting。

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

In `@src/lib/utils/special-settings.ts` around lines 25 - 27,
buildUnifiedSpecialSettings currently ignores the legacy context1mApplied flag
but usage-logs still passes it in, so restore a fallback: in
buildUnifiedSpecialSettings (or the function that normalizes special settings)
detect a missing anthropic_context_1m_header_override and, if context1mApplied
is true (or truthy), set the equivalent special setting to preserve 1M audit
behavior; ensure you reference and handle the context1mApplied input from the
call site in src/repository/usage-logs.ts so old logs without explicit
anthropic_context_1m_header_override still honor the legacy value until all
writers persist the explicit special setting.
tests/integration/billing-model-source.test.ts (1)

383-442: ⚠️ Potential issue | 🟡 Minor

把这个 nested pricing 场景补成和前面一致的覆盖矩阵。

这里现在仍然只测了非流式,而且没有断言 RateLimitService.trackCost。如果 provider-aware pricing 只接上了 DB / Session 其中一条链路,或者只在 stream 分支里有问题,这个用例会漏报。建议至少补一个 stream case,并把 DB、Session、RateLimit 三条计费链路都断言出来。

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

In `@tests/integration/billing-model-source.test.ts` around lines 383 - 442, The
test only covers non-stream pricing and asserts DB and Session updates but
misses the stream path and RateLimitService; update the "nested pricing:
gpt-5.4..." test to also exercise the streaming branch (use or add a stream
response factory, e.g., createStreamResponse or equivalent) and repeat the
dispatch+drainAsyncTasks flow, and add assertions that
RateLimitService.trackUserDailyCost was called with the expected cost in both
non-stream and stream cases; ensure you explicitly assert the three billing
chains by checking the mocked updateMessageRequestCost (dbCosts),
SessionManager.updateSessionUsage (sessionCosts), and
vi.mocked(RateLimitService.trackUserDailyCost) call arguments for the expected
"50" value so the test fails if any one chain is missing.
messages/ja/settings/prices.json (1)

196-220: ⚠️ Potential issue | 🟡 Minor

日语资源仍缺少 badges.multi

这次已经补了多供应商价格相关文案,但 badges 里仍只有 local。如果价格列表继续渲染 multi 徽标,ja locale 会直接出现缺失翻译。请把 badges.multi 一并补上。As per coding guidelines, "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text".

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

In `@messages/ja/settings/prices.json` around lines 196 - 220, The ja locale is
missing the badges.multi translation causing missing-text at runtime; add a
"badges.multi" key with an appropriate Japanese string (matching style of
existing badge translations, e.g., "マルチ" or the agreed term) alongside the
existing badges.local entry in messages/ja/settings/prices.json so the
multi-provider badge renders correctly (ensure the key name is exactly "multi"
under the badges object to mirror other locales).
src/app/v1/_lib/proxy/response-handler.ts (1)

3009-3021: ⚠️ Potential issue | 🟡 Minor

usageMetrics 为空时,请求持续时间未被持久化。

usageMetricsnull 时,函数在第 3021 行直接返回,但 updateMessageRequestDuration 从未被调用。虽然 updateMessageRequestDetails 中设置了 ttfbMs,但实际的请求总时长(duration 字段)未被保存。

此外,ProxyStatusTracker.endRequest 也未在此路径中调用,可能导致追踪状态残留。

修复建议
   if (!usageMetrics) {
+    await updateMessageRequestDuration(messageContext.id, duration);
+    ProxyStatusTracker.getInstance().endRequest(messageContext.user.id, messageContext.id);
+
     await updateMessageRequestDetails(messageContext.id, {
       statusCode: statusCode,
       // ... existing fields
     });
     return null;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/response-handler.ts` around lines 3009 - 3021, When
usageMetrics is null the handler returns after calling
updateMessageRequestDetails and never persists the total request duration nor
ends the proxy tracking; modify the branch where usageMetrics is falsy (around
the check for usageMetrics and the updateMessageRequestDetails call) to also
call updateMessageRequestDuration(messageContext.id, { duration }) and ensure
ProxyStatusTracker.endRequest(...) is invoked (using the same session/context
and providerIdForPersistence as in the other path) before returning so both
duration and request tracking are properly finalized.
🧹 Nitpick comments (2)
src/app/v1/_lib/proxy/session.ts (1)

126-127: 这个缓存目前只覆盖串行调用,并发命中同一 key 时仍会重复查库。

resolvedPricingCache 存的是最终值,不存 in-flight promise。两个并发的 getResolvedPricingByBillingSource() 都会在 has(cacheKey) 之前 miss,然后各自执行一次 findLatestPriceByModel()。如果这条路径会在同一请求内被多个异步阶段读取,建议像 billingModelSourcePromise 一样缓存 promise。

建议改法
- private resolvedPricingCache = new Map<string, ResolvedPricing | null>();
+ private resolvedPricingCache = new Map<string, Promise<ResolvedPricing | null>>();

  async getResolvedPricingByBillingSource(
    provider?: Provider | null
  ): Promise<ResolvedPricing | null> {
    ...
-   if (this.resolvedPricingCache.has(cacheKey)) {
-     return this.resolvedPricingCache.get(cacheKey) ?? null;
-   }
+   const cached = this.resolvedPricingCache.get(cacheKey);
+   if (cached) {
+     return cached;
+   }

-   const primaryRecord = primaryModel ? await findLatestPriceByModel(primaryModel) : null;
-   let resolved = resolvePricingForModelRecords(...);
-   ...
-   this.resolvedPricingCache.set(cacheKey, resolved ?? null);
-   return resolved ?? null;
+   const pending = (async () => {
+     const primaryRecord = primaryModel ? await findLatestPriceByModel(primaryModel) : null;
+     let resolved = resolvePricingForModelRecords(...);
+     ...
+     return resolved ?? null;
+   })().catch((error) => {
+     this.resolvedPricingCache.delete(cacheKey);
+     throw error;
+   });
+
+   this.resolvedPricingCache.set(cacheKey, pending);
+   return pending;
  }

Also applies to: 752-780

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

In `@src/app/v1/_lib/proxy/session.ts` around lines 126 - 127, 当前
resolvedPricingCache 存的是最终 ResolvedPricing 值,导致并发调用
getResolvedPricingByBillingSource() 在 has(cacheKey) 前都会 miss 并各自触发
findLatestPriceByModel();请改为像 billingModelSourcePromise 那样在 resolvedPricingCache
或新增 resolvedPricingPromiseCache 中缓存正在进行的 Promise(而不是最终值),在首次请求时将查库的 Promise
存入缓存并返回该
Promise,查库成功后用结果替换或清理缓存项/存最终值以保持语义一致,从而避免并发重复查库;涉及标识符:resolvedPricingCache、getResolvedPricingByBillingSource、findLatestPriceByModel、billingModelSourcePromise(同时在文件中
752-780 相同逻辑处一并修复)。
src/lib/utils/pricing-resolution.ts (1)

96-123: ModelPriceData 类型定义中显式声明 model_family 字段可以改进类型安全性。

model_family 字段来自云端价格同步数据(通过 TOML 解析自 https://claude-code-hub.app/config/prices-base.toml),并在 getOfficialProviderKeys 中被访问。虽然当前代码通过 typeof 检查进行了防御性处理,但该字段未在 ModelPriceData 类型中显式声明,只能通过索引签名 [key: string]: unknown 允许。

建议在 ModelPriceData 接口中添加:

model_family?: string;

这样可以提高类型安全性,改善 IDE 自动补全,并使代码意图更加清晰。

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

In `@src/lib/utils/pricing-resolution.ts` around lines 96 - 123, Add an explicit
optional model_family?: string; property to the ModelPriceData interface so
accesses in getOfficialProviderKeys and other callers are type-safe; update the
type definition for ModelPriceData to include model_family?: string (keep it
optional) and leave the existing defensive checks in getOfficialProviderKeys
(typeof priceData?.model_family === "string") as-is to preserve runtime safety.
🤖 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/app/v1/_lib/proxy/response-handler.ts`:
- Around line 1311-1327: The else branch in the pushChunk logic causes infinite
recursion because it calls pushChunk(text, bytes) when inTailMode is true or
headBufferedBytes >= MAX_STATS_HEAD_BYTES; change that call to invoke the
captured pushToTail() closure instead (use pushToTail() where pushChunk(text,
bytes) is currently called) so the already-captured text/bytes are pushed to the
tail buffer without re-entering pushChunk; verify pushToTail, inTailMode,
headChunks, headBufferedBytes and MAX_STATS_HEAD_BYTES are used consistently.

In `@src/lib/utils/cost-calculation.ts`:
- Around line 219-230: The code currently uses a single guard
(hasRealCacheCreationBase / hasRealCacheReadBase) that only checks
cache_creation_input_token_cost and cache_read_input_token_cost, causing
1h-specific base rates (cache_creation_input_token_cost_above_1hr* /
cache_read_input_token_cost_above_1hr*) and their long-context branches to be
skipped; update the logic to split these into separate guards for 5m vs 1h
(e.g., hasRealCacheCreationBase5m, hasRealCacheCreationBase1h,
hasRealCacheReadBase5m, hasRealCacheReadBase1h) and use the appropriate guard
when deciding to fall back to base/derived prices inside
resolveLongContextThreshold/getRequestInputContextTokens handling and pricing
branches; apply the same split-fix in both calculateRequestCostBreakdown() and
calculateRequestCost() (and the analogous blocks at the other mentioned ranges:
~296-304, ~446-457, ~521-528) so 1h long-context rates are honored when only 1h
base prices are present.

In `@tests/integration/billing-model-source.test.ts`:
- Around line 444-620: The three new "codex fast" tests only exercise non-stream
responses and assert DB/Session costs but miss stream coverage and rate-limit
assertions; add parallel stream test cases using createStreamResponse (or the
project's streaming helper) and dispatch via ProxyResponseHandler.dispatch,
mock/spy RateLimitService.trackUserDailyCost (and/or RateLimitService.trackCost
if used) to assert it was called with the expected cost, and also assert
updateMessageRequestCost and SessionManager.updateSessionUsage were invoked with
the same computed priority/non-priority amounts so the DB, Session and RateLimit
chains are all covered for service_tier handling.

---

Duplicate comments:
In `@messages/ja/settings/prices.json`:
- Around line 196-220: The ja locale is missing the badges.multi translation
causing missing-text at runtime; add a "badges.multi" key with an appropriate
Japanese string (matching style of existing badge translations, e.g., "マルチ" or
the agreed term) alongside the existing badges.local entry in
messages/ja/settings/prices.json so the multi-provider badge renders correctly
(ensure the key name is exactly "multi" under the badges object to mirror other
locales).

In `@src/app/v1/_lib/proxy/response-handler.ts`:
- Around line 3009-3021: When usageMetrics is null the handler returns after
calling updateMessageRequestDetails and never persists the total request
duration nor ends the proxy tracking; modify the branch where usageMetrics is
falsy (around the check for usageMetrics and the updateMessageRequestDetails
call) to also call updateMessageRequestDuration(messageContext.id, { duration })
and ensure ProxyStatusTracker.endRequest(...) is invoked (using the same
session/context and providerIdForPersistence as in the other path) before
returning so both duration and request tracking are properly finalized.

In `@src/lib/utils/special-settings.ts`:
- Around line 25-27: buildUnifiedSpecialSettings currently ignores the legacy
context1mApplied flag but usage-logs still passes it in, so restore a fallback:
in buildUnifiedSpecialSettings (or the function that normalizes special
settings) detect a missing anthropic_context_1m_header_override and, if
context1mApplied is true (or truthy), set the equivalent special setting to
preserve 1M audit behavior; ensure you reference and handle the context1mApplied
input from the call site in src/repository/usage-logs.ts so old logs without
explicit anthropic_context_1m_header_override still honor the legacy value until
all writers persist the explicit special setting.

In `@tests/integration/billing-model-source.test.ts`:
- Around line 383-442: The test only covers non-stream pricing and asserts DB
and Session updates but misses the stream path and RateLimitService; update the
"nested pricing: gpt-5.4..." test to also exercise the streaming branch (use or
add a stream response factory, e.g., createStreamResponse or equivalent) and
repeat the dispatch+drainAsyncTasks flow, and add assertions that
RateLimitService.trackUserDailyCost was called with the expected cost in both
non-stream and stream cases; ensure you explicitly assert the three billing
chains by checking the mocked updateMessageRequestCost (dbCosts),
SessionManager.updateSessionUsage (sessionCosts), and
vi.mocked(RateLimitService.trackUserDailyCost) call arguments for the expected
"50" value so the test fails if any one chain is missing.

---

Nitpick comments:
In `@src/app/v1/_lib/proxy/session.ts`:
- Around line 126-127: 当前 resolvedPricingCache 存的是最终 ResolvedPricing 值,导致并发调用
getResolvedPricingByBillingSource() 在 has(cacheKey) 前都会 miss 并各自触发
findLatestPriceByModel();请改为像 billingModelSourcePromise 那样在 resolvedPricingCache
或新增 resolvedPricingPromiseCache 中缓存正在进行的 Promise(而不是最终值),在首次请求时将查库的 Promise
存入缓存并返回该
Promise,查库成功后用结果替换或清理缓存项/存最终值以保持语义一致,从而避免并发重复查库;涉及标识符:resolvedPricingCache、getResolvedPricingByBillingSource、findLatestPriceByModel、billingModelSourcePromise(同时在文件中
752-780 相同逻辑处一并修复)。

In `@src/lib/utils/pricing-resolution.ts`:
- Around line 96-123: Add an explicit optional model_family?: string; property
to the ModelPriceData interface so accesses in getOfficialProviderKeys and other
callers are type-safe; update the type definition for ModelPriceData to include
model_family?: string (keep it optional) and leave the existing defensive checks
in getOfficialProviderKeys (typeof priceData?.model_family === "string") as-is
to preserve runtime safety.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 97131ec7-6136-41d0-945d-d4745b31751c

📥 Commits

Reviewing files that changed from the base of the PR and between 543ae83 and b1c027d.

📒 Files selected for processing (22)
  • 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/app/[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx
  • src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx
  • src/app/[locale]/settings/prices/_components/price-list.tsx
  • src/app/[locale]/settings/prices/_components/provider-pricing-dialog.tsx
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/lib/utils/cost-calculation.ts
  • src/lib/utils/pricing-resolution.ts
  • src/lib/utils/special-settings.ts
  • src/types/special-settings.ts
  • tests/integration/billing-model-source.test.ts
  • tests/unit/lib/cost-calculation-long-context.test.ts
  • tests/unit/lib/cost-calculation-priority.test.ts
  • tests/unit/lib/utils/pricing-resolution.test.ts
  • tests/unit/lib/utils/special-settings.test.ts
  • tests/unit/proxy/pricing-no-price.test.ts
  • tests/unit/proxy/session.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • messages/en/settings/prices.json
  • messages/zh-CN/settings/prices.json
  • messages/ru/settings/prices.json
  • messages/zh-TW/settings/prices.json
  • tests/unit/lib/cost-calculation-long-context.test.ts

Comment on lines +1311 to 1327
if (!inTailMode && headBufferedBytes < MAX_STATS_HEAD_BYTES) {
const remainingHeadBytes = MAX_STATS_HEAD_BYTES - headBufferedBytes;
if (remainingHeadBytes > 0 && bytes > remainingHeadBytes) {
const headPart = text.substring(0, remainingHeadBytes);
const tailPart = text.substring(remainingHeadBytes);

pushChunk(headPart, remainingHeadBytes);

inTailMode = true;
pushChunk(tailPart, bytes - remainingHeadBytes);
} else {
headChunks.push(text);
headBufferedBytes += bytes;
return;
}

inTailMode = true;
} else {
pushChunk(text, bytes);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

递归调用导致无限循环(栈溢出)。

inTailModetrueheadBufferedBytes >= MAX_STATS_HEAD_BYTES 时,第 1326 行的 pushChunk(text, bytes) 会无限递归调用自身:

  1. 进入 pushChunk("data", 100)
  2. 检查 !inTailMode && headBufferedBytes < MAX_STATS_HEAD_BYTESfalse
  3. 进入 else 分支,再次调用 pushChunk("data", 100)
  4. 重复步骤 2-3,直到栈溢出

pushToTail() 闭包已捕获 textbytes,应直接调用它。

修复建议
             } else {
-              pushChunk(text, bytes);
+              inTailMode = true;
+              pushToTail();
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/response-handler.ts` around lines 1311 - 1327, The else
branch in the pushChunk logic causes infinite recursion because it calls
pushChunk(text, bytes) when inTailMode is true or headBufferedBytes >=
MAX_STATS_HEAD_BYTES; change that call to invoke the captured pushToTail()
closure instead (use pushToTail() where pushChunk(text, bytes) is currently
called) so the already-captured text/bytes are pushed to the tail buffer without
re-entering pushChunk; verify pushToTail, inTailMode, headChunks,
headBufferedBytes and MAX_STATS_HEAD_BYTES are used consistently.

Comment on lines +219 to +230
const cacheCreation1hAboveThreshold =
priceData.cache_creation_input_token_cost_above_1hr_above_272k_tokens ??
priceData.cache_creation_input_token_cost_above_1hr_above_200k_tokens ??
cacheCreationAboveThreshold;
const cacheReadAboveThreshold =
priceData.cache_read_input_token_cost_above_272k_tokens ??
priceData.cache_read_input_token_cost_above_200k_tokens;
const longContextThreshold = resolveLongContextThreshold(priceData);
const longContextThresholdExceeded =
getRequestInputContextTokens(usage, cache5mTokens, cache1hTokens) > longContextThreshold;
const hasRealCacheCreationBase = priceData.cache_creation_input_token_cost != null;
const hasRealCacheReadBase = priceData.cache_read_input_token_cost != null;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

1h cache 的 long-context 显式费率仍可能被跳过。

hasRealCacheCreationBase 现在只检查 cache_creation_input_token_cost。如果某个模型只配置了 1h 基础价(cache_creation_input_token_cost_above_1hr)和对应的 long-context 1h 费率(cache_creation_input_token_cost_above_1hr_above_*),这两个 1h 分支都会落回普通/派生价格,导致超阈值的 1h cache creation 少计费。这里需要把 5m/1h 的“是否有真实基础价”拆成两个 guard。

建议修改
-  const hasRealCacheCreationBase = priceData.cache_creation_input_token_cost != null;
+  const hasRealCacheCreation5mBase = priceData.cache_creation_input_token_cost != null;
+  const hasRealCacheCreation1hBase =
+    priceData.cache_creation_input_token_cost_above_1hr != null ||
+    priceData.cache_creation_input_token_cost != null;

   // Cache creation 5m
   if (
     longContextThresholdExceeded &&
-    hasRealCacheCreationBase &&
+    hasRealCacheCreation5mBase &&
     cacheCreationAboveThreshold != null &&
     cache5mTokens != null
   ) {
     ...
   }

   // Cache creation 1h
   if (
     longContextThresholdExceeded &&
-    hasRealCacheCreationBase &&
+    hasRealCacheCreation1hBase &&
     cacheCreation1hAboveThreshold != null &&
     cache1hTokens != null
   ) {
     ...
   }

同样的修正需要在 calculateRequestCostBreakdown()calculateRequestCost() 两处同时应用。

Also applies to: 296-304, 446-457, 521-528

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

In `@src/lib/utils/cost-calculation.ts` around lines 219 - 230, The code currently
uses a single guard (hasRealCacheCreationBase / hasRealCacheReadBase) that only
checks cache_creation_input_token_cost and cache_read_input_token_cost, causing
1h-specific base rates (cache_creation_input_token_cost_above_1hr* /
cache_read_input_token_cost_above_1hr*) and their long-context branches to be
skipped; update the logic to split these into separate guards for 5m vs 1h
(e.g., hasRealCacheCreationBase5m, hasRealCacheCreationBase1h,
hasRealCacheReadBase5m, hasRealCacheReadBase1h) and use the appropriate guard
when deciding to fall back to base/derived prices inside
resolveLongContextThreshold/getRequestInputContextTokens handling and pricing
branches; apply the same split-fix in both calculateRequestCostBreakdown() and
calculateRequestCost() (and the analogous blocks at the other mentioned ranges:
~296-304, ~446-457, ~521-528) so 1h long-context rates are honored when only 1h
base prices are present.

Comment on lines +444 to +620
it("codex fast: uses priority pricing when response reports service_tier=priority", async () => {
vi.mocked(getSystemSettings).mockResolvedValue(makeSystemSettings("redirected"));
vi.mocked(updateMessageRequestDetails).mockResolvedValue(undefined);
vi.mocked(updateMessageRequestDuration).mockResolvedValue(undefined);
vi.mocked(SessionManager.storeSessionResponse).mockResolvedValue(undefined);
vi.mocked(RateLimitService.trackUserDailyCost).mockResolvedValue(undefined);
vi.mocked(SessionTracker.refreshSession).mockResolvedValue(undefined);

vi.mocked(findLatestPriceByModel).mockImplementation(async (modelName: string) => {
if (modelName === "gpt-5.4") {
return makePriceRecord(modelName, {
mode: "responses",
model_family: "gpt",
litellm_provider: "chatgpt",
pricing: {
openai: {
input_cost_per_token: 1,
output_cost_per_token: 10,
input_cost_per_token_priority: 2,
output_cost_per_token_priority: 20,
},
},
});
}
return null;
});

const dbCosts: string[] = [];
vi.mocked(updateMessageRequestCost).mockImplementation(
async (_id: number, costUsd: unknown) => {
dbCosts.push(String(costUsd));
}
);

const sessionCosts: string[] = [];
vi.mocked(SessionManager.updateSessionUsage).mockImplementation(
async (_sessionId: string, payload: Record<string, unknown>) => {
if (typeof payload.costUsd === "string") {
sessionCosts.push(payload.costUsd);
}
}
);

const session = createSession({
originalModel: "gpt-5.4",
redirectedModel: "gpt-5.4",
sessionId: "sess-gpt54-priority-actual",
messageId: 3200,
providerOverrides: {
name: "ChatGPT",
url: "https://chatgpt.com/backend-api/codex",
providerType: "codex",
},
requestMessage: { service_tier: "default" },
});

const response = createNonStreamResponse(
{ input_tokens: 2, output_tokens: 3 },
{ service_tier: "priority" }
);
await ProxyResponseHandler.dispatch(session, response);
await drainAsyncTasks();

expect(dbCosts[0]).toBe("64");
expect(sessionCosts[0]).toBe("64");
});

it("codex fast: falls back to requested priority pricing when response omits service_tier", async () => {
vi.mocked(getSystemSettings).mockResolvedValue(makeSystemSettings("redirected"));
vi.mocked(updateMessageRequestDetails).mockResolvedValue(undefined);
vi.mocked(updateMessageRequestDuration).mockResolvedValue(undefined);
vi.mocked(SessionManager.storeSessionResponse).mockResolvedValue(undefined);
vi.mocked(RateLimitService.trackUserDailyCost).mockResolvedValue(undefined);
vi.mocked(SessionTracker.refreshSession).mockResolvedValue(undefined);

vi.mocked(findLatestPriceByModel).mockImplementation(async (modelName: string) => {
if (modelName === "gpt-5.4") {
return makePriceRecord(modelName, {
mode: "responses",
model_family: "gpt",
litellm_provider: "chatgpt",
pricing: {
openai: {
input_cost_per_token: 1,
output_cost_per_token: 10,
input_cost_per_token_priority: 2,
output_cost_per_token_priority: 20,
},
},
});
}
return null;
});

const dbCosts: string[] = [];
vi.mocked(updateMessageRequestCost).mockImplementation(
async (_id: number, costUsd: unknown) => {
dbCosts.push(String(costUsd));
}
);

const session = createSession({
originalModel: "gpt-5.4",
redirectedModel: "gpt-5.4",
sessionId: "sess-gpt54-priority-requested",
messageId: 3201,
providerOverrides: {
name: "ChatGPT",
url: "https://chatgpt.com/backend-api/codex",
providerType: "codex",
},
requestMessage: { service_tier: "priority" },
});

const response = createNonStreamResponse({ input_tokens: 2, output_tokens: 3 });
await ProxyResponseHandler.dispatch(session, response);
await drainAsyncTasks();

expect(dbCosts[0]).toBe("64");
});

it("codex fast: does not use priority pricing when response explicitly reports non-priority tier", async () => {
vi.mocked(getSystemSettings).mockResolvedValue(makeSystemSettings("redirected"));
vi.mocked(updateMessageRequestDetails).mockResolvedValue(undefined);
vi.mocked(updateMessageRequestDuration).mockResolvedValue(undefined);
vi.mocked(SessionManager.storeSessionResponse).mockResolvedValue(undefined);
vi.mocked(RateLimitService.trackUserDailyCost).mockResolvedValue(undefined);
vi.mocked(SessionTracker.refreshSession).mockResolvedValue(undefined);

vi.mocked(findLatestPriceByModel).mockImplementation(async (modelName: string) => {
if (modelName === "gpt-5.4") {
return makePriceRecord(modelName, {
mode: "responses",
model_family: "gpt",
litellm_provider: "chatgpt",
pricing: {
openai: {
input_cost_per_token: 1,
output_cost_per_token: 10,
input_cost_per_token_priority: 2,
output_cost_per_token_priority: 20,
},
},
});
}
return null;
});

const dbCosts: string[] = [];
vi.mocked(updateMessageRequestCost).mockImplementation(
async (_id: number, costUsd: unknown) => {
dbCosts.push(String(costUsd));
}
);

const session = createSession({
originalModel: "gpt-5.4",
redirectedModel: "gpt-5.4",
sessionId: "sess-gpt54-priority-downgraded",
messageId: 3202,
providerOverrides: {
name: "ChatGPT",
url: "https://chatgpt.com/backend-api/codex",
providerType: "codex",
},
requestMessage: { service_tier: "priority" },
});

const response = createNonStreamResponse(
{ input_tokens: 2, output_tokens: 3 },
{ service_tier: "default" }
);
await ProxyResponseHandler.dispatch(session, response);
await drainAsyncTasks();

expect(dbCosts[0]).toBe("32");
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

codex fast 新计费路径也缺少流式与限流链路覆盖。

这三条新用例都只走了 non-stream,且大多只校验了 DB / Session 成本,没有把 RateLimitService.trackCost 纳入断言。service_tier 解析和 priority 定价是在新逻辑里新增的,最好至少补一组 stream 用例,并和非流式一样覆盖 DB、Session、RateLimit 三条链路,否则这条计费路径的回归很容易漏掉。

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

In `@tests/integration/billing-model-source.test.ts` around lines 444 - 620, The
three new "codex fast" tests only exercise non-stream responses and assert
DB/Session costs but miss stream coverage and rate-limit assertions; add
parallel stream test cases using createStreamResponse (or the project's
streaming helper) and dispatch via ProxyResponseHandler.dispatch, mock/spy
RateLimitService.trackUserDailyCost (and/or RateLimitService.trackCost if used)
to assert it was called with the expected cost, and also assert
updateMessageRequestCost and SessionManager.updateSessionUsage were invoked with
the same computed priority/non-priority amounts so the DB, Session and RateLimit
chains are all covered for service_tier handling.

@ding113 ding113 merged commit 2e663cd into dev Mar 6, 2026
11 of 13 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Mar 6, 2026
Comment on lines +91 to +105
function resolveLongContextThreshold(priceData: ModelPriceData): number {
const has272kFields =
typeof priceData.input_cost_per_token_above_272k_tokens === "number" ||
typeof priceData.output_cost_per_token_above_272k_tokens === "number" ||
typeof priceData.cache_creation_input_token_cost_above_272k_tokens === "number" ||
typeof priceData.cache_read_input_token_cost_above_272k_tokens === "number" ||
typeof priceData.cache_creation_input_token_cost_above_1hr_above_272k_tokens === "number";

const modelFamily = typeof priceData.model_family === "string" ? priceData.model_family : "";
if (has272kFields || modelFamily === "gpt" || modelFamily === "gpt-pro") {
return OPENAI_LONG_CONTEXT_TOKEN_THRESHOLD;
}

return CONTEXT_1M_TOKEN_THRESHOLD;
}
Copy link

Choose a reason for hiding this comment

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

Threshold/rate mismatch for GPT models with only 200k pricing fields

resolveLongContextThreshold returns OPENAI_LONG_CONTEXT_TOKEN_THRESHOLD (272k) for any model whose model_family is "gpt" or "gpt-pro", even when the price record has no 272k fields at all (only input_cost_per_token_above_200k_tokens).

In calculateRequestCostBreakdown / calculateRequestCost, inputAboveThreshold is then computed as:

const inputAboveThreshold =
  priceData.input_cost_per_token_above_272k_tokens ??
  priceData.input_cost_per_token_above_200k_tokens;

So for such a model:

  • The premium activates at 272k total tokens (272k threshold)
  • But the rate used is the 200k pricing tier

This means a GPT model with 220k tokens would not be billed at premium (because 220k < 272k), even though it crossed the 200k pricing tier. Conversely, a model with 280k tokens would be billed at the 200k-tier rate with the 272k threshold, which may or may not match the provider's actual billing.

If the 272k threshold is intentional for all GPT-family models regardless of whether 272k pricing fields exist, this should be documented with a comment. If not, the model_family === "gpt" fallback should only apply when has272kFields is already true.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/utils/cost-calculation.ts
Line: 91-105

Comment:
**Threshold/rate mismatch for GPT models with only 200k pricing fields**

`resolveLongContextThreshold` returns `OPENAI_LONG_CONTEXT_TOKEN_THRESHOLD` (272k) for any model whose `model_family` is `"gpt"` or `"gpt-pro"`, even when the price record has **no** 272k fields at all (only `input_cost_per_token_above_200k_tokens`).

In `calculateRequestCostBreakdown` / `calculateRequestCost`, `inputAboveThreshold` is then computed as:
```ts
const inputAboveThreshold =
  priceData.input_cost_per_token_above_272k_tokens ??
  priceData.input_cost_per_token_above_200k_tokens;
```

So for such a model:
- The premium activates at **272k** total tokens (272k threshold)
- But the rate used is the **200k** pricing tier

This means a GPT model with 220k tokens would **not** be billed at premium (because 220k < 272k), even though it crossed the 200k pricing tier. Conversely, a model with 280k tokens would be billed at the 200k-tier rate with the 272k threshold, which may or may not match the provider's actual billing.

If the 272k threshold is intentional for all GPT-family models regardless of whether 272k pricing fields exist, this should be documented with a comment. If not, the `model_family === "gpt"` fallback should only apply when `has272kFields` is already true.

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

Comment on lines +125 to +170
export function resolvePricingKeyCandidates(
provider: Provider | null | undefined,
modelName: string | null | undefined,
priceData?: ModelPriceData
): PricingKeyCandidate[] {
const candidates: PricingKeyCandidate[] = [];
const name = normalizeText(provider?.name);
const url = normalizeText(provider?.url);
const host = extractHost(provider?.url);

if (name.includes("openrouter") || host.includes("openrouter")) {
pushUnique(candidates, "openrouter", "exact");
}
if (name.includes("opencode") || host.includes("opencode")) {
pushUnique(candidates, "opencode", "exact");
}
if (
name.includes("cloudflare") ||
host.includes("cloudflare") ||
url.includes("cloudflare-ai-gateway")
) {
pushUnique(candidates, "cloudflare-ai-gateway", "exact");
}
if (name.includes("github") || name.includes("copilot") || host.includes("githubcopilot")) {
pushUnique(candidates, "github-copilot", "exact");
}
if (name.includes("chatgpt") || host.includes("chatgpt.com")) {
pushUnique(candidates, "chatgpt", "exact");
}
if (name.includes("openai") || host.includes("openai.com") || host.includes("api.openai.com")) {
pushUnique(candidates, "openai", "exact");
}
if (name.includes("anthropic") || host.includes("anthropic.com")) {
pushUnique(candidates, "anthropic", "exact");
}
if (name.includes("vertex") || host.includes("googleapis.com") || name.includes("google")) {
pushUnique(candidates, "vertex_ai", "exact");
pushUnique(candidates, "vertex", "exact");
pushUnique(candidates, "google", "exact");
}

for (const officialKey of getOfficialProviderKeys(modelName, priceData)) {
pushUnique(candidates, officialKey, "official");
}

return candidates;
Copy link

Choose a reason for hiding this comment

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

Azure OpenAI misidentified as direct OpenAI for pricing

resolvePricingKeyCandidates uses host.includes("openai.com") to identify OpenAI providers:

if (name.includes("openai") || host.includes("openai.com") || host.includes("api.openai.com")) {
  pushUnique(candidates, "openai", "exact");
}

Azure OpenAI endpoint URLs are formatted as https://{resource-name}.openai.azure.com/.... Since the host myresource.openai.azure.com contains openai.com, Azure providers will be classified as "openai" key candidates and will resolve against the OpenAI pricing node in the model's pricing map.

Azure OpenAI pricing may differ from OpenAI direct pricing (e.g. for GPT-5.4, Azure may have different rates). If a model record contains both "openai" and "azure" (or "azure_openai") pricing nodes, Azure-hosted deployments would incorrectly resolve against the OpenAI node rather than the Azure-specific node.

Consider adding an explicit Azure check before the generic OpenAI check:

if (host.includes("openai.azure.com") || name.includes("azure")) {
  pushUnique(candidates, "azure_openai", "exact");
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/utils/pricing-resolution.ts
Line: 125-170

Comment:
**Azure OpenAI misidentified as direct OpenAI for pricing**

`resolvePricingKeyCandidates` uses `host.includes("openai.com")` to identify OpenAI providers:

```ts
if (name.includes("openai") || host.includes("openai.com") || host.includes("api.openai.com")) {
  pushUnique(candidates, "openai", "exact");
}
```

Azure OpenAI endpoint URLs are formatted as `https://{resource-name}.openai.azure.com/...`. Since the host `myresource.openai.azure.com` contains `openai.com`, Azure providers will be classified as `"openai"` key candidates and will resolve against the OpenAI pricing node in the model's pricing map.

Azure OpenAI pricing may differ from OpenAI direct pricing (e.g. for GPT-5.4, Azure may have different rates). If a model record contains both `"openai"` and `"azure"` (or `"azure_openai"`) pricing nodes, Azure-hosted deployments would incorrectly resolve against the OpenAI node rather than the Azure-specific node.

Consider adding an explicit Azure check before the generic OpenAI check:
```ts
if (host.includes("openai.azure.com") || name.includes("azure")) {
  pushUnique(candidates, "azure_openai", "exact");
}
```

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/lib/utils/special-settings.ts (1)

180-185: 共享常量不要在兼容回退里写死。

src/lib/special-attributes/index.ts 已经定义了 CONTEXT_1M_BETA_HEADERsrc/app/v1/_lib/proxy/forwarder.ts 也在用同一个常量。这里单独写 "context-1m-2025-08-07",后续 flag 轮换时很容易只改一处,结果把回退审计写成过期值。

可选修正
+import { CONTEXT_1M_BETA_HEADER } from "@/lib/special-attributes";
 import type { SpecialSetting } from "@/types/special-settings";
@@
       type: "anthropic_context_1m_header_override",
       scope: "request_header",
       hit: true,
       header: "anthropic-beta",
-      flag: "context-1m-2025-08-07",
+      flag: CONTEXT_1M_BETA_HEADER,
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/special-settings.ts` around lines 180 - 185, The fallback uses
a hardcoded flag string ("context-1m-2025-08-07"); replace this literal with the
shared constant CONTEXT_1M_BETA_HEADER (the constant defined in
src/lib/special-attributes/index.ts and used by forwarder.ts) by importing
CONTEXT_1M_BETA_HEADER into special-settings.ts and using it for the
derived.flag (and header if appropriate) so future rotations only require
updating the single shared constant.
tests/unit/lib/utils/special-settings.test.ts (1)

196-236: 补一条 pricing_resolution 命中优先级的回归测试。

当前新增用例只覆盖了 codex_service_tier_result,没有覆盖 getPricingResolutionSpecialSetting()hit: falsehit: true 并存时应优先返回命中记录的场景。这个分支一旦回归,日志页展示的 provider/source 会直接错而测试拦不住。

As per coding guidelines, **/*.test.{ts,tsx,js,jsx}: All new features must have unit test coverage of at least 80%

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

In `@tests/unit/lib/utils/special-settings.test.ts` around lines 196 - 236, Add a
regression unit test that verifies getPricingResolutionSpecialSetting() prefers
and returns the entry with hit: true when both hit: false and hit: true
pricing_resolution records are present; create a new test case (in the same
suite as hasPriorityServiceTierSpecialSetting tests) that passes an array
containing two pricing_resolution special-setting objects (one with hit: false
and one with hit: true, differing provider/source metadata) and assert that
getPricingResolutionSpecialSetting(...) returns the hit:true record (or the
expected provider/source) to cover the hit-preference branch.
🤖 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/lib/utils/special-settings.ts`:
- Around line 25-27: 注释与实现不一致:变量 context1mApplied 的注释说“不再自动派生”,但代码仍在兼容路径回退生成
anthropic_context_1m_header_override;请在 special-settings.ts 中二选一:要保留兼容回退则更新
context1mApplied 的注释以明确说明“保留用于兼容,会在必要时回退生成
anthropic_context_1m_header_override”;要移除回退则删除/禁用生成
anthropic_context_1m_header_override 的兼容分支(并清理相关测试/调用点);在修改时参考 context1mApplied
和 anthropic_context_1m_header_override 的使用处以确保行为与注释一致。
- Around line 248-260: getPricingResolutionSpecialSetting currently returns the
first pricing_resolution entry which can expose a miss to the UI when a later
hit exists; change getPricingResolutionSpecialSetting to search specialSettings
for an item with type === "pricing_resolution" and hit === true first (return
that), and only if none found return the first pricing_resolution entry (or null
if none); reference the specialSettings parameter, the type:
"pricing_resolution" discriminator and the hit boolean so the function prefers
hit:true over hit:false to keep buildSettingKey()/UI logs correct.

---

Nitpick comments:
In `@src/lib/utils/special-settings.ts`:
- Around line 180-185: The fallback uses a hardcoded flag string
("context-1m-2025-08-07"); replace this literal with the shared constant
CONTEXT_1M_BETA_HEADER (the constant defined in
src/lib/special-attributes/index.ts and used by forwarder.ts) by importing
CONTEXT_1M_BETA_HEADER into special-settings.ts and using it for the
derived.flag (and header if appropriate) so future rotations only require
updating the single shared constant.

In `@tests/unit/lib/utils/special-settings.test.ts`:
- Around line 196-236: Add a regression unit test that verifies
getPricingResolutionSpecialSetting() prefers and returns the entry with hit:
true when both hit: false and hit: true pricing_resolution records are present;
create a new test case (in the same suite as
hasPriorityServiceTierSpecialSetting tests) that passes an array containing two
pricing_resolution special-setting objects (one with hit: false and one with
hit: true, differing provider/source metadata) and assert that
getPricingResolutionSpecialSetting(...) returns the hit:true record (or the
expected provider/source) to cover the hit-preference branch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 291a5224-ba67-4759-a094-4bfd507130e1

📥 Commits

Reviewing files that changed from the base of the PR and between b1c027d and cffef3b.

📒 Files selected for processing (2)
  • src/lib/utils/special-settings.ts
  • tests/unit/lib/utils/special-settings.test.ts

Comment on lines +25 to 27
* 1M 上下文是否应用(保留参数用于兼容调用方;不再自动派生 header 覆写审计
*/
context1mApplied?: boolean | null;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

参数注释与实际行为不一致。

这里写的是“不再自动派生”,但 Line 175-185 仍会在兼容场景下回退生成 anthropic_context_1m_header_override。这会误导后续维护者,并让这段兼容逻辑看起来像死代码。

可选修正
-   * 1M 上下文是否应用(保留参数用于兼容调用方;不再自动派生 header 覆写审计)
+   * 1M 上下文是否应用(保留参数用于兼容调用方;当缺少显式记录且不存在 codex service tier result 时,回退派生 header 覆写审计)
📝 Committable suggestion

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

Suggested change
* 1M 上下文是否应用(保留参数用于兼容调用方;不再自动派生 header 覆写审计)
*/
context1mApplied?: boolean | null;
* 1M 上下文是否应用(保留参数用于兼容调用方;当缺少显式记录且不存在 codex service tier result 时,回退派生 header 覆写审计)
*/
context1mApplied?: boolean | null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/special-settings.ts` around lines 25 - 27, 注释与实现不一致:变量
context1mApplied 的注释说“不再自动派生”,但代码仍在兼容路径回退生成
anthropic_context_1m_header_override;请在 special-settings.ts 中二选一:要保留兼容回退则更新
context1mApplied 的注释以明确说明“保留用于兼容,会在必要时回退生成
anthropic_context_1m_header_override”;要移除回退则删除/禁用生成
anthropic_context_1m_header_override 的兼容分支(并清理相关测试/调用点);在修改时参考 context1mApplied
和 anthropic_context_1m_header_override 的使用处以确保行为与注释一致。

Comment on lines +248 to +260
export function getPricingResolutionSpecialSetting(
specialSettings?: SpecialSetting[] | null
): Extract<SpecialSetting, { type: "pricing_resolution" }> | null {
if (!Array.isArray(specialSettings) || specialSettings.length === 0) {
return null;
}

return (
specialSettings.find(
(setting): setting is Extract<SpecialSetting, { type: "pricing_resolution" }> =>
setting.type === "pricing_resolution"
) ?? null
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

这里会把 pricing_resolution 的 miss 记录错误地暴露给 UI。

buildSettingKey() 现在允许 hit: falsehit: true 并存,但这里直接拿第一个 pricing_resolution。如果数组顺序是 miss 在前、hit 在后,日志详情/汇总展示的 provider/source 就会错。

建议修正
 export function getPricingResolutionSpecialSetting(
   specialSettings?: SpecialSetting[] | null
 ): Extract<SpecialSetting, { type: "pricing_resolution" }> | null {
   if (!Array.isArray(specialSettings) || specialSettings.length === 0) {
     return null;
   }
 
   return (
     specialSettings.find(
+      (setting): setting is Extract<SpecialSetting, { type: "pricing_resolution" }> =>
+        setting.type === "pricing_resolution" && setting.hit
+    ) ??
+    specialSettings.find(
       (setting): setting is Extract<SpecialSetting, { type: "pricing_resolution" }> =>
         setting.type === "pricing_resolution"
     ) ?? null
   );
 }
📝 Committable suggestion

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

Suggested change
export function getPricingResolutionSpecialSetting(
specialSettings?: SpecialSetting[] | null
): Extract<SpecialSetting, { type: "pricing_resolution" }> | null {
if (!Array.isArray(specialSettings) || specialSettings.length === 0) {
return null;
}
return (
specialSettings.find(
(setting): setting is Extract<SpecialSetting, { type: "pricing_resolution" }> =>
setting.type === "pricing_resolution"
) ?? null
);
export function getPricingResolutionSpecialSetting(
specialSettings?: SpecialSetting[] | null
): Extract<SpecialSetting, { type: "pricing_resolution" }> | null {
if (!Array.isArray(specialSettings) || specialSettings.length === 0) {
return null;
}
return (
specialSettings.find(
(setting): setting is Extract<SpecialSetting, { type: "pricing_resolution" }> =>
setting.type === "pricing_resolution" && setting.hit
) ??
specialSettings.find(
(setting): setting is Extract<SpecialSetting, { type: "pricing_resolution" }> =>
setting.type === "pricing_resolution"
) ?? null
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/special-settings.ts` around lines 248 - 260,
getPricingResolutionSpecialSetting currently returns the first
pricing_resolution entry which can expose a miss to the UI when a later hit
exists; change getPricingResolutionSpecialSetting to search specialSettings for
an item with type === "pricing_resolution" and hit === true first (return that),
and only if none found return the first pricing_resolution entry (or null if
none); reference the specialSettings parameter, the type: "pricing_resolution"
discriminator and the hit boolean so the function prefers hit:true over
hit:false to keep buildSettingKey()/UI logs correct.

@github-actions github-actions bot mentioned this pull request Mar 6, 2026
11 tasks
miraserver pushed a commit to miraserver/claude-code-hub that referenced this pull request Mar 7, 2026
All caused by commit 2e663cd which changed billing model and session
API without updating tests/translations:

- i18n: add missing `prices.badges.multi` key to ja/ru/zh-TW locales
- tests: update cost-calculation expectations to match full-request
  pricing (all tokens at premium rate when context > 200K threshold)
- tests: fix lease-decrement session mock to use
  getResolvedPricingByBillingSource instead of removed method

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ding113 pushed a commit that referenced this pull request Mar 8, 2026
…#876)

* feat(providers): add sub-navigation and Options tab to provider form sidebar

Add Scheduling, Circuit Breaker, Timeout sub-items under their parent tabs
in the desktop sidebar for quick scroll access. Promote Options to a
top-level tab. Includes scroll tracking, i18n (5 langs), and 13 tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(providers): add Active Time sub-item under Options tab

Add activeTime sub-navigation for quick scroll access to Scheduled Active
Time section. Also add variant="highlight" to Options SectionCard for
consistent visual styling with other main sections.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(providers): show green ShieldCheck icon when proxy is configured

Separate icon with tooltip next to endpoint count on provider list.
Visible only when proxyUrl is set. i18n for 5 languages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(providers): fix 7 review issues in sub-nav and form architecture

- Derive TAB_CONFIG, TAB_ORDER, NAV_ORDER, PARENT_MAP from NAV_CONFIG (DRY)
- Add sub-nav to tablet and mobile breakpoints
- Move activeSubTab from useState into form reducer
- Compute Options tab status from routing state instead of hardcoding
- Lift TooltipProvider from per-item to list container level
- Fix RU i18n: singular form for timeout label
- Add 8 new tests covering derived constants and responsive sub-nav

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(providers): extract OptionsSection, perf fixes, batch dialog fix

- Extract OptionsSection from RoutingSection (963 -> 432 lines)
- Throttle scroll handler via requestAnimationFrame
- Merge double dispatch into single SET_ACTIVE_NAV action
- Derive sectionRefs from NAV_ORDER instead of manual record
- Add NAV_BY_ID lookup map for O(1) tablet/mobile nav access
- Add excludeTabs prop to FormTabNav, hide Options in batch dialog
- Clean up setTimeout/rAF refs on unmount

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(providers): add Options tab to batch edit dialog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(providers): show proxy badge for providers without resolved vendor

Move ShieldCheck proxy indicator out of the vendor-specific branch
so it renders for all providers with a configured proxyUrl.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(providers): remove dead SET_ACTIVE_SUB_TAB, add status grouping comments

Address Gemini Code Assist review findings:
- Remove unused SET_ACTIVE_SUB_TAB action type and reducer case (superseded by SET_ACTIVE_NAV)
- Add grouping comments to options tab status conditional for readability

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(providers): address CodeRabbit review findings

- Fix zh-CN/zh-TW i18n terminology consistency (供应商/供應商 instead of 提供者)
- Add activeTimeEnd check to options tab status indicator
- Add focus-visible ring to tablet/mobile sub-nav buttons for accessibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(providers): use i18n fallback for unknown tag errors and scrollend event for scroll lock

- Replace raw reason string fallback with tUI("unknownError") i18n key
  in tag validation callbacks (routing-section.tsx)
- Add "unknownError" key to tagInput namespace in all 5 locales
- Use scrollend event with { once: true } + 1000ms fallback timer
  instead of fixed 500ms setTimeout for scroll lock release (index.tsx)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(providers): add unit tests for OptionsSection component (25 tests)

Covers rendering, conditional display by provider type (claude/codex/gemini),
batch mode, dispatch actions, active time UI, disabled state, edit mode IDs,
and batch-only badges.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(providers): remove stale scrollend listener on rapid tab clicks

Store previous scrollend listener in a ref and remove it at the start
of each scrollToSection call, preventing premature unlock when multiple
smooth scrolls overlap during fast sequential tab clicks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(providers): fix 3 style-level review findings

- Remove dead subSectionRefs.options property from OptionsSection
  (parent div in index.tsx already tracks this ref)
- Use filteredNav.find() instead of NAV_BY_ID for tablet/mobile
  sub-row lookup so excludeTabs is respected; remove unused NAV_BY_ID
- Replace non-null assertion with guarded clearTimeout in scrollend
  handler

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ci): resolve 3 pre-existing test failures from PR #873

All caused by commit 2e663cd which changed billing model and session
API without updating tests/translations:

- i18n: add missing `prices.badges.multi` key to ja/ru/zh-TW locales
- tests: update cost-calculation expectations to match full-request
  pricing (all tokens at premium rate when context > 200K threshold)
- tests: fix lease-decrement session mock to use
  getResolvedPricingByBillingSource instead of removed method

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(types): narrow Recharts v3 dataKey type for React Key/ReactNode compat

Recharts v3 widened `dataKey` to `string | number | ((obj: any) => any) | undefined`,
which is incompatible with React `Key` and `ReactNode`. Wrap with `String()` in 2 files
to satisfy tsgo in CI. Pre-existing issue from main, not introduced by this branch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: format code (feat-providers-list-3-0732ba6)

* ci: retrigger CI after auto-format fix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(types): narrow Recharts ValueType for formatter call in chart.tsx

Removing the explicit .map() parameter type exposed ValueType
(string | number | readonly (string|number)[]) which is too wide
for the formatter's (string | number) parameter. Cast item.value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(types): restore narrowing annotation for Recharts v3 tooltip map

Bring back the explicit type annotation on .map() callback but extend
dataKey to include ((obj: any) => any) to match Recharts v3 DataKey<any>.
This keeps value/name narrowed for formatter compatibility while making
the annotation assignable from TooltipPayloadEntry.

Replaces the previous approach of removing the annotation entirely,
which exposed ValueType and NameType width issues one by one.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(types): use inline casts instead of annotation for Recharts v3 compat

Remove the .map() parameter annotation entirely (it causes TS2345 due to
contravariance — our narrower type is not assignable from TooltipPayloadEntry).
Instead, let TS infer the full type and cast only at the two call sites where
formatter needs narrower types: item.value as string|number, item.name as string.

All other usages of item.dataKey, item.name, item.value are compatible with
the wider Recharts v3 types (String() wraps, template literals, ReactNode).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: John Doe <johndoe@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
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 enhancement New feature or request size/XL Extra Large PR (> 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant