anthropics / anthropics/claude-agent-sdk-python

claude-sonnet-5 overreporting cost

Open
#1,182 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
8.1k
Forks
1.3k
Avg merge
2d 31m
Merged PRs (30d)
1

Description

# `total_cost_usd` reports Opus rates for `claude-sonnet-5`

## Summary

`ResultMessage.total_cost_usd` prices `claude-sonnet-5` at **$5 / $25 per Mtok** (Opus 5's published
rate) instead of Sonnet 5's published **$3 / $15**. `claude-sonnet-4-6` in the same test run prices
correctly, so this looks specific to the `claude-sonnet-5` model ID rather than to the Sonnet family.

The response identifies itself as `claude-sonnet-5`, so this appears to be the reported cost figure,
not the model being served. I have no way to check from a subscription whether plan accounting uses
the same figure — that part is a question, not a claim.

## Environment

- Claude Code 2.1.195
- claude-agent-sdk 0.2.108
- Python 3.12.9, Windows 11 (10.0.26200)
- Max subscription auth (no `ANTHROPIC_API_KEY`)

## Reproduction

```python
import asyncio, claude_agent_sdk as sdk

async def one(model_id):
opts = sdk.ClaudeAgentOptions(model=model_id, max_turns=1, allowed_tools=[],
system_prompt="Be terse.", setting_sources=[])
cost, u = None, {}
async for msg in sdk.query(prompt="Reply with exactly: OK", options=opts):
if isinstance(msg, sdk.ResultMessage):
cost, u = msg.total_cost_usd, (msg.usage or {})
i, o = u.get("input_tokens", 0), u.get("output_tokens", 0)
cr, cw = u.get("cache_read_input_tokens", 0), u.get("cache_creation_input_tokens", 0)
den = i + 5 * o + 0.1 * cr + 2.0 * cw # published 1:5 in:out, 0.1x read, 2.0x write
print(f"{model_id:20s} in={i} out={o} cr={cr} cw={cw} "
f"cost={cost} -> implied ${cost * 1e6 / den:.2f}/Mtok input")

async def main():
for m in ("claude-haiku-4-5", "claude-sonnet-4-6", "claude-sonnet-5", "claude-opus-5"):
await one(m)

asyncio.run(main())
```

## Observed

| requested | served (`msg.model`) | implied input $/Mtok | published |
|---|---|---|---|
| `claude-haiku-4-5` | `claude-haiku-4-5-20251001` | $1.04 | $1.00 ✓ |
| `claude-sonnet-4-6` | `claude-sonnet-4-6` | $3.01 | $3.00 ✓ |
| `claude-sonnet-5` | `claude-sonnet-5` | **$5.08** | $3.00 ✗ |
| `claude-opus-5` | `claude-opus-5` | $5.09 | $5.00 ✓ |

The cleanest evidence is a **zero-cache** `claude-sonnet-5` call, where no cache multiplier
assumptions enter the arithmetic at all:

```
in=682 out=6 cache_creation=0 cache_read=0 total_cost_usd=0.003560
682 x $5/M + 6 x $25/M = $0.003560 <- exact match, Opus rates
682 x $3/M + 6 x $15/M = $0.002136 <- Sonnet 5 published
682 x $2/M + 6 x $10/M = $0.001424 <- Sonnet 5 introductory
```

Reproduced across three independent calls with different token mixes: implied $5.08, $5.085, $5.00.

## Expected

`total_cost_usd` for `claude-sonnet-5` computed at Sonnet 5 rates ($3/$15, or $2/$10 during the
introductory period).

## Ruled out

- **Not the wrong model being served.** `msg.model` reports `claude-sonnet-5`, and `claude-sonnet-4-6`
in the same run prices correctly — so the model field does vary meaningfully.
- **Not "undiscounted Sonnet."** Sonnet 5 without the introductory discount is $3/$15, which predicts
$0.002136 on the call above. The measured figure is $0.003560.
- **Not a cache-multiplier assumption.** The decisive call has zero cache tokens in both directions.

## Not established

Whether an unrecognised model ID falls back to Opus rates — I could not construct the control,
because the API rejects IDs it cannot serve, so there is no "known-unknown" model to probe with.

## Impact

Any tool that trusts `total_cost_usd` over-books Sonnet 5 usage by ~2.5x. In our case that inflates
per-window utilisation figures and erases the measured cost gap between Sonnet and Opus, which is
the gap a cost-aware model-routing policy exists to exploit — so the reported number actively
inverts the routing decision it is used to make.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by tracing ResultMessage.total_cost_usd through the claude_agent_sdk implementation and inspect how model IDs are mapped to pricing. Run the zero-cache reproduction for claude-sonnet-5, then compare the reported value with the published $3/$15 Sonnet rates. Done means the reported cost uses the expected Sonnet pricing rather than Opus rates.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
64/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.