agentshim 0.24.0__tar.gz → 0.24.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentshim-0.24.0 → agentshim-0.24.1}/CHANGELOG.md +73 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/PKG-INFO +1 -1
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/__init__.py +1 -1
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/transcripts.py +21 -11
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/usage.py +12 -4
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/cost.py +35 -6
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/model_data.py +3 -1
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/parser.py +14 -13
- agentshim-0.24.1/agentshim/providers/claude/transcripts.py +342 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/app_server/transport.py +1 -1
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/transcripts.py +10 -1
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/rates.py +88 -18
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/transcripts.py +107 -32
- {agentshim-0.24.0 → agentshim-0.24.1}/pyproject.toml +1 -1
- agentshim-0.24.1/tests/fixtures/claude_session/README.md +19 -0
- agentshim-0.24.0/agentshim/providers/claude/transcripts.py +0 -209
- {agentshim-0.24.0 → agentshim-0.24.1}/.gitignore +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/LICENSE +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/README.md +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/agent.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/__init__.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/_files.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/auth.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/checkpoints.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/clock.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/conversation.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/env.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/errors.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/events.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/home.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/ids.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/mcp.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/models.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/permissions.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/plugin.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/pricing.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/profile.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/provider.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/rates.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/schema.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/session_policy.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/skills.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/status.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/stream.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/core/turn.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/execution/__init__.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/execution/confinement.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/execution/docker.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/execution/executor.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/execution/host.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/execution/process.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/execution/readiness.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/execution/transform.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/models.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/oneshot.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/pricing.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/probe.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/__init__.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/_testing.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/catalog.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/__init__.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/events.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/failures.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/hooks/__init__.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/plugin.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/prices_generated.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/probe.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/provider.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/rate_limits.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/sandbox.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/scripted.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/stream_argv.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/stream_transport.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/testing.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/tools.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/claude/user_hooks.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/__init__.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/_toml.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/app_server/__init__.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/app_server/_wire.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/app_server/approvals.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/app_server/channel.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/app_server/errors.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/app_server/items.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/app_server/mcp.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/app_server/permissions.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/app_server/profile.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/app_server/protocol.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/app_server/rate_limits.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/app_server/usage.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/events.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/failures.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/mcp_scope.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/model_data.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/parser.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/plugin.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/prices_generated.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/probe.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/provider.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/rules.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/sandbox.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/scripted.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/skills.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/speed.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/codex/testing.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/copilot/__init__.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/copilot/events.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/copilot/parser.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/copilot/plugin.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/copilot/probe.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/copilot/provider.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/copilot/scripted.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/copilot/testing.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/gemini/__init__.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/gemini/events.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/gemini/parser.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/gemini/plugin.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/gemini/probe.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/gemini/provider.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/gemini/scripted.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/gemini/testing.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/opencode/__init__.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/opencode/events.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/opencode/model_data.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/opencode/parser.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/opencode/plugin.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/opencode/probe.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/opencode/provider.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/opencode/scripted.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/opencode/testing.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/providers/static_plugin.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/py.typed +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/runtime.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/session.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/testing/__init__.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/testing/claude_stream.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/testing/clock.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/testing/codex_app_server.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/testing/confinement.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/testing/contracts.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/testing/conversation.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/testing/probe.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/testing/process.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/agentshim/testing/rates.py +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/tests/fixtures/claude/README.md +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/tests/fixtures/claude_stream/README.md +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/tests/fixtures/codex/README.md +0 -0
- {agentshim-0.24.0 → agentshim-0.24.1}/tests/fixtures/codex_app_server/README.md +0 -0
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.24.1 (2026-10-31)
|
|
4
|
+
|
|
5
|
+
Fixes the usage numbers 0.24.0 got wrong. Recorded cost does not move for any single-result turn:
|
|
6
|
+
every price, `price_for` and `cost_usd` are as in 0.24.0, and `tests/fixtures/recorded_costs.json`
|
|
7
|
+
still proves it.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Claude transcripts undercounted output by about a third.** The CLI writes one line per content
|
|
12
|
+
block of a message, and the lines do not repeat the same usage: the prompt side is identical, but
|
|
13
|
+
`output_tokens` grows as the reply streams, from a few tokens on the first line to the whole reply
|
|
14
|
+
on the last. 0.24.0 kept the first line and dropped the rest. A message is now counted once, at
|
|
15
|
+
the most any line reported, and the records of a message add up to its final usage: the first
|
|
16
|
+
line yields the request (`turns=1`), each later line yields only the tokens it added (`turns=0`,
|
|
17
|
+
usually only output). Sum tokens over records, and count requests by summing `turns`. An advisor
|
|
18
|
+
call that shows only on a later line is counted too. A position saved by 0.24.0 has no such
|
|
19
|
+
counts, so it is refused and the file is read again from the start (`reset`), which also corrects
|
|
20
|
+
what 0.24.0 missed.
|
|
21
|
+
- **`ProviderUsage.by_model` counted the first turn after a steer twice.** `modelUsage` is
|
|
22
|
+
cumulative over the CLI process, and the parser added each result's rows to the last. The rows of
|
|
23
|
+
a run are now those of its last result, which cover the run once, and the rows sum to
|
|
24
|
+
`total_cost_usd`. The same seam is fixed for the primary row's `turns` (the run's, not the last
|
|
25
|
+
result's) and for the one-hour share of cache writes (the run's, applied to the run's writes), so a
|
|
26
|
+
steered run whose results differ in cache TTL is no longer priced at the last result's share.
|
|
27
|
+
A single result is unchanged.
|
|
28
|
+
- **A moved Codex session file was counted twice.** Codex archives a rollout by moving it from
|
|
29
|
+
`sessions` to `archived_sessions` under the same name. agentshim now owns that: a file with the
|
|
30
|
+
name of one already read is read on from the furthest position held for it (after the usual check
|
|
31
|
+
of the bytes before that position), and a copy that is shorter than what was read holds nothing
|
|
32
|
+
new. Keep one `known` mapping for all roots of a provider, as before. A caller no longer
|
|
33
|
+
de-duplicates anything. **Removed:** `TranscriptRoot.shared_sessions` and
|
|
34
|
+
`TranscriptFormat.shared_sessions`, which only told the caller to do this itself; a format sets
|
|
35
|
+
`session_file` (the name every copy of a session's file has) instead.
|
|
36
|
+
- **A fetched LiteLLM document took known prices away.** It replaced the pinned table whole, so a
|
|
37
|
+
model LiteLLM dropped or renamed lost its estimate, and a row listed at zero before its price was
|
|
38
|
+
known turned a known price into $0. A document is now laid over the pinned table: a model it lacks
|
|
39
|
+
keeps its pinned rates, a row with no input or output price does not replace a pinned price, and a
|
|
40
|
+
speed tier with no price under a priced standard is dropped. A model only the document knows is
|
|
41
|
+
taken as listed, free or not. `RateRead.known_models` counts the result.
|
|
42
|
+
- A `modelUsage` key with no name no longer raises out of the parser; its tokens are the row
|
|
43
|
+
`unknown`, which has no price, so the turn is `UNKNOWN_MODEL` as in 0.23.1. The CLI's
|
|
44
|
+
`<synthetic>` placeholder bills nothing and no longer makes a turn `UNKNOWN_MODEL`.
|
|
45
|
+
- `ultrafast` is read as `UsageSpeed.ULTRAFAST` in a Claude result and in a transcript (it read as
|
|
46
|
+
none, and as standard). A speed agentshim has no name for is still `None` (read as standard).
|
|
47
|
+
- `read_rates` and `refresh_rates` reject a naive `now` with `ValueError` before anything is
|
|
48
|
+
fetched. A clock set back is tolerated: a time stamped ahead of `now` by more than the floor has an
|
|
49
|
+
unknown age and counts as expired (it was fresh until the clock caught up), and an attempt more than
|
|
50
|
+
the floor away no longer holds off a refresh.
|
|
51
|
+
- The Codex app-server transport skips the `by_model` row of a model name that is blank.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- `read_transcript_usage(..., include_cwd=False)`: records carry `cwd` only when asked. It is an
|
|
56
|
+
absolute path on the user's machine, so a record a caller stores or sends holds no personal path by
|
|
57
|
+
default.
|
|
58
|
+
- `ModelUsage.recorded_usd` is documented as what it was: the pinned table's standard-tier list
|
|
59
|
+
price, no bill. A row whose `speed` is fast or ultrafast was billed more; docs/pricing.md shows
|
|
60
|
+
how to price the tier with `rates_for`, `price_tokens` and `speed_premium_usd`. Recorded cost still
|
|
61
|
+
does not model speed tiers.
|
|
62
|
+
- docs/transcripts.md states that `since` selects files, not records (filter on `timestamp`), and
|
|
63
|
+
that a line is held in memory whole while it is read.
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- Tests built from data shaped like what the CLIs write, not idealised input:
|
|
68
|
+
`tests/unit/transcripts/shapes.py` (streamed Claude messages whose output grows, advisor calls
|
|
69
|
+
that show late, Codex session files that grow, move and are copied),
|
|
70
|
+
`tests/unit/stream_shapes.py` (a steered Claude process with cumulative `modelUsage`),
|
|
71
|
+
`tests/unit/rate_shapes.py` (partial, renamed and placeholder LiteLLM documents), and a synthetic
|
|
72
|
+
session file shaped like a real streamed message (`tests/fixtures/claude_session`). Properties over
|
|
73
|
+
them: records summed over any read split equal the final usage; a moved or copied session is
|
|
74
|
+
counted once; steered results equal one result of the whole run; no known price becomes 0.
|
|
75
|
+
|
|
3
76
|
## 0.24.0 (2026-10-30)
|
|
4
77
|
|
|
5
78
|
Everything here is additive. Recorded cost does not move: every price, `price_for`, `cost_usd`,
|
|
@@ -226,7 +226,7 @@ from .runtime import Agent
|
|
|
226
226
|
from .session import Session, Turn, TurnTicket, map_mcp_servers
|
|
227
227
|
from .transcripts import read_transcript_usage, transcript_providers, transcript_roots
|
|
228
228
|
|
|
229
|
-
__version__ = "0.24.
|
|
229
|
+
__version__ = "0.24.1"
|
|
230
230
|
|
|
231
231
|
__all__ = [ # noqa: RUF022 - public API test uses Python's case-sensitive order
|
|
232
232
|
"Agent",
|
|
@@ -8,6 +8,8 @@ is each provider's :class:`TranscriptFormat`.
|
|
|
8
8
|
|
|
9
9
|
agentshim keeps no state between calls. The caller remembers a
|
|
10
10
|
:class:`FilePosition` per file and hands it back, and gets only what is new.
|
|
11
|
+
The positions of every root of a provider go in one mapping: a session file
|
|
12
|
+
that moves between roots is then read on from where the old path stopped.
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
15
|
from __future__ import annotations
|
|
@@ -40,28 +42,30 @@ class TranscriptRoot:
|
|
|
40
42
|
Attributes:
|
|
41
43
|
provider: The provider that wrote them.
|
|
42
44
|
path: The directory, resolved from the environment (it may not exist).
|
|
43
|
-
shared_sessions: Whether one session's records can appear in more than
|
|
44
|
-
one file under this provider, because a session file moved after
|
|
45
|
-
it was read. A caller then counts each session from one file only.
|
|
46
45
|
"""
|
|
47
46
|
|
|
48
47
|
provider: str
|
|
49
48
|
path: Path
|
|
50
|
-
shared_sessions: bool = False
|
|
51
49
|
|
|
52
50
|
|
|
53
51
|
@dataclass(frozen=True)
|
|
54
52
|
class TranscriptRecord:
|
|
55
|
-
"""One model request's usage,
|
|
53
|
+
"""One model request's usage, or the part of it one line of a transcript added.
|
|
56
54
|
|
|
57
55
|
Attributes:
|
|
58
|
-
timestamp: When the request was recorded (timezone-aware).
|
|
56
|
+
timestamp: When the request was recorded (timezone-aware). For the
|
|
57
|
+
tokens a later line of the file added, when that line was written.
|
|
59
58
|
model: The model id as the transcript names it.
|
|
60
59
|
session_id: The session it belongs to; a subagent's records carry
|
|
61
60
|
their parent's session.
|
|
62
|
-
cwd: The working directory the session ran in
|
|
61
|
+
cwd: The working directory the session ran in: an absolute path on the
|
|
62
|
+
user's machine, so a personal one. ``None`` unless the caller asked
|
|
63
|
+
for it with ``include_cwd``, and then only when the file says.
|
|
63
64
|
tokens: The request's tokens. ``input_tokens`` includes cache reads and
|
|
64
|
-
writes, as everywhere in agentshim.
|
|
65
|
+
writes, as everywhere in agentshim. A request is counted once, at
|
|
66
|
+
the most any line of the file reported: the records of one request
|
|
67
|
+
add up to its final usage, the first holding the request and its
|
|
68
|
+
``turns``, and later ones (``turns`` 0) only what a later line added.
|
|
65
69
|
speed: The service speed it was billed at.
|
|
66
70
|
reported_usd: A cost the file states. ``None`` for every shipped
|
|
67
71
|
format: a CLI's own figure is not trusted (see :mod:`agentshim.providers.claude.cost`).
|
|
@@ -153,7 +157,9 @@ class TranscriptScan:
|
|
|
153
157
|
Attributes:
|
|
154
158
|
files: The files read this call, in path order.
|
|
155
159
|
unchanged: Files whose size and modification time match the known
|
|
156
|
-
position of a finished read,
|
|
160
|
+
position of a finished read, and files that are earlier copies of
|
|
161
|
+
a session already read further under another path, so nothing was
|
|
162
|
+
read.
|
|
157
163
|
skipped: Files too old to matter for the window.
|
|
158
164
|
pending: Files not finished: the byte budget ran out first. Call again
|
|
159
165
|
with the new positions until it is zero.
|
|
@@ -211,7 +217,11 @@ class TranscriptFormat:
|
|
|
211
217
|
suffix: Which file names are transcripts.
|
|
212
218
|
unreadable_suffixes: Transcript names agentshim cannot read (a
|
|
213
219
|
compressed archive). They are reported in ``failed``, not skipped.
|
|
214
|
-
|
|
220
|
+
session_file: The name every copy of one session's file has, for a
|
|
221
|
+
provider that moves or copies a file between its directories after
|
|
222
|
+
it was read. A file with the name of one already read is read on
|
|
223
|
+
from there, so the move adds nothing twice. ``None`` when a file
|
|
224
|
+
keeps its path.
|
|
215
225
|
"""
|
|
216
226
|
|
|
217
227
|
subdirs: tuple[str, ...]
|
|
@@ -220,7 +230,7 @@ class TranscriptFormat:
|
|
|
220
230
|
parser: Callable[[Path, bytes | None], TranscriptParser | None]
|
|
221
231
|
suffix: str = ".jsonl"
|
|
222
232
|
unreadable_suffixes: tuple[str, ...] = field(default=())
|
|
223
|
-
|
|
233
|
+
session_file: Callable[[Path], str] | None = None
|
|
224
234
|
|
|
225
235
|
def __post_init__(self) -> None:
|
|
226
236
|
"""Reject a format that would read nothing, or read non-ASCII markers."""
|
|
@@ -293,9 +293,14 @@ class ModelUsage:
|
|
|
293
293
|
model: The model id as the provider reports it.
|
|
294
294
|
tokens: This turn's tokens for this model. Only the primary model
|
|
295
295
|
carries the turn's ``turns`` count.
|
|
296
|
-
recorded_usd: The pinned-table price of ``tokens
|
|
297
|
-
table has no price for the model. This is
|
|
298
|
-
recorded cost sums, never the CLI's own.
|
|
296
|
+
recorded_usd: The pinned-table price of ``tokens`` at the standard
|
|
297
|
+
tier; ``None`` when the table has no price for the model. This is
|
|
298
|
+
the figure a turn's recorded cost sums, never the CLI's own. It is
|
|
299
|
+
not a bill: the pinned table has no speed tiers, so a row whose
|
|
300
|
+
``speed`` is fast or ultrafast was billed more than this. For the
|
|
301
|
+
price at the tier, use :func:`agentshim.price_tokens` with the
|
|
302
|
+
entry's rates at ``speed``, or add
|
|
303
|
+
:func:`agentshim.speed_premium_usd`.
|
|
299
304
|
speed: The service speed the turn ran at, on the primary model only;
|
|
300
305
|
``None`` when the provider did not report one (read as standard).
|
|
301
306
|
primary: Whether this is the model the turn was run with. Exactly one
|
|
@@ -332,7 +337,10 @@ class ModelUsage:
|
|
|
332
337
|
def merge_model_usage(
|
|
333
338
|
first: Sequence[ModelUsage], second: Sequence[ModelUsage]
|
|
334
339
|
) -> tuple[ModelUsage, ...]:
|
|
335
|
-
"""Sum the per-model usage of two turns
|
|
340
|
+
"""Sum the per-model usage of two separate turns into one.
|
|
341
|
+
|
|
342
|
+
This adds turns that each have their own rows. Rows a parser built from a
|
|
343
|
+
CLI's cumulative counts already cover the whole run and must not be added.
|
|
336
344
|
|
|
337
345
|
Rows with the same model and speed add up, and a sum is unpriced if either
|
|
338
346
|
part was. The primary row of *first* (else of *second*) stays the only
|
|
@@ -22,12 +22,16 @@ from agentshim.core.rates import candidate_names
|
|
|
22
22
|
from agentshim.core.usage import CostSource, ModelUsage, TokenUsage, UsageSpeed, normalized_usage
|
|
23
23
|
from agentshim.pricing import price_for
|
|
24
24
|
|
|
25
|
+
from .model_data import SYNTHETIC_MODEL
|
|
26
|
+
|
|
25
27
|
if TYPE_CHECKING:
|
|
26
28
|
from collections.abc import Iterable, Mapping, Sequence
|
|
27
29
|
|
|
28
30
|
from agentshim.core.pricing import PricingTable
|
|
29
31
|
|
|
30
32
|
PROVIDER_NAME = "claude"
|
|
33
|
+
#: What a ``modelUsage`` key with no name is called: its tokens are real, but no price can match.
|
|
34
|
+
UNNAMED_MODEL = "unknown"
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
@dataclass(frozen=True)
|
|
@@ -60,6 +64,17 @@ class ModelCounts:
|
|
|
60
64
|
canonical=self.canonical,
|
|
61
65
|
)
|
|
62
66
|
|
|
67
|
+
def plus(self, other: ModelCounts) -> ModelCounts:
|
|
68
|
+
"""These counts and *other*'s added together, naming the model as this one does."""
|
|
69
|
+
return ModelCounts(
|
|
70
|
+
input_tokens=self.input_tokens + other.input_tokens,
|
|
71
|
+
output_tokens=self.output_tokens + other.output_tokens,
|
|
72
|
+
cache_read_tokens=self.cache_read_tokens + other.cache_read_tokens,
|
|
73
|
+
cache_write_tokens=self.cache_write_tokens + other.cache_write_tokens,
|
|
74
|
+
reasoning_tokens=self.reasoning_tokens + other.reasoning_tokens,
|
|
75
|
+
canonical=self.canonical or other.canonical,
|
|
76
|
+
)
|
|
77
|
+
|
|
63
78
|
@property
|
|
64
79
|
def empty(self) -> bool:
|
|
65
80
|
"""Whether no token was counted."""
|
|
@@ -87,7 +102,12 @@ class TurnCost:
|
|
|
87
102
|
|
|
88
103
|
|
|
89
104
|
def parse_model_usage(raw: object) -> dict[str, ModelCounts] | None:
|
|
90
|
-
"""Read a ``result`` frame's ``modelUsage``; ``None`` when it is absent or malformed.
|
|
105
|
+
"""Read a ``result`` frame's ``modelUsage``; ``None`` when it is absent or malformed.
|
|
106
|
+
|
|
107
|
+
A key with no name (empty or blank) is a CLI fault, not a reason to fail the turn: its
|
|
108
|
+
tokens are kept under :data:`UNNAMED_MODEL`, which has no price, so the turn is
|
|
109
|
+
``UNKNOWN_MODEL`` with no cost as it is for any other model agentshim cannot price.
|
|
110
|
+
"""
|
|
91
111
|
if not isinstance(raw, dict):
|
|
92
112
|
return None
|
|
93
113
|
parsed: dict[str, ModelCounts] = {}
|
|
@@ -96,7 +116,7 @@ def parse_model_usage(raw: object) -> dict[str, ModelCounts] | None:
|
|
|
96
116
|
return None
|
|
97
117
|
fields = cast("Mapping[str, Any]", counts)
|
|
98
118
|
canonical = fields.get("canonicalModel")
|
|
99
|
-
|
|
119
|
+
read = ModelCounts(
|
|
100
120
|
input_tokens=_count(fields, "inputTokens"),
|
|
101
121
|
output_tokens=_count(fields, "outputTokens"),
|
|
102
122
|
cache_read_tokens=_count(fields, "cacheReadInputTokens"),
|
|
@@ -104,6 +124,8 @@ def parse_model_usage(raw: object) -> dict[str, ModelCounts] | None:
|
|
|
104
124
|
reasoning_tokens=_count(fields, "thinkingTokens"),
|
|
105
125
|
canonical=canonical if isinstance(canonical, str) else None,
|
|
106
126
|
)
|
|
127
|
+
name = model if model.strip() else UNNAMED_MODEL
|
|
128
|
+
parsed[name] = parsed[name].plus(read) if name in parsed else read
|
|
107
129
|
return parsed
|
|
108
130
|
|
|
109
131
|
|
|
@@ -118,13 +140,15 @@ def priced_turn(
|
|
|
118
140
|
|
|
119
141
|
*one_hour_share* is the fraction of cache writes billed at the one-hour
|
|
120
142
|
rate. A model with tokens in the turn and no price makes the whole turn
|
|
121
|
-
``UNKNOWN_MODEL`` with no cost: a partial sum would read as complete.
|
|
143
|
+
``UNKNOWN_MODEL`` with no cost: a partial sum would read as complete. The
|
|
144
|
+
CLI's own placeholder for a message it wrote itself bills nothing and has
|
|
145
|
+
no row.
|
|
122
146
|
"""
|
|
123
147
|
total: float | None = 0.0
|
|
124
148
|
rows: list[ModelUsage] = []
|
|
125
149
|
for model, counts in model_usage.items():
|
|
126
150
|
delta = counts.since(baseline.get(model))
|
|
127
|
-
if delta.empty:
|
|
151
|
+
if delta.empty or model == SYNTHETIC_MODEL:
|
|
128
152
|
continue
|
|
129
153
|
pricing = price_for(PROVIDER_NAME, model, table)
|
|
130
154
|
if pricing is None and delta.canonical is not None:
|
|
@@ -181,9 +205,14 @@ def mark_primary(
|
|
|
181
205
|
|
|
182
206
|
|
|
183
207
|
def report_speed(usage: Mapping[str, Any] | None) -> UsageSpeed | None:
|
|
184
|
-
"""The speed a ``
|
|
208
|
+
"""The speed a ``usage`` mapping says the request ran at.
|
|
209
|
+
|
|
210
|
+
``None`` when it says none, or names a speed agentshim has no
|
|
211
|
+
:class:`UsageSpeed` for: the caller then reads it as standard, and the
|
|
212
|
+
figure is a list price of the standard tier whatever was billed.
|
|
213
|
+
"""
|
|
185
214
|
speed = None if usage is None else usage.get("speed")
|
|
186
|
-
return
|
|
215
|
+
return next((tier for tier in UsageSpeed if tier.value == speed), None)
|
|
187
216
|
|
|
188
217
|
|
|
189
218
|
def _tokens(
|
|
@@ -93,6 +93,8 @@ PRICING_ENTRIES = tuple(_anthropic(entry) for entry in RATE_ENTRIES)
|
|
|
93
93
|
#: Flat rates with speed tiers, for estimates; the same generated numbers as ``PRICING_ENTRIES``.
|
|
94
94
|
RATES = RateTable(SNAPSHOT_DATE, {entry.name: entry for entry in RATE_ENTRIES})
|
|
95
95
|
|
|
96
|
+
#: The ``model`` of a message the CLI wrote itself rather than a model call, which bills nothing.
|
|
97
|
+
SYNTHETIC_MODEL = "<synthetic>"
|
|
96
98
|
#: Names the CLI reports that are not a model with a price: its placeholder for a message that
|
|
97
99
|
#: never reached the API, and the bare family names, which do not say which generation ran.
|
|
98
|
-
UNPRICEABLE_MODELS = frozenset({
|
|
100
|
+
UNPRICEABLE_MODELS = frozenset({SYNTHETIC_MODEL, "synthetic", "opus", "sonnet", "haiku", "fable"})
|
|
@@ -26,7 +26,6 @@ from agentshim.core.usage import (
|
|
|
26
26
|
ModelUsage,
|
|
27
27
|
ProviderUsage,
|
|
28
28
|
TokenUsage,
|
|
29
|
-
merge_model_usage,
|
|
30
29
|
normalized_usage,
|
|
31
30
|
)
|
|
32
31
|
|
|
@@ -41,6 +40,7 @@ from .events import (
|
|
|
41
40
|
parse_frame,
|
|
42
41
|
)
|
|
43
42
|
from .failures import SCHEMA_RETRIES_SUBTYPE, classify_failure
|
|
43
|
+
from .model_data import SYNTHETIC_MODEL
|
|
44
44
|
from .rate_limits import RateLimitFrame
|
|
45
45
|
|
|
46
46
|
if TYPE_CHECKING:
|
|
@@ -96,10 +96,6 @@ def fold_usage(usage: Mapping[str, Any] | None, turns: int = 0) -> TokenUsage:
|
|
|
96
96
|
)
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
#: The ``model`` of a message Claude Code wrote itself rather than a model.
|
|
100
|
-
SYNTHETIC_MODEL = "<synthetic>"
|
|
101
|
-
|
|
102
|
-
|
|
103
99
|
def context_tokens(usage: Mapping[str, Any]) -> int:
|
|
104
100
|
"""Tokens one model call leaves in the context: its whole prompt plus its output.
|
|
105
101
|
|
|
@@ -343,14 +339,14 @@ class ClaudeStreamParser:
|
|
|
343
339
|
self._final_text = frame.text
|
|
344
340
|
self.total_cost_usd = frame.total_cost_usd
|
|
345
341
|
tokens = fold_usage(frame.usage, turns=frame.num_turns or 0)
|
|
346
|
-
self._price(frame, tokens)
|
|
347
|
-
self._structured = self._structured_payload(frame)
|
|
348
|
-
by_model = self._by_model
|
|
349
342
|
if self._results:
|
|
350
|
-
# A steered message can start a follow-on turn inside this run
|
|
351
|
-
#
|
|
343
|
+
# A steered message can start a follow-on turn inside this run. Its ``usage`` reports
|
|
344
|
+
# only its own tokens, so the run sums them; ``modelUsage`` and ``total_cost_usd``
|
|
345
|
+
# are cumulative over the process, so the cost and the per-model rows priced from
|
|
346
|
+
# them already cover every result of the run and are taken as they are, not added.
|
|
352
347
|
tokens = self._usage.tokens + tokens
|
|
353
|
-
|
|
348
|
+
self._price(frame, tokens)
|
|
349
|
+
self._structured = self._structured_payload(frame)
|
|
354
350
|
self._results += 1
|
|
355
351
|
self._usage = ProviderUsage(
|
|
356
352
|
tokens=tokens,
|
|
@@ -358,7 +354,7 @@ class ClaudeStreamParser:
|
|
|
358
354
|
provider=PROVIDER_NAME,
|
|
359
355
|
raw=frame.usage,
|
|
360
356
|
cost_source=self._cost_source,
|
|
361
|
-
by_model=
|
|
357
|
+
by_model=self._by_model,
|
|
362
358
|
)
|
|
363
359
|
self._emit(UsageReport(self._usage, self._cost_usd))
|
|
364
360
|
if frame.is_error:
|
|
@@ -369,7 +365,12 @@ class ClaudeStreamParser:
|
|
|
369
365
|
self._emit(ProviderError(self._error))
|
|
370
366
|
|
|
371
367
|
def _price(self, frame: ResultFrame, tokens: TokenUsage) -> None:
|
|
372
|
-
"""Set the
|
|
368
|
+
"""Set the run's cost from its per-model tokens, or from the CLI's when it names none.
|
|
369
|
+
|
|
370
|
+
*tokens* are the run's so far, the same span the cumulative ``modelUsage`` covers: their
|
|
371
|
+
one-hour share of cache writes applies to those writes, and their turn count goes on the
|
|
372
|
+
primary row.
|
|
373
|
+
"""
|
|
373
374
|
if frame.model_usage is not None:
|
|
374
375
|
self.model_usage_total = frame.model_usage
|
|
375
376
|
writes = tokens.cache_write_input_tokens
|