Repository navigation
Price Codex at OpenAI's list rates on the day they applied, and mark a borrowed rate with "≈" - #623
Conversation
|
Merge train with #622 (all three after 1.55 is cut). #622 adds Before merging this PR on a
Also from #622, now pushed (5adc088): |
|
Review fixes are in 33e21a4. The merge is still on hold until 1.55 is cut. Applied
Skipped
Verification
🤖 Generated with Claude Code |
…a borrowed rate with "≈" The Mac's Codex price table stopped at gpt-5.5, which it charged at gpt-5.4's $2.50 / $15 pending an official price. Every newer model fell back to that row, so gpt-6-astra ($10 / $50) and gpt-5.6-sol ($4 / $20) were charged $2.50 / $15, and nothing on screen said the rate was borrowed. - CodexPricingTable (new, CLIPulseCore, shared): the Codex rows from CodexBar's CostUsagePricing.swift at 25bba9b7 under its MIT notice, re-checked against developers.openai.com on 2026-09-30. gpt-5.5 and gpt-5.5-pro at OpenAI's rates; gpt-6-astra, gpt-5.6 Sol/Terra/Luna and the two Cyber rows added; gpt-6-sol, gpt-6-luna and gpt-6.1-sol added from OpenAI's pages (not yet upstream). The three gpt-5.5 rows OpenAI never listed (-codex, -mini, -nano) are removed. - Dated rates: Sol at $5 / $30 before 2026-08-21, Terra at $2.50 / $15 and Luna at $1 / $6 before 2026-07-30. The scanner prices a day of Codex usage at the rate in force at that day's local noon. - Pricing aliases (gpt-5.6, gpt-reserve, the Daybreak names) choose a row without renaming the model, so no cached key changes. - The 272K long-context tier is in the table and in requestCostUSD for per-request pricing; day sums stay at standard rates. - Approximate class: the scanner records when a Codex or Claude model was charged at a neighbour's rate (DailyEntry.priceIsApproximate). CostCoverage counts those tokens as approximate, the cost badge gains an "Approximate" state, and the Mac cost card, Providers bar and provider cards show those figures as "≈$…", with a line naming the models. Six languages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rovider card's windows Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st the Claude side through the scanner
- Strings, all six languages: the summary line no longer reuses the Partial
and Estimated badges' words ("≈ N model(s) priced at a related model's
rate"), and the help no longer promises the "closest" model, a price
"yet", or a charge. Korean badge: 근사치. Translator comments match.
- "≈" on the Overview's top Cost Today card when it shows the local today
total, and on the API-equivalent half of the subscription line.
- A tooltip list cut at six models ends in "…".
- By Model cost column shrinks instead of wrapping when "≈" adds a character.
- A dated spelling of an alias (gpt-5.6-2026-08-01) is the alias's exact
price; the stored name is unchanged.
- Tests: an unpriced Claude model borrows its family's rate through the
scanner and is marked; earlier rates run oldest first and belong to a
model with a row today; the summary shares no word with the other badges.
- The file header says why Fast pricing is not ported (the rollout JSONL
has no service tier; upstream reads the CLI's trace database), a dated
reminder sits by Sol's promotional rate, and an owner-machine figure is
gone from a comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
33e21a4 to
c660aa2
Compare
|
Rebased onto Conflicts. Only
No Checks on
The full matrix runs on CI. Still true for the merge train:
🤖 Generated with Claude Code |
…rices each request as it is read #625's five commits (74f2a18..bff04cd), squashed and rebased onto main after 1.55 was cut and #623 landed. The counting rules, fixtures, replica and comparison script are unchanged. The pricing half is rebuilt on #623's CodexPricingTable, following the six steps in #623's merge-order notes: - Each token_count event is priced at CodexPricingTable.rates(forKey:at:) for the event's own time, through codexPriceResolution (aliases and dated aliases included). CodexModel, its longContext tier, codexEarlierRates, ratesInEffect and codexRequestCostUSD are gone. - codexEventCostUSD keeps its rule on the shared table: the event's own last_token_usage decides the long-context tier (a new tierInputTokens argument on CodexPricingTable.requestCostUSD), and growth beyond that request is billed at base rates (aggregateCostUSD). - Pricing.codexCostUSD prices one request at a moment (at:), so the local-noon rule (codexPricingDate) and the read-time repricing in entriesFromCodexCache and computeCodexCost are gone. Rows carry their stored cost in slot 3; priceIsApproximate is still resolved from the name on read. - The Codex fingerprint covers every field of CodexPricingTable.current, the superseded list in lookup order, the aliases, and how a fixed name list (now with alias and dated-alias spellings) resolves. The pin is updated; the Codex rules version stays 5 (not shipped). - test_codexCacheRowsCarryNoCost is replaced by its inverse: rows carry their requests' cost, and a repriced row, a tier rate, a new row, a dated rate's end and an alias each move the fingerprint. - The comments on both cache versions say which changes need which bump. #623's end-to-end tests keep their assertions (1M Sol tokens $5 then $4, Luna $1 then $0.20, gpt-5.7 at Sol's rate and marked approximate); their fixture now writes requests of at most 250K, since a single 1M request is long-context. New end-to-end tests: two Sol requests a minute either side of the repricing are each charged their own rate, and a 300K request pays the long-context rate while three 100K requests do not. The gpt-5.5 rate tests price ten 100K requests for the same reason. The replica reads the row names from CodexPricingTable.swift, with a test that it finds them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rices each request as it is read (#625) #625's five commits (74f2a18..bff04cd), squashed and rebased onto main after 1.55 was cut and #623 landed. The counting rules, fixtures, replica and comparison script are unchanged. The pricing half is rebuilt on #623's CodexPricingTable, following the six steps in #623's merge-order notes: - Each token_count event is priced at CodexPricingTable.rates(forKey:at:) for the event's own time, through codexPriceResolution (aliases and dated aliases included). CodexModel, its longContext tier, codexEarlierRates, ratesInEffect and codexRequestCostUSD are gone. - codexEventCostUSD keeps its rule on the shared table: the event's own last_token_usage decides the long-context tier (a new tierInputTokens argument on CodexPricingTable.requestCostUSD), and growth beyond that request is billed at base rates (aggregateCostUSD). - Pricing.codexCostUSD prices one request at a moment (at:), so the local-noon rule (codexPricingDate) and the read-time repricing in entriesFromCodexCache and computeCodexCost are gone. Rows carry their stored cost in slot 3; priceIsApproximate is still resolved from the name on read. - The Codex fingerprint covers every field of CodexPricingTable.current, the superseded list in lookup order, the aliases, and how a fixed name list (now with alias and dated-alias spellings) resolves. The pin is updated; the Codex rules version stays 5 (not shipped). - test_codexCacheRowsCarryNoCost is replaced by its inverse: rows carry their requests' cost, and a repriced row, a tier rate, a new row, a dated rate's end and an alias each move the fingerprint. - The comments on both cache versions say which changes need which bump. #623's end-to-end tests keep their assertions (1M Sol tokens $5 then $4, Luna $1 then $0.20, gpt-5.7 at Sol's rate and marked approximate); their fixture now writes requests of at most 250K, since a single 1M request is long-context. New end-to-end tests: two Sol requests a minute either side of the repricing are each charged their own rate, and a 300K request pays the long-context rate while three 100K requests do not. The gpt-5.5 rate tests price ten 100K requests for the same reason. The replica reads the row names from CodexPricingTable.swift, with a test that it finds them. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…re on main #623 (Codex at OpenAI's list prices) and #625 (every rollout counted once, subagents included) landed before this pull request, so on main the tripwires require both reasons: testThePublishedPricesLineMatchesThePriceTable and testTheSubagentLineMatchesTheScanner fail with Reason.shipped holding only .cachedInputCountedOnce. Reason.shipped now lists all three, in the order the card shows them. Their text was already in all six catalogues. Also pins the clock the rebase unified: record() without a now takes Claude Code's cleanup reach from the manager's clock, the one the note is dated by (testRecordTakesTheCleanupReachFromTheManagersClock). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…c that Codex figures changed (#622) * Count Codex cached input once in the usage history, and say on the Mac that Codex figures changed The usage history adds input + cached + output for each day. Claude's input leaves cache out, so that counts each token once. Codex's input already includes its cached input, the way OpenAI reports it, so the history counted Codex's cached input twice: on the Mac's archive (scan and cloud fill) and on the iPhone's, which rebuilds from the server's rows. - ArchiveTokenBasis, ScanEntry(archiving:) and CloudEntry(archiving:) take the cached share out of Codex's input where rows enter the archive. The archive's sum and its stored version are unchanged: a version bump makes load() return an empty archive. - The iPhone's fold is AppState.usageArchive(fromCloudRows:), testable, and corrects every day of the year because the server keeps the split. Days the Mac recorded before cannot be corrected in place; a scan recounts the days it reads. - CodexEstimateChangeNote: a dated card at the top of the Mac's Overview, dismissible, gone after 30 days, only for a Mac that had recorded Codex usage before this version first wrote the archive. It says what changed, one line per reason, and which older days keep the old figures. Only Reason.shipped is shown; tripwire tests hold each reason to the scanner, the price table and the archive in both directions. - Five strings in six languages, reusing the dashboard's cost disclaimer and "Got it". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * The price line does not say every Codex model has a published price A model with no published price still borrows a neighbour's rate, so "the API prices OpenAI publishes for each model" said more than the change it describes. Six languages. The recount doc now says the Mac's cloud fill has no caller today. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Track the Codex days still counted the old way, say so in the Usage Dashboard, and count each token once in the coverage share Review of the first version found that on most updated Macs, Codex days older than the routine month never get recounted: the year-long read ran before 1.55 for everyone whose scan worked, and it does not run again. The card's "days before X" line came from the earliest day any scan returned, so a Codex day a scan had no entry for (log deleted, or an unreadable Codex folder in the App Store build) kept its double count while the line said nothing older was left, and the card itself goes after 30 days. - The note keeps the old days themselves (oldCodexDays): every Codex day the archive held when it started, less each day whose Codex share a later write took from a read or the cloud. mergeScanEntries, mergeScanEntriesByProvider and mergeCloudDays return those days: every day replaced whole, and a day merged provider by provider (the year-long read, the routine read's oldest day) only where the read has Codex entries for it, since otherwise it keeps its stored Codex slice. A day folded into its month stays old; months folded before the change are not guessed at. - DailyUsageArchiveManager.record reads the manager's clock when no now is passed, so the cleanup reach and the note's date come from one clock. - The Usage Dashboard shows a line under the heatmap for as long as old days remain, card or no card, dated from the last old day, with a "some" wording when days counted the new way lie among them. - A reason that ships in a later version starts a note of its own, with its own date and its own "Got it" (the dismissal now stores the note's id). - CostCoverage.from and the scanner's unpriced log line count each token once (ArchiveTokenBasis.tokens), so Codex cached input no longer weighs twice in "priced N% of tokens". - Wording from the language review in all six languages: the prices line is about cost estimates, not prices; "now" on each reason; zh-Hans uses 子 Agent; the history lines are new keys (history_through / history_some_through), plus dashboard_through / dashboard_some_through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Show the subagent and published-price lines, now that #623 and #625 are on main #623 (Codex at OpenAI's list prices) and #625 (every rollout counted once, subagents included) landed before this pull request, so on main the tripwires require both reasons: testThePublishedPricesLineMatchesThePriceTable and testTheSubagentLineMatchesTheScanner fail with Reason.shipped holding only .cachedInputCountedOnce. Reason.shipped now lists all three, in the order the card shows them. Their text was already in all six catalogues. Also pins the clock the rebase unified: record() without a now takes Claude Code's cleanup reach from the manager's clock, the one the note is dated by (testRecordTakesTheCleanupReachFromTheManagersClock). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Merge order: this PR first, then #625 rebases onto it. The two build different Codex pricing mechanisms in the same code; see Merge order with #625 for what that rebase has to reconcile.
What people saw
Every Codex model newer than gpt-5.4 was charged gpt-5.4's rate. The Mac's Codex table ended at a
gpt-5.5row that copied gpt-5.4's $2.50 / $15 (per 1M tokens), with a note to replace it "when official".gpt-6-astraand the threegpt-5.6models had no row, so the version fallback added in 1.50 charged them the newest base row at or below their version, which was that samegpt-5.5row. Against OpenAI's list prices:gpt-6-astragpt-5.5gpt-5.6-solgpt-5.6-terragpt-5.6-lunaSo Astra read about a quarter of its list price and Luna about twelve times it.
A price that changed was charged at one rate for every day. OpenAI cut Sol from $5 / $30 to $4 / $20 on 2026-08-21, and Terra by 20% and Luna by 80% on 2026-07-30. One rate per model misprices every day on the other side of the cut.
A borrowed rate looked like a listed one. When a model has no row, both fallbacks (Codex by version, Claude by family) charge a neighbour's rate. That beats the $0 such a model used to read, but the scanner threw away the fact that the rate was borrowed. The figure reached the card looking like a listed price, under a badge that said "Exact".
What changed
The table (
CodexPricingTable.swift, new, CLIPulseCore, shared). The Codex rows come from CodexBar'sSources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swiftat upstream commit25bba9b7, with the full MIT notice and a header listing what is not verbatim. Every row for a model that OpenAI's pricing page lists was re-checked against https://developers.openai.com/api/docs/pricing and the model pages on 2026-09-30, and each row's comment cites its source.gpt-5.5andgpt-5.5-proat OpenAI's rates.gpt-6-astra,gpt-5.6-sol,-terra,-luna,gpt-5.6-cyberandgpt-5.5-cyber. Long-context rates are carried on every model OpenAI publishes them for.gpt-6-sol,gpt-6-lunaandgpt-6.1-sol, released after25bba9b7(OpenAI changelog, 2026-09-22 and 2026-09-29). Without rows they would borrow Astra's rate, five times theirs. Also ours: the long-context tier on the two-prorows, which OpenAI lists and upstream leaves out.gpt-5.5-codex,gpt-5.5-miniandgpt-5.5-nano. OpenAI lists none of them; they were gpt-5.4's rates copied. Those names now borrow a neighbour's rate (gpt-5.5,gpt-5.4-mini,gpt-5.4-nano) and are marked approximate.Dated rates.
CodexPricingTable.supersededholds Sol at $5 / $30 before 2026-08-21 and Terra at $2.50 / $15 and Luna at $1 / $6 before 2026-07-30. The instants are 00:00 UTC, as upstream dates them. The scanner sums Codex usage per local day before pricing it, so a day is charged the rate in force at its local noon (Pricing.codexPricingDate), which is the rate that applied for most of that day in any time zone. Codex rows in the cache hold tokens only and are priced on every read, so the dated rates reach every cached day without a rescan and without acostUsageCachePricingVersionbump. A test fails the day a Codex row starts storing a cost (test_codexCacheRowsCarryNoCost), and the comment on the version constant now says when a Codex change does need a bump.Aliases pick a row without renaming the model.
gpt-5.6(Sol),gpt-reserve(Luna) and the two Daybreak names are priced as their targets, at an exact price. Upstream renames the model innormalizeCodexModel; here the name Codex wrote is kept, so no cached key changes and the model's history is not split between two names.The 272K tier is in the table and not yet applied.
CodexPricingTable.requestCostUSDcharges a request over 272K input tokens the long-context rates on every token, as OpenAI's pages describe. The scanner still prices day sums (aggregateCostUSD), and a sum never takes the tier, since testing the threshold against a day's total would put every busy day on long-context rates. It applies once usage is priced request by request.Approximate prices are marked "≈".
Pricing.codexPriceResolutionandclaudePriceResolutionreturn the row a model is charged at and whether it was borrowed. The scanner records that onDailyEntry.priceIsApproximate.CostCoveragecounts those tokens as approximate: they carried a rate, so they still count toward "Priced N% of tokens", but not as exact. On the Mac:CostSummary.todayCoverage), so a borrowed rate last week does not mark today's figure;CostSummary.todayFigureIsApproximate), and the API-equivalent half of a subscription's "$api / $sub" line (providerFigureIsApproximate), so one screen never shows the same figure with and without "≈";Three new keys in all six languages. The summary deliberately shares no word with the Partial or Estimated badge, which it used to be built from; a test holds that in every language. zh-Hant: 價格資料 and 單價 as in
cost.coverage_helpandcost.coverage_summary. Korean badge: 근사치 (근사 alone also reads as "splendid"). "≈" is a symbol and stays as is in every language, like "$".Where "≈" will show first: Claude. Claude models newer than the Claude table (a new minor version such as Opus 5.5 or Fable 5.1) go through the family fallback, so they are the likeliest figures to carry "≈" on a real Mac. Their borrowed rates can be wrong where Anthropic's price differs from the family's newest row, most visibly on cache reads. Adding those Claude rows is a separate change in 1.56; this PR only makes the borrowing visible.
Dated spellings of an alias.
gpt-5.6-2026-08-01and the like are priced as the alias's target at an exact price (CodexPricingTable.aliasTarget).normalizeCodexModelis unchanged, so the stored name is still the one Codex wrote.Both Mac builds, iPhone, Watch. The scanner and the table are shared by the Mac App Store and Developer ID builds; nothing here is build-specific. iPhone and Watch get cost from the cloud, which is a server estimate: coverage there is unknown by design, so they show no "≈". Their figures change as Macs on 1.56 upload days priced with the new table.
How it is tested
CodexPricingTableTests(new, 24 tests)gpt-6.1-sol), cache write 1.25x, and above 272K 2x input and cache and 1.5x output.rates(forKey:at:)relies on).gpt-5.7session is priced at Sol's rate, keeps its name, and reachesCostCoverageand the badge as approximate.claude-opus-6response in a Claude Code project log is priced at Opus 5's rate, keeps its name and is approximate; aclaude-opus-5one is exact;CostCoveragenames Claude.ApproximatePriceCoverageTests(new, 15 tests) andApproximatePriceWiringTests(new, 3 tests)AppState: a scan with a borrowed rate three days ago and a listed one today gives an approximate 30-day coverage, an exact today and the Approximate badge; a borrowed rate today marks today; the provider card's Today and This Week helpers, per provider, with an entry outside the rolling week.CodexPricingGpt55Tests(OpenAI's rate forgpt-5.5; the removed rows borrow and say so),Claude5PricingTests(the unknown model in the Codex fallback tests is nowgpt-5.7, sincegpt-5.6-solhas a row; the tie-break test now matters for price, as the four 5.6 rows differ up to 60x),CostCoverageTests(four badge states).swift test(the suites above plusClaudeTieredAndCodexEdgeCostTests,UnpricedCoverageTests,CostUsageScanResultTests,ClaudePricingOpus47Tests,CostUsageCacheIOTests), then the twoApproximatePrice…suites withCostSummaryFallbackTests-Xswiftc -DDEVID_BUILDNegative controls (each on a throwaway CI branch, deleted afterwards):
Four branches, each this PR's head plus the breaks listed. The first carries seven breaks at once and the fourth eight, because the tests each one should trip do not overlap (the two breaks that share a test say so); every test that failed on a branch is one listed for a break on that branch.
test_solChargesItsOldRateUntilTheSecondItWasRepriced,test_codexCostUsesTheRateOfTheDateItIsGiven,test_aScannedDayIsChargedTheRateInForceThatDaytest_aBorrowedRateIsMarkedApproximate,test_aScannedModelWithNoRowIsPricedApproximatelyAndSaysSo,testUnlistedGpt55VariantsBorrowARateAndSaySotest_anAliasIsAnExactPriceAndKeepsItsNametest_aDayIsChargedTheRateInForceForMostOfIttestTheTextInChinese, and the Apple strings parity gate (run on this Mac)testTheScanReachesTheCardsCoveragetest_codexCacheRowsCarryNoCostCostCoverage.fromcounts borrowed rates as priced (second branch)ApproximatePriceCoverageTests, both scan tests inApproximatePriceWiringTests, and the scanner's end-to-end approximate testtest_aSumNeverTakesTheLongContextTier,test_publishedRatesPerMillionTokens, and every other test that prices a 1M-token sumtest_aScannedClaudeModelWithNoRowIsPricedApproximatelyAndSaysSotest_aDatedSpellingOfAnAliasIsTheAlias,test_aDatedAliasIsAnExactPriceAndKeepsItsNametest_everyModelWithEarlierRatesHasARowToday, andtest_tieredRowsFollowOpenAIsDocumentedMultipliers(its row count)test_eachModelsEarlierRatesRunOldestFirst, and the same row counttestTheTopCardsTodayIsMarkedOnlyWhenItIsTheLocalFiguretestAProvidersFigureIsMarkedOnlyForThatProvidertestATooltipListSaysWhenItIsCuttestTheSummaryDoesNotBorrowTheOtherBadgesWordsDeliberately not done
requestCostUSDwith each request's own timestamp. Until then, a day that straddles a repricing takes the rate in force for most of it, and requests over 272K are charged standard rates.feedback_log_bodyin itslogstable) and charges them OpenAI's Fast rates (2x, 2.5x forgpt-5.5). So on a day with Fast turns CodexBar's cost is higher than ours; a day-by-day comparison with CodexBar should list that as a known difference.cli-pulse-desktopcarries its own table (pricing.rs), which must mirror these rows (the three removedgpt-5.5rows, the added rows and the dated rates). That is its own pull request in that repository (fix(cost): price Codex per request at OpenAI's current and dated rates, and count each rollout's tokens once cli-pulse-desktop#10). It already prices each request at its own timestamp with the 272K tier, so until Codex usage counts every rollout file once, subagents included, and prices each request as it is read #625 lands the Mac (day sums at local noon, no tier) and the desktop app can differ on a day that straddles a repricing or has requests over 272K; they converge once Codex usage counts every rollout file once, subagents included, and prices each request as it is read #625 prices per request. The desktop app has no Codex version fallback, so a model with no row (saygpt-5.7) has no price there and "≈" Sol's on the Mac; that difference predates this PR.gpt-5.4-codex: they keep borrowing a neighbour's rate, now marked "≈".Merge order with #625
#625 (Codex accounting) also changes the Codex pricing path, with a different design: it keeps
CodexModelwith a long-context tier and its own dated rates, storescostNanosin each Codex cache row, and adds a Codex cache version and fingerprint. This PR prices token-only rows on every read and asserts they stay token-only. The two conflict inCostUsageScanner.swiftandCostUsageCache.swift, and resolving the markers is not enough.This PR lands first: it carries the table checked against OpenAI and the "≈" class, and its end-to-end tests (Sol $5 then $4 and Luna $1 then $0.20 through the scanner,
gpt-5.7marked "≈") then guard #625's rebase. The rebase of #625:CodexPricingTable.rates(forKey:at:)at the event's time andCodexPricingTable.requestCostUSD, resolving the row throughPricing.codexPriceResolution(aliases and dated aliases included), and drops its ownCodexModel.longContextandcodexEarlierRates;CodexPricingTable.current,supersededandaliases: an alias change moves which rate a storedcostNanosused without changing any rate row;Pricing.codexPricingDate(the local-noon rule) and the read-time repricing inentriesFromCodexCache;test_codexCacheRowsCarryNoCostwith its inverse: a table change changes the Codex fingerprint, so cached days are read again;costUsageCachePricingVersion;priceIsApproximateresolved from the model name on read, which stays consistent because the fingerprint covers the key set.🤖 Generated with Claude Code