fix: support model-level priority service tier pricing#1117
Conversation
📝 WalkthroughWalkthrough将服务层级定价( Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Code Review Summary
This PR implements model-level priority service tier pricing support with a clean fallback chain architecture. The changes add a new service_tier_pricing structure that takes precedence over legacy *_priority fields, enabling more flexible pricing configurations per model.
PR Size: M
- Lines changed: 557 (494 additions, 63 deletions)
- Files changed: 11 (5 source files, 6 test files)
- Assessment: Well-scoped feature addition with comprehensive test coverage. No splitting needed.
Issues Found
| Category | Critical | High | Medium | Low |
|---|---|---|---|---|
| Logic/Bugs | 0 | 0 | 0 | 0 |
| Security | 0 | 0 | 0 | 0 |
| Error Handling | 0 | 0 | 0 | 0 |
| Types | 0 | 0 | 0 | 0 |
| Comments/Docs | 0 | 0 | 0 | 0 |
| Tests | 0 | 0 | 0 | 0 |
| Simplification | 0 | 0 | 0 | 0 |
Key Implementation Details Verified
-
Pricing Resolution Chain (src/lib/utils/cost-calculation.ts:125-136)
- Correct fallback order:
serviceTierRate ?? legacyPriorityRate ?? baseRate - Properly handles
service_tier_pricing.priority.long_context_pricingoverride
- Correct fallback order:
-
Field Classification (src/lib/utils/model-price-fields.ts:457-484)
- New
SUPPORTED_SERVICE_TIER_KEYScorrectly includes billing keys + nestedlong_context_pricing isCoreFieldupdated to recognizeservice_tier_pricing.*paths
- New
-
Detail Score Calculation (src/lib/utils/pricing-resolution.ts:497)
service_tier_pricingadded toDETAIL_SCORE_OBJECT_FIELDSfor proper provider selection
-
Test Coverage
- Priority tier precedence: 6 new unit tests covering service tier vs legacy priority fields
- Long-context priority matching: 1 new test for nested
service_tier_pricing.priority.long_context_pricing - Field classification: 1 new test verifying
service_tier_pricing.priority.*as "supported" - Integration: 1 new test for Codex fast path with service tier pricing
Review Coverage
- Logic and correctness - Clean, proper fallback chains implemented
- Security (OWASP Top 10) - Clean, no injection risks or data exposure
- Error handling - Proper null/undefined checks with type guards
- Type safety - New
ServiceTierPricinginterface properly defined with index signature - Documentation accuracy - Comments match implementation behavior
- Test coverage - 9 new tests across unit and integration layers
- Code clarity - Functions are focused, names are descriptive
CLAUDE.md Compliance
- No emoji in code (Rule 1)
- Test coverage exceeds 80% threshold (Rule 2)
- No user-facing strings requiring i18n (internal pricing logic only) (Rule 3)
Automated review by Claude AI
There was a problem hiding this comment.
Code Review
This pull request introduces a new service_tier_pricing structure to handle model-specific pricing for different service tiers, specifically focusing on the 'priority' tier. The cost calculation logic in cost-calculation.ts has been updated to prioritize these new tier-specific rates over legacy priority fields for input, output, cache, and long-context costs. Additionally, the proxy response handler and pricing resolution utilities were updated to support this new data structure, supported by comprehensive integration and unit tests. I have no feedback to provide as there were no review comments to assess.
| const SUPPORTED_SERVICE_TIER_KEYS = new Set([ | ||
| ...SUPPORTED_TOP_LEVEL_BILLING_KEYS, | ||
| "long_context_pricing", | ||
| ]); |
There was a problem hiding this comment.
Nested
long_context_pricing multiplier fields misclassified as "unsupported"
SUPPORTED_SERVICE_TIER_KEYS spreads SUPPORTED_TOP_LEVEL_BILLING_KEYS but not SUPPORTED_LONG_CONTEXT_KEYS. As a result, when pushEntries recurses into service_tier_pricing.priority.long_context_pricing.*, keys like input_multiplier, output_multiplier, cache_creation_input_multiplier, and cache_read_input_multiplier are not in the set. Because those keys also match isPriceLikeFieldPath, classifyField returns "unsupported" for them rather than "supported". Keys that happen to be in both sets (e.g., input_cost_per_token) are classified correctly, so only the multiplier-style keys are affected.
| const SUPPORTED_SERVICE_TIER_KEYS = new Set([ | |
| ...SUPPORTED_TOP_LEVEL_BILLING_KEYS, | |
| "long_context_pricing", | |
| ]); | |
| const SUPPORTED_SERVICE_TIER_KEYS = new Set([ | |
| ...SUPPORTED_TOP_LEVEL_BILLING_KEYS, | |
| ...SUPPORTED_LONG_CONTEXT_KEYS, | |
| "long_context_pricing", | |
| ]); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/utils/model-price-fields.ts
Line: 62-65
Comment:
**Nested `long_context_pricing` multiplier fields misclassified as `"unsupported"`**
`SUPPORTED_SERVICE_TIER_KEYS` spreads `SUPPORTED_TOP_LEVEL_BILLING_KEYS` but not `SUPPORTED_LONG_CONTEXT_KEYS`. As a result, when `pushEntries` recurses into `service_tier_pricing.priority.long_context_pricing.*`, keys like `input_multiplier`, `output_multiplier`, `cache_creation_input_multiplier`, and `cache_read_input_multiplier` are not in the set. Because those keys also match `isPriceLikeFieldPath`, `classifyField` returns `"unsupported"` for them rather than `"supported"`. Keys that happen to be in both sets (e.g., `input_cost_per_token`) are classified correctly, so only the multiplier-style keys are affected.
```suggestion
const SUPPORTED_SERVICE_TIER_KEYS = new Set([
...SUPPORTED_TOP_LEVEL_BILLING_KEYS,
...SUPPORTED_LONG_CONTEXT_KEYS,
"long_context_pricing",
]);
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fa3531da8a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| priorityTierPricing?.cache_creation_input_token_cost_above_272k_tokens ?? | ||
| priorityTierPricing?.cache_creation_input_token_cost_above_200k_tokens ?? | ||
| priceData.cache_creation_input_token_cost_above_272k_tokens ?? |
There was a problem hiding this comment.
Apply tier cache threshold rates when tier base pricing exists
These tier-specific cache_creation_*_above_* rates are collected, but the same function later gates threshold cache billing on hasRealCacheCreationBase/hasRealCacheReadBase, which still only checks top-level priceData.cache_* fields. When a model defines cache base + above-threshold pricing only under service_tier_pricing.priority, priority requests over the threshold are billed with non-threshold cache rates instead of the configured tier threshold rates.
Useful? React with 👍 / 👎.
| ): ResolvedLongContextPricing | null { | ||
| const pricing = priceData.long_context_pricing; | ||
| const serviceTierPricing = getServiceTierPricing(priceData, serviceTier); | ||
| const pricing = serviceTierPricing?.long_context_pricing ?? priceData.long_context_pricing; |
There was a problem hiding this comment.
Fall back to top-level long-context config when tier is partial
Selecting serviceTierPricing?.long_context_pricing unconditionally means a partial tier override (for example, overriding multipliers but not threshold_tokens) suppresses the valid top-level long_context_pricing: normalizeThresholdTokens fails and the function returns null, so priority requests stop matching long-context pricing. This should merge/fallback required fields from top-level long-context settings.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/lib/utils/cost-calculation.ts (1)
176-224:⚠️ Potential issue | 🟡 Minor增加测试覆盖缺失的长上下文定价服务等级回退场景
代码确实在 Line 181 实现了回退逻辑:当
service_tier_pricing.priority缺少long_context_pricing时,会回退到顶层priceData.long_context_pricing。同时,Lines 192-224 计算的baseInputCost/baseOutputCost/baseCacheReadCost已是 tier 感知的(优先级价格)。这意味着优先级服务等级的基础单价会与顶层长上下文乘数组合。类型定义表明
ServiceTierPricing.long_context_pricing是可选的(可选字段 +??运算符),说明这个回退设计看似刻意为之。但当前测试套件中缺少明确的测试用例来文档化这个场景:配置service_tier_pricing.priority包含基础价格但不包含long_context_pricing,同时顶层有long_context_pricing。建议:增加一个测试用例,明确验证当优先级 tier 缺少
long_context_pricing时,长上下文请求应使用 tier 基础价 × 顶层乘数,以确保这个行为符合预期。suggested test
test("uses top-level long-context pricing with priority base rates when tier lacks long_context_pricing", () => { const usage = { input_tokens: 101, output_tokens: 2 }; const priceData = makePriceData({ service_tier_pricing: { priority: { input_cost_per_token: 4, output_cost_per_token: 40, // 注意:priority tier 未定义 long_context_pricing }, }, long_context_pricing: { threshold_tokens: 100, input_multiplier: 2, output_multiplier: 2, }, }); const cost = calculateRequestCost(usage, priceData, { priorityServiceTierApplied: true, }); // 应验证使用了优先级基础价 (4, 40) × 顶层乘数 (2, 2) expect(Number(cost.toString())).toBe(172); // 4*2*101 + 40*2*2 });🤖 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 176 - 224, Add a unit test that verifies resolveLongContextPricing (and ultimately calculateRequestCost) falls back to the top-level long_context_pricing when ServiceTierPricing (e.g., service_tier_pricing.priority) lacks long_context_pricing, and that the resolved long-context costs use the tier-aware base rates (from priority input_cost_per_token/output_cost_per_token via resolvePriorityAwareRate/getServiceTierPricing) multiplied by the top-level multipliers; create a test (similar to the suggested "uses top-level long-context pricing with priority base rates when tier lacks long_context_pricing") using makePriceData with priority base rates present but no long_context_pricing and top-level long_context_pricing present, then assert the computed cost from calculateRequestCost matches the expected baseRate * multiplier math.
🧹 Nitpick comments (6)
src/lib/utils/model-price-fields.ts (1)
62-66:SUPPORTED_SERVICE_TIER_KEYS中的"long_context_pricing"实际不可达,且嵌套乘数字段无法被分类为 supported。
pushEntries在遇到对象值时直接递归而不 push 对应 entry,因此classifyField永远不会以key === "long_context_pricing"被调用,集合中的该字符串等同于死代码。同时,递归进入service_tier_pricing.<tier>.long_context_pricing.*后,键名会变为input_multiplier/output_multiplier等,但SUPPORTED_SERVICE_TIER_KEYS仅扩展自SUPPORTED_TOP_LEVEL_BILLING_KEYS(不含这些 multiplier 键),导致这些嵌套字段会被降级为unsupported。非阻塞,仅影响字段展示分类。如果后续允许 tier 内嵌长上下文乘数,建议把
SUPPORTED_LONG_CONTEXT_KEYS也合并进来或在classifyField中显式处理service_tier_pricing.*.long_context_pricing.*路径;否则可移除"long_context_pricing"以减少误导。♻️ 可选改法
const SUPPORTED_SERVICE_TIER_KEYS = new Set([ ...SUPPORTED_TOP_LEVEL_BILLING_KEYS, - "long_context_pricing", + ...SUPPORTED_LONG_CONTEXT_KEYS, ]);Also applies to: 129-143
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/utils/model-price-fields.ts` around lines 62 - 66, SUPPORTED_SERVICE_TIER_KEYS currently includes "long_context_pricing" which is never reached because pushEntries recurses on object values without pushing an entry and classifyField never sees a top-level key === "long_context_pricing"; additionally nested multiplier keys (input_multiplier/output_multiplier) aren’t in the set so they fall back to unsupported. Fix by either removing the misleading "long_context_pricing" from SUPPORTED_SERVICE_TIER_KEYS, or merge SUPPORTED_LONG_CONTEXT_KEYS into SUPPORTED_SERVICE_TIER_KEYS so nested long_context fields are recognized, or alter classifyField to explicitly treat the path service_tier_pricing.*.long_context_pricing.* as supported (also apply the same change where the duplicated logic appears around the other block at the 129-143 area); update pushEntries/classifyField accordingly so nested multiplier keys are classified correctly.src/lib/utils/cost-calculation.ts (2)
43-43:ServiceTierName当前是字面量类型,后续扩展时记得同步 fallback 链。
type ServiceTierName = "priority"目前与service_tier_pricingschema 中已定义的 tier 一一对应,可读性好。但getServiceTierPricing和所有调用站点都基于这个字面量做控制流(例如 Line 191serviceTier === "priority"直接转成priorityServiceTierApplied布尔)。后续若 schema 新增非 priority tier(如
"flex"/"auto"),需要同时:
- 扩展该 union;
- 重新审视
priorityServiceTierApplied这个二值化布尔的语义(它隐式假设了"非 priority 即非分层");- 确认
resolvePriorityAwareRate的 legacy*_priority字段仅对 priority tier 生效。当前 PR 不需要立刻处理,标记一下避免后续扩展时漏改。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/utils/cost-calculation.ts` at line 43, ServiceTierName is currently a string literal type ("priority") which ties control flow in getServiceTierPricing and call sites (e.g. the priorityServiceTierApplied boolean and resolvePriorityAwareRate) to a single-tier assumption; update ServiceTierName to a union of all possible tiers when you add new tiers, audit and replace any binary checks like serviceTier === "priority" or priorityServiceTierApplied with explicit tier-aware logic, and ensure resolvePriorityAwareRate and legacy *_priority fields are only applied for the true priority tier; in short, whenever adding a new tier, extend the ServiceTierName union, revisit uses of priorityServiceTierApplied, and make resolvePriorityAwareRate explicitly conditional on the priority tier rather than implicitly assuming any non-priority is "non-tiered".
459-515:calculateRequestCostBreakdown与calculateRequestCost之间的 tier 解析逻辑大量重复,建议后续抽取共享 helper。Line 459-515(breakdown)与 Line 749-804(总价)中关于
inputCostPerToken/outputCostPerToken/inputCostPerRequest/cacheCreation5mCost/cacheCreation1hCost/cacheReadCost的解析几乎逐行一致。本 PR 把每条字段都增补了priorityTierPricing?.* ??前缀,意味着将来 schema 再增字段时这两处必须同步修改 —— 维护风险点。建议(可延后)抽出一个
resolveBaseRates(priceData, options)返回完整的{inputCostPerToken, outputCostPerToken, inputCostPerRequest, cacheCreation5mCost, cacheCreation1hCost, cacheReadCost},两个函数共用。本 PR 内若不便重构,至少加一行注释提示两处需联动维护。🤖 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 459 - 515, The logic that computes inputCostPerToken, outputCostPerToken, inputCostPerRequest, cacheCreation5mCost, cacheCreation1hCost and cacheReadCost is duplicated between calculateRequestCostBreakdown and calculateRequestCost; extract a shared helper (e.g., resolveBaseRates(priceData, options)) that calls getPriorityServiceTierPricing and resolvePriorityAwareRate and returns { inputCostPerToken, outputCostPerToken, inputCostPerRequest, cacheCreation5mCost, cacheCreation1hCost, cacheReadCost } preserving the exact fallback rules (including cacheCreation1hCost falling back to cacheCreation5mCost) and then replace the duplicated blocks in calculateRequestCostBreakdown and calculateRequestCost with calls to resolveBaseRates; if you cannot refactor now, add a clear TODO comment above both duplicated blocks referencing resolveBaseRates(priceData, options) so future schema changes are kept in sync.tests/unit/lib/cost-calculation-priority.test.ts (2)
192-218:long_context_pricing在 service tier 下的匹配用例覆盖了关键路径。
input_tokens=101触发threshold_tokens=100后,基于 tier 内input_cost_per_token=4、output_cost_per_token=40与 multiplier=2,得到101*8 + 2*80 = 968,与 Line 217 断言一致。同时通过expect(match).not.toBeNull()(Line 216)显式锁定了 tier-aware 的matchLongContextPricing行为,这点很好。可选补充:再加一个对照用例,断言相同
priceData在serviceTier传null时matchLongContextPricing返回null(因为顶层priceData.long_context_pricing未配置),从而锁死"tier 阈值不会泄漏到非 priority 调用"的行为。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/lib/cost-calculation-priority.test.ts` around lines 192 - 218, Add a negative test that verifies tier-specific long_context_pricing does not leak when no service tier is provided: using the same makePriceData(...) with service_tier_pricing.priority.long_context_pricing set, call matchLongContextPricing(usage, priceData, null) (or pass null for the serviceTier argument) and assert the result is null; this ensures matchLongContextPricing only returns the tier pricing when a matching serviceTier (e.g., "priority") is supplied.
19-90: 新增 3 个 priority service tier 用例覆盖到位。期望值经核对均正确:
- Line 36
97.5 = 2*3 + 3*30 + 5*0.3(tier 覆盖 legacy)- Line 56
32.5 = 2*1 + 3*10 + 5*0.1(非 priority 时退回 base,即使 tier 配置存在)- Line 88-89 两组独立 priceData 的单价分别加和
可选改进:Line 59 用例标题为 "allows different models to define different priority tier prices",但实测代码并未参数化模型名,只是用两组不同
priceData各跑一次。建议改名为类似"applies provided priority pricing per priceData snapshot",避免读者误以为还覆盖了 model 维度的解析逻辑。另外建议补一个用例:
service_tier_pricing.priority存在但只覆盖input_cost_per_token,确认output/cache_read仍能回退到*_prioritylegacy 字段(当前 "before legacy" 用例完全替换了三个字段,未覆盖部分覆盖场景)。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/lib/cost-calculation-priority.test.ts` around lines 19 - 90, Rename the test titled "allows different models to define different priority tier prices" to "applies provided priority pricing per priceData snapshot" to avoid implying model-specific behavior, and add one new unit test for calculateRequestCost that uses makePriceData where service_tier_pricing.priority only sets input_cost_per_token (leave output and cache_read undefined) to assert that input uses the priority price while output_tokens and cache_read_input_tokens fall back to the legacy *_priority fields; reference calculateRequestCost and makePriceData in the new test and mirror the existing usage pattern (priorityServiceTierApplied true) to validate partial overrides.src/app/v1/_lib/proxy/response-handler.ts (1)
1260-1264: 多处调用matchLongContextPricing均一致传入了 priority service tier,看起来不错。四处调用站点(Line 1260-1264、2379-2383、3475-3479、3794-3798)均按
priorityServiceTierApplied ? "priority" : null派发,与cost-calculation.ts中新签名对齐。当未启用 priority tier 时行为保持向后兼容。可选小重构:这段三元表达式在四个地方完全重复,后续若新增更多 service tier(例如
"flex")时可考虑提取一个本地 helper(例如resolveServiceTierName(decision)),以便集中演进。当前规模下并非必须。🤖 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 1260 - 1264, The ternary expression priorityServiceTierApplied ? "priority" : null is duplicated across multiple calls to matchLongContextPricing; extract a small helper (e.g., resolveServiceTierName(decision) or resolveServiceTierName({priorityServiceTierApplied})) that centralizes this logic and returns the appropriate tier string (e.g., "priority") or null, then replace the inline ternaries at each call site (the calls to matchLongContextPricing) with a call to that helper so future new tiers (like "flex") can be added in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tests/unit/lib/utils/pricing-resolution.test.ts`:
- Around line 229-273: The test currently uses a provider with name "OpenAI" and
URL "https://api.openai.com/v1/responses" which matches pricingMap exact keys so
resolveFromPricingMap returns immediately and never exercises
resolveDetailedFallback or the DETAIL_SCORE_OBJECT_FIELDS changes; update the
test (in pricing-resolution.test.ts) to use a provider that does not match any
exact/official keys (e.g., name "AnonymousProvider" or "Unknown" and a
non-openai URL like "https://api.example.com") so resolvePricingKeyCandidates
will not produce an "exact" hit, thereby forcing resolvePricingForModelRecords
to call resolveDetailedFallback; also change the expected source in the
assertion from the default to "priority_fallback" to confirm the detailed
scoring chose the openai node with service_tier_pricing.
---
Outside diff comments:
In `@src/lib/utils/cost-calculation.ts`:
- Around line 176-224: Add a unit test that verifies resolveLongContextPricing
(and ultimately calculateRequestCost) falls back to the top-level
long_context_pricing when ServiceTierPricing (e.g.,
service_tier_pricing.priority) lacks long_context_pricing, and that the resolved
long-context costs use the tier-aware base rates (from priority
input_cost_per_token/output_cost_per_token via
resolvePriorityAwareRate/getServiceTierPricing) multiplied by the top-level
multipliers; create a test (similar to the suggested "uses top-level
long-context pricing with priority base rates when tier lacks
long_context_pricing") using makePriceData with priority base rates present but
no long_context_pricing and top-level long_context_pricing present, then assert
the computed cost from calculateRequestCost matches the expected baseRate *
multiplier math.
---
Nitpick comments:
In `@src/app/v1/_lib/proxy/response-handler.ts`:
- Around line 1260-1264: The ternary expression priorityServiceTierApplied ?
"priority" : null is duplicated across multiple calls to
matchLongContextPricing; extract a small helper (e.g.,
resolveServiceTierName(decision) or
resolveServiceTierName({priorityServiceTierApplied})) that centralizes this
logic and returns the appropriate tier string (e.g., "priority") or null, then
replace the inline ternaries at each call site (the calls to
matchLongContextPricing) with a call to that helper so future new tiers (like
"flex") can be added in one place.
In `@src/lib/utils/cost-calculation.ts`:
- Line 43: ServiceTierName is currently a string literal type ("priority") which
ties control flow in getServiceTierPricing and call sites (e.g. the
priorityServiceTierApplied boolean and resolvePriorityAwareRate) to a
single-tier assumption; update ServiceTierName to a union of all possible tiers
when you add new tiers, audit and replace any binary checks like serviceTier ===
"priority" or priorityServiceTierApplied with explicit tier-aware logic, and
ensure resolvePriorityAwareRate and legacy *_priority fields are only applied
for the true priority tier; in short, whenever adding a new tier, extend the
ServiceTierName union, revisit uses of priorityServiceTierApplied, and make
resolvePriorityAwareRate explicitly conditional on the priority tier rather than
implicitly assuming any non-priority is "non-tiered".
- Around line 459-515: The logic that computes inputCostPerToken,
outputCostPerToken, inputCostPerRequest, cacheCreation5mCost,
cacheCreation1hCost and cacheReadCost is duplicated between
calculateRequestCostBreakdown and calculateRequestCost; extract a shared helper
(e.g., resolveBaseRates(priceData, options)) that calls
getPriorityServiceTierPricing and resolvePriorityAwareRate and returns {
inputCostPerToken, outputCostPerToken, inputCostPerRequest, cacheCreation5mCost,
cacheCreation1hCost, cacheReadCost } preserving the exact fallback rules
(including cacheCreation1hCost falling back to cacheCreation5mCost) and then
replace the duplicated blocks in calculateRequestCostBreakdown and
calculateRequestCost with calls to resolveBaseRates; if you cannot refactor now,
add a clear TODO comment above both duplicated blocks referencing
resolveBaseRates(priceData, options) so future schema changes are kept in sync.
In `@src/lib/utils/model-price-fields.ts`:
- Around line 62-66: SUPPORTED_SERVICE_TIER_KEYS currently includes
"long_context_pricing" which is never reached because pushEntries recurses on
object values without pushing an entry and classifyField never sees a top-level
key === "long_context_pricing"; additionally nested multiplier keys
(input_multiplier/output_multiplier) aren’t in the set so they fall back to
unsupported. Fix by either removing the misleading "long_context_pricing" from
SUPPORTED_SERVICE_TIER_KEYS, or merge SUPPORTED_LONG_CONTEXT_KEYS into
SUPPORTED_SERVICE_TIER_KEYS so nested long_context fields are recognized, or
alter classifyField to explicitly treat the path
service_tier_pricing.*.long_context_pricing.* as supported (also apply the same
change where the duplicated logic appears around the other block at the 129-143
area); update pushEntries/classifyField accordingly so nested multiplier keys
are classified correctly.
In `@tests/unit/lib/cost-calculation-priority.test.ts`:
- Around line 192-218: Add a negative test that verifies tier-specific
long_context_pricing does not leak when no service tier is provided: using the
same makePriceData(...) with service_tier_pricing.priority.long_context_pricing
set, call matchLongContextPricing(usage, priceData, null) (or pass null for the
serviceTier argument) and assert the result is null; this ensures
matchLongContextPricing only returns the tier pricing when a matching
serviceTier (e.g., "priority") is supplied.
- Around line 19-90: Rename the test titled "allows different models to define
different priority tier prices" to "applies provided priority pricing per
priceData snapshot" to avoid implying model-specific behavior, and add one new
unit test for calculateRequestCost that uses makePriceData where
service_tier_pricing.priority only sets input_cost_per_token (leave output and
cache_read undefined) to assert that input uses the priority price while
output_tokens and cache_read_input_tokens fall back to the legacy *_priority
fields; reference calculateRequestCost and makePriceData in the new test and
mirror the existing usage pattern (priorityServiceTierApplied true) to validate
partial overrides.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 73f5d561-3f19-4739-9458-8531c96a0e56
📒 Files selected for processing (11)
src/app/v1/_lib/proxy/response-handler.tssrc/lib/utils/cost-calculation.tssrc/lib/utils/model-price-fields.tssrc/lib/utils/pricing-resolution.tssrc/types/model-price.tstests/integration/billing-model-source.test.tstests/unit/lib/cost-calculation-breakdown.test.tstests/unit/lib/cost-calculation-priority.test.tstests/unit/lib/model-price-fields.test.tstests/unit/lib/price-data-price-like-fields.test.tstests/unit/lib/utils/pricing-resolution.test.ts
| test("provider detail scoring counts service_tier_pricing", () => { | ||
| const cloudRecord = makeRecord("gpt-5.5", { | ||
| mode: "responses", | ||
| model_family: "gpt", | ||
| pricing: { | ||
| fallback: { | ||
| input_cost_per_token: 0.000005, | ||
| output_cost_per_token: 0.00003, | ||
| }, | ||
| openai: { | ||
| input_cost_per_token: 0.000005, | ||
| output_cost_per_token: 0.00003, | ||
| service_tier_pricing: { | ||
| priority: { | ||
| input_cost_per_token: 0.0000125, | ||
| output_cost_per_token: 0.000075, | ||
| cache_read_input_token_cost: 0.00000125, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| const resolved = resolvePricingForModelRecords({ | ||
| provider: { | ||
| id: 5, | ||
| name: "OpenAI", | ||
| url: "https://api.openai.com/v1/responses", | ||
| } as never, | ||
| primaryModelName: "gpt-5.5", | ||
| fallbackModelName: null, | ||
| primaryRecord: cloudRecord, | ||
| fallbackRecord: null, | ||
| }); | ||
|
|
||
| expect(resolved).not.toBeNull(); | ||
| expect(resolved?.resolvedPricingProviderKey).toBe("openai"); | ||
| expect(resolved?.priceData.service_tier_pricing).toEqual({ | ||
| priority: { | ||
| input_cost_per_token: 0.0000125, | ||
| output_cost_per_token: 0.000075, | ||
| cache_read_input_token_cost: 0.00000125, | ||
| }, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
用例名为 “provider detail scoring counts service_tier_pricing”,但实际并未走到 resolveDetailedFallback 的打分路径。
provider 配置 name: "OpenAI"、url: "https://api.openai.com/v1/responses" 会在 resolvePricingKeyCandidates 中匹配 name.includes("openai") / host.includes("openai.com"),从而把 "openai" 作为 exact 候选键。在 resolvePricingForModelRecords 中,resolveFromPricingMap(..., "exact") 会立刻命中 pricingMap["openai"] 并返回,根本不会进入 resolveDetailedFallback,因此本 PR 在 DETAIL_SCORE_OBJECT_FIELDS 上的改动并未被该用例真正验证——当前断言即便回退到旧的打分逻辑(不计 service_tier_pricing)也会通过。
建议构造一个无法命中任何 exact/official 键的 provider(例如匿名 provider 或与 pricing map 中两个 key 都不匹配的名称/URL),以真正触发打分分支。
🔍 验证用例改造建议
- const resolved = resolvePricingForModelRecords({
- provider: {
- id: 5,
- name: "OpenAI",
- url: "https://api.openai.com/v1/responses",
- } as never,
+ const resolved = resolvePricingForModelRecords({
+ provider: {
+ id: 5,
+ name: "unknown-aggregator",
+ url: "https://example.invalid/api",
+ } as never,
primaryModelName: "gpt-5.5",
fallbackModelName: null,
primaryRecord: cloudRecord,
fallbackRecord: null,
});并将断言中的 source 从默认改为 "priority_fallback",以确认打分路径选中带 service_tier_pricing 的 openai 节点。
📝 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.
| test("provider detail scoring counts service_tier_pricing", () => { | |
| const cloudRecord = makeRecord("gpt-5.5", { | |
| mode: "responses", | |
| model_family: "gpt", | |
| pricing: { | |
| fallback: { | |
| input_cost_per_token: 0.000005, | |
| output_cost_per_token: 0.00003, | |
| }, | |
| openai: { | |
| input_cost_per_token: 0.000005, | |
| output_cost_per_token: 0.00003, | |
| service_tier_pricing: { | |
| priority: { | |
| input_cost_per_token: 0.0000125, | |
| output_cost_per_token: 0.000075, | |
| cache_read_input_token_cost: 0.00000125, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }); | |
| const resolved = resolvePricingForModelRecords({ | |
| provider: { | |
| id: 5, | |
| name: "OpenAI", | |
| url: "https://api.openai.com/v1/responses", | |
| } as never, | |
| primaryModelName: "gpt-5.5", | |
| fallbackModelName: null, | |
| primaryRecord: cloudRecord, | |
| fallbackRecord: null, | |
| }); | |
| expect(resolved).not.toBeNull(); | |
| expect(resolved?.resolvedPricingProviderKey).toBe("openai"); | |
| expect(resolved?.priceData.service_tier_pricing).toEqual({ | |
| priority: { | |
| input_cost_per_token: 0.0000125, | |
| output_cost_per_token: 0.000075, | |
| cache_read_input_token_cost: 0.00000125, | |
| }, | |
| }); | |
| }); | |
| test("provider detail scoring counts service_tier_pricing", () => { | |
| const cloudRecord = makeRecord("gpt-5.5", { | |
| mode: "responses", | |
| model_family: "gpt", | |
| pricing: { | |
| fallback: { | |
| input_cost_per_token: 0.000005, | |
| output_cost_per_token: 0.00003, | |
| }, | |
| openai: { | |
| input_cost_per_token: 0.000005, | |
| output_cost_per_token: 0.00003, | |
| service_tier_pricing: { | |
| priority: { | |
| input_cost_per_token: 0.0000125, | |
| output_cost_per_token: 0.000075, | |
| cache_read_input_token_cost: 0.00000125, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }); | |
| const resolved = resolvePricingForModelRecords({ | |
| provider: { | |
| id: 5, | |
| name: "unknown-aggregator", | |
| url: "https://example.invalid/api", | |
| } as never, | |
| primaryModelName: "gpt-5.5", | |
| fallbackModelName: null, | |
| primaryRecord: cloudRecord, | |
| fallbackRecord: null, | |
| }); | |
| expect(resolved).not.toBeNull(); | |
| expect(resolved?.resolvedPricingProviderKey).toBe("openai"); | |
| expect(resolved?.priceData.service_tier_pricing).toEqual({ | |
| priority: { | |
| input_cost_per_token: 0.0000125, | |
| output_cost_per_token: 0.000075, | |
| cache_read_input_token_cost: 0.00000125, | |
| }, | |
| }); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/unit/lib/utils/pricing-resolution.test.ts` around lines 229 - 273, The
test currently uses a provider with name "OpenAI" and URL
"https://api.openai.com/v1/responses" which matches pricingMap exact keys so
resolveFromPricingMap returns immediately and never exercises
resolveDetailedFallback or the DETAIL_SCORE_OBJECT_FIELDS changes; update the
test (in pricing-resolution.test.ts) to use a provider that does not match any
exact/official keys (e.g., name "AnonymousProvider" or "Unknown" and a
non-openai URL like "https://api.example.com") so resolvePricingKeyCandidates
will not produce an "exact" hit, thereby forcing resolvePricingForModelRecords
to call resolveDetailedFallback; also change the expected source in the
assertion from the default to "priority_fallback" to confirm the detailed
scoring chose the openai node with service_tier_pricing.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/proxy.ts (1)
50-54: 裸路径/system-status重定向丢失用户语言偏好当用户访问无语言前缀的
/system-status时,这里直接重定向到/${defaultLocale}/status(即/zh-CN/status),完全忽略了NEXT_LOCALEcookie 与Accept-Language头。对比同文件下方的鉴权重定向(第 122-125 行)以及现有/status走intlMiddleware的处理方式,这条别名路径成为唯一不尊重用户语言偏好的入口,会让英文/日文等用户被强制跳转到中文页面。建议参考下方
getLocaleFromValue(...localeCookieName)的现有写法挑选目标语言。♻️ 建议改动
if (pathname === SYSTEM_STATUS_ALIAS_PATH) { const url = request.nextUrl.clone(); - url.pathname = `/${defaultLocale}/status`; + const localeFromCookie = getLocaleFromValue( + sanitizedRequest.cookies.get(localeCookieName)?.value + ); + const targetLocale = localeFromCookie ?? routing.defaultLocale; + url.pathname = `/${targetLocale}/status`; return NextResponse.redirect(url); }如果不打算消费 cookie,也可以直接
url.pathname = "/status"让下游intlMiddleware完成语言协商,但会多一次跳转。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/proxy.ts` around lines 50 - 54, The redirect for the alias path handled in the block checking pathname === SYSTEM_STATUS_ALIAS_PATH always forces url.pathname = `/${defaultLocale}/status`, ignoring the user's NEXT_LOCALE cookie and Accept-Language negotiation; update this logic to pick the target locale the same way other redirects do (use getLocaleFromValue(NEXT_LOCALE cookie) and fall back to Accept-Language/intlMiddleware) before building the redirect URL or, alternatively, set url.pathname = "/status" and let intlMiddleware perform locale resolution; adjust the code around request.nextUrl.clone() / NextResponse.redirect(...) to use the resolved locale instead of defaultLocale.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/proxy.ts`:
- Around line 50-54: The redirect for the alias path handled in the block
checking pathname === SYSTEM_STATUS_ALIAS_PATH always forces url.pathname =
`/${defaultLocale}/status`, ignoring the user's NEXT_LOCALE cookie and
Accept-Language negotiation; update this logic to pick the target locale the
same way other redirects do (use getLocaleFromValue(NEXT_LOCALE cookie) and fall
back to Accept-Language/intlMiddleware) before building the redirect URL or,
alternatively, set url.pathname = "/status" and let intlMiddleware perform
locale resolution; adjust the code around request.nextUrl.clone() /
NextResponse.redirect(...) to use the resolved locale instead of defaultLocale.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1ac63c2b63
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| serviceTierAbove272k: priorityTierPricing?.input_cost_per_token_above_272k_tokens, | ||
| serviceTierAbove200k: priorityTierPricing?.input_cost_per_token_above_200k_tokens, |
There was a problem hiding this comment.
Include tier fields when detecting long-context threshold
This adds serviceTierAbove272k/200k rates, but threshold activation is still driven by resolveLongContextThreshold(priceData), which does not inspect service_tier_pricing.priority.*_above_* fields. In priority requests where above-threshold rates are defined only under service_tier_pricing (and model_family is not forcing 272k), longContextThresholdExceeded remains false until 1M tokens, so these new tier rates are skipped and mid-range requests are billed at base rates. Threshold detection needs to include the new service-tier above-threshold fields.
Useful? React with 👍 / 👎.
|
您好,项目当前云端价格表位于 https://claude-code-hub.app/config/prices-base.toml ,如果您需要优化相关计价,请优先考虑云端价格表格式和数据。 |
Summary
This PR adds support for model-level priority service tier pricing, allowing different models to define their own priority tier rates in a structured
service_tier_pricingobject. The implementation prefers these model-specific priority rates over legacy*_prioritytop-level fields.Problem
Previously, the system only supported flat priority tier pricing through top-level fields like
input_cost_per_token_priority. This approach had limitations:Solution
New
service_tier_pricingdata structure (src/types/model-price.ts)ServiceTierPricinginterface with full pricing fieldsservice_tier_pricing?: Record<string, ServiceTierPricing>toModelPriceDataPriority-aware cost calculation (
src/lib/utils/cost-calculation.ts)getServiceTierPricing(),resolvePriorityAwareRate()resolveLongContextPricing()andmatchLongContextPricing()to accept service tier parameterservice_tier_pricing.priority→ legacy*_priorityfields → base ratesIntegration (
src/app/v1/_lib/proxy/response-handler.ts)Field support (
src/lib/utils/model-price-fields.ts)service_tier_pricing.*pathsProvider scoring (
src/lib/utils/pricing-resolution.ts)service_tier_pricingto detail scoring for provider selectionChanges
Core Changes
src/types/model-price.ts- AddedServiceTierPricinginterface and fieldsrc/lib/utils/cost-calculation.ts- Priority-aware pricing resolution (164 additions, 51 deletions)src/app/v1/_lib/proxy/response-handler.ts- Integrate priority tier in billing flowSupporting Changes
src/lib/utils/model-price-fields.ts- Field validation for service tier pricingsrc/lib/utils/pricing-resolution.ts- Provider detail scoringTest Coverage
tests/unit/lib/cost-calculation-priority.test.ts- Priority tier pricing teststests/unit/lib/cost-calculation-breakdown.test.ts- Breakdown calculation teststests/unit/lib/model-price-fields.test.ts- Field validation teststests/unit/lib/price-data-price-like-fields.test.ts- Price data validation teststests/unit/lib/utils/pricing-resolution.test.ts- Provider resolution teststests/integration/billing-model-source.test.ts- Integration billing testsTesting
Automated Tests
All tests pass:
Test Coverage Includes
priorityServiceTierApplied=true)Example Price Data
{ "input_cost_per_token": 0.000005, "output_cost_per_token": 0.00003, "input_cost_per_token_priority": 0.00001, "service_tier_pricing": { "priority": { "input_cost_per_token": 0.0000125, "output_cost_per_token": 0.000075, "cache_read_input_token_cost": 0.00000125, "long_context_pricing": { "threshold_tokens": 272000, "input_multiplier": 2.0 } } } }Breaking Changes
None - this is backward compatible. Legacy
*_priorityfields continue to work as fallbacks.Checklist
Enhanced description preserving original author verification steps
Greptile Summary
This PR introduces a structured
service_tier_pricingobject onModelPriceDatathat allows models to define their own priority tier rates, with the resolution orderservice_tier_pricing.priority→ legacy*_prioritytop-level fields → base rates. The implementation is well-tested and backward-compatible.src/proxy.tscontains unrelated redirect logic (/system-status→/{defaultLocale}/status) not mentioned in the PR description — it should be moved to a separate PR to keep the change set coherent.Confidence Score: 5/5
Safe to merge; no P0/P1 findings in the pricing logic itself.
All pricing-path changes are backward-compatible, resolution order is correct, and the integration test validates the end-to-end billing math. The only findings are a P2 style note about an unrelated proxy redirect bundled into this PR, and the previously-flagged SUPPORTED_LONG_CONTEXT_KEYS omission in model-price-fields.ts.
src/proxy.ts — contains unrelated /system-status redirect that belongs in a separate PR.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Request with priorityServiceTierApplied] --> B{service_tier_pricing.priority\nexists on model?} B -- Yes --> C[Use service_tier_pricing.priority rates] B -- No --> D{Legacy *_priority\nfield exists?} D -- Yes --> E[Use legacy *_priority rate] D -- No --> F[Use base rate] C --> G[resolvePriorityAwareRate] E --> G F --> G G --> H{long_context_pricing\nin service tier?} H -- Yes --> I[matchLongContextPricing\nwith service tier LCP] H -- No --> J{long_context_pricing\nat top level?} J -- Yes --> K[matchLongContextPricing\nwith top-level LCP + priority base rates] J -- No --> L[No long-context multiplier] I --> M[calculateRequestCost / calculateRequestCostBreakdown] K --> M L --> MPrompt To Fix All With AI
Reviews (2): Last reviewed commit: "fix: preserve public system status alias" | Re-trigger Greptile