Skip to content

feat(claude): surface per-turn usage as an opt-in footer#391

Open
herikwebb wants to merge 7 commits into
plmbr:mainfrom
herikwebb:feat/claude-turn-usage
Open

feat(claude): surface per-turn usage as an opt-in footer#391
herikwebb wants to merge 7 commits into
plmbr:mainfrom
herikwebb:feat/claude-turn-usage

Conversation

@herikwebb

Copy link
Copy Markdown
Contributor

Problem

The Claude Agent SDK ends every turn with a ResultMessage carrying duration_ms, token usage, and total_cost_usd. The receive loop dropped it in its silent else branch, so users had no signal of what a turn cost short of typing /cost.

Change

  • New format_result_usage helper renders a small italic footer streamed after the turn's content, e.g. *12.3s · 45.2K in (38.1K cached) / 1.2K out · $0.0842*. Prefixed with a paragraph break so it can't concatenate onto prose that lacks a trailing blank line.
  • Opt-in via a new show_turn_usage Claude setting (default off), surfaced as a Settings-panel toggle — a footer on every turn is persistent noise, so it's off unless the user asks for it.
  • Cost is only shown when it can be trusted. total_cost_usd is priced from the CLI's public list rates, which are wrong for subscription logins (marginal cost $0), enterprise-negotiated rates, and non-first-party endpoints. _should_show_turn_cost gates the $ segment on both a direct API key and Anthropic's own endpoint, resolving each from the environment the CLI actually inherits (ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL), not just the settings panel. Duration and token counts — always accurate — still render when cost is suppressed.
  • Error results produce no footer; malformed usage values degrade to whatever segments remain rather than failing the turn.
  • 20 unit tests: the formatter (real ResultMessage instances), the cost gate (API key / base_url / env-resolution matrix), and the paragraph-break guard.

Testing

Full Python suite passes. Manually verified in JupyterLab:

  • Default (toggle off): no footer.
  • Toggle on + direct API key: time · tokens · $cost.
  • Toggle on, key cleared (subscription): time · tokens, no $.
  • Toggle on, custom base_url + key: time · tokens, no $ (proxy/gateway billing can't be trusted).

Herik Webb added 7 commits July 5, 2026 09:10
…Message

The Claude Agent SDK ends every turn with a ResultMessage carrying
duration_ms, token usage, and total_cost_usd. The receive loop dropped
it in its silent else branch, so users had no signal of what a turn
cost short of typing /cost.

Format it as a small italic footer (e.g. '12.3s · 45.2K in (38.1K
cached) / 1.2K out · $0.0842') streamed after the turn's content.
Error results produce no footer (the error path already reports), zero
cost is omitted (subscription sessions report 0.0 and showing $0.0000
would read as a billing claim), and malformed usage values degrade to
whatever segments remain rather than failing the turn.
Review follow-up (PR #47): the footer is streamed as another
MarkdownData chunk right after the assistant's last text block; without
a leading paragraph break it concatenated onto prose that doesn't end
in a blank line ('Done.*12.3s ...*'), breaking the emphasis markup.
Prefix the footer with a blank line and pin it with a test.
The footer streamed after every turn is persistent visual noise, and its
cost figure comes from the SDK's total_cost_usd, which the CLI prices
from public list rates — wrong for subscription logins (marginal cost is
$0), enterprise-negotiated rates, and non-first-party endpoints.

Add a 'show_turn_usage' claude_setting (default off) surfaced as a
Settings-panel toggle, and only emit the footer when it is on. Gate the
$ segment on a direct API key being configured so the duration/token
counts (always accurate) still show while a dollar amount we can't stand
behind is omitted.
The cost gate only checked for a direct API key, but a user with a
custom base_url (proxy, gateway, Bedrock/Vertex front, OpenAI-compatible
endpoint) plus a key would still see Claude public-list-price costs that
don't match their actual billing — the very case the footer's cost
suppression is meant to avoid.

Extract _should_show_turn_cost, requiring both a direct API key and
Anthropic's own endpoint (unset or api.anthropic.com base_url); a custom
base_url now suppresses the dollar figure while duration/tokens still
render. Add regression tests covering the base_url cases.
…st settings

The cost gate read api_key/base_url only from claude_settings, but NBI
overlays ANTHROPIC_API_KEY/ANTHROPIC_BASE_URL onto the CLI subprocess env
merged over the server's os.environ — so a blank setting falls through to
an ambient value the CLI actually uses. An env-provided custom base_url
would then show Anthropic list-price cost for a proxy/gateway endpoint.

Add _resolve_effective_credential mirroring that precedence (setting wins,
else env) and route _should_show_turn_cost through it, so the gate honors
env-provided keys and endpoints. Add env-resolution regression tests and
an autouse fixture keeping the suite hermetic.
Drop the billing caveats from the settings toggle title — those belong
in the docs, not the settings UI. Keep a one-line description.
Explain the Show usage after each turn toggle and when the cost figure is
shown vs omitted (direct API key on the default endpoint only), matching
the setting's trimmed UI description.
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