Skip to content

fix(ai): claude quota extra-usage spend publishes negative amounts — "$-5.00 / $10.00", and a negative-only payload still returns ok:true #1919

Closed

Description

@developing-today

fix(ai): claude quota extra-usage spend publishes negative amounts — "$-5.00 / $10.00", and a negative-only payload still returns ok:true

Verified 2026-11-07 on origin/main cfc5e3d82c (merge of PR #1916), in a laptop worktree (~/git/fleet/probe-2026-11-06-creator-2335, kept).

The defect

packages/ai/src/quota/providers/claude.ts:

  • toAmount (:128-133) converts a money object with minor / 10 ** exponent and no sign check — a negative amount_minor yields a negative amount.
  • The spend block (:206-213) formats that amount straight into the extra_usage window's label via formatSpendLabel (:136-142). The window's usedPercent comes from the separate wire spend.percent (clamped), so nothing cross-checks the label's sign.
  • addWindow (:156-166) publishes the window whenever a percent OR a valueLabel exists — so a spend object whose only figure is a negative amount still mints an extra_usage window, and fetchQuota then returns ok: true (the fix(ai): claude quota returns ok:true with empty windows for a 200 payload carrying no usable quota data #1915 no-data contract only fires when windows AND models are both empty).

A negative spend/limit is a figure that cannot exist. Sibling code in this same quota module treats it as absent: meta's parseBillingBanner maps a negative remaining_minor_units to null (pinned by test), turso's numNonNegative, opencode-go's toMicroCents (rejects < 0), and command-code's monthlyRemaining >= 0 gate (#1744) all read a negative amount as no data. OpenRouter's identical unlimited-path defect was filed as #1913. The claude spend path is the remaining raw-negative money label.

Runtime probe (laptop worktree, probe-claude-spend.ts, kept in the worktree)

Driving the real exported toClaudeUsage:

  • { spend: { enabled: true, percent: 50, used: { amount_minor: -500, exponent: 2, currency: "USD" }, limit: { amount_minor: 1000, exponent: 2, currency: "USD" } } } → extra_usage window, valueLabel: "$-5.00 / $10.00".
  • Same with the limit negative (amount_minor: -1000) → valueLabel: "$5.00 / $-10.00".
  • { spend: { enabled: true, used: { amount_minor: -500, exponent: 2, currency: "USD" } } } (no percent, no limit) → extra_usage window with usedPercent: null and valueLabel: "$-5.00" — the negative amount is the window's ONLY figure, so through fetchQuota this payload alone publishes ok: true.
  • Controls: positive { amount_minor: 500 } / { amount_minor: 1000 } → "$5.00 / $10.00" (correct); zero used → "$0.00 / $10.00" (correct); { spend: { enabled: true } } with no figures → no window (correct, unchanged).

Why it matters

The extra_usage window is the paid-overage signal for a Claude subscription: downstream consumers read its label as money actually spent against a real limit. A negative spend is impossible wire data (a billing glitch, a refund/adjustment spelled as spend, a mimicking gateway); publishing it as ok: true quota data — in the negative-only case as the provider's sole result, defeating the #1915 no-data contract in spirit — teaches consumers a figure that cannot be true, in the one shape (money amounts) where this fleet has repeatedly chosen "bad data reads as absent" over fabrication.

Acceptance criteria

  • A negative amount_minor on spend.used or spend.limit gets one explicit disposition — read as absent (the meta parseBillingBanner precedent in this same module) is the sibling-consistent choice; any other disposition is chosen and recorded in the closing note.
  • A spend payload whose only figure is a negative amount publishes no extra_usage window, so through fetchQuota it reaches the existing "No quota data in response" contract when nothing else is usable.
  • Positive and zero amounts are unchanged ($5.00 / $10.00, $0.00 / $10.00 controls), and a valid percent-only spend window still publishes.
  • packages/ai/test/quota.test.ts pins the negative-used, negative-limit, and negative-only cases beside the existing claude describes (the spend path currently has no tests at all).
  • Existing quota suite stays green.

Dedup

Searched open + closed issues this run for claude spend / negative amount / amount_minor: no covering issue. #1913 (open) is OpenRouter's unlimited-path usage_monthly, a different provider and field; #1915 (closed) is the figure-less-200 no-data contract for limits/legacy windows and does not touch the spend block; #1882 (open) is codex spend_control, a different provider. The spend path in claude.ts has no test coverage today (no spend/extra_usage cases in quota.test.ts beyond a meta zero-limit test that mentions the word).

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/aiPackage: ai (pkgs/ or apps/ in this repo)
    on Nov 7, 2026
  2. developing-today commented on Nov 7, 2026

    @developing-today
    Author

    🤖 Agent note · filed

  3. dezren39 commented on Nov 7, 2026

    @dezren39

    🤖 Agent note · adopted as design phase 2 by repo design architect (opencode pass 2026-11-07)

  4. dezren39 commented on Nov 7, 2026

    @dezren39

    Picked up by repo-worker-opencode — working this now. (Fleet queue fallback mode: the queue backend is down, so this issue's comments carry the claim. The claim stays live while repo-worker-opencode posts progress markers; after 30 minutes of silence any other worker may pick this up.)

  5. added
    queue/claimedA queue worker has claimed the task for this item
    work/assignedAn agent holds this item via the queue (claimed task)
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 7, 2026
  6. dezren39 commented on Nov 7, 2026

    @dezren39

    Progress from repo-worker-opencode: verified: issue open, no hard-stop labels, triage fix spec clear; next: branch_created

  7. dezren39 commented on Nov 7, 2026

    @dezren39

    Progress from repo-worker-opencode: implementation_complete: toAmount reads negative amount_minor as absent; tests added in quota.test.ts; next: targeted tests

  8. dezren39 commented on Nov 7, 2026

    @dezren39

    Progress from repo-worker-opencode: pr_opened: PR #1957; targeted tests 145 pass; next: check CI/Codex then merge

  9. dezren39 commented on Nov 7, 2026

    @dezren39

    Released by repo-worker-opencode (outcome: done). fixed_merged: PR #1957 merged 8cd974d; toAmount reads negative amount_minor as absent; quota.test.ts 145 pass on laptop; issue auto-closed

  10. removed
    queue/claimedA queue worker has claimed the task for this item
    work/assignedAn agent holds this item via the queue (claimed task)
    on Nov 7, 2026
  11. dezren39 commented on Nov 7, 2026

    @dezren39

    🤖 Agent note · done

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/aiPackage: ai (pkgs/ or apps/ in this repo)kind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions