stack 2/7: price long-context requests at the published long rate (#908) - #952
Conversation
) 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
📝 WalkthroughWalkthroughThe 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. ChangesLong-context pricing
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
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Stack navigation
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 — Carried in #953, with authorship preserved: #939, #942, #943, #944, #945, #948. |
There was a problem hiding this comment.
💡 Codex Review
Line 334 in c72dc99
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.
Lines 381 to 382 in c72dc99
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".
| const OPENAI_GPT56_CONTEXT_MODELS = [ | ||
| "gpt-5.6-sol", | ||
| "gpt-5.6-terra", | ||
| "gpt-5.6-luna", |
There was a problem hiding this comment.
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 👍 / 👎.
| export const CONTEXT_TIERS: readonly ContextTier[] = [ | ||
| ...["openai", "openai-apikey"].flatMap(provider => | ||
| OPENAI_GPT56_CONTEXT_MODELS.map((modelId): ContextTier => ({ |
There was a problem hiding this comment.
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 👍 / 👎.
| { 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" }, |
There was a problem hiding this comment.
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 👍 / 👎.
Stack navigation
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 — #954 needs human security review per Carried in #953, with authorship preserved: #939, #942, #943, #944, #945, #948. |
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.
|
✅ PR quality gates passed This pull request now targets The |
Stack navigation — 7 layers, review and merge bottom-up
Each layer targets the branch below it, so its diff only makes sense on that base — Note for the merge sequence: retargeting a child after its parent merges emits an |
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
src/server/management/shared.tssrc/usage/cost.tssrc/usage/expected-prices.tssrc/usage/summary.tstests/usage-cost.test.ts
| 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); |
There was a problem hiding this comment.
📐 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 -40Repository: 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 || trueRepository: 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.tsRepository: 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 || trueRepository: 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"
doneRepository: 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.tsRepository: 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"))
PYRepository: 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,
});
JSRepository: 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 || trueRepository: 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.
| 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); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🎯 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
There was a problem hiding this comment.
💡 Codex Review
Line 311 in c72dc99
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.
Lines 338 to 341 in c72dc99
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", |
There was a problem hiding this comment.
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 👍 / 👎.
| const OPENAI_GPT56_CONTEXT_MODELS = [ | ||
| "gpt-5.6-sol", | ||
| "gpt-5.6-terra", | ||
| "gpt-5.6-luna", |
There was a problem hiding this comment.
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 👍 / 👎.
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.
Cost4is flat andresolveMatchedPrice()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.ContextTierregistry with exact provider+model rules, each carrying its source URL andverifiedAtcalculateCost();resolveMatchedPrice()stays token-independent so its provider/model memoization is untouchedcontextTiersurfaced onAttemptCostEstimateandCostEstimate, propagated to combo results-provirtual aliasesgpt-5.6-sol/-terra/-luna(+-pro)>grok-4.5>=MiniMax-M3>Verified 2026-08-03 against the published tables — OpenAI's
>272Kis exclusive, xAI's≥ 200kis 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
prioritywas 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 collapsedeffectiveServiceTier()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) andMiniMax-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-prohad no base price at all — the virtual resolver keeps the selected id in the usage log while cost resolution deliberately does not fall back throughresolvedModel. A probe against the real resolver returnednullfor all three, meaning-prousage 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.jsonlives inlidge-jun/jawcode). Note for whoever lands it:PRIORITY_MULTIPLIERSstores 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 0bun test tests/usage-cost.test.ts— 51 pass, 0 failapplyContextTier()fails 8 of the new testsbun run test— 7691 pass, 8 skip, 0 fail, 507 filesbun run privacy:scan— passedThe 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
Bug Fixes