Skip to content

Add a per-call spend threshold (claude --max-budget-usd) and report call cost - #241

Merged
tzhouam merged 3 commits into
mainfrom
kb/init-spend-cap
Sep 30, 2026
Merged

tzhouam merged 3 commits into
mainfrom
kb/init-spend-cap

Conversation

@tzhouam

@tzhouam tzhouam commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

Part 3b/9 of kb init (design v3, sol-approved): the building block for making budget_usd a hard ceiling.

Why

kb init reserves spend before each model call. max_tokens can't bound spend for CLI transports (they ignore it). The claude CLI's --max-budget-usd is checked after each API request completes (see Anthropic's claude-agent-sdk-python example max_budget_usd.py). It stops the next request and can't cut the current one short. So it's a stop threshold, not a hard cap, and this PR names it that way. The hard ceiling is the caller's reservation of threshold + one request's worst case (kb init budget, a later PR).

What

  • providers
    • complete() takes max_budget_usd.
    • HarnessTransport.stops_at_spend (default False) says whether the CLI starts no further API request once the call's spend reaches the threshold.
    • Only ClaudeCodeTransport sets it (--max-budget-usd). A call the CLI ends on its budget comes back with stop_reason="max_budget" and no text.
    • The CLI's total_cost_usd is reported as Reply.usage["cost_usd"].
    • codex / cursor / deepseek accept and ignore the argument.
  • kb_service.models
    • ModelGateway.call_json(..., max_budget_usd=None) refuses before dispatch when the transport can't stop at a threshold (fail-closed).
    • A call stopped at its threshold raises ModelUnavailable and is recorded as a failure.
    • ModelReply.cost_usd is new; the trace record gains cost_usd + max_budget_usd.
    • Callers without a threshold are unchanged: the kwarg is never sent.
  • SPEC: providers/{base,claude_code,codex,cursor,deepseek}.md, kb_service.md.

Tests

  • test/test_kb_spend_cap.py (new, offline fake CLI):
    • the flag is in argv only when a threshold is given
    • stop_reason="max_budget" handling
    • non-positive threshold rejected
    • only claude-code claims stops_at_spend
    • regression: no transport may advertise a hard per-call cap
    • the gateway refuses an uncapped transport before dispatch
    • cost flows to ModelReply and the trace
    • unchanged behaviour without a threshold
  • The full local CI mirror is green: pytest, doc links/citations, check_spec_freshness --strict, knowledge linters.

Review gate

The commit gate (gpt-6-astra-high) first flagged that the original supports_spend_cap promised a hard cap the CLI can't give. I reworked it to threshold semantics, and the gate approved it on iteration 2.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-30T09:09:17.166872Z bf29bbb PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: bf29bbb782

ℹ️ 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".

tzhouam added a commit that referenced this pull request Sep 30, 2026
Two defects found by the commit gate while integrating #241:

- claude_code formatted the threshold with `.4f`, moving the boundary:
  0.00001 became "0.0000" (no threshold) and 0.12345 became "0.1235" (a higher
  one). budget_arg() now emits the float's shortest round-trip decimal in plain
  notation, rejects non-positive/non-finite values, and is tested on tiny
  thresholds.
- runtime.trace_recorder dropped max_budget_usd (and cost_usd), so production
  traces never persisted them. They now land in the model_call record's
  result (null when unset/unknown), including transport-failure records;
  tested through the real recorder and TraceStore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tzhouam added a commit that referenced this pull request Sep 30, 2026
Two defects found by the commit gate while integrating #241:

- claude_code formatted the threshold with `.4f`, moving the boundary:
  0.00001 became "0.0000" (no threshold) and 0.12345 became "0.1235" (a higher
  one). budget_arg() now emits the float's shortest round-trip decimal in plain
  notation, rejects non-positive/non-finite values, and is tested on tiny
  thresholds.
- runtime.trace_recorder dropped max_budget_usd (and cost_usd), so production
  traces never persisted them. They now land in the model_call record's
  result (null when unset/unknown), including transport-failure records;
  tested through the real recorder and TraceStore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tzhouam and others added 3 commits September 30, 2026 22:21
…all cost

kb init's budget_usd must be a hard ceiling (design v3, sol-approved), but
max_tokens is no spend bound for CLI transports (they ignore it), and the
claude CLI checks --max-budget-usd only after each API request completes: it
stops the NEXT request, it cannot cut the current one short. So this is a stop
threshold, not a cap, and is named that way; the hard ceiling is the caller's
reservation of threshold + one request's worst case (kb init budget, later PR).

- providers: complete() takes max_budget_usd; HarnessTransport.stops_at_spend
  (default False) says whether the CLI starts no further request once the
  call's spend reaches it. Only claude-code does (--max-budget-usd); a call the
  CLI ends on its budget comes back with stop_reason "max_budget" and no text.
  The CLI's total_cost_usd is reported as usage["cost_usd"]. No transport may
  advertise a hard per-call cap (regression test).
- kb_service.models: call_json(max_budget_usd=...) refuses before dispatch when
  the transport cannot stop at a threshold, treats a call stopped at it as
  ModelUnavailable, and surfaces ModelReply.cost_usd (also in the trace
  record). Callers without a threshold are unchanged: the kwarg is never sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two defects found by the commit gate while integrating #241:

- claude_code formatted the threshold with `.4f`, moving the boundary:
  0.00001 became "0.0000" (no threshold) and 0.12345 became "0.1235" (a higher
  one). budget_arg() now emits the float's shortest round-trip decimal in plain
  notation, rejects non-positive/non-finite values, and is tested on tiny
  thresholds.
- runtime.trace_recorder dropped max_budget_usd (and cost_usd), so production
  traces never persisted them. They now land in the model_call record's
  result (null when unset/unknown), including transport-failure records;
  tested through the real recorder and TraceStore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The zcode provider (#238) landed after the spend-threshold change and
kept the old complete() signature. ModelGateway already refuses a
threshold for transports that cannot stop at one before dispatch, so
nothing broke, but every transport now takes max_budget_usd; zcode
accepts and ignores it (stops_at_spend stays False). A test now checks
every transport class, so the next provider cannot miss it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tzhouam
tzhouam merged commit fd79058 into main Sep 30, 2026
2 checks passed
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