Skip to content

fix(submit): bypass cost-without-tokens check for Cursor legacy premium-tool-call - #612

Merged
junhoyeo merged 3 commits into
mainfrom
fix/cursor-legacy-premium-tool-call-bypass
May 27, 2026
Merged

junhoyeo merged 3 commits into
mainfrom
fix/cursor-legacy-premium-tool-call-bypass

Conversation

@junhoyeo

@junhoyeo junhoyeo commented May 27, 2026

Copy link
Copy Markdown
Owner

What I hit

Running npx tokscale@latest submit against a freshly synced local payload (5,038 Cursor CSV rows, 230 active days, 59.07B tokens across 10 clients) failed at server-side validation with a long list of Cost submitted without tokens errors. The full failure mode looks like this:

Tokscale - Submit Usage Data
  Syncing Cursor usage data...
  Cursor sync failed; using cached data: <…>
  Scanning local session data...
  Data to submit:
    Date range: 2024-06-06 to 2026-05-27
    Active days: 230
    Total tokens: 59,067,653,128
    Total cost: $45618.04
    Clients: amp, claude, codex, copilot, cursor, droid, gemini, openclaw, opencode, pi
    Models: 83 models

  Submitting to server...

  Error: Validation failed
    - Day 2025-04-29: Cost submitted without tokens
    - Client cursor/premium-tool-call on 2025-04-29: Cost submitted without tokens
    - Day 2025-05-01: Cost submitted without tokens
    - Client cursor/premium-tool-call on 2025-05-01: Cost submitted without tokens
    ...
    - Client cursor/claude-3-5-sonnet on 2025-05-18: Cost submitted without tokens
    - Client cursor/auto on 2025-05-28: Cost submitted without tokens
    - Day 2025-06-03: Cost submitted without tokens
    - Client cursor/claude-4-sonnet-thinking on 2025-06-03: Cost submitted without tokens
    - Client cursor/o3 on 2025-06-13: Cost submitted without tokens
    ...

The entire 230-day submission was rejected.

Root cause

pushCostSanityErrors in packages/frontend/src/lib/validation/submission.ts (introduced by PR #557 β€” fix(submit): reject implausible submitted totals) treats any (date, client, modelId) aggregate with cost > 0 && tokens === 0 as fatal:

if (cost > 0 && tokens === 0) {
  errors.push(`${label}: Cost submitted without tokens`);
  return;
}

The check fires at three levels: per-client, per-day total, and overall summary total. Any single offending row fails the whole submission.

Cursor's CSV usage exports have legitimate cost-only rows whose token columns are blank:

Source What it is Token columns
cursor/premium-tool-call pre-2025-05 per-tool-call billing event always empty
cursor/auto, cursor/claude-3.5-sonnet, cursor/claude-4-sonnet-thinking, cursor/o3 individual model calls Cursor records with Cost but no Total Tokens (sporadic) sometimes empty

crates/tokscale-core/src/sessions/cursor.rs (the Rust CSV parser) preserves these rows verbatim, so they reach the server as cost > 0, tokens = 0 aggregates.

Forensic count from my local cache

I crawled ~/.config/tokscale/cursor-cache/usage.csv (the same file the CLI submits) and summed every (date, model) aggregate where cost > 0 && totalTokens === 0:

Model Affected days Rows Total cost
premium-tool-call 14 871 $40.55
claude-4-sonnet-thinking 11 132 $4.44
auto 4 20 $0.68
claude-3.5-sonnet 2 2 $0.08
o3 1 2 $0.04
Total 1,027 $45.79
  • premium-tool-call accounts for 89% of the rejected cost and is the only model that is always tokenless by design (it disappeared from Cursor's schema in May 2025).
  • The other models are sporadic and far smaller (~$5.20 combined across 26 days).
  • $45.79 of legacy data blocked submission of $45,618 of valid totals β€” 0.1% of the payload locked out the other 99.9%.

The fix

Two parts, both inside packages/frontend/src/lib/validation/submission.ts:

1. Cursor legacy carve-out (the actual unblock)

Add a narrow allowlist for the one model that legitimately and consistently has no token attribution:

// Cursor legacy: pre-2025-05 Cursor usage exports include `premium-tool-call`
// rows that are billed per tool invocation and carry no token attribution.
// They legitimately have cost > 0 with all token fields = 0 and must bypass
// the "Cost submitted without tokens" sanity check β€” otherwise any user with
// historical Cursor data is permanently locked out of `tokscale submit`.
const CURSOR_LEGACY_TOKENLESS_MODELS: ReadonlySet<string> = new Set([
  "premium-tool-call",
]);

function isLegacyTokenlessCursorClient(client: ClientLike): boolean {
  return (
    client.client === "cursor" &&
    CURSOR_LEGACY_TOKENLESS_MODELS.has(client.modelId) &&
    tokenTotal(client.tokens) === 0
  );
}

This carve-out is applied at all three check sites:

  • Client level β€” skip the check entirely for legacy rows.
  • Day level β€” subtract legacy cost from day.totals.cost before testing. If the day has only legacy rows the check no longer fires; if the day has a mix of legacy + a real tokenless regression, the remaining (non-legacy) cost still trips the check.
  • Submission summary level β€” same subtraction logic across all contributions.

Other cursor models (auto, claude-3.5-sonnet, claude-4-sonnet-thinking, o3) are explicitly not included in the allowlist. They sometimes have tokenless rows, but unlike premium-tool-call those tokenless rows look like genuine parser/API regressions that we want to keep surfacing. Hiding them under a blanket cursor allowlist would mask real bugs.

2. Detailed English error messages

The old Cost submitted without tokens line was unactionable β€” operators had to re-run the CLI in debug mode to figure out which row failed. The new messages embed the full row context:

Client cursor/claude-3.5-sonnet (provider=anthropic) on 2025-05-18: Cost submitted without tokens (cost=$0.0400, tokens={input=0, output=0, cacheRead=0, cacheWrite=0, reasoning=0})

Day 2025-04-29: Cost submitted without tokens (cost=$2.0500, total tokens=0; offending clients: cursor/auto (provider=cursor) cost=$1.5000; cursor/claude-4-sonnet-thinking (provider=anthropic) cost=$0.5500)

Submission summary: Cost submitted without tokens (cost=$45.7900, total tokens=0; offending clients: cursor/claude-3.5-sonnet (provider=anthropic) cost=$0.0400; ...)

All three levels now report client, providerId, modelId, full cost, and the full token breakdown. Day- and summary-level errors also enumerate which clients on that scope were responsible.

The legacy pushCostSanityErrors helper was split into:

  • A pure cost-per-million check (pushCostPerMillionError), unchanged in behavior.
  • Inline cost-without-tokens checks at each site, which is the only way to include the per-site context (offender list, token breakdown) in the error string.

Why not the alternatives

Option Why rejected
Drop tokenless Cursor rows in the Rust parser Loses real cost attribution that the leaderboard depends on β€” the user paid for it, it should count.
Loosen cost > 0 && tokens === 0 to a warning globally Re-opens the original PR #557 vulnerability (implausible cost-only submissions).
Estimate tokens from cost Fabricates data; downstream cost-per-million sanity checks would fight the estimate.
Allow all cursor models with cost-only rows Hides parser bugs in non-premium-tool-call paths.

Test plan

  • npx vitest run __tests__/api/submit.test.ts β€” 47/47 pass (was 44 before, +3 new tests).
  • npx vitest run (full frontend suite) β€” 283/283 pass.
  • npx eslint src/lib/validation/submission.ts __tests__/api/submit.test.ts β€” clean.
  • Existing rejects submitted cost without corresponding tokens test still passes (the new message still starts with the same prefix).
  • New allows cursor legacy premium-tool-call rows that lack token attribution test verifies the unblock works for the exact shape I hit locally (date 2025-04-29, cost $2.05, all-zero tokens).
  • New does not extend the cursor legacy bypass to other cursor models test pins the carve-out to premium-tool-call only β€” cursor/claude-3.5-sonnet with cost-only still fails.
  • New allows cursor legacy rows mixed with normal token-bearing rows test covers the realistic mixed-day case.
  • New includes client/provider/model/cost/tokens detail in tokenless-cost errors test pins the new English error format so it doesn't silently regress.

Files changed

  • packages/frontend/src/lib/validation/submission.ts β€” carve-out + richer error messages.
  • packages/frontend/__tests__/api/submit.test.ts β€” 4 new tests, existing assertions intact.

Rollback

git revert HEAD β€” single commit, isolated to the validation module.


Summary by cubic

Unblocks submissions with historical Cursor data by bypassing the β€œcost without tokens” check for legacy cursor/premium-tool-call rows and excluding their cost from cost-per-million checks. Real tokenless-cost regressions still fail and are easier to debug.

  • Bug Fixes
    • Allowlisted legacy cursor/premium-tool-call rows (cost > 0, tokens = 0) at client/day/summary levels; other Cursor models remain strict.
    • Day/summary checks subtract legacy cost before validating and before the cost-per-million cap to avoid false positives on mixed days.
    • Use a small float epsilon when comparing post-subtraction cost to avoid rounding-noise rejections on all-legacy submissions.
    • Error messages now include client/provider/model, cost, and token breakdown; day/summary errors list offending clients.

Written for commit 8eb7bfa. Summary will update on new commits. Review in cubic

…um-tool-call

Cursor's pre-2025-05 usage exports include `premium-tool-call` rows that are billed per tool invocation and carry no token attribution at all (input/output/cache columns are empty). PR #557 introduced a sanity check that rejects any submission where cost > 0 and tokens === 0, which permanently locks out any user with historical Cursor data β€” even a few cents of legacy tool-call charges block the entire upload (we observed a real submission where ~$45 of premium-tool-call rows blocked $45,618 of otherwise valid data, ~0.1% of the payload).

Allow `cursor/premium-tool-call` specifically by adding a single legacy carve-out. The check still fires for every other `(client, model)` combination, including other cursor models, so genuine parser regressions remain visible.

Also enrich the cost-without-tokens error messages so operators can read the offending row directly out of the CLI output:
- Client-level errors now include the client, modelId, providerId, full cost (`cost=$X.XXXX`), and the full token breakdown (`input/output/cacheRead/cacheWrite/reasoning`).
- Day-level errors list which clients on that day were responsible (`offending clients: cursor/premium-tool-call (provider=cursor) cost=$2.0500; ...`).
- Submission-summary errors include the same per-client offender list.

Constraint: cannot drop Cursor `premium-tool-call` rows in the CLI parser because their cost is a legitimate spend the user wants reflected in totals.
Rejected: Drop tokenless rows in the CLI scanner | loses real cost attribution that the leaderboard depends on.
Rejected: Loosen the check globally (cost > 0 && tokens === 0 β†’ warning) | re-opens the original PR #557 vulnerability (implausible cost-only submissions).
Confidence: high
Scope-risk: narrow
Directive: Only add new entries to CURSOR_LEGACY_TOKENLESS_MODELS for Cursor billing events that legitimately lack token attribution β€” never for parser bugs (fix the parser instead).
Not-tested: A Cursor user whose entire history is exclusively `premium-tool-call` (summary-level legacy subtraction path is exercised only by unit tests, not by an end-to-end submit).
@vercel

vercel Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
tokscale Ready Ready Preview, Comment May 27, 2026 11:04am

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: 66f7bbd91c

ℹ️ 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 on lines +462 to +463
day.totals.cost,
day.totals.tokens

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exclude legacy Cursor cost from CPM checks

When a legacy cursor/premium-tool-call row shares a day with any token-bearing usage, this branch runs because day.totals.tokens > 0, and the cost-per-million check uses the full day cost including the tokenless legacy charge. A historical Cursor day such as $2.05 in premium tool calls plus 100 normal tokens now computes $20,500/M and is rejected, even though the legacy row is supposed to be skipped; subtract the legacy tokenless cost (and do the same for the summary check) before applying this sanity cap.

Useful? React with πŸ‘Β / πŸ‘Ž.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch β€” fixed in 5ab9a79. The day-level (and summary-level) branches now compute a shared checkableCost = max(0, totalCost - legacyCost) and pass that to both the cost-without-tokens check and pushCostPerMillionError, so a legacy premium-tool-call charge sharing a day with normal token-bearing usage no longer inflates the cost-per-million ratio. Added excludes cursor legacy cost from the cost-per-million sanity cap as a regression test exercising the exact $2.05 + 100 tokens shape you described β€” it previously would have computed $20,600/M and failed; now it passes.

Address Codex review (P1) on PR #612: when a legacy `cursor/premium-tool-call` row shares a day with a small amount of token-bearing usage, the day-level branch falls through to the cost-per-million check because `day.totals.tokens > 0`. Using the full day cost (legacy + real) as the numerator meant tiny token counts tripped the $10k/M ceiling even though the legacy row is supposed to be skipped β€” e.g. $2.05 in legacy tool calls plus 100 normal tokens computed $20,600/M and was rejected.

Subtract the legacy tokenless Cursor cost before applying the cost-per-million cap at both day and summary levels (was previously only subtracted for the cost-without-tokens branch). The cost-without-tokens branch behavior is unchanged because both branches now read from the same `checkableCost` value.

Add a regression test (`excludes cursor legacy cost from the cost-per-million sanity cap`) that pins the exact mixed-day shape from the review.

Confidence: high
Scope-risk: narrow
Directive: Any future cost-related sanity check on day/summary aggregates must also subtract `legacyCost` first β€” wire it off the same `checkableCost` local instead of recomputing.

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

1 issue found across 2 files

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

Re-trigger cubic

Comment thread packages/frontend/src/lib/validation/submission.ts Outdated
Address cubic review (P2) on PR #612: the new legacy-cost subtraction used strict `> 0` float comparison at both the day and summary cost-without-tokens checks. Floating-point summation residue could trip the check on a valid all-legacy submission β€” e.g. `0.1 + 0.2 === 0.30000000000000004` while IEEE `0.3 β‰ˆ 0.299999999999999988`, so `totalCost - legacyClientCost β‰ˆ 5.5e-17 > 0` even though the user truly has $0.30 of legacy cost and nothing else.

Introduce `LEGACY_COST_FLOAT_EPSILON = 1e-6` and use it as the threshold on both check sites. 1e-6 is well below any realistic LLM charge, so any legitimate non-legacy cost still trips the check; only FP rounding noise is absorbed.

Add a regression test (`tolerates floating-point residue when subtracting cursor legacy cost`) that constructs the exact `0.1 + 0.2` vs `0.3` scenario and asserts the residue is positive but the submission still validates.

Confidence: high
Scope-risk: narrow
Directive: Any cost-vs-cost float comparison on aggregated sums should use LEGACY_COST_FLOAT_EPSILON (or a comparable epsilon) β€” strict `> 0` is a footgun on summed IEEE 754 values.
@junhoyeo junhoyeo changed the title fix(submit): bypass cost-without-tokens check for Cursor legacy premium-tool-call fix(submit): bypass cost-without-tokens check for Cursor legacy premium-tool-call May 27, 2026
@junhoyeo
junhoyeo merged commit 2402fb2 into main May 27, 2026
7 checks passed
@junhoyeo
junhoyeo deleted the fix/cursor-legacy-premium-tool-call-bypass branch May 27, 2026 11:15
leecoder pushed a commit to leecoder/tokscale that referenced this pull request May 29, 2026
…ium-tool-call` (junhoyeo#612)

* fix(submit): bypass cost-without-tokens check for Cursor legacy premium-tool-call

Cursor's pre-2025-05 usage exports include `premium-tool-call` rows that are billed per tool invocation and carry no token attribution at all (input/output/cache columns are empty). PR junhoyeo#557 introduced a sanity check that rejects any submission where cost > 0 and tokens === 0, which permanently locks out any user with historical Cursor data β€” even a few cents of legacy tool-call charges block the entire upload (we observed a real submission where ~$45 of premium-tool-call rows blocked $45,618 of otherwise valid data, ~0.1% of the payload).

Allow `cursor/premium-tool-call` specifically by adding a single legacy carve-out. The check still fires for every other `(client, model)` combination, including other cursor models, so genuine parser regressions remain visible.

Also enrich the cost-without-tokens error messages so operators can read the offending row directly out of the CLI output:
- Client-level errors now include the client, modelId, providerId, full cost (`cost=$X.XXXX`), and the full token breakdown (`input/output/cacheRead/cacheWrite/reasoning`).
- Day-level errors list which clients on that day were responsible (`offending clients: cursor/premium-tool-call (provider=cursor) cost=$2.0500; ...`).
- Submission-summary errors include the same per-client offender list.

Constraint: cannot drop Cursor `premium-tool-call` rows in the CLI parser because their cost is a legitimate spend the user wants reflected in totals.
Rejected: Drop tokenless rows in the CLI scanner | loses real cost attribution that the leaderboard depends on.
Rejected: Loosen the check globally (cost > 0 && tokens === 0 β†’ warning) | re-opens the original PR junhoyeo#557 vulnerability (implausible cost-only submissions).
Confidence: high
Scope-risk: narrow
Directive: Only add new entries to CURSOR_LEGACY_TOKENLESS_MODELS for Cursor billing events that legitimately lack token attribution β€” never for parser bugs (fix the parser instead).
Not-tested: A Cursor user whose entire history is exclusively `premium-tool-call` (summary-level legacy subtraction path is exercised only by unit tests, not by an end-to-end submit).

* fix(submit): exclude Cursor legacy cost from cost-per-million sanity cap

Address Codex review (P1) on PR junhoyeo#612: when a legacy `cursor/premium-tool-call` row shares a day with a small amount of token-bearing usage, the day-level branch falls through to the cost-per-million check because `day.totals.tokens > 0`. Using the full day cost (legacy + real) as the numerator meant tiny token counts tripped the $10k/M ceiling even though the legacy row is supposed to be skipped β€” e.g. $2.05 in legacy tool calls plus 100 normal tokens computed $20,600/M and was rejected.

Subtract the legacy tokenless Cursor cost before applying the cost-per-million cap at both day and summary levels (was previously only subtracted for the cost-without-tokens branch). The cost-without-tokens branch behavior is unchanged because both branches now read from the same `checkableCost` value.

Add a regression test (`excludes cursor legacy cost from the cost-per-million sanity cap`) that pins the exact mixed-day shape from the review.

Confidence: high
Scope-risk: narrow
Directive: Any future cost-related sanity check on day/summary aggregates must also subtract `legacyCost` first β€” wire it off the same `checkableCost` local instead of recomputing.

* fix(submit): use float-epsilon when subtracting Cursor legacy cost

Address cubic review (P2) on PR junhoyeo#612: the new legacy-cost subtraction used strict `> 0` float comparison at both the day and summary cost-without-tokens checks. Floating-point summation residue could trip the check on a valid all-legacy submission β€” e.g. `0.1 + 0.2 === 0.30000000000000004` while IEEE `0.3 β‰ˆ 0.299999999999999988`, so `totalCost - legacyClientCost β‰ˆ 5.5e-17 > 0` even though the user truly has $0.30 of legacy cost and nothing else.

Introduce `LEGACY_COST_FLOAT_EPSILON = 1e-6` and use it as the threshold on both check sites. 1e-6 is well below any realistic LLM charge, so any legitimate non-legacy cost still trips the check; only FP rounding noise is absorbed.

Add a regression test (`tolerates floating-point residue when subtracting cursor legacy cost`) that constructs the exact `0.1 + 0.2` vs `0.3` scenario and asserts the residue is positive but the submission still validates.

Confidence: high
Scope-risk: narrow
Directive: Any cost-vs-cost float comparison on aggregated sums should use LEGACY_COST_FLOAT_EPSILON (or a comparable epsilon) β€” strict `> 0` is a footgun on summed IEEE 754 values.
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