Skip to content

Price Codex at OpenAI's list rates on the day they applied, and mark a borrowed rate with "≈" - #623

Merged
JasonYeYuhe merged 3 commits into
mainfrom
p156-codex-pricing
Oct 3, 2026
Merged

JasonYeYuhe merged 3 commits into
mainfrom
p156-codex-pricing

Conversation

@JasonYeYuhe

@JasonYeYuhe JasonYeYuhe commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

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.5 row that copied gpt-5.4's $2.50 / $15 (per 1M tokens), with a note to replace it "when official". gpt-6-astra and the three gpt-5.6 models 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 same gpt-5.5 row. Against OpenAI's list prices:

model OpenAI list (input / cached / output) charged until now
gpt-6-astra $10 / $1 / $50 $2.50 / $0.25 / $15
gpt-5.5 $5 / $0.50 / $30 $2.50 / $0.25 / $15
gpt-5.6-sol $4 / $0.40 / $20 $2.50 / $0.25 / $15
gpt-5.6-terra $2 / $0.20 / $12 $2.50 / $0.25 / $15
gpt-5.6-luna $0.20 / $0.02 / $1.20 $2.50 / $0.25 / $15

So 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's Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift at upstream commit 25bba9b7, 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.5 and gpt-5.5-pro at OpenAI's rates.
  • Rows added: gpt-6-astra, gpt-5.6-sol, -terra, -luna, gpt-5.6-cyber and gpt-5.5-cyber. Long-context rates are carried on every model OpenAI publishes them for.
  • Ours, not upstream's: gpt-6-sol, gpt-6-luna and gpt-6.1-sol, released after 25bba9b7 (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 -pro rows, which OpenAI lists and upstream leaves out.
  • Removed: gpt-5.5-codex, gpt-5.5-mini and gpt-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.superseded holds 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 a costUsageCachePricingVersion bump. 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 in normalizeCodexModel; 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.requestCostUSD charges 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.codexPriceResolution and claudePriceResolution return the row a model is charged at and whether it was borrowed. The scanner records that on DailyEntry.priceIsApproximate. CostCoverage counts those tokens as approximate: they carried a rate, so they still count toward "Priced N% of tokens", but not as exact. On the Mac:

  • the cost badge has a fourth state, "Approximate", between Exact and Partial. Partial still wins when some tokens had no rate at all;
  • the Overview's Today, 30-day, per-provider, By Model and API-equivalent figures, the Providers tab's cost bar and each provider card's Today and This Week cost read "≈$…" when part of them was charged a borrowed rate. Today has its own coverage (CostSummary.todayCoverage), so a borrowed rate last week does not mark today's figure;
  • so do the Overview's top "Cost Today" card, when it shows the local today total rather than the server's (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 "≈";
  • a line under the API-equivalent row, "≈ N model(s) priced at a related model's rate", with help naming the models ("…" when the list is cut at six).

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_help and cost.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-01 and the like are priced as the alias's target at an exact price (CodexPricingTable.aliasTarget). normalizeCodexModel is 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)
    • Published rates per 1M tokens for twelve rows.
    • Every GPT-5.6 and GPT-6 row, the dated ones included, against OpenAI's documented multipliers: cached 10% of input (5% on gpt-6.1-sol), cache write 1.25x, and above 272K 2x input and cache and 1.5x output.
    • The repricing instants equal OpenAI's dates at 00:00 UTC.
    • One boundary test per repricing (one second before, and at the instant).
    • A 272,000-token request is short context and 272,001 moves every token; a day's sum never takes the tier; cached and cache-write tokens are clamped inside input.
    • Every alias names a row. An alias, or a dated spelling of one, is an exact price and keeps its name; a missing row is approximate for Codex and Claude; a model with no neighbour stays unpriced.
    • Every model with earlier rates has a row today, and its earlier rates run oldest first (what rates(forKey:at:) relies on).
    • Which day gets which rate in Tokyo and Los Angeles.
    • End to end through the scanner: 1M Sol tokens two days either side of 2026-08-21 cost $5 and $4, and Luna either side of 2026-07-30 $1 and $0.20. A gpt-5.7 session is priced at Sol's rate, keeps its name, and reaches CostCoverage and the badge as approximate.
    • The Claude side through the scanner: a claude-opus-6 response in a Claude Code project log is priced at Opus 5's rate, keeps its name and is approximate; a claude-opus-5 one is exact; CostCoverage names Claude.
    • Codex cache rows hold three numbers.
  • ApproximatePriceCoverageTests (new, 15 tests) and ApproximatePriceWiringTests (new, 3 tests)
    • A borrowed rate is approximate, not priced, and still counts as having a rate.
    • Ordering of the model list, and the providers it names.
    • An unpriced entry is never approximate, and a server estimate never claims approximate prices.
    • The badge: Approximate, and Partial when both are present.
    • Today's coverage is separate from the 30 days'.
    • The "≈" formatter.
    • The three strings asserted in zh-Hans, and in every language the mark, the count and the model list; in every language the summary shares no word with the Partial or Estimated badge.
    • A tooltip list cut at six models ends in "…".
    • Outside the card: a provider's 30-day figure is marked only for that provider and only from a local scan; the top card's Cost Today only when it is the local today total.
    • Wiring through 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.
  • Updated: CodexPricingGpt55Tests (OpenAI's rate for gpt-5.5; the removed rows borrow and say so), Claude5PricingTests (the unknown model in the Codex fallback tests is now gpt-5.7, since gpt-5.6-sol has a row; the tie-break test now matters for price, as the four 5.6 rows differ up to 60x), CostCoverageTests (four badge states).
run where result
filtered swift test (the suites above plus ClaudeTieredAndCodexEdgeCostTests, UnpricedCoverageTests, CostUsageScanResultTests, ClaudePricingOpus47Tests, CostUsageCacheIOTests), then the two ApproximatePrice… suites with CostSummaryFallbackTests this Mac, on the first head (a383551); the review fixes ran on CI only, as this Mac is short of disk during the 1.55 release 140 tests, then 20, 0 failures
gates: Apple strings parity (1,763 keys x 6), hardcoded UI strings, zh-Hant terms, copyright, duplicate sources, internal docs, personal-info mask, SwiftLint on the changed files this Mac, on this head all pass
CLIPulseCore full suite, plain CI, this head (run 36751136593) 3661 tests, 0 failures (4 skipped); the Japanese, ROC and Buddhist calendar runs 0 failures each. Every test in the four new or rewritten suites, the nine added in review included, ran and passed in all five passes
CLIPulseCore full suite, -Xswiftc -DDEVID_BUILD CI, this head 3698 tests, 0 failures (6 skipped)
the six schemes (CLI Pulse Bar, iOS, Watch, Widgets, CLIPulseHelper, CLIPulse QA) CI all build
HelperSwift and SensorProbe package tests, SwiftLint, repo hygiene, secret scan CI pass
MAS archive check and signed Developer ID app build CI both pass

Negative 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.

break tests that failed
Sol's earlier rate never applies (its end moved to the distant past) test_solChargesItsOldRateUntilTheSecondItWasRepriced, test_codexCostUsesTheRateOfTheDateItIsGiven, test_aScannedDayIsChargedTheRateInForceThatDay
a borrowed Codex rate reported as exact test_aBorrowedRateIsMarkedApproximate, test_aScannedModelWithNoRowIsPricedApproximatelyAndSaysSo, testUnlistedGpt55VariantsBorrowARateAndSaySo
an alias renames the model, as upstream does test_anAliasIsAnExactPriceAndKeepsItsName
a day priced at its midnight instead of its noon test_aDayIsChargedTheRateInForceForMostOfIt
the zh-Hans summary string removed testTheTextInChinese, and the Apple strings parity gate (run on this Mac)
today's coverage built from all 30 days testTheScanReachesTheCardsCoverage
a Codex cache row gains a fourth number test_codexCacheRowsCarryNoCost
CostCoverage.from counts borrowed rates as priced (second branch) the approximate, badge and today tests in ApproximatePriceCoverageTests, both scan tests in ApproximatePriceWiringTests, and the scanner's end-to-end approximate test
day sums take the 272K tier (third branch) test_aSumNeverTakesTheLongContextTier, test_publishedRatesPerMillionTokens, and every other test that prices a 1M-token sum
the Claude entries rebuilt from the cache drop the "≈" flag (fourth branch, review fixes; CI run 36751224552: 3,661 tests, 14 assertion failures, all in the ten tests in these eight rows) test_aScannedClaudeModelWithNoRowIsPricedApproximatelyAndSaysSo
a dated spelling of an alias is not recognised test_aDatedSpellingOfAnAliasIsTheAlias, test_aDatedAliasIsAnExactPriceAndKeepsItsName
a model with earlier rates and no row today test_everyModelWithEarlierRatesHasARowToday, and test_tieredRowsFollowOpenAIsDocumentedMultipliers (its row count)
a model's earlier rates out of order test_eachModelsEarlierRatesRunOldestFirst, and the same row count
the top card's Cost Today marked whatever figure it shows testTheTopCardsTodayIsMarkedOnlyWhenItIsTheLocalFigure
a provider's figure marked from a server summary testAProvidersFigureIsMarkedOnlyForThatProvider
a cut tooltip list without "…" testATooltipListSaysWhenItIsCut
the Spanish summary back to the Estimated badge's word testTheSummaryDoesNotBorrowTheOtherBadgesWords

Deliberately not done

  • Pricing each request. The long-context tier and exact repricing instants need usage priced per request; the scanner sums per day first. That is Codex usage counts every rollout file once, subagents included, and prices each request as it is read #625, which rebases onto this PR and calls requestCostUSD with 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.
  • "≈" on the cost forecast. The forecast mixes the cloud's daily rows with this scan's days and already carries its own "Estimate" badge; it would need to know which of the days it took from the scan had a borrowed rate. Left as is.
  • The Claude table. Adding the Claude models that have no row yet, at Anthropic's rates, is a separate PR (see "Where ≈ will show first").
  • "≈" outside the Mac cost card and Providers tab. The usage archive (Usage Dashboard, heatmap), the Sessions tab and the cloud rows (iPhone, Watch, widgets) store a cost without a price basis, so they cannot tell a borrowed rate from a listed one. They keep their existing "estimate" labels.
  • Rendered UI. The app targets have no test bundle and the offscreen QA renderer has no local-scan scenario, so the views are covered by the CLIPulseCore logic they read and by CI building all six schemes, not by a render.
  • Not ported from upstream: models.dev lookups, the custom-pricing overlay, API Fast (priority) multipliers and the pricing fingerprint. On Fast: the rollout JSONL this app reads does not carry a request's service tier. Upstream finds Fast turns in the Codex CLI's trace database (feedback_log_body in its logs table) and charges them OpenAI's Fast rates (2x, 2.5x for gpt-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.
  • The desktop app. cli-pulse-desktop carries its own table (pricing.rs), which must mirror these rows (the three removed gpt-5.5 rows, 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 (say gpt-5.7) has no price there and "≈" Sol's on the Mac; that difference predates this PR.
  • Sol's promotional rate. OpenAI calls Sol's $4 / $20 promotional "at least through November 21, 2026". The table cannot hold a change that has not happened, so the row carries a dated reminder to re-check it then.
  • Names OpenAI does not list, such as 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 CodexModel with a long-context tier and its own dated rates, stores costNanos in 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 in CostUsageScanner.swift and CostUsageCache.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.7 marked "≈") then guard #625's rebase. The rebase of #625:

  1. prices each request with CodexPricingTable.rates(forKey:at:) at the event's time and CodexPricingTable.requestCostUSD, resolving the row through Pricing.codexPriceResolution (aliases and dated aliases included), and drops its own CodexModel.longContext and codexEarlierRates;
  2. builds its Codex fingerprint from CodexPricingTable.current, superseded and aliases: an alias change moves which rate a stored costNanos used without changing any rate row;
  3. deletes Pricing.codexPricingDate (the local-noon rule) and the read-time repricing in entriesFromCodexCache;
  4. replaces test_codexCacheRowsCarryNoCost with its inverse: a table change changes the Codex fingerprint, so cached days are read again;
  5. rewrites the comment on costUsageCachePricingVersion;
  6. keeps priceIsApproximate resolved from the model name on read, which stays consistent because the fingerprint covers the key set.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings September 30, 2026 14:33

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@JasonYeYuhe

Copy link
Copy Markdown
Collaborator Author

Merge train with #622 (all three after 1.55 is cut).

#622 adds CodexEstimateChangeTripwireTests, which hold each line of the Mac's "Codex figures changed" note to the change behind it. This PR moves the Codex price table off the interim rows, so once both are on main, testThePublishedPricesLineMatchesThePriceTable fails with "the Codex price table changed: add .publishedPrices to Reason.shipped". CI with #622 merged into this PR (at a383551) showed exactly that one failure and nothing else.

Before merging this PR on a main that contains #622:

  1. Rebase onto main.
  2. Add .publishedPrices to CodexEstimateChangeNote.Reason.shipped (its text is already in all six catalogues).
  3. Re-run CI, and merge only when it is green. PR CI does not re-run by itself when a sibling lands.

Also from #622, now pushed (5adc088): CostCoverage.from and CostUsageScanner.reportUnpricedModels count each token once (ArchiveTokenBasis.tokens(of:)), so Codex's cached input no longer weighs twice in "priced N% of tokens". The approximate share this PR adds uses the same tokens local, so it follows without changes here. git merge-tree of #622 with this head (33e21a4) is clean. This PR and #625 conflict with each other in CostUsageCache.swift and CostUsageScanner.swift.

@JasonYeYuhe

Copy link
Copy Markdown
Collaborator Author

Review fixes are in 33e21a4. The merge is still on hold until 1.55 is cut.

Applied

Skipped

  • Resolving the Codex price once per row on read (nit f). It is optional, the lookup is cheap, and Codex usage counts every rollout file once, subagents included, and prices each request as it is read #625's rebase replaces that read path; leaving it avoids adding to the conflict area.
  • The companion Claude-table PR (Opus 5.5, Fable 5.1, Sonnet 5.5, Sonnet 5 at $2 / $10). This is a separate PR outside this one's scope. I flagged it rather than opening it here so it does not get built twice.
  • Rendering the By Model column (see above).

Verification

  • Local on this head: Apple strings parity (1,763 keys x 6), hardcoded UI strings, zh-Hant terms, copyright, internal docs, duplicate sources, personal-info mask, SwiftLint on the changed files (the one implicitly_unwrapped_optional warning in CodexPricingTableTests is fixed too) and swiftc -parse: all pass. I did not run swift test locally, because this Mac is short of disk during the 1.55 release. CI is the runner.
  • Negative controls: eight breaks on nc/p156-codex-pricing-4, CI run 36751224552. 3,661 tests ran with 14 assertion failures, all in exactly the ten tests listed for those breaks; the table is in the body. The branch is deleted.
  • CI on 33e21a4: Swift CI run 36751136593 is green across the full matrix. CLIPulseCore ran 3,661 tests with 0 failures in the plain pass and in each of the three calendar passes, and 3,698 with 0 failures under -DDEVID_BUILD; all nine new tests passed in all five passes. The six schemes, the MAS archive check and the signed Developer ID build all pass. The CI Gate timed out while runners were queued and passed on rerun.

🤖 Generated with Claude Code

JasonYeYuhe and others added 3 commits October 3, 2026 17:23
…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>
@JasonYeYuhe

Copy link
Copy Markdown
Collaborator Author

Rebased onto main after 1.55 was cut (ad256465, #633). The new head is c660aa2e, and the hold line is gone from the body.

Conflicts. Only CLI Pulse Bar/OverviewTab.swift conflicted: four hunks across two of the three commits. In every one, 1.55 had changed the figure's font from design: .monospaced to .monospacedDigit(), and this PR had wrapped the same Text in CostFormatter.format(…, approximate:). Each resolution keeps both:

  • the provider rows of the cost breakdown,
  • the 30-day API-equivalent row,
  • the By Model column, which keeps .lineLimit(1).minimumScaleFactor(0.75). Its comment now says "≈" can overflow 55 pt, because the 1.55 font sets only the digits at a fixed width,
  • the $api / $sub subscription line.

No design: .monospaced is left in the file, and no line in the file differs from main except this PR's own changes. Everything else merged without conflict, including the six Localizable.strings files after #640's catalogue polish, AppState, DataRefreshManager, Components and L10n. CostUsageScanner.swift and CostUsageCache.swift did not change on main after this PR branched.

Checks on c660aa2e, on this Mac:

  • Apple strings parity: 1,777 keys × 6, translation debt zero.
  • Hardcoded UI strings, zh-Hant terms, copyright, duplicate sources, internal docs, personal-info mask and migration numbers: all pass.
  • Filtered swift test, 267 tests, 0 failures. It ran the suites this PR adds or changes (CodexPricingTableTests, ApproximatePriceCoverageTests, ApproximatePriceWiringTests, CodexPricingGpt55Tests, Claude5PricingTests, CostCoverageTests). It also ran the suites next to them that 1.55 touched: CostSummaryFallbackTests, DemoMatchesProductionTests, DailyUsageArchiveByProviderMergeTests, DailyUsageClaudeCleanupEdgeTests, LocalScanConsentCopyTests, PrivacyCopyTruthTests, CostWordingCopyTests, CostUsageScanner* and the cost/coverage suites.

The full matrix runs on CI.

Still true for the merge train:

🤖 Generated with Claude Code

@JasonYeYuhe
JasonYeYuhe merged commit d978000 into main Oct 3, 2026
50 checks passed
@JasonYeYuhe
JasonYeYuhe deleted the p156-codex-pricing branch October 3, 2026 08:49
JasonYeYuhe added a commit that referenced this pull request Oct 3, 2026
…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>
JasonYeYuhe added a commit that referenced this pull request Oct 3, 2026
…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>
JasonYeYuhe added a commit that referenced this pull request Oct 3, 2026
…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>
JasonYeYuhe added a commit that referenced this pull request Oct 3, 2026
…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>
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.

2 participants