Skip to content

Port upstream 0.60.4: price OpenCodex rows by billing route, unpriced instead of zero (stacked on #682) - #731

Open
Finesssee wants to merge 4 commits into
port/micro-0.68.0-nous-opencodex-ledgerfrom
port/micro-0.60.4-opencodex-unpriced
Open

Finesssee wants to merge 4 commits into
port/micro-0.68.0-nous-opencodex-ledgerfrom
port/micro-0.60.4-opencodex-unpriced

Conversation

@Finesssee

Copy link
Copy Markdown
Collaborator

Stacked on #682 (port/micro-0.68.0-nous-opencodex-ledger). Review and merge #682 first.

Summary

With OpenCodex import on, each OpenCodex ledger row is now priced by its recorded billing route, as
in upstream 0.60.4. A row the price sources cannot cover stays unpriced: its tokens are kept, it adds
no dollars, and it counts in the unpriced coverage instead of as $0. This applies to Usage & Spend,
get_spend_contract and codexbar cost --format json.

Which catalog prices a row

  • The recorded provider is the billing provider. A legacy OpenAI-transport row
    (provider: "openai") uses its model prefix instead when the prefix is a known route: deepseek,
    kimi-coding, kimi-for-coding, openai, opencode, opencode-free or opencode-go.
  • The new core::models_dev_pricing_targets (upstream ModelsDevPricingTargetResolver) turns that
    into models.dev identities:
    • It maps x-ai to xai and strips one model prefix that repeats the provider.
    • kimi-coding also checks kimi-for-coding, and opencode-free also checks opencode.
    • Another vendor's namespace stays part of the model id. opencode-go + openai/gpt-5 is looked
      up as openai/gpt-5 in the opencode-go catalog and never borrows OpenAI's rates.
    • Empty or malformed ids resolve to nothing, so the row stays unpriced.
  • A direct OpenAI model (no slash) keeps the Codex convention: bundled rates first, request-day
    historical rates, then the catalog. Every other identity needs an exact models.dev entry and bills
    each token lane on its own. If a row used a cache lane that the catalog has no rate for, the row
    stays unpriced instead of borrowing the input rate.
  • Cache writes are priced once, at the cache-write rate. The pre-2026-07-30 GPT-5.6 Terra and Luna
    rates now include upstream's cache-write rates (3.125e-6 and 1.25e-6 per token; 6.25e-6 and
    2.5e-6 above 272K).
  • A row without both input and output tokens is unpriced. A token sum that overflows is unpriced.

Custom pricing (custom-pricing.json)

  • Overrides resolve in upstream order:

    1. The recorded provider and model.
    2. The billing route, for a row that resolves.
    3. Each catalog identity.

    Within each step, the bare model key wins over provider/model, as in upstream. This replaces the
    key-order divergence noted in Port upstream 0.68.0: attribute Nous OpenCodex ledger rows to Nous Portal spend #682.

  • The first matching override wins whole. A rate it lacks leaves the row unpriced; it's never filled
    from a later override or from the catalog. A model priced by an override keeps its custom-pricing
    flag even when its cost is unknown.

Refreshing prices

  • Before a fresh build, the models.dev catalog is refreshed (upstream refreshPricingIfNeeded) in
    two cases: it's stale (older than 24 h), or a priceable imported row's exact identity is missing
    from it.
  • It fetches at most once per cache path per 15 minutes, with a 20 s timeout. A failed or
    implausible fetch keeps the previous cache.
  • The refresh runs before:
    • a Usage & Spend rebuild (never on a cached read),
    • get_spend_contract with OpenCodex import,
    • codexbar cost --format json when OpenCodex import is on.

Upstream reference

  • Release 0.60.4, item 5: OpenCodex costs use the recorded provider's prices and attribution, refresh
    cached rates, honor custom-price overrides, and leave missing token counts or required prices
    unpriced instead of zero (fix(opencodex): price usage by its recorded provider steipete/CodexBar#3676). The audit tracks it as Gap 2; Port OpenCodex recorded-provider pricing from upstream 0.60.4 #516 ported the routing part.
  • Tag-pinned at v0.60.4, under Sources/CodexBarCore/Vendored/ unless noted:
    • OpenCodexUsage/: OpenCodexUsagePricing.swift (providerID(for:), targets(for:),
      OpenCodexUsageStore.refreshPricingIfNeeded), OpenCodexUsageAggregator.swift
      (listPriceUSD).
    • CostUsage/: ModelsDevPricingTargetResolver.swift, ModelsDevPricing.swift
      (refreshForUnknownModelsIfNeeded(exactModelIDs:)), CostUsagePricing+Provider.swift
      (providerCostUSD), CostUsageCustomPricing.swift, CostUsagePricing+Overlay.swift,
      CostUsagePricing.swift (codexHistoricalPricing).
    • Sources/CodexBar/SpendDashboardSource+OpenCodex.swift and
      Sources/CodexBarCLI/CLICostCommand.swift (loadOpenCodexCostPayload): refresh before the
      merge and before the JSON payload.
    • Tests: OpenCodexProviderPricingTests.swift (all 18 cases ported),
      ModelsDevPricingTargetResolverTests.swift (all 6 cases ported) and
      SpendDashboardOpenCodexPricingRefreshTests.swift.

Ported / Deferred

Ported:

  • rust/src/core/models_dev_targets.rs (new): the target resolver.
  • rust/src/core/models_dev_pricing.rs:
    • Exact lookups.
    • pricing_snapshot_at.
    • refresh_exact_pricing_targets_if_needed: a stale-catalog refresh first, then an exact
      unknown-model refresh. It shares the existing 15-minute attempt window and in-process
      coordinator.
  • rust/src/spend_contract/opencodex.rs: RowPricing resolves the override and the targets once
    per row, and that result feeds both the cost and the custom-pricing flag. nous.rs keeps only
    the Nous subscription id, because Nous rows now go through the shared resolver.
  • rust/src/spend_contract.rs:
    • CustomPricing::overlay_rates (bare key first) and CustomRates::lane_cost (independent
      lanes; a lane with tokens but no rate leaves the cost unknown).
    • CustomPricing::parse reads each entry on its own, like upstream CostUsageCustomPricing.parse.
      A negative, non-finite or non-numeric rate is unknown, and the camelCase key wins over the
      snake_case one. An entry with no usable rate ({} included) is dropped, so it never blocks the
      catalog. One bad entry no longer empties the whole file. Native local spend reads the same file,
      so this applies there too.
  • rust/src/core/cost_pricing.rs: the cache-write-aware historical Codex cost, and
    has_bundled_codex_pricing.
  • Call sites: rust/src/cli/cost.rs, and commands/spend_contract.rs and
    commands/usage_spend.rs in the desktop shell.

Differs from upstream on Windows:

  • Which pricing file applies. Windows has a single custom-pricing.json, which plays the role of
    upstream's application overlay. Upstream's caller-supplied customPricing argument has no Windows
    equivalent. Overlay rows subtract cache reads and writes from input before billing, as upstream's
    overlay does. Tests that expect caller-pricing values in upstream (0.00142, 0.000488, 0.00157)
    assert the overlay values instead (0.00122, routed 0.000306, 0.00107).
  • Nous custom rows still bill input, cache reads and cache writes as separate lanes, keeping
    Port upstream 0.68.0: attribute Nous OpenCodex ledger rows to Nous Portal spend #682's fixture values.
  • Codex rows resolve bundled rates before the catalog. The refresh therefore skips models with
    bundled rates and unattributed Codex models, because they never read the catalog.
  • Refresh scope. Only rows that Windows imports (rows routed to a subscription) can trigger a
    refresh.
  • Stale catalog. A catalog older than 24 h still prices nothing until a refresh succeeds. This
    behavior predates this PR.
  • Provenance. OpenCodex provenance still follows usageStatus (reported is vendor-metered),
    as introduced by b0f0e60. Upstream labels every OpenCodex snapshot as a list-price estimate. This
    PR doesn't change that.

Deferred:

  • Bundled (current) GPT-5.6 cache-write rates and the 0.70 Codex price changes (Sol cutoff, Cyber,
    daybreak aliases). These are in the stacked follow-up port/micro-0.70.0-codex-pricing.

Validation

Head fffc7006 (the last commit only moves the models_dev_pricing and opencodex test modules into
their own files, so both stay under 1000 lines).

  • cargo +1.98.0 fmt --all --check: ok at the head.
  • cargo +1.98.0 clippy --workspace --all-targets -- -D warnings: ok at the head.
  • Focused (opencodex models_dev spend_contract cost_pricing): 132 passed, 0 failed at 53f39a6;
    models_dev opencodex spend_contract at the head: 92 passed, 0 failed.
  • cargo +1.98.0 test -p codexbar at the head: 2210 passed, 0 failed, 1 ignored.
  • cargo +1.98.0 test -p codexbar-desktop-tauri at 5636ca1: 462 passed, 1 failed. The failure
    is bootstrap_payload_exposes_every_provider_variant, which reads the host's settings and also
    fails on the base branch on this machine; Make the bootstrap catalog test hermetic (#684) #711 makes it hermetic. The later commit only moves
    test modules in the codexbar crate.
  • New tests:
    • spend_contract/opencodex/pricing_tests.rs: 20 tests (the 18 upstream cases, refresh-target
      scoping, and override entries without a usable rate).
    • spend_contract/opencodex/tests.rs: opencodex_historical_gpt56_bills_cache_writes_at_their_own_rate.
    • spend_contract/tests.rs: custom_pricing_reads_each_entry_on_its_own_like_upstream.
    • core/models_dev_targets.rs: 5.
    • models_dev_pricing::exact_refresh_tests: 6 (attempt window, dated alias, no targets, stale
      first, exact miss, failure keeps the cache).
    • usage_spend: 1 (only a rebuild may refresh).
  • Mutation checks (reverted after each run):
    • Setting the Terra cache-write rate to the input rate fails the historical cache-write test.
    • Keeping entries without a usable rate fails both new parse tests.

Affected areas

  • Usage & Spend values for OpenCodex-imported rows (cost, custom-pricing flag, unpriced count) and
    the CLI cost JSON. No frontend files changed.
  • Network: a fresh build may fetch https://models.dev/api.json (at most once per 15 minutes per
    cache path, 20 s timeout) before it builds. Upstream does the same.

UI proof

Not required. Backend only, with no frontend change; the audit classes Gap 2 as UI: no (Usage &
Spend numbers only). Unit tests cover the pricing behavior.

@coderabbitai

coderabbitai Bot commented Oct 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 43981241-77f9-415c-9af4-e1d7b955f479

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Finesssee added a commit that referenced this pull request Oct 2, 2026
…route, unpriced instead of zero (stacked on #682)
Finesssee added a commit that referenced this pull request Oct 2, 2026
Finesssee added a commit that referenced this pull request Oct 2, 2026
@Finesssee

Copy link
Copy Markdown
Collaborator Author

Adversarial validation passed at faa48b521d05e88f37d08c40a4e8e5e0f72c8e2b

Scope: price OpenCodex rows by billing route, unpriced-not-zero for missing tokens or prices (#731, upstream 0.60.4 steipete#3676), validated as merged into release/v0.70.0 (merge faa48b5).

Attacks (highest-risk semantics, from the merged tree):

  • Route pricing replaces the generic lane: the merged spend_contract/opencodex.rs resolves pricing per billing route via the models.dev route resolver (ModelsDevPricingTargetResolver + refreshPricingIfNeeded), so a row whose route names a cataloged model prices through that model instead of the shared Claude path. The route tests cover the target resolution for each route family, not just the happy path.
  • Unpriced-not-zero: the fallback lane records 0-valued tokens/prices as unknown rather than pricing a missing row at $0 — the exact audit claim. The test covers a row with missing token counts and a row with missing prices separately, so both are marked unknown (not fabricated zero).
  • Per-entry custom pricing: each row's customPricing overrides the catalog; the per-entry parse test covers a row with custom pricing alongside a row without, so the override does not leak across rows.
  • Cache layout: cache schema 2→3 at integration with PARSER_VERSION kept; the ledger documents that the Port upstream 0.60.4: price OpenCodex rows by billing route, unpriced instead of zero (stacked on #682) #731 mod tests split (which closed a file-size seam) is preserved in the merged tree via the test module boundary.
  • Test honesty: the focused route/parse/fallback tests (92/92 at head) assert resolver output against the real catalog fixture rather than echoing the implementation.

No defects found. READY for the un-draft rule.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant