Skip to content

stack 2/7: price long-context requests at the published long rate (#908) - #952

Merged
lidge-jun merged 2 commits into
devfrom
codex/908-long-context-pricing
Aug 4, 2026
Merged

stack 2/7: price long-context requests at the published long rate (#908)#952
lidge-jun merged 2 commits into
devfrom
codex/908-long-context-pricing

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Stack

2/3 — long-context pricing tiers

Base: codex/bug-stack-plan (#951)
Next: carried contributor bug fixes (#952)

Summary

Several vendors reprice the entire request once the prompt crosses a token threshold. Cost4 is flat and resolveMatchedPrice() never saw a token count, so there was nowhere to express "this rate depends on how big the prompt is" — every request billed at the short rate, including the long ones, which are the expensive ones.

  • new ContextTier registry with exact provider+model rules, each carrying its source URL and verifiedAt
  • tier selection between base-price resolution and calculateCost(); resolveMatchedPrice() stays token-independent so its provider/model memoization is untouched
  • contextTier surfaced on AttemptCostEstimate and CostEstimate, propagated to combo results
  • base prices for the three -pro virtual aliases
Model Threshold Operator Effect
gpt-5.6-sol / -terra / -luna (+ -pro) 272,000 > 2× input, 2× cached, 2× cache write, 1.5× output
grok-4.5 200,000 >= 2× uniform
MiniMax-M3 512,000 > 2× uniform

Verified 2026-08-03 against the published tables — OpenAI's >272K is exclusive, xAI's ≥ 200k is inclusive.

Three things worth reviewing closely

The threshold reads raw usage.inputTokens, not normalized input. normalizeCostTokens() subtracts cache read/write to produce billable input, so a 280k prompt with a 200k cache read has 80k billable input and still crosses OpenAI's boundary. Deciding after normalization would have silently under-billed exactly the cache-heavy long requests. Covered by L3.

Long context and Fast are mutually exclusive, not composable. An earlier draft multiplied both. OpenAI's Fast guide states plainly that "Long context, fine-tuned models, and embeddings are not supported", so that product cannot exist. Exclusivity keys on the response-confirmed tier: a >272k request merely tagged priority was necessarily downgraded and must still bill long — suppressing the tier there would under-bill the downgraded request. That required passing tier provenance rather than the collapsed effectiveServiceTier() scalar to all four estimator call sites. Covered by L8.

MiniMax casing is exact on purpose. The bundle carries both minimax-m3 (0.6/2.4/0.12/0) and MiniMax-M3 (0.3/1.2/0.06/0); case-folding would select the wrong base row. Covered by L5.

A separate bug this surfaced

gpt-5.6-sol-pro, -terra-pro, -luna-pro had no base price at all — the virtual resolver keeps the selected id in the usage log while cost resolution deliberately does not fall back through resolvedModel. A probe against the real resolver returned null for all three, meaning -pro usage rendered no cost estimate whatsoever, and a context-tier row alone could never have been reached. Base rows added; L10 covers both halves.

Not fixed here

Terra and Luna still carry pre-price-cut base rates — that is #907, and it cannot be fixed in this repository (canonical models.json lives in lidge-jun/jawcode). Note for whoever lands it: PRIORITY_MULTIPLIERS stores Fast pricing as ratios calibrated against the stale bases, so correcting them without recomputing those ratios would fix an overcharge and introduce an undercharge.

Verification

  • bun x tsc --noEmit — exit 0
  • bun test tests/usage-cost.test.ts — 51 pass, 0 fail
  • red-green: ablating applyContextTier() fails 8 of the new tests
  • bun run test — 7691 pass, 8 skip, 0 fail, 507 files
  • bun run privacy:scan — passed

The existing Fast fixture used a 1M-token prompt that now crosses the threshold; it moved to 200k so those cases still isolate the Fast multiplier, with totals recomputed.

Fixes #908

Summary by CodeRabbit

  • New Features

    • Added pricing support for long-context requests, including provider- and model-specific thresholds and multipliers.
    • Added GPT-5.6 and GPT-5.6 Pro pricing.
    • Cost estimates and usage summaries now indicate when long-context pricing applies.
    • Improved service-tier handling to distinguish requested, configured, and response-confirmed tiers.
  • Bug Fixes

    • Prevented priority and long-context pricing from being applied simultaneously.
    • Improved cost calculations for request, combo, and summary estimates.

)

Several vendors reprice the entire request once the prompt crosses a token
threshold, and a flat Cost4 could not express it — so every request billed at
the short rate, including the long ones, which are the expensive ones.

The threshold reads raw usage.inputTokens, not normalized billable input: a
280k prompt with a 200k cache read has 80k billable input and still crosses
OpenAI's 272k boundary. Deciding after normalization would have under-billed
exactly the cache-heavy long requests.

Long context and Fast are mutually exclusive, not composable. OpenAI does not
serve long context in Fast mode, so exclusivity keys on the response-confirmed
tier: a >272k request merely tagged priority was necessarily downgraded and
bills long. That needed tier provenance at all four estimator call sites
instead of the collapsed scalar.

Also adds base prices for the three -pro virtual aliases, which resolved to
null and rendered no cost estimate at all.

Fixes #908
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds provider-specific long-context pricing for GPT-5.6, Grok 4.5, and MiniMax M3. Cost estimators now use service-tier provenance, apply mutually exclusive long-context and priority pricing, and expose the applied context tier through request, attempt, combo, and summary calculations.

Changes

Long-context pricing

Layer / File(s) Summary
Pricing rules and context-tier lookup
src/usage/expected-prices.ts
Adds GPT-5.6 pricing and -pro overlays. Defines exact provider/model context tiers, raw-token thresholds, inclusivity rules, multipliers, sources, and verification dates.
Context pricing and service-tier provenance
src/usage/cost.ts
Adds ServiceTierContext, ServiceTierInput, and contextTier metadata. Applies long-context pricing before priority pricing and skips priority multiplication when long-context pricing applies.
Aggregation and management integration
src/usage/summary.ts, src/server/management/shared.ts
Uses serviceTierContext for request, combo, model, provider, and summary cost calculations.
Pricing and provenance validation
tests/usage-cost.test.ts
Tests threshold boundaries, raw-token handling, provider/model matching, Fast provenance, combo propagation, aliases, overlays, and pricing metadata.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant UsageRecord
  participant serviceTierContext
  participant estimateRequestCost
  participant findContextTier
  participant isLongContext
  UsageRecord->>serviceTierContext: Resolve service-tier provenance
  serviceTierContext-->>estimateRequestCost: ServiceTierContext
  estimateRequestCost->>findContextTier: Find provider/model rule
  findContextTier-->>estimateRequestCost: ContextTier
  estimateRequestCost->>isLongContext: Evaluate raw input threshold
  isLongContext-->>estimateRequestCost: Long-context status
  estimateRequestCost-->>UsageRecord: Cost estimate with contextTier
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: wibias, ingwannu

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: applying published long-context pricing.
Linked Issues check ✅ Passed The changes satisfy issue [#908] by applying provider-specific raw-token thresholds, preserving Fast-mode interaction, and propagating long-context metadata.
Out of Scope Changes check ✅ Passed The changes remain within issue [#908], including pricing data, tier resolution, service-tier provenance, aliases, summaries, and related tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/908-long-context-pricing

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

@lidge-jun

Copy link
Copy Markdown
Owner Author

Stack navigation

  1. stack 1/6: triage the open issue surface and lock the bug plan #951 — triage the open issue surface and lock the bug plan (base dev)
  2. stack 2/7: price long-context requests at the published long rate (#908) #952 — long-context pricing tiers, Cost estimates ignore published long-context pricing tiers (OpenAI >272k, xAI >=200k) #908 (base stack 1/6: triage the open issue surface and lock the bug plan #951)
  3. stack 3/7: carry six contributor bug fixes with authorship intact #953 — carry six contributor bug fixes (base stack 2/7: price long-context requests at the published long rate (#908) #952)

Review and merge bottom-up. Each PR targets the preceding stack branch, so its Files changed view contains only that layer.

The layers touch disjoint files — devlog/, src/usage/, and the carried contributors' paths — so any layer can be retargeted to dev and taken independently without a rebase conflict if you prefer.

Carried in #953, with authorship preserved: #939, #942, #943, #944, #945, #948.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

if (isConfirmedFast(tier)) return [cost4, undefined];

P2 Badge Scope Fast suppression to the priced attempt

In a mixed-provider combo, the request-level tier context is passed to every attempt even though attempts carry no service-tier provenance. If the final OpenAI response confirms priority, this unconditional return also suppresses long-context pricing for earlier xAI or MiniMax attempts; for example, a preceding 600K MiniMax-M3 attempt is charged at the short rate despite OpenAI Fast restrictions being irrelevant to it. Gate suppression on the current provider/model being eligible for OpenAI Fast, or persist and use service-tier provenance per attempt.


opencodex/src/usage/cost.ts

Lines 381 to 382 in c72dc99

const [tieredCost4, contextTier] = applyContextTier(
price.cost4, attempt.provider, attempt.model, attempt.usage.inputTokens, serviceTier,

P1 Badge Select context tiers before aggregating upstream calls

The usage passed here can represent multiple upstream requests rather than one pricing decision: runWithImageBridge() explicitly sums every hidden and final iteration into one OcxUsage, then stores that aggregate on the active attempt. Applying the threshold to that sum overcharges media-loop requests when, for example, two xAI calls each use fewer than 200K input tokens but together exceed 200K; neither individual call qualifies for the long rate, yet the estimator doubles both. Preserve per-send usage and apply the context tier to each upstream call before summing its cost.

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +217 to +220
const OPENAI_GPT56_CONTEXT_MODELS = [
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include the generic GPT-5.6 alias in context tiers

The OpenAI API catalog exposes gpt-5.6, and the repository's API contract records that this upstream alias routes to Sol, but this exact-match list begins at gpt-5.6-sol. Consequently, findContextTier() returns no rule for API-key requests selecting the generic alias, so prompts above 272K still use the short rate—for example, 300K input plus 20K output remains $2.10 instead of $3.90. Include gpt-5.6, preferably by deriving this list from the canonical provider registry rather than maintaining another independent model list.

AGENTS.md reference: src/AGENTS.md:L18-L18

Useful? React with 👍 / 👎.

Comment on lines +228 to +230
export const CONTEXT_TIERS: readonly ContextTier[] = [
...["openai", "openai-apikey"].flatMap(provider =>
OPENAI_GPT56_CONTEXT_MODELS.map((modelId): ContextTier => ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Document the user-visible long-context pricing rules

This registry changes the estimated amounts displayed by the Logs and Usage surfaces whenever OpenAI, xAI, or MiniMax prompts cross the new thresholds, including the non-obvious Fast-mode exclusivity rule, but the commit contains no docs-site/ update explaining those calculations. Add the thresholds and estimation behavior to the English documentation and keep the translated versions consistent so users can interpret the changed dashboard totals.

AGENTS.md reference: AGENTS.md:L212-L213

Useful? React with 👍 / 👎.

Comment on lines +89 to +91
{ provider: "openai-apikey", modelId: "gpt-5.6-sol-pro", cost4: GPT56_SOL, source: `collapsed base ID ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-03", status: "verified-derived" },
{ provider: "openai-apikey", modelId: "gpt-5.6-terra-pro", cost4: GPT56_TERRA, source: `collapsed base ID ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-03", status: "verified-derived" },
{ provider: "openai-apikey", modelId: "gpt-5.6-luna-pro", cost4: GPT56_LUNA, source: `collapsed base ID ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-03", status: "verified-derived" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Apply Fast multipliers to newly priced Pro aliases

These rows make the *-pro selections priceable while their selected suffix remains in the usage log, but applyPriorityMultiplier() consequently looks up names such as gpt-5.6-sol-pro in PRIORITY_MULTIPLIERS, which only contains the base slugs. The API-key provider still sends service_tier=priority after rewriting the virtual selection to its base wire model, so a response-confirmed Fast Sol Pro request with 200K input and 20K output is reported as $1.60 instead of the $3.20 Fast rate. Resolve the multiplier through the virtual model's base ID, or derive corresponding alias entries and cover them with a Fast regression test.

AGENTS.md reference: src/AGENTS.md:L18-L18

Useful? React with 👍 / 👎.

@lidge-jun

Copy link
Copy Markdown
Owner Author

Stack navigation

  1. stack 1/6: triage the open issue surface and lock the bug plan #951 — triage the open issue surface and lock the bug plan (base dev)
  2. stack 2/7: price long-context requests at the published long rate (#908) #952 — long-context pricing tiers, Cost estimates ignore published long-context pricing tiers (OpenAI >272k, xAI >=200k) #908 (base stack 1/6: triage the open issue surface and lock the bug plan #951)
  3. stack 3/7: carry six contributor bug fixes with authorship intact #953 — carry six contributor bug fixes (base stack 2/7: price long-context requests at the published long rate (#908) #952)
  4. stack 4/7: keep an explicit thinking disable through translation (#545) #954 — Claude Desktop classifier thinking round-trip, Claude Desktop 3P Auto Mode classifier retries after 64-token Anthropic OAuth outputs #545 (base stack 3/7: carry six contributor bug fixes with authorship intact #953)

Review and merge bottom-up. Each PR targets the preceding stack branch, so its Files changed view contains only that layer.

The layers touch disjoint files — devlog/, src/usage/, the carried contributors' paths, and src/claude/ + src/adapters/anthropic.ts — so any layer can be retargeted to dev and taken independently without a rebase conflict.

#954 needs human security review per MAINTAINERS.md: it changes request construction on an Anthropic OAuth execution path. It is last in the stack so the first three can land without waiting on that.

Carried in #953, with authorship preserved: #939, #942, #943, #944, #945, #948.

@lidge-jun lidge-jun changed the title stack 2/3: price long-context requests at the published long rate (#908) stack 2/4: price long-context requests at the published long rate (#908) Aug 3, 2026
@lidge-jun

Copy link
Copy Markdown
Owner Author

Stack navigation

  1. stack 1/6: triage the open issue surface and lock the bug plan #951 — triage the open issue surface and lock the bug plan (base dev)
  2. stack 2/7: price long-context requests at the published long rate (#908) #952 — long-context pricing tiers, Cost estimates ignore published long-context pricing tiers (OpenAI >272k, xAI >=200k) #908 (base stack 1/6: triage the open issue surface and lock the bug plan #951)
  3. stack 3/7: carry six contributor bug fixes with authorship intact #953 — carry six contributor bug fixes (base stack 2/7: price long-context requests at the published long rate (#908) #952)
  4. stack 4/7: keep an explicit thinking disable through translation (#545) #954 — Claude Desktop classifier thinking round-trip, Claude Desktop 3P Auto Mode classifier retries after 64-token Anthropic OAuth outputs #545 (base stack 3/7: carry six contributor bug fixes with authorship intact #953)
  5. stack 5/7: probe reset-derived cooldowns without waiting to be selected (#915) #955 — cooldown early-recovery probe, [Bug]: Reset-derived cooldowns can miss early recovery while another pool account remains eligible #915 (base stack 4/7: keep an explicit thinking disable through translation (#545) #954)

Review and merge bottom-up. Each PR targets the preceding stack branch, so its Files changed view contains only that layer.

The layers touch disjoint files — devlog/, src/usage/, the carried contributors' paths, src/claude/ + src/adapters/anthropic.ts, and src/codex/ — so any layer can be retargeted to dev and taken independently without a rebase conflict.

#954 needs human security review per MAINTAINERS.md: it changes request construction on an Anthropic OAuth execution path. #955 sits above it in the chain but is independent of it in code, so if that review blocks, #955 can be retargeted to #953 without conflict.

Carried in #953, with authorship preserved: #939, #942, #943, #944, #945, #948.

@lidge-jun lidge-jun changed the title stack 2/4: price long-context requests at the published long rate (#908) stack 2/5: price long-context requests at the published long rate (#908) Aug 3, 2026
@lidge-jun

Copy link
Copy Markdown
Owner Author

Stack navigation

  1. stack 1/6: triage the open issue surface and lock the bug plan #951 — triage the open issue surface and lock the bug plan (base dev)
  2. stack 2/7: price long-context requests at the published long rate (#908) #952 — long-context pricing tiers, Cost estimates ignore published long-context pricing tiers (OpenAI >272k, xAI >=200k) #908 (base stack 1/6: triage the open issue surface and lock the bug plan #951)
  3. stack 3/7: carry six contributor bug fixes with authorship intact #953 — carry six contributor bug fixes (base stack 2/7: price long-context requests at the published long rate (#908) #952)
  4. stack 4/7: keep an explicit thinking disable through translation (#545) #954 — Claude Desktop classifier thinking round-trip, Claude Desktop 3P Auto Mode classifier retries after 64-token Anthropic OAuth outputs #545 (base stack 3/7: carry six contributor bug fixes with authorship intact #953)
  5. stack 5/7: probe reset-derived cooldowns without waiting to be selected (#915) #955 — cooldown early-recovery probe, [Bug]: Reset-derived cooldowns can miss early recovery while another pool account remains eligible #915 (base stack 4/7: keep an explicit thinking disable through translation (#545) #954)
  6. stack 6/7: triage the overnight PRs and fix the #955 defects they found #973 — overnight PR triage + the stack 5/7: probe reset-derived cooldowns without waiting to be selected (#915) #955 defects it surfaced (base stack 5/7: probe reset-derived cooldowns without waiting to be selected (#915) #955)

Review and merge bottom-up. Each PR targets the preceding stack branch, so its Files changed view contains only that layer.

The layers touch largely disjoint files, so any layer can be retargeted to dev and taken independently. The one real dependency is #973 on #955 — it fixes defects in #955's own code, so those two should land together or in that order.

#954 needs human security review per MAINTAINERS.md (Anthropic OAuth request construction). It is deliberately below #955/#973 so the first three can land without waiting on it.

Carried with authorship preserved: #939, #942, #943, #944, #945, #948 in #953; #965, #967, #968 in #973.

@lidge-jun lidge-jun changed the title stack 2/5: price long-context requests at the published long rate (#908) stack 2/6: price long-context requests at the published long rate (#908) Aug 4, 2026
chrisae9 pushed a commit to chrisae9/opencodex that referenced this pull request Aug 4, 2026
All four PRs of the lidge-jun#951-lidge-jun#955 stack carried no type label while the `label`
check reported success. `stack 1/5:` fails the conventional-commit regex — the
`1/5` sits between the word and the colon — and then reaches the sentence-case
fallback, which extracts `stack`. That has no entry in PREFIX_TO_LABEL, so
`planTypeLabelSync` returns `{skip: true, reason: "no-prefix"}`, and a skip is
not a failure. The check stays green and nothing is labeled.

This is not a stacked-PR bug; the labeler has no branch filter and ran fine on
all four. It is a title-vocabulary bug that the stack happened to expose: any
title with an unrecognised prefix word is silently unlabeled.

The commits underneath are conventional even when the title is not, so they
answer what the title cannot. Adding `stack` to PREFIX_TO_LABEL was rejected —
a stack PR can carry fixes, features, or docs, so any fixed mapping would be a
lie.

The unanimity rule this started with was falsified by running it on the real
data. lidge-jun#952 gives `{bug: 1}` and labels, but lidge-jun#955 gives `{bug: 4, chore: 1}` and
would abstain — four `fix(codex):` commits plus one `test(codex):`, which is a
bug fix by any honest reading. A rule that abstains there abstains on most real
PRs, since nearly every substantial change carries a test or chore commit.

So `chore` is supporting, not competing: `test:`, `ci:`, `chore:`, `style:`,
`refactor:`, and `build:` all map to it, and none of them says what a PR is FOR.
It drops out of the tally when a non-chore type is present. An all-chore PR
still gets `chore`, and a genuine `fix:`-plus-`feat:` mix is still left
unlabeled rather than guessed.

The title stays authoritative when it classifies, so a well-formed title is
never overridden by what happens to be committed under it, and the existing
human-override gate still runs first. No permission change: `pulls.listCommits`
is covered by the existing `contents: read`.

Both rules were driven red: removing the fallback fails the stack-PR test and
nothing else; removing the chore-demotion fails the lidge-jun#955-shape test and nothing
else.

The docs now also state the promotion model, which was only a code comment
before: `enforce-target` and `label` run on `pull_request_target` and are loaded
from the default branch, so merging either to `dev` does not change live
behavior until promotion to `main`.

Verified: node --test .github/scripts/pr-labeler.test.cjs 24 pass; bun test
tests/ci-workflows.test.ts 83 pass; typecheck and privacy:scan pass.
@lidge-jun lidge-jun changed the title stack 2/6: price long-context requests at the published long rate (#908) stack 2/7: price long-context requests at the published long rate (#908) Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

PR quality gates passed

This pull request now targets dev with acceptable ancestry and description.

The [WRONG BRANCH] title prefix has been removed. The pull request has been marked ready for review again.

@github-actions github-actions Bot changed the title stack 2/7: price long-context requests at the published long rate (#908) [WRONG BRANCH] stack 2/7: price long-context requests at the published long rate (#908) Aug 4, 2026
@github-actions
github-actions Bot marked this pull request as draft August 4, 2026 03:29
@lidge-jun

Copy link
Copy Markdown
Owner Author

Stack navigation — 7 layers, review and merge bottom-up

Layer PR Contents
1/7 #951 merged af3ddedb4 — 22 label corrections + the plan unit
2/7 #952 long-context pricing tiers (#908)
3/7 #953 six carried contributor bug fixes, authorship intact
4/7 #954 explicit thinking disable through translation (#545)
5/7 #955 cooldown early-recovery probe (#915)
6/7 #973 overnight PR triage + fixes to #955's own defects
7/7 #980 NIM vision classification (#956), service repair (#970), qwen3.8-max rename

Each layer targets the branch below it, so its diff only makes sense on that base — enforce-target skips the wrong-base gate for stacked children by design (AGENTS.md, Branch policy). Review bottom-up; a layer cannot merge before its parent lands.

Note for the merge sequence: retargeting a child after its parent merges emits an edited event, which ci.yml does not listen for. A green check on the same head sha therefore proves nothing about the new merge base — merge current dev into the child to force a synchronize run before merging it.

@lidge-jun
lidge-jun changed the base branch from codex/bug-stack-plan to dev August 4, 2026 03:32
@github-actions github-actions Bot changed the title [WRONG BRANCH] stack 2/7: price long-context requests at the published long rate (#908) stack 2/7: price long-context requests at the published long rate (#908) Aug 4, 2026
@github-actions
github-actions Bot marked this pull request as ready for review August 4, 2026 03:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/usage-cost.test.ts`:
- Around line 636-643: Update the existing L4 and L5 tests for grok-4.5 and
MiniMax-M3 to add cost assertions that pin both input and output rates to the
required 2x multipliers. Keep their current contextTier assertions and use equal
token counts or an appropriately precise total-ratio comparison so the
assertions directly detect incorrect shipped multiplier fields.
- Around line 523-524: Update the sol helper’s usage parameter to use the
exported OcxUsage type instead of Record<string, number>. Add a type-only
OcxUsage import from src/types.ts, while preserving the existing
estimateRequestCost call and serviceTier typing.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 32c2d716-0510-466d-95f1-cf81afaf4c91

📥 Commits

Reviewing files that changed from the base of the PR and between c040242 and c72dc99.

📒 Files selected for processing (5)
  • src/server/management/shared.ts
  • src/usage/cost.ts
  • src/usage/expected-prices.ts
  • src/usage/summary.ts
  • tests/usage-cost.test.ts

Comment thread tests/usage-cost.test.ts
Comment on lines +523 to +524
const sol = (usage: Record<string, number>, serviceTier?: Parameters<typeof estimateRequestCost>[0]["serviceTier"]) =>
estimateRequestCost({ provider: "openai", model: "gpt-5.6-sol", usageStatus: "reported", usage, serviceTier }, SOL);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Locate the OcxUsage type declaration and confirm its exported field names.
set -euo pipefail

# Find where OcxUsage is declared.
rg -nP --type=ts -C2 '\b(export\s+)?(interface|type)\s+OcxUsage\b'

# Show the full shape so field names can be checked against the tests.
fd -e ts | xargs rg -lP '\b(interface|type)\s+OcxUsage\b' | while IFS= read -r f; do
  ast-grep outline "$f" --match OcxUsage --view expanded
done

# Confirm the field names used by the new tests exist on the type.
rg -nP --type=ts '\b(inputTokens|outputTokens|cachedInputTokens|cacheReadTokens|contextTotalTokens)\b\s*\??\s*:'

# Confirm how the test file currently imports from src/usage.
rg -nP -C1 "^import|from ['\"]" tests/usage-cost.test.ts | head -40

Repository: lidge-jun/opencodex

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(tests/usage-cost\.test\.ts|src/usage/cost\.ts|.*usage.*\.(ts|tsx|js|jsx))$' || true

printf '%s\n' '--- OcxUsage references ---'
rg -n -C3 'OcxUsage|cachedInputTokens|cacheReadTokens|contextTotalTokens' . --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' || true

printf '%s\n' '--- test imports and helper area ---'
test -f tests/usage-cost.test.ts && sed -n '1,80p;500,585p' tests/usage-cost.test.ts || true

printf '%s\n' '--- cost source ---'
test -f src/usage/cost.ts && sed -n '1,220p' src/usage/cost.ts || true

Repository: lidge-jun/opencodex

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- OcxUsage declaration ---'
rg -n -C8 '^export (interface|type) OcxUsage\b' src --glob '*.ts'

printf '%s\n' '--- cost imports and token normalization ---'
rg -n -C8 'OcxUsage|normalizeCostTokens|usage\.inputTokens|usage\.cachedInputTokens' src/usage/cost.ts

printf '%s\n' '--- usage-cost imports and helpers ---'
sed -n '1,35p;515,580p' tests/usage-cost.test.ts

printf '%s\n' '--- direct OcxUsage imports in usage-cost test ---'
rg -n 'OcxUsage|from ["'\'']\.\./src/types|from ["'\'']\.\./src/usage' tests/usage-cost.test.ts || true

printf '%s\n' '--- exact type field declarations ---'
sed -n '215,255p' src/types.ts

Repository: lidge-jun/opencodex

Length of output: 1479


🏁 Script executed:

#!/bin/bash
set +e

printf '%s\n' '--- OcxUsage full declaration ---'
sed -n '320,350p' src/types.ts

printf '%s\n' '--- cost source imports and normalization ---'
sed -n '1,150p' src/usage/cost.ts

printf '%s\n' '--- usage-cost test imports ---'
sed -n '1,35p' tests/usage-cost.test.ts

printf '%s\n' '--- usage-cost helper region ---'
sed -n '515,580p' tests/usage-cost.test.ts

printf '%s\n' '--- all OcxUsage references in relevant files ---'
rg -n -C3 'OcxUsage' src/usage/cost.ts tests/usage-cost.test.ts src/types.ts || true

Repository: lidge-jun/opencodex

Length of output: 13621


🏁 Script executed:

#!/bin/bash
set +e

printf '%s\n' '--- estimateRequestCost signature and usage access ---'
rg -n -C12 'export function estimateRequestCost|function estimateRequestCost|estimateRequestCost\s*=' src/usage/cost.ts

printf '%s\n' '--- usage type declarations in cost input types ---'
rg -n -C8 'usage(Status|:)|usage\??:|interface .*Request|type .*Request' src/usage/cost.ts | head -160

printf '%s\n' '--- TypeScript tooling availability ---'
command -v tsc || true
command -v bun || true
command -v node || true

printf '%s\n' '--- relevant compiler configuration ---'
git ls-files | rg '(^|/)(tsconfig(\..*)?\.json|package\.json)$' | head -30 | while IFS= read -r f; do
  echo "### $f"
  sed -n '1,160p' "$f"
done

Repository: lidge-jun/opencodex

Length of output: 6636


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- estimateRequestCost occurrences ---'
rg -n -C10 'estimateRequestCost' src/usage/cost.ts tests/usage-cost.test.ts

printf '%s\n' '--- cost file size and exported declarations ---'
wc -l src/usage/cost.ts
rg -n '^(export )?(async )?(function|const|interface|type|class) ' src/usage/cost.ts

Repository: lidge-jun/opencodex

Length of output: 21327


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("src/usage/cost.ts")
text = p.read_bytes().decode("utf-8", "replace")
for needle in ("estimateRequestCost", "RequestCost", "usage: OcxUsage", "usage?: OcxUsage"):
    print(f"--- {needle} ---")
    start = 0
    found = False
    while True:
        i = text.find(needle, start)
        if i < 0:
            break
        found = True
        line = text.count("\n", 0, i) + 1
        lo = max(0, text.rfind("\n", 0, max(0, i - 500)))
        hi = text.find("\n", i + 1000)
        if hi < 0:
            hi = len(text)
        print(f"line {line}")
        print(text[lo + 1:hi])
        start = i + len(needle)
    if not found:
        print("not found")
PY

printf '%s\n' '--- NUL-byte locations ---'
python3 - <<'PY'
from pathlib import Path
b = Path("src/usage/cost.ts").read_bytes()
print("bytes", len(b), "nul_count", b.count(b"\\0"))
PY

Repository: lidge-jun/opencodex

Length of output: 8139


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

cat >"$tmpdir/check.ts" <<'TS'
interface OcxUsage {
  inputTokens: number;
  outputTokens: number;
  contextTotalTokens?: number;
  totalTokens?: number;
  cachedInputTokens?: number;
  cacheReadInputTokens?: number;
  cacheCreationInputTokens?: number;
  reasoningOutputTokens?: number;
  estimated?: boolean;
}

declare function estimateRequestCost(input: {
  provider: string;
  model: string;
  usage?: OcxUsage;
  usageStatus: "reported";
  serviceTier?: unknown;
}): { contextTier?: "long" } | null;

const sol = (usage: Record<string, number>) =>
  estimateRequestCost({
    provider: "openai",
    model: "gpt-5.6-sol",
    usageStatus: "reported",
    usage,
  });

sol({ inputToken: 272_001, outputTokens: 10_000 });
TS

printf '%s\n' '--- TypeScript assignability check ---'
tsc --strict --noEmit --skipLibCheck "$tmpdir/check.ts" 2>&1 || true

printf '%s\n' '--- Runtime normalization check for the misspelled key ---'
node - <<'JS'
function normalizeCostTokens(usage) {
  const input = usage.inputTokens;
  const output = usage.outputTokens;
  const cacheWrite = usage.cacheCreationInputTokens ?? 0;
  const primaryRead = usage.cacheReadInputTokens ?? usage.cachedInputTokens ?? 0;
  for (const cacheRead of [primaryRead]) {
    if (![input, output, cacheRead, cacheWrite].every(
      value => Number.isFinite(value) && value >= 0
    )) return null;
    if (cacheRead + cacheWrite > input) continue;
    return { input: Math.max(0, input - cacheRead - cacheWrite), output, cacheRead, cacheWrite };
  }
  return null;
}
const usage = { inputToken: 272001, outputTokens: 10000 };
console.log({
  normalized: normalizeCostTokens(usage),
  estimateRequestCostResult: normalizeCostTokens(usage) ? "continues" : null,
  inputTokens: usage.inputTokens,
  contextTier: undefined,
});
JS

Repository: lidge-jun/opencodex

Length of output: 507


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

cat >"$tmpdir/check.ts" <<'TS'
interface OcxUsage {
  inputTokens: number;
  outputTokens: number;
  contextTotalTokens?: number;
  totalTokens?: number;
  cachedInputTokens?: number;
  cacheReadInputTokens?: number;
  cacheCreationInputTokens?: number;
  reasoningOutputTokens?: number;
  estimated?: boolean;
}

declare function estimateRequestCost(input: {
  provider: string;
  model: string;
  usage?: OcxUsage;
  usageStatus: "reported";
  serviceTier?: unknown;
}): { contextTier?: "long" } | null;

const sol = (usage: Record<string, number>) =>
  estimateRequestCost({
    provider: "openai",
    model: "gpt-5.6-sol",
    usageStatus: "reported",
    usage,
  });

sol({ inputToken: 272_001, outputTokens: 10_000 });
TS

tsc --strict --noEmit --skipLibCheck --ignoreConfig "$tmpdir/check.ts" 2>&1 || true

Repository: lidge-jun/opencodex

Length of output: 329


Type sol's usage parameter as OcxUsage. OcxUsage is exported from src/types.ts and includes cachedInputTokens. Record<string, number> accepts misspelled keys at call sites; malformed usage makes normalizeCostTokens return null, not zero. Import OcxUsage as a type and update tests/usage-cost.test.ts:523.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/usage-cost.test.ts` around lines 523 - 524, Update the sol helper’s
usage parameter to use the exported OcxUsage type instead of Record<string,
number>. Add a type-only OcxUsage import from src/types.ts, while preserving the
existing estimateRequestCost call and serviceTier typing.

Comment thread tests/usage-cost.test.ts
Comment on lines +636 to +643
test("L11. every tier rule records a source and a verification date", () => {
expect(CONTEXT_TIERS.length).toBeGreaterThan(0);
for (const tier of CONTEXT_TIERS) {
expect(tier.source).toMatch(/^https:\/\//);
expect(tier.verifiedAt).toMatch(/^\d{4}-\d{2}-\d{2}$/);
expect(tier.thresholdInputTokens).toBeGreaterThan(0);
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the tier multipliers for grok-4.5 and MiniMax-M3, not only the tier flag.

L11 checks metadata shape only. The threshold values and inclusivity are covered behaviorally by L1, L4 and L5, because applyContextTier looks the rule up by provider and model and therefore reads the shipped CONTEXT_TIERS even when the test passes a local overlay array. The multipliers are not covered the same way.

L1 pins the OpenAI rule at 2x input and 1.5x output on Line 534 through Line 536. L4 on Line 563 and Line 564, and L5 on Line 573 through Line 576, assert contextTier only. Issue #908 specifies 2x on all rates for both grok-4.5 and MiniMax-M3. If a rule shipped 1.5x output instead of 2x, every test in this file would still pass and the estimator would under-bill long xAI and MiniMax requests.

Add one cost assertion to each of those two tests.

💚 Proposed fix to pin the 2x-all-rates rules
     const at = (n: number) => estimateRequestCost({ provider: "xai", model: "grok-4.5", usageStatus: "reported", usage: { inputTokens: n, outputTokens: 1_000 } }, overlays);
     expect(at(199_999)!.contextTier).toBeUndefined();
     expect(at(200_000)!.contextTier).toBe("long");
+    // 2x on every rate: input 2 -> 4, output 6 -> 12.
+    expect(at(200_000)!.cost.input).toBeCloseTo(200_000 / 1e6 * 4, 9);
+    expect(at(200_000)!.cost.output).toBeCloseTo(1_000 / 1e6 * 12, 9);
+    expect(at(200_000)!.cost.total).toBeCloseTo(at(199_999)!.cost.total * 2, 6);
   });
     expect(at("MiniMax-M3", 512_000)!.contextTier).toBeUndefined();
     expect(at("MiniMax-M3", 512_001)!.contextTier).toBe("long");
+    // 2x on every rate: input 0.3 -> 0.6, output 1.2 -> 2.4.
+    expect(at("MiniMax-M3", 512_001)!.cost.input).toBeCloseTo(512_001 / 1e6 * 0.6, 9);
+    expect(at("MiniMax-M3", 512_001)!.cost.output).toBeCloseTo(1_000 / 1e6 * 2.4, 9);
     // The bundle carries both ids at different rates; case-folding would pick the wrong row.
     expect(at("minimax-m3", 512_001)!.contextTier).toBeUndefined();
   });

The toBeCloseTo(..., 6) on the total ratio absorbs the one-token difference across the boundary. Alternatively compare at equal token counts as L1 does.

As per path instructions: "A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem."

Run the following script to confirm the shipped multiplier fields and their values before writing the assertions:

#!/bin/bash
# Description: Inspect the shipped CONTEXT_TIERS rule table: thresholds, inclusivity, and multipliers.
set -euo pipefail

# Locate the declaration.
rg -nP --type=ts -C3 '\bCONTEXT_TIERS\b'

# Print the full rule table with its element type.
fd -e ts | xargs rg -lP '\bCONTEXT_TIERS\b\s*(:|=)' | while IFS= read -r f; do
  ast-grep outline "$f" --items all --view expanded
done

# Show the rule fields the tests rely on.
rg -nP --type=ts -C1 '\b(thresholdInputTokens|inclusive|multiplier|inputMultiplier|outputMultiplier|cachedInputMultiplier)\b'

# Show applyContextTier so the multiplier application order is clear.
ast-grep run --pattern 'function applyContextTier($$$) { $$$ }' --lang typescript src/usage/cost.ts
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/usage-cost.test.ts` around lines 636 - 643, Update the existing L4 and
L5 tests for grok-4.5 and MiniMax-M3 to add cost assertions that pin both input
and output rates to the required 2x multipliers. Keep their current contextTier
assertions and use equal token counts or an appropriately precise total-ratio
comparison so the assertions directly detect incorrect shipped multiplier
fields.

Source: Path instructions

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

return typeof tier === "object" && tier.responseServiceTier === "priority";

P2 Badge Normalize fast before tier pricing

When callers use the service_tier: "fast" spelling that the rest of this repo already treats as the Fast tier, this helper returns false. For a response-confirmed Fast OpenAI request over 272K input tokens, applyContextTier() then treats it as a downgraded long-context request and suppresses the Fast multiplier, so Logs/Usage show the wrong price; normalize fast and priority together here and in the scalar tier path.


opencodex/src/usage/cost.ts

Lines 338 to 341 in c72dc99

input: cost4.input * rule.multiplier.input,
output: cost4.output * rule.multiplier.output,
cacheRead: cost4.cacheRead * rule.multiplier.cacheRead,
cacheWrite: cost4.cacheWrite * rule.multiplier.cacheWrite,

P2 Badge Use API base rates before applying long multipliers

For base openai-apikey/gpt-5.6-terra and gpt-5.6-luna requests, resolveMatchedPrice() still falls back to the generated OpenAI bundle instead of the new API-rate constants, so this new long-context branch multiplies stale short rates; for example Luna long context becomes $2/$9 per 1M input/output instead of the $0.40/$1.80 long row implied by the API constants added in this commit. Carry the published long Cost4, or make the base API aliases resolve to the API short overlay before multiplying.

ℹ️ 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".

),
{
provider: "xai",
modelId: "grok-4.5",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add the other xAI long-context models

The xAI pricing table lists the same long-context threshold for several exposed xAI IDs, not only grok-4.5; grok-4.3, grok-build-0.1, and the grok-4.20-0309-* models are also seeded in the provider registry. With only this row, a 250K-token xai/grok-4.3 request still resolves a normal price but never finds a context tier, so the Logs/Usage estimate stays at the short rate instead of doubling the full request.

Useful? React with 👍 / 👎.

Comment on lines +217 to +220
const OPENAI_GPT56_CONTEXT_MODELS = [
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include gpt-5.5 in long-context tiers

The OpenAI API provider exposes gpt-5.5 with the same 1.05M context path as these GPT-5.6 models, and the pricing source used here publishes a long-context row for it, but this hard-coded GPT-5.6-only list never creates a tier for that default API-key model. As a result, an openai-apikey/gpt-5.5 request above 272K input still uses the short $5/$30 rates instead of the long $10/$45 rates in Logs/Usage.

Useful? React with 👍 / 👎.

@lidge-jun
lidge-jun merged commit d086a54 into dev Aug 4, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant