Skip to content

fix(submit): exclude unpriced usage without aborting - #1053

Merged
junhoyeo merged 2 commits into
junhoyeo:mainfrom
Soju06:fix/submit-exclude-unpriced-usage
Aug 6, 2026
Merged

fix(submit): exclude unpriced usage without aborting#1053
junhoyeo merged 2 commits into
junhoyeo:mainfrom
Soju06:fix/submit-exclude-unpriced-usage

Conversation

@Soju06

@Soju06 Soju06 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • exclude token-bearing messages that have neither a provider-reported cost nor pricing for every populated token bucket
  • keep the priced remainder internally consistent and submit it instead of aborting the entire batch
  • report each excluded provider/model with its message count, token count, and whether pricing is missing or incomplete
  • preserve unpriced usage in local reports and preserve the existing hard failure when no pricing dataset loads at all

Why

Submission validation currently aborts the whole batch when any message has an incomplete price. This still affects valid model rows whose input, output, and cache-read rates resolve but whose cache-write rate is absent, as well as isolated local test-model usage. A single such row prevents all otherwise priced history from being submitted.

This change excludes the entire affected message rather than pricing the missing bucket at zero or submitting only part of its tokens. That keeps submitted token totals and costs consistent. The existing CLI exclusion warning makes the loss explicit, and a batch containing only excluded usage still reports that there is no usage to submit.

Follow-up to #1013.

Tests

  • cargo fmt --all --check
  • cargo test -p tokscale-core (1,479 passed, 1 ignored; all integration suites passed)
  • cargo test -p tokscale-cli -- --skip headless_capture (1,022 unit tests and 148 CLI integration tests passed; 1 unit test ignored)
  • target/debug/tokscale --no-spinner submit --dry-run against real local history containing 10 affected provider/model groups (exit 0, warnings emitted, priced remainder produced, no submission performed)

The three skipped headless_capture_* tests are existing wall-clock threshold tests. They also exceeded their timing bounds when rerun alone on this host; no headless code is changed here.

@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
tokscale Ignored Ignored Preview Aug 6, 2026 2:43am

Request Review

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9deb76344c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

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

Comment thread crates/tokscale-core/src/lib.rs Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/tokscale-core/src/lib.rs
Comment thread crates/tokscale-cli/tests/cli_tests.rs
@pengtianabc

Copy link
Copy Markdown

does #1052 is better?

@junhoyeo junhoyeo self-assigned this Aug 6, 2026
@junhoyeo
junhoyeo merged commit 15cad3a into junhoyeo:main Aug 6, 2026
20 checks passed
@junhoyeo

junhoyeo commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Merged. This was a direct call against #1052, which fixes the same abort from the opposite direction, so recording the reasoning in both places.

@pengtianabc β€” two points on why this one:

1. Excluding vs. zeroing. #1052 submits unpriced token-bearing usage at cost = 0.0. #1044 exists specifically to document why that isn't available to us yet: packages/frontend/src/app/api/submit/route.ts upserts cost = EXCLUDED.cost unconditionally, so a submission carrying fabricated zeros overwrites the cost already recorded for that day. One dataset failing to load, a stale cache, or a proxy blocking the fetch would permanently lower a user's recorded spend, with a stdout warning as the only trace. #1044 also documents three client-side guards we tried and abandoned, so this isn't a gap someone can close in the CLI. Excluding the whole message keeps submitted tokens and cost mutually consistent β€” we never claim N tokens cost $0 β€” which is the only shape that's safe against that write path today.

2. Blast radius and verification. This is 3 files, +199/βˆ’53, and it generalizes machinery already in the tree (exclude_generic_unpriced_submission_messages, the gemini-default path from #1026) rather than adding a concept. #1052 is 14 files, +538/βˆ’67, rewrites authoritative-cost detection across 11 parsers, and bumps two cache versions β€” and its build, test, and lint checks never ran, so none of that is verified on any platform. This one is green across all 9 build targets plus Windows, and both review findings came back fixed with regression coverage, including restoring the "no pricing dataset at all β†’ hard fail" test that the first pass had dropped.

To be clear about what this does not do: it is a stopgap for the v4.9.0 abort regression, not a fix for #1044. Excluded rows still shrink that day's tokens and cost, and the server still overwrites both β€” so a user with several thousand cursor/auto rows loses that usage from the leaderboard rather than seeing it mispriced. The real fix is still the costIsComplete flag and the accompanying migration in #1044. #1013 should not be treated as fully closed by this.

Thanks @Soju06.

junhoyeo pushed a commit that referenced this pull request Aug 6, 2026
…1051)

parse_gen_metadata read the model from chatModel.#19 and fell back to the
literal string "unknown", which inferred_provider_from_model cannot place,
producing unpriceable antigravity/unknown rows. Antigravity CLI drops a
whole group of chatModel fields (1, 2, 8, 10, 16, 19) on some continuation
and tool turns, but #21 β€” the model display label β€” survives on every one,
and sibling rows carry the machine id next to the identical label.

Turns are now attributed by resolving that label against labels seen
elsewhere in the same database.

Rejected: fall back to the conversation's sole model whenever a row lacks
a label | a conversation containing a model switch would bill the wrong
model instead of staying unknown (cubic P1, fixed in 73d5937 by
withholding the fallback when any label is unresolved)
Confidence: high
Scope-risk: narrow
Directive: post-#1053 these rows no longer abort a submission β€” they are
excluded silently, so a regression here is invisible rather than loud
Not-tested: a database where the same display label maps to two different
machine ids
junhoyeo added a commit that referenced this pull request Aug 6, 2026
#1053 made submission exclude any token-bearing message the pricing service
cannot cover. A service with no dataset covers nothing, so a cold cache with
no network excluded the whole batch, left total_tokens at 0, and let the CLI
print "No usage data found to submit" and exit 0 β€” indistinguishable from an
empty history, and reported as success to autosubmit. Verified against
3ab58f5: the same fixture exits 1 there.

Exclusions are now rejected when no dataset loaded. Both conditions are
required: a batch whose costs are all provider-reported never consults
pricing and still submits, and a populated dataset that merely lacks a price
for some model stays non-fatal.

The existing hard-fail path never fired because generate_submission_graph
always passes Some(..) β€” get_or_init degrades each failed source to an empty
map rather than erroring β€” so the guarding test asserted an unreachable None.
Two #1053 core tests and one CLI test used an empty PricingService to mean
"this model is unpriced", the same conflation; all now use a populated,
non-covering dataset.

Constraint: server upserts daily_breakdown.cost unconditionally (#1044), so a
submission must never be quietly emptied by degraded pricing
Rejected: fail whenever everything was excluded | breaks autosubmit for users
whose usage is legitimately unpriceable, the trap #1044 documents
Rejected: guard before exclusion | fires when every cost is authoritative and
pricing is irrelevant
Confidence: high
Scope-risk: narrow
Directive: cursor/sakana must stay out of has_pricing_data β€” counting them
reports healthy pricing during a total upstream outage
Not-tested: partial degradation (one upstream cached, two empty), which still
under-reports a day and remains #1044's to fix
junhoyeo added a commit that referenced this pull request Aug 6, 2026
…e mode (#1057)

The guard added in #1045 was documented as preventing an abort with
"pricing is unavailable for submitted token usage". #1053 replaced that
abort with per-model exclusion, so the comment was stale.

The first correction overstated in the other direction. Adversarial review
found three inaccuracies in it, all fixed here:

- exclusion is not unconditional. With no pricing dataset loaded at all,
  #1055's require_trustworthy_exclusions fails the submission.
- "char-based estimate" no longer describes production. #1037 set
  allow_char_estimate: false for Claude transcripts, so a text-only tool
  result yields no usage and is dropped; only explicit tool-result token
  metadata reaches the model-inheritance branch.
- "invisible" was wrong. The CLI prints a named exclusion warning, and a
  batch left empty prints "No usage data found to submit" without
  submitting.

Also removes the opposite stale claim in the same file, where a test still
described submission as rejecting the escaped usage.

Comment-only; 1,490 core tests pass, fmt and clippy --all-targets clean.

Confidence: high
Scope-risk: narrow
Directive: this comment describes behavior owned by lib.rs, not this file β€”
re-check it against the submission path when either changes
@junhoyeo

junhoyeo commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Shipped in v4.11.0 β€” https://github.com/junhoyeo/tokscale/releases/tag/v4.11.0

@Soju06 thanks for this. It's the headline fix of the release and it unblocked several people at once (#1013, #1021, #1035). The way you scoped it β€” generalising the existing gemini-default exclusion rather than inventing a new mechanism, and keeping the whole thing out of the reporting path β€” is why it went in cleanly.

Two follow-ups landed on top of it, both from reviewing the shape you established:

@pengtianabc thanks for the discussion on the alternative approach; the sequencing argument from that thread fed directly into #1055.

If tokscale has been useful to you, a ⭐ on the repo helps other people find it.

junhoyeo added a commit that referenced this pull request Aug 8, 2026
custom-pricing.json is the documented escape hatch for models no upstream dataset publishes, but it required at least one rate to be POSITIVE. A free tier was therefore unexpressable: the user knows the price is $0, the file refuses to record it, and the usage stays unpriced and excluded from submission with no way to correct it (#1021).

Same free-vs-absent confusion as the Cursor cache-creation gap, at the user-facing layer. 0.0 is an assertion ("this is free"); None is an absence. Only absence is rejected now β€” reading a rate-less row as free would invent a $0 total for unknown pricing. Negative rates still rejected.

All four READMEs said a rate must be present and positive, which would have kept users away from the hatch this adds; updated in the same PR.

The reporter's other two asks need no code: #1053 made unpriced rows exclude-with-warning by default (their --prune-unpriced request), and zai/glm-4.5-flash resolves today.

Confidence: high
Scope-risk: narrow
Directive: 0.0 and None are not interchangeable in pricing rows anywhere in this crate β€” 0.0 prices, None excludes
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.

3 participants