Skip to content

feat(m3): usage_source=oauth_estimated cost plumbing#14

Open
suzuke wants to merge 1 commit into
feat/m3-smolagents-cc-bridgefrom
feat/m3-oauth-estimated-cost
Open

feat(m3): usage_source=oauth_estimated cost plumbing#14
suzuke wants to merge 1 commit into
feat/m3-smolagents-cc-bridgefrom
feat/m3-oauth-estimated-cost

Conversation

@suzuke
Copy link
Copy Markdown
Owner

@suzuke suzuke commented Apr 26, 2026

Summary

Stacked on #13 (M3 PR 19 smolagents+CC bridge). Closes the deferred TODO from PR 19 round 2: surface API-equivalent cost estimate from claude_agent_sdk.ResultMessage onto AttemptNode with the new usage_source="oauth_estimated" enum value (per spec §4.1).

Why a new enum value? Disambiguates "API-equivalent estimate of what would have been billed" from "actual metered API cost". Important for the smolagents+claude-subscription path where the user's actual bill is a flat subscription fee — postmortem shouldn't mislead them into thinking they're double-billed.

What changed

  1. ledger.UsageSource Literal extended: now "api" | "cli_estimated" | "oauth_estimated" | "unavailable". Per-value docs distinguish semantics.
  2. ClaudeAgentSDKModel accumulates ResultMessage.total_cost_usd across generate() calls; reset_cumulative_cost() + property accessor.
  3. SmolagentsBackend.generate_edit resets at start of agent.run() (per-attempt isolation); reads cumulative after; populates agent_result.backend_metadata with cost_usd + usage_source="oauth_estimated" when non-zero.
  4. Orchestrator._make_record reuses _extract_backend_metadata (PR 17 helper); backend_metadata's cost/usage_source override the default record.usage-derived path.

LiteLLM path unaffected — guarded by hasattr checks.

Reviewer trail

Single round, VERIFIED. Math reconciles (PR 19 baseline 2774 + R2 fix 1 + PR 19a 8 = 2783).

Reviewer also confirmed the two PR 19 round-2 non-blocking nits were closed in interim commit a15b606 (typed auth-error classification + docstring future-tense) — landed between PRs as the established pattern.

Stats

git diff --stat HEAD~1 HEAD:

  • 5 files changed, +278 / -1 LOC (test file: +203 LOC)
  • 8 new tests in tests/test_oauth_estimated_cost.py
  • Full suite: 2783 passed + 1 pre-existing failure unchanged + 4 skipped. 0 regressions from PR 19a.

Test plan

  • oauth_estimated value in UsageSource Literal
  • Cumulative cost starts/resets at zero
  • Cost accumulates across multiple generate() calls
  • Reset clears for per-attempt isolation
  • backend_metadata populated when cost > 0
  • backend_metadata empty when cost == 0
  • LiteLLM path doesn't set oauth_estimated (back-compat invariant)

🤖 Generated with Claude Code

Closes the loop on PR 19's deferred TODO. ClaudeAgentSDKModel now
accumulates `ResultMessage.total_cost_usd` across generate() calls;
SmolagentsBackend reads the cumulative estimate and surfaces it via
backend_metadata; orchestrator copies onto AttemptNode with
`usage_source="oauth_estimated"`.

Per spec §4.1 enum, "oauth_estimated" is a new fourth UsageSource
value alongside "api" / "cli_estimated" / "unavailable". Disambiguates
"API-equivalent estimate of what would have been billed" from "actual
metered API cost" — important for the smolagents+claude-subscription
path where the user's actual bill is a flat subscription fee.

Wiring:
- `ledger.UsageSource` Literal extended with "oauth_estimated"
- `ClaudeAgentSDKModel._cumulative_cost_estimate_usd` accumulates per
  ResultMessage; `reset_cumulative_cost()` + property accessor
- `SmolagentsBackend.generate_edit` resets at start of agent.run() so
  cost is per-attempt; reads cumulative after; populates
  `agent_result.backend_metadata['cost_usd']` + ['usage_source']
- `Orchestrator._make_record` reads backend_metadata; if it has
  usage_source + cost_usd, those override the default record.usage
  path (backend knows its own billing model)
- LiteLLM path unaffected (no cumulative_cost_estimate_usd attribute)

Tests: 8 new in `test_oauth_estimated_cost.py`:
- "oauth_estimated" in UsageSource enum
- Cumulative cost starts/resets at zero
- Cost accumulates across generate() calls
- Reset clears for new attempt
- backend_metadata includes oauth_estimated cost
- backend_metadata omits cost when zero
- LiteLLM path doesn't set oauth_estimated metadata (back-compat)

Stats: 4 files, +96/-3 LOC. Full suite 2783 passed + 1 pre-existing
failure unchanged + 4 skipped, 0 regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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