Skip to content

Fix KeyError: 'cost_usd' for Max subscription users - #14

Closed
unclecode wants to merge 1 commit into
anthropics:mainfrom
unclecode:fix-cost-usd-max-subscription
Closed

unclecode wants to merge 1 commit into
anthropics:mainfrom
unclecode:fix-cost-usd-max-subscription

Conversation

@unclecode

Copy link
Copy Markdown

Summary

This PR fixes issue #2 where Max subscription users encounter a KeyError when the API response doesn't include the cost_usd field.

Problem

Users with Max subscriptions were getting the following error:

KeyError: 'cost_usd'

This occurs because the SDK was expecting cost_usd to always be present in the response, but Max subscription responses don't include cost information.

Solution

  • Modified ResultMessage dataclass to make cost_usd and total_cost_usd optional (float | None)
  • Updated client.py to use .get() method with None default for cost-related fields
  • Maintains backward compatibility for users who do have cost data

Testing

  • All existing tests pass
  • Type checking passes with mypy
  • Linting passes with ruff

Fixes #2

This fix addresses issue anthropics#2 where Max subscription users encounter a KeyError
when the API response doesn't include the 'cost_usd' field.

Changes:
- Modified ResultMessage dataclass to make cost_usd and total_cost_usd optional (float | None)
- Updated client.py to use .get() with None default for cost-related fields
- Maintains backward compatibility for users with cost data

This ensures the SDK works for all subscription types, including Max subscriptions
that don't have access to cost information.

By: Unclecode <https://github.com/unclecode>
@ltawfik

ltawfik commented Jun 18, 2025

Copy link
Copy Markdown
Collaborator

thank you @unclecode . Closing this PR as this issue is fixed with latest release

@ltawfik ltawfik closed this Jun 18, 2025
qing-ant added a commit that referenced this pull request Apr 21, 2026
…ist-all (#847)

## Problem

`list_sessions_from_store()` calls `store.list_sessions()` for IDs, then
`store.load()` **per session** to derive title/first-prompt/branch/etc.
With N sessions that's N full-transcript round-trips just to render a
list. #846 (`load_range`) cut bytes-per-session but not round-trips —
500 sessions still meant 500+ store calls.

## Public API change — additive only, no breaking changes

```python
# New TypedDict (3 fields; `data` is opaque, stores persist verbatim)
class SessionSummaryEntry(TypedDict):
    session_id: str
    mtime: int
    data: dict[str, Any]

# New helper — adapters call this from append()
def fold_session_summary(
    prev: SessionSummaryEntry | None,
    key: SessionKey,
    entries: list[SessionStoreEntry],
) -> SessionSummaryEntry: ...

# New optional Protocol method (default: raise NotImplementedError)
class SessionStore(Protocol):
    async def list_session_summaries(self, project_key: str) -> list[SessionSummaryEntry]: ...
```

**Unchanged:**
`SessionStore.{append,load,list_sessions,delete,list_subkeys}`,
`list_sessions_from_store()` signature/return,
`get_session_info_from_store()`, `SDKSessionInfo`,
`SessionStoreListEntry`, `InMemorySessionStore` public methods. Stores
that don't implement `list_session_summaries` work exactly as before.

## Approach

Every field the list view needs is **append-incremental**: 4 are
set-once from the first entries (`is_sidechain`, `created_at`, `cwd`,
`first_prompt`), 7 are last-write-wins from the tail (`custom_title`,
`ai_title`, `last_prompt`, `summary_hint`, `git_branch`, `tag`,
`mtime`), 0 require a full scan. So a store can maintain a small summary
record alongside each session, updated inside `append()` with the
entries already in hand — no re-reads.

This PR adds:

- **`SessionSummaryEntry`** (TypedDict) — 3-field record (`session_id`,
`mtime`, opaque `data`). Stores persist it verbatim and never interpret
`data`.
- **`fold_session_summary(prev, key, entries)`** — pure helper that
folds new entries into the previous summary. Adapters call this from
`append()` so derivation logic lives in one place (no per-adapter
drift). `created_at` latches the first *parseable* entry timestamp — a
documented divergence from the lite-parse path only when the very first
entry lacks a timestamp (never happens in CLI-produced transcripts).
- **`SessionStore.list_session_summaries(project_key)`** — optional
Protocol method returning all summaries for a project in one call.
- **Fast path in `list_sessions_from_store()`** — when the store
implements `list_session_summaries`: build a unified slot list
(summary-derived slots + gap-fill placeholders for sessions present in
`list_sessions()` but lacking a sidecar), sort by `mtime`, apply
`offset`/`limit`, then `load()` only the placeholders that landed in the
page. Summary-backed sidechain/empty sessions are pre-filtered *before*
pagination so they don't consume page positions (matching disk/slow-path
filter-then-paginate); only gap-fill placeholders that resolve to `None`
after load can short-page, so a store with complete sidecars never
short-pages. `load()` count is bounded by page size, not total missing —
zero `load()` calls when sidecars are complete. Gap-fill is best-effort:
if the store lacks `list_sessions` it's skipped with a debug log.
Otherwise falls back to the existing per-session `load()` path (bounded
at 16 concurrent).
- **`InMemorySessionStore`** reference impl (entry-timestamp-derived
`mtime` so fast/slow paths sort on one clock) + conformance contract
#14.

`get_session_info_from_store()` (single session) is unchanged — full
`load()` is fine there.

## Correctness

`tests/test_session_summary.py::TestParityWithLiteParse` proves
`summary_entry_to_sdk_info(fold_session_summary(...))` produces the same
`SDKSessionInfo` as the existing `_parse_session_info_from_lite` batch
path on the same entry stream.

## For reviewers

- `_internal/session_summary.py` — fold logic, esp. first-prompt skip
filter (mirrors `_extract_first_prompt_from_head`)
- `_internal/sessions.py` fast-path block — fallback semantics
- `types.py` — public surface: `SessionSummaryEntry`,
`fold_session_summary`, `list_session_summaries`

Supersedes #846. TS port: anthropics/claude-cli-internal#30520.
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.

KeyError: 'cost_usd' with Max subsription

3 participants