agentshim 0.22.0__tar.gz → 0.23.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.22.0 → agentshim-0.23.1}/CHANGELOG.md +49 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/PKG-INFO +1 -1
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/__init__.py +1 -1
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/agent.py +1 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/errors.py +5 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/provider.py +3 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/session_policy.py +7 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/turn.py +9 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/pricing.py +1 -1
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/events.py +16 -1
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/failures.py +26 -5
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/parser.py +34 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/scripted.py +6 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/stream_transport.py +1 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/app_server/errors.py +4 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/app_server/transport.py +5 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/failures.py +33 -1
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/model_data.py +7 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/scripted.py +10 -1
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/opencode/parser.py +9 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/testing/conversation.py +3 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/pyproject.toml +1 -1
- agentshim-0.23.1/tests/fixtures/claude/README.md +13 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/tests/fixtures/codex/README.md +6 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/tests/fixtures/codex_app_server/README.md +2 -1
- {agentshim-0.22.0 → agentshim-0.23.1}/.gitignore +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/LICENSE +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/README.md +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/__init__.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/_files.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/auth.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/checkpoints.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/clock.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/conversation.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/env.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/events.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/home.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/ids.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/mcp.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/models.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/permissions.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/plugin.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/pricing.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/profile.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/schema.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/skills.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/status.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/stream.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/core/usage.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/execution/__init__.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/execution/confinement.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/execution/docker.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/execution/executor.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/execution/host.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/execution/process.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/execution/readiness.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/execution/transform.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/models.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/oneshot.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/probe.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/__init__.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/_testing.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/catalog.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/__init__.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/cost.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/hooks/__init__.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/model_data.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/plugin.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/probe.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/provider.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/rate_limits.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/sandbox.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/stream_argv.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/testing.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/tools.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/claude/user_hooks.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/__init__.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/_toml.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/app_server/__init__.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/app_server/_wire.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/app_server/approvals.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/app_server/channel.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/app_server/items.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/app_server/mcp.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/app_server/permissions.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/app_server/profile.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/app_server/protocol.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/app_server/rate_limits.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/app_server/usage.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/events.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/mcp_scope.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/parser.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/plugin.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/probe.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/provider.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/rules.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/sandbox.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/skills.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/codex/testing.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/copilot/__init__.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/copilot/events.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/copilot/parser.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/copilot/plugin.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/copilot/probe.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/copilot/provider.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/copilot/scripted.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/copilot/testing.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/gemini/__init__.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/gemini/events.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/gemini/parser.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/gemini/plugin.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/gemini/probe.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/gemini/provider.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/gemini/scripted.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/gemini/testing.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/opencode/__init__.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/opencode/events.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/opencode/model_data.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/opencode/plugin.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/opencode/probe.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/opencode/provider.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/opencode/scripted.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/opencode/testing.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/providers/static_plugin.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/py.typed +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/runtime.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/session.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/testing/__init__.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/testing/claude_stream.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/testing/clock.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/testing/codex_app_server.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/testing/confinement.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/testing/contracts.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/testing/probe.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/agentshim/testing/process.py +0 -0
- {agentshim-0.22.0 → agentshim-0.23.1}/tests/fixtures/claude_stream/README.md +0 -0
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.23.1 (2026-10-30)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `gpt-6.1-sol` has a price. The catalogue listed it since 0.18.2 but the price table did not, so
|
|
8
|
+
`price_for("codex", "gpt-6.1-sol")` returned `None` and its turns recorded no cost. The rates are
|
|
9
|
+
the standard short-context tier on https://developers.openai.com/api/docs/pricing, read
|
|
10
|
+
2026-10-30: input 2.00, cache read 0.10, cache write 2.50, output 10.00 USD per million tokens.
|
|
11
|
+
The cache read rate is half of `gpt-6-sol`'s. The table version is `2026-10-30`.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- A test that every model the catalogue or a provider profile lists is priced or named, with its
|
|
16
|
+
reason, in an exclusion list. `gemini-3-pro-preview` is the one exclusion: Google's pricing page
|
|
17
|
+
no longer lists it, so no official number can be checked, and Gemini models are not priced.
|
|
18
|
+
|
|
19
|
+
## 0.23.0 (2026-10-20)
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `FailureKind.CONTEXT_OVERFLOW`: the provider refused the turn because the conversation and the
|
|
24
|
+
new prompt do not fit the model's context window. Claude is read from the result frame's
|
|
25
|
+
`terminal_reason` (`prompt_too_long`, `blocking_limit`, `rapid_refill_breaker`), and from the
|
|
26
|
+
text `Prompt is too long` on builds without it; this wins over the 503 a failed compaction
|
|
27
|
+
appends, which used to make the overflow look `TRANSIENT`. Codex is read from the app-server's
|
|
28
|
+
`codexErrorInfo: contextWindowExceeded`, from the Responses API error body's `error.code`
|
|
29
|
+
`context_length_exceeded` (which the app-server relays under `other`, and `codex exec` as its
|
|
30
|
+
only report), and from Codex's "ran out of room in the model's context window". Before, every
|
|
31
|
+
one of these was `OTHER` or `TRANSIENT`. Gemini CLI 0.25.2 reports an overflow as a `success`
|
|
32
|
+
with zero tokens, so it cannot be classified; Copilot and opencode have no recorded overflow
|
|
33
|
+
and stay `OTHER`.
|
|
34
|
+
- `TurnResult.context_tokens` (and `ParsedTurn.context_tokens`): how many tokens the
|
|
35
|
+
conversation's context holds after the turn, which is what the next resume starts from. It is
|
|
36
|
+
the turn's last model call, its whole prompt (uncached input, cache reads, cache writes) plus
|
|
37
|
+
its output, not the turn's summed `usage`. Claude: the last main-thread assistant frame's usage,
|
|
38
|
+
skipping subagent frames and the CLI's synthetic error messages; the output count on that frame
|
|
39
|
+
is taken when the reply starts, so it can undercount the final reply. Codex app-server:
|
|
40
|
+
`last.totalTokens` of the turn's last `thread/tokenUsage/updated`. opencode: the last
|
|
41
|
+
`step_finish`, without reasoning. `None` for `codex exec` (thread totals only), Gemini (one sum
|
|
42
|
+
per turn), Copilot (per-request usage is not printed), and a turn with no model call.
|
|
43
|
+
- `FakeTurn(context_tokens=...)` scripts the reading in `agentshim.testing`.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- A session forgets its conversation after a `CONTEXT_OVERFLOW`, whether or not the turn resumed
|
|
48
|
+
it, since the conversation cannot take another turn. Strict and pinned turns keep it.
|
|
49
|
+
- `scripted_failure("codex", FailureKind.OTHER)` scripts a refused model; its old text was a
|
|
50
|
+
context overflow.
|
|
51
|
+
|
|
3
52
|
## 0.22.0 (2026-10-19)
|
|
4
53
|
|
|
5
54
|
### Fixed
|
|
@@ -186,7 +186,7 @@ from .providers.opencode import OpencodeProvider
|
|
|
186
186
|
from .runtime import Agent
|
|
187
187
|
from .session import Session, Turn, TurnTicket, map_mcp_servers
|
|
188
188
|
|
|
189
|
-
__version__ = "0.
|
|
189
|
+
__version__ = "0.23.1"
|
|
190
190
|
|
|
191
191
|
__all__ = [ # noqa: RUF022 - public API test uses Python's case-sensitive order
|
|
192
192
|
"Agent",
|
|
@@ -33,6 +33,11 @@ class FailureKind(str, Enum):
|
|
|
33
33
|
#: schema. ``detail`` carries the last validation errors it reported, and
|
|
34
34
|
#: the conversation survives: a correction can be sent as the next turn.
|
|
35
35
|
SCHEMA = "schema"
|
|
36
|
+
#: The conversation plus the new prompt do not fit the model's context
|
|
37
|
+
#: window, so the provider refused the turn. Sending the same turn to the
|
|
38
|
+
#: same conversation fails again; a fresh conversation, or a shorter
|
|
39
|
+
#: prompt, is the way forward.
|
|
40
|
+
CONTEXT_OVERFLOW = "context_overflow"
|
|
36
41
|
#: Anything else, including every failure a provider cannot classify.
|
|
37
42
|
OTHER = "other"
|
|
38
43
|
|
|
@@ -67,6 +67,9 @@ class ParsedTurn:
|
|
|
67
67
|
error: str | None = None
|
|
68
68
|
#: The parser's classification of ``error``; ``OTHER`` when it cannot tell.
|
|
69
69
|
error_kind: FailureKind = FailureKind.OTHER
|
|
70
|
+
#: Tokens in the conversation's context after the turn; see
|
|
71
|
+
#: ``TurnResult.context_tokens``. ``None`` when the stream cannot say.
|
|
72
|
+
context_tokens: int | None = None
|
|
70
73
|
|
|
71
74
|
|
|
72
75
|
class StreamParser(Protocol):
|
|
@@ -16,6 +16,8 @@ What the policy decides, per turn:
|
|
|
16
16
|
did not itself resume (the conversation was lost under it) forgets the
|
|
17
17
|
conversation without replaying the turn.
|
|
18
18
|
* An unclassified failure of a resumed turn forgets the conversation.
|
|
19
|
+
* A context overflow forgets the conversation, resumed or not: it cannot take
|
|
20
|
+
another turn.
|
|
19
21
|
* After a successful turn that exhausts the provider's ``RenewalBudget`` the
|
|
20
22
|
conversation is retired.
|
|
21
23
|
* An idle conversation is released at prepare time and reopened by id.
|
|
@@ -87,6 +89,8 @@ class Outcome(Enum):
|
|
|
87
89
|
USAGE_LIMIT = "usage_limit"
|
|
88
90
|
AUTH = "auth"
|
|
89
91
|
SCHEMA = "schema"
|
|
92
|
+
#: The conversation no longer fits the model's context window.
|
|
93
|
+
CONTEXT_OVERFLOW = "context_overflow"
|
|
90
94
|
#: A failure the provider could not classify.
|
|
91
95
|
OTHER = "other"
|
|
92
96
|
#: The provider no longer has the conversation the attempt tried to resume.
|
|
@@ -103,6 +107,7 @@ _OUTCOME_OF_KIND = {
|
|
|
103
107
|
FailureKind.USAGE_LIMIT: Outcome.USAGE_LIMIT,
|
|
104
108
|
FailureKind.AUTH: Outcome.AUTH,
|
|
105
109
|
FailureKind.SCHEMA: Outcome.SCHEMA,
|
|
110
|
+
FailureKind.CONTEXT_OVERFLOW: Outcome.CONTEXT_OVERFLOW,
|
|
106
111
|
FailureKind.OTHER: Outcome.OTHER,
|
|
107
112
|
}
|
|
108
113
|
|
|
@@ -536,6 +541,8 @@ class SessionPolicy:
|
|
|
536
541
|
return self._on_resume_refused(state, turn, event)
|
|
537
542
|
if event.outcome is Outcome.OTHER and turn.resumed and not turn.keeps_conversation:
|
|
538
543
|
return _forget_conversation(state, event.now)
|
|
544
|
+
if event.outcome is Outcome.CONTEXT_OVERFLOW and not turn.keeps_conversation:
|
|
545
|
+
return _forget_conversation(state, event.now)
|
|
539
546
|
return _give_up(state, event.now)
|
|
540
547
|
|
|
541
548
|
def _on_resume_refused(self, state: SessionState, turn: TurnState, event: Failed) -> Step:
|
|
@@ -69,6 +69,15 @@ class TurnResult:
|
|
|
69
69
|
#: The turn ended because it was interrupted. The conversation is kept; the
|
|
70
70
|
#: text and usage are whatever the turn produced before it stopped.
|
|
71
71
|
interrupted: bool = False
|
|
72
|
+
#: How many tokens the conversation's context holds after this turn: what
|
|
73
|
+
#: the next turn that resumes it starts from. It is the last model call of
|
|
74
|
+
#: the turn, counted as the prompt that call sent (uncached input, cache
|
|
75
|
+
#: reads and cache writes) plus the output it wrote, not ``usage``, which
|
|
76
|
+
#: sums every call of the turn and so counts the prompt once per call.
|
|
77
|
+
#: Calls made by a subagent are not this conversation's and are skipped.
|
|
78
|
+
#: Each provider's source is listed in ``docs/sessions.md``. ``None`` when
|
|
79
|
+
#: the provider does not report a single model call's usage.
|
|
80
|
+
context_tokens: int | None = None
|
|
72
81
|
|
|
73
82
|
|
|
74
83
|
def coerce_request(request: TurnRequest | str) -> TurnRequest:
|
|
@@ -106,4 +106,4 @@ def default_pricing(*, catalog: ProviderCatalog | None = None) -> PricingTable:
|
|
|
106
106
|
by_key[entry.key] = entry
|
|
107
107
|
entries = tuple(by_key.values())
|
|
108
108
|
updated = max((entry.checked for entry in entries), default=dt.date(2026, 9, 27))
|
|
109
|
-
return PricingTable(version="2026-10-
|
|
109
|
+
return PricingTable(version="2026-10-30", last_updated=updated, entries=entries)
|
|
@@ -67,6 +67,14 @@ class AssistantMessage:
|
|
|
67
67
|
#: when a request fails (``overloaded``, ``rate_limit``, ...); ``None`` on
|
|
68
68
|
#: a normal reply.
|
|
69
69
|
error: str | None = None
|
|
70
|
+
#: The API message id; the frames of one model call share it.
|
|
71
|
+
message_id: str | None = None
|
|
72
|
+
#: The model that wrote the message: ``<synthetic>`` for a message the CLI
|
|
73
|
+
#: wrote itself (an error) rather than a model call.
|
|
74
|
+
model: str | None = None
|
|
75
|
+
#: The ``Task`` call whose subagent wrote this, or ``None`` for the main
|
|
76
|
+
#: conversation.
|
|
77
|
+
parent_tool_use_id: str | None = None
|
|
70
78
|
|
|
71
79
|
|
|
72
80
|
@dataclass(frozen=True)
|
|
@@ -157,7 +165,14 @@ def _assistant(data: Mapping[str, Any]) -> AssistantMessage | None:
|
|
|
157
165
|
error = _str_or_none(data.get("error"))
|
|
158
166
|
if not blocks and usage is None and error is None:
|
|
159
167
|
return None
|
|
160
|
-
return AssistantMessage(
|
|
168
|
+
return AssistantMessage(
|
|
169
|
+
blocks=tuple(blocks),
|
|
170
|
+
usage=usage,
|
|
171
|
+
error=error,
|
|
172
|
+
message_id=_str_or_none(payload.get("id")),
|
|
173
|
+
model=_str_or_none(payload.get("model")),
|
|
174
|
+
parent_tool_use_id=_str_or_none(data.get("parent_tool_use_id")),
|
|
175
|
+
)
|
|
161
176
|
|
|
162
177
|
|
|
163
178
|
def _content_block(raw: object) -> ContentBlock | None:
|
|
@@ -7,6 +7,11 @@ kind on the synthetic assistant message it writes for the failure, an
|
|
|
7
7
|
have the text, so all three are read. A turn whose ``StructuredOutput`` calls
|
|
8
8
|
never validated ends with the ``result`` subtype
|
|
9
9
|
``error_max_structured_output_retries`` instead.
|
|
10
|
+
|
|
11
|
+
A prompt the context window cannot hold is named by the ``result`` frame's
|
|
12
|
+
``terminal_reason``. Builds without that field write ``Prompt is too long``
|
|
13
|
+
as the message, sometimes followed by why the CLI's own compaction failed
|
|
14
|
+
(which can be an API 503), so the overflow is checked before the status.
|
|
10
15
|
"""
|
|
11
16
|
|
|
12
17
|
from __future__ import annotations
|
|
@@ -43,22 +48,38 @@ _TRANSIENT_STATUSES = frozenset({408, 429})
|
|
|
43
48
|
_SERVER_ERRORS = range(500, 600)
|
|
44
49
|
#: The ``result`` subtype of a turn that used up its output-schema retries.
|
|
45
50
|
SCHEMA_RETRIES_SUBTYPE = "error_max_structured_output_retries"
|
|
51
|
+
#: ``terminal_reason`` values Claude Code groups as a context limit: the API
|
|
52
|
+
#: refused the prompt, the CLI's own estimate was over its blocking limit, or
|
|
53
|
+
#: the context refilled right after a compaction.
|
|
54
|
+
CONTEXT_LIMIT_REASONS = frozenset({"prompt_too_long", "blocking_limit", "rapid_refill_breaker"})
|
|
55
|
+
#: The CLI's message for the same, and the API's (``prompt is too long: N
|
|
56
|
+
#: tokens > M maximum``), for builds that write no ``terminal_reason`` or a
|
|
57
|
+
#: path that sets another one (a compaction that failed on its own error).
|
|
58
|
+
_CONTEXT_LIMIT_TEXT = re.compile(r"\bprompt is too long\b", re.IGNORECASE)
|
|
46
59
|
|
|
47
60
|
|
|
48
61
|
def classify_failure(
|
|
49
|
-
*,
|
|
62
|
+
*,
|
|
63
|
+
subtype: str | None,
|
|
64
|
+
api_error: str | None,
|
|
65
|
+
status: int | None,
|
|
66
|
+
text: str,
|
|
67
|
+
terminal_reason: str | None = None,
|
|
50
68
|
) -> FailureKind:
|
|
51
69
|
"""Classify one failed turn from what Claude reported about it.
|
|
52
70
|
|
|
53
71
|
``subtype`` is the error ``result`` frame's subtype, ``api_error`` the
|
|
54
72
|
assistant frame's ``error`` kind, ``status`` the
|
|
55
|
-
result frame's ``api_error_status``,
|
|
56
|
-
stream (or, when the stream had none, stderr) carried
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
result frame's ``api_error_status``, ``text`` the error text the
|
|
74
|
+
stream (or, when the stream had none, stderr) carried, and
|
|
75
|
+
``terminal_reason`` why the CLI said it stopped. Tool output is never part
|
|
76
|
+
of ``text``, so a tool that printed an API error cannot make the turn
|
|
77
|
+
look like one.
|
|
59
78
|
"""
|
|
60
79
|
if subtype == SCHEMA_RETRIES_SUBTYPE:
|
|
61
80
|
return FailureKind.SCHEMA
|
|
81
|
+
if terminal_reason in CONTEXT_LIMIT_REASONS or _CONTEXT_LIMIT_TEXT.search(text):
|
|
82
|
+
return FailureKind.CONTEXT_OVERFLOW
|
|
62
83
|
return _classify_api_failure(api_error=api_error, status=status, text=text)
|
|
63
84
|
|
|
64
85
|
|
|
@@ -89,6 +89,28 @@ def fold_usage(usage: Mapping[str, Any] | None, turns: int = 0) -> TokenUsage:
|
|
|
89
89
|
)
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
#: The ``model`` of a message Claude Code wrote itself rather than a model.
|
|
93
|
+
SYNTHETIC_MODEL = "<synthetic>"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def context_tokens(usage: Mapping[str, Any]) -> int:
|
|
97
|
+
"""Tokens one model call leaves in the context: its whole prompt plus its output.
|
|
98
|
+
|
|
99
|
+
The prompt is ``input_tokens`` (uncached) plus the cache reads and cache
|
|
100
|
+
writes, which Anthropic reports disjoint from it. The output count is the
|
|
101
|
+
one Claude Code writes on the message's frames. It is taken when the reply
|
|
102
|
+
starts, so it can fall short of the final reply by the tokens generated
|
|
103
|
+
after that; most of those are thinking, which the API drops from the
|
|
104
|
+
prompt of later turns anyway.
|
|
105
|
+
"""
|
|
106
|
+
return (
|
|
107
|
+
_int(usage.get("input_tokens"))
|
|
108
|
+
+ _int(usage.get("cache_read_input_tokens"))
|
|
109
|
+
+ _int(usage.get("cache_creation_input_tokens"))
|
|
110
|
+
+ _int(usage.get("output_tokens"))
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
92
114
|
def _int(value: object) -> int:
|
|
93
115
|
if isinstance(value, bool):
|
|
94
116
|
return 0
|
|
@@ -172,9 +194,12 @@ class ClaudeStreamParser:
|
|
|
172
194
|
self._api_error_text: str | None = None
|
|
173
195
|
self._api_error_status: int | None = None
|
|
174
196
|
self._subtype: str | None = None
|
|
197
|
+
self._terminal_reason: str | None = None
|
|
175
198
|
# The validation errors of the last rejected StructuredOutput call.
|
|
176
199
|
self._schema_errors: str | None = None
|
|
177
200
|
self._stderr: list[str] = []
|
|
201
|
+
# The context the main conversation's last model call left.
|
|
202
|
+
self._context_tokens: int | None = None
|
|
178
203
|
|
|
179
204
|
def feed_stdout(self, line: str) -> None:
|
|
180
205
|
"""Consume one stdout line, emitting the events its frame implies.
|
|
@@ -211,6 +236,7 @@ class ClaudeStreamParser:
|
|
|
211
236
|
cost_usd=self._cost_usd,
|
|
212
237
|
error=self._error,
|
|
213
238
|
error_kind=self._error_kind(),
|
|
239
|
+
context_tokens=self._context_tokens,
|
|
214
240
|
)
|
|
215
241
|
|
|
216
242
|
def _error_kind(self) -> FailureKind:
|
|
@@ -226,6 +252,7 @@ class ClaudeStreamParser:
|
|
|
226
252
|
api_error=self._api_error,
|
|
227
253
|
status=self._api_error_status,
|
|
228
254
|
text=text,
|
|
255
|
+
terminal_reason=self._terminal_reason,
|
|
229
256
|
)
|
|
230
257
|
|
|
231
258
|
def _handle(self, frame: object) -> None:
|
|
@@ -251,6 +278,12 @@ class ClaudeStreamParser:
|
|
|
251
278
|
self._api_error_text = "\n".join(
|
|
252
279
|
block.text for block in frame.blocks if isinstance(block, TextBlock)
|
|
253
280
|
)
|
|
281
|
+
if (
|
|
282
|
+
frame.usage is not None
|
|
283
|
+
and frame.parent_tool_use_id is None
|
|
284
|
+
and frame.model != SYNTHETIC_MODEL
|
|
285
|
+
):
|
|
286
|
+
self._context_tokens = context_tokens(frame.usage)
|
|
254
287
|
if frame.usage is not None:
|
|
255
288
|
self._emit(
|
|
256
289
|
UsageReport(
|
|
@@ -313,6 +346,7 @@ class ClaudeStreamParser:
|
|
|
313
346
|
if frame.is_error:
|
|
314
347
|
self._api_error_status = frame.api_error_status
|
|
315
348
|
self._subtype = frame.subtype
|
|
349
|
+
self._terminal_reason = frame.terminal_reason
|
|
316
350
|
self._error = self._error_text(frame)
|
|
317
351
|
self._emit(ProviderError(self._error))
|
|
318
352
|
|
|
@@ -129,8 +129,12 @@ _FAILURES: dict[FailureKind, tuple[str, int, str]] = {
|
|
|
129
129
|
),
|
|
130
130
|
FailureKind.USAGE_LIMIT: ("rate_limit", 429, "You've hit your limit \u00b7 resets 3pm (UTC)"),
|
|
131
131
|
FailureKind.AUTH: ("authentication_failed", 401, "Invalid API key \u00b7 Please run /login"),
|
|
132
|
+
FailureKind.CONTEXT_OVERFLOW: ("invalid_request", 400, "Prompt is too long"),
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
#: Why Claude Code says a turn of each kind stopped, where that is its signal.
|
|
136
|
+
_TERMINAL_REASONS: dict[FailureKind, str] = {FailureKind.CONTEXT_OVERFLOW: "prompt_too_long"}
|
|
137
|
+
|
|
134
138
|
#: The validation errors Claude's ``StructuredOutput`` tool returns for a
|
|
135
139
|
#: submission that does not match the schema.
|
|
136
140
|
SCHEMA_ERRORS = "Output does not match required schema: root: must have required property 'answer'"
|
|
@@ -170,6 +174,8 @@ def failure_lines(
|
|
|
170
174
|
message = {"role": "assistant", "content": [{"type": "text", "text": text}]}
|
|
171
175
|
lines.append(_line({"type": "assistant", "message": message, "error": error}))
|
|
172
176
|
result.update(subtype="success", result=text, api_error_status=status)
|
|
177
|
+
if kind in _TERMINAL_REASONS:
|
|
178
|
+
result["terminal_reason"] = _TERMINAL_REASONS[kind]
|
|
173
179
|
lines.append(_line(result))
|
|
174
180
|
return lines, [], 1
|
|
175
181
|
|
|
@@ -9,6 +9,9 @@ source; when it says only ``other`` (it then carries the raw HTTP body in
|
|
|
9
9
|
a lost connection or response stream (unless it ended in a client error
|
|
10
10
|
other than 408 and 429): ``TRANSIENT``.
|
|
11
11
|
- ``unauthorized``, or a connection failure with HTTP 401 or 403: ``AUTH``.
|
|
12
|
+
- ``contextWindowExceeded``: ``CONTEXT_OVERFLOW``. A request the Responses API
|
|
13
|
+
refuses for its length arrives as ``other`` with the API's error body, and
|
|
14
|
+
the text classifier reads its ``context_length_exceeded`` code.
|
|
12
15
|
- anything else: the text classifier, which returns ``OTHER`` for text it does
|
|
13
16
|
not recognize.
|
|
14
17
|
"""
|
|
@@ -35,6 +38,7 @@ _BY_KIND: dict[CodexErrorInfoKind, FailureKind] = {
|
|
|
35
38
|
CodexErrorInfoKind.INTERNAL_SERVER_ERROR: FailureKind.TRANSIENT,
|
|
36
39
|
CodexErrorInfoKind.FLEX_UNAVAILABLE: FailureKind.TRANSIENT,
|
|
37
40
|
CodexErrorInfoKind.UNAUTHORIZED: FailureKind.AUTH,
|
|
41
|
+
CodexErrorInfoKind.CONTEXT_WINDOW_EXCEEDED: FailureKind.CONTEXT_OVERFLOW,
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
_AUTH_STATUSES = frozenset({401, 403})
|
|
@@ -318,6 +318,9 @@ class _Turn:
|
|
|
318
318
|
turn_id: str | None = None
|
|
319
319
|
items: list[ThreadItem] = field(default_factory=_no_items)
|
|
320
320
|
total: Totals | None = None
|
|
321
|
+
#: ``last.totalTokens`` of the turn's latest usage report: the context
|
|
322
|
+
#: its last model call left.
|
|
323
|
+
context_tokens: int | None = None
|
|
321
324
|
completed: Turn | None = None
|
|
322
325
|
error: TurnError | None = None
|
|
323
326
|
fail: TurnFailedError | None = None
|
|
@@ -752,6 +755,7 @@ class _Conversation:
|
|
|
752
755
|
elif isinstance(params, ThreadTokenUsageUpdatedNotification):
|
|
753
756
|
if self._is_mine(params.thread_id, params.turn_id, turn):
|
|
754
757
|
turn.total = totals_of(params.token_usage.total)
|
|
758
|
+
turn.context_tokens = params.token_usage.last.total_tokens
|
|
755
759
|
elif isinstance(params, ErrorNotification):
|
|
756
760
|
self._on_error(params, turn)
|
|
757
761
|
elif isinstance(params, TurnCompletedNotification):
|
|
@@ -909,6 +913,7 @@ class _Conversation:
|
|
|
909
913
|
duration_ms=duration_ms,
|
|
910
914
|
exit_code=0,
|
|
911
915
|
interrupted=interrupted,
|
|
916
|
+
context_tokens=turn.context_tokens,
|
|
912
917
|
)
|
|
913
918
|
return result if skills is None else replace(result, skills=skills)
|
|
914
919
|
|
|
@@ -6,14 +6,27 @@ Codex's own: it names a usage limit ("You've hit your usage limit",
|
|
|
6
6
|
``usage_limit_reached``), retries a lost stream ("stream disconnected",
|
|
7
7
|
"Reconnecting...") and, once its retries run out, reports
|
|
8
8
|
"exceeded retry limit, last status: <status>".
|
|
9
|
+
|
|
10
|
+
A prompt the context window cannot hold arrives two ways. A request the
|
|
11
|
+
Responses API refuses is relayed as the API's own JSON error body, whose
|
|
12
|
+
``error.code`` is ``context_length_exceeded``; that code is read from the
|
|
13
|
+
parsed body, never searched for in prose. An overflow Codex's own accounting
|
|
14
|
+
catches is its message "Codex ran out of room in the model's context window".
|
|
9
15
|
"""
|
|
10
16
|
|
|
11
17
|
from __future__ import annotations
|
|
12
18
|
|
|
19
|
+
import json
|
|
13
20
|
import re
|
|
21
|
+
from typing import Any, cast
|
|
14
22
|
|
|
15
23
|
from agentshim.core.errors import FailureKind
|
|
16
24
|
|
|
25
|
+
#: The Responses API's ``error.code`` for a prompt longer than the window.
|
|
26
|
+
CONTEXT_LENGTH_EXCEEDED = "context_length_exceeded"
|
|
27
|
+
#: Codex's own message for ``CodexErr::ContextWindowExceeded``.
|
|
28
|
+
_CONTEXT_WINDOW = re.compile(r"ran out of room in the model's context window", re.IGNORECASE)
|
|
29
|
+
|
|
17
30
|
_USAGE_LIMIT = re.compile(
|
|
18
31
|
r"hit your usage limit|usage_limit_(?:reached|exceeded)|insufficient_quota", re.IGNORECASE
|
|
19
32
|
)
|
|
@@ -32,8 +45,12 @@ def classify_failure(text: str) -> FailureKind:
|
|
|
32
45
|
|
|
33
46
|
The order matters: a usage limit is also reported with a 429 status, and
|
|
34
47
|
an exhausted retry can end on a 401, so the specific kinds are checked
|
|
35
|
-
before the generic transient one.
|
|
48
|
+
before the generic transient one. A JSON error body is classified by its
|
|
49
|
+
code before any phrase is looked for.
|
|
36
50
|
"""
|
|
51
|
+
code = api_error_code(text)
|
|
52
|
+
if code == CONTEXT_LENGTH_EXCEEDED or _CONTEXT_WINDOW.search(text):
|
|
53
|
+
return FailureKind.CONTEXT_OVERFLOW
|
|
37
54
|
if _USAGE_LIMIT.search(text):
|
|
38
55
|
return FailureKind.USAGE_LIMIT
|
|
39
56
|
if _AUTH.search(text):
|
|
@@ -41,3 +58,18 @@ def classify_failure(text: str) -> FailureKind:
|
|
|
41
58
|
if _TRANSIENT.search(text):
|
|
42
59
|
return FailureKind.TRANSIENT
|
|
43
60
|
return FailureKind.OTHER
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def api_error_code(text: str) -> str | None:
|
|
64
|
+
"""The ``error.code`` of *text* when it is an API error body, else ``None``."""
|
|
65
|
+
try:
|
|
66
|
+
body: object = json.loads(text)
|
|
67
|
+
except ValueError:
|
|
68
|
+
return None
|
|
69
|
+
if not isinstance(body, dict):
|
|
70
|
+
return None
|
|
71
|
+
error = cast("dict[str, Any]", body).get("error")
|
|
72
|
+
if not isinstance(error, dict):
|
|
73
|
+
return None
|
|
74
|
+
code = cast("dict[str, Any]", error).get("code")
|
|
75
|
+
return code if isinstance(code, str) else None
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Codex-owned model and price facts."""
|
|
2
2
|
|
|
3
3
|
import datetime as dt
|
|
4
|
+
from dataclasses import replace
|
|
4
5
|
|
|
5
6
|
from agentshim.core.models import ModelSpec
|
|
6
7
|
from agentshim.core.pricing import ModelPricing
|
|
@@ -9,6 +10,8 @@ OPENAI_MODELS_URL = "https://developers.openai.com/api/docs/models"
|
|
|
9
10
|
OPENAI_PRICING_URL = "https://developers.openai.com/api/docs/pricing"
|
|
10
11
|
_MODEL_CHECKED = dt.date(2026, 10, 17)
|
|
11
12
|
_PRICE_CHECKED = dt.date(2026, 9, 27)
|
|
13
|
+
#: ``gpt-6.1-sol`` was read from the page on its own date, after the rest of the table.
|
|
14
|
+
_SOL_6_1_CHECKED = dt.date(2026, 10, 30)
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
def _openai_model(
|
|
@@ -88,6 +91,10 @@ def _openai(
|
|
|
88
91
|
|
|
89
92
|
PRICING_ENTRIES = (
|
|
90
93
|
_openai("gpt-6-astra", 10.00, 1.00, 50.00, cache_write_usd=12.50),
|
|
94
|
+
replace(
|
|
95
|
+
_openai("gpt-6.1-sol", 2.00, 0.10, 10.00, cache_write_usd=2.50),
|
|
96
|
+
checked=_SOL_6_1_CHECKED,
|
|
97
|
+
),
|
|
91
98
|
_openai("gpt-6-sol", 2.00, 0.20, 10.00, cache_write_usd=2.50),
|
|
92
99
|
_openai("gpt-6-luna", 0.10, 0.01, 0.50, cache_write_usd=0.125),
|
|
93
100
|
_openai("gpt-5.6-sol", 4.00, 0.40, 20.00, cache_write_usd=5.00),
|
|
@@ -131,7 +131,16 @@ _FAILURES: dict[FailureKind, str] = {
|
|
|
131
131
|
"(https://chatgpt.com/explore/plus), or try again in 2 hours."
|
|
132
132
|
),
|
|
133
133
|
FailureKind.AUTH: "unexpected status 401 Unauthorized: Missing bearer authentication",
|
|
134
|
-
FailureKind.
|
|
134
|
+
FailureKind.CONTEXT_OVERFLOW: (
|
|
135
|
+
'{"type":"error","error":{"message":"Your input exceeds the context window of this '
|
|
136
|
+
'model. Please adjust your input and try again.","type":"invalid_request_error",'
|
|
137
|
+
'"param":"input","code":"context_length_exceeded"},"status":400}'
|
|
138
|
+
),
|
|
139
|
+
FailureKind.OTHER: (
|
|
140
|
+
'{"type":"error","status":400,"error":{"type":"invalid_request_error","message":'
|
|
141
|
+
"\"The 'no-such-model-xyz' model is not supported when using Codex with a ChatGPT "
|
|
142
|
+
'account."}}'
|
|
143
|
+
),
|
|
135
144
|
}
|
|
136
145
|
|
|
137
146
|
|
|
@@ -114,6 +114,8 @@ class OpencodeStreamParser:
|
|
|
114
114
|
self._cost_usd: float | None = None
|
|
115
115
|
self._raw_tokens: Mapping[str, Any] | None = None
|
|
116
116
|
self._error: str | None = None
|
|
117
|
+
# The context the last step (one model call) left.
|
|
118
|
+
self._context_tokens: int | None = None
|
|
117
119
|
|
|
118
120
|
def feed_stdout(self, line: str) -> None:
|
|
119
121
|
"""Consume one stdout line."""
|
|
@@ -143,6 +145,7 @@ class OpencodeStreamParser:
|
|
|
143
145
|
usage=self._usage(),
|
|
144
146
|
cost_usd=self._cost_usd,
|
|
145
147
|
error=self._error,
|
|
148
|
+
context_tokens=self._context_tokens,
|
|
146
149
|
)
|
|
147
150
|
|
|
148
151
|
def _usage(self) -> ProviderUsage:
|
|
@@ -201,6 +204,12 @@ class OpencodeStreamParser:
|
|
|
201
204
|
def _step_finish(self, frame: StepFinishEvent) -> None:
|
|
202
205
|
step = fold_usage(frame.tokens)
|
|
203
206
|
self._tokens = self._tokens + step
|
|
207
|
+
if frame.tokens is not None:
|
|
208
|
+
# One step is one model call: its whole prompt plus the answer it
|
|
209
|
+
# wrote. Reasoning is not sent back in the next step's prompt.
|
|
210
|
+
self._context_tokens = (
|
|
211
|
+
step.input_tokens + step.output_tokens - step.reasoning_output_tokens
|
|
212
|
+
)
|
|
204
213
|
self._raw_tokens = frame.tokens if frame.tokens is not None else self._raw_tokens
|
|
205
214
|
if frame.cost is not None:
|
|
206
215
|
self._cost_usd = (self._cost_usd or 0.0) + frame.cost
|
|
@@ -54,6 +54,8 @@ class FakeTurn:
|
|
|
54
54
|
duration_ms: int = 0
|
|
55
55
|
events: Sequence[AgentEvent] = ()
|
|
56
56
|
during: Callable[[], None] | None = None
|
|
57
|
+
#: The ``TurnResult.context_tokens`` the turn reports.
|
|
58
|
+
context_tokens: int | None = None
|
|
57
59
|
|
|
58
60
|
|
|
59
61
|
FakeOutcome = FakeTurn | AgentShimError
|
|
@@ -156,6 +158,7 @@ class FakeConversation:
|
|
|
156
158
|
duration_ms=outcome.duration_ms,
|
|
157
159
|
exit_code=0,
|
|
158
160
|
interrupted=interrupted,
|
|
161
|
+
context_tokens=outcome.context_tokens,
|
|
159
162
|
)
|
|
160
163
|
|
|
161
164
|
def steer(self, text: str) -> None:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Claude Code stream-json recordings
|
|
2
|
+
|
|
3
|
+
Real `claude -p --output-format stream-json --verbose` stdout. The `init`
|
|
4
|
+
frame's lists of slash commands, skills, plugins, agents and MCP servers are
|
|
5
|
+
emptied, and local paths are replaced with `/workspace`; every other frame is
|
|
6
|
+
verbatim.
|
|
7
|
+
|
|
8
|
+
| File | Covers |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| `haiku45_two_model_calls.jsonl` | Claude Code 2.1.304, `claude-haiku-4-5`: a `Read` call, then the answer, so two model calls in one turn |
|
|
11
|
+
| `haiku45_prompt_too_long_blocking_limit.jsonl` | Claude Code 2.1.304, `claude-haiku-4-5`: a prompt over the CLI's own blocking limit; compaction fails, no request is sent, `terminal_reason: blocking_limit` |
|
|
12
|
+
| `haiku45_prompt_too_long_api_400.jsonl` | Claude Code 2.1.304, `claude-haiku-4-5`: a prompt the CLI underestimated (random CJK text), refused by the API with a 400, `terminal_reason: prompt_too_long` |
|
|
13
|
+
| `haiku_task_subagent.jsonl`, `skill_*_haiku.jsonl`, `sonnet_1h_cache.jsonl` | older recordings: a Task subagent, skill loads, one-hour cache writes |
|
|
@@ -16,3 +16,9 @@ contained no two-invocation recording.
|
|
|
16
16
|
Codex 0.144.4's JSON event processor copies all five fields from the thread
|
|
17
17
|
usage `total` into `turn.completed.usage`; it does not expose a per-turn field:
|
|
18
18
|
[provider source](https://github.com/openai/codex/blob/rust-v0.144.4/codex-rs/exec/src/event_processor_with_jsonl_output.rs).
|
|
19
|
+
|
|
20
|
+
`exec_context_overflow_gpt6_luna.jsonl` is recorded `codex exec --json` stdout
|
|
21
|
+
(codex-cli 0.160.0, `gpt-6-luna`, 2026-10-20) for a prompt of 700,000 random
|
|
22
|
+
CJK characters, about 1.4 million tokens. The Responses API refuses it, and
|
|
23
|
+
Codex relays the API's error body, code `context_length_exceeded`, as the
|
|
24
|
+
`error` and `turn.failed` message.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Codex app-server transcripts
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Four recorded sessions of `codex app-server` (codex-cli 0.160.0, model
|
|
4
4
|
`gpt-6-luna`, low effort) over stdio. Each line is
|
|
5
5
|
`{"dir": "out" | "in", "msg": {...}}`: `out` is what the client wrote, `in` what
|
|
6
6
|
the server wrote. They are scrubbed: the Codex home is `$CODEX_HOME`, the
|
|
@@ -12,6 +12,7 @@ placeholders.
|
|
|
12
12
|
| `session_1.jsonl` | handshake, `thread/resume` errors (unknown and malformed id), two threads in one process, a plain turn |
|
|
13
13
|
| `session_2.jsonl` | `workspace-write` with `untrusted` approvals: a declined command approval, a command that runs, `turn/interrupt` |
|
|
14
14
|
| `session_3.jsonl` | `thread/resume` across processes, a structured-output turn, per-thread MCP config that fails to start, a failed turn (unknown model) |
|
|
15
|
+
| `session_4.jsonl` | a plain turn whose `last` and `total` usage agree, then a turn of 700,000 random CJK characters that the Responses API refuses with `context_length_exceeded`, relayed under `codexErrorInfo: other`; recorded 2026-10-20, the long prompt is elided |
|
|
15
16
|
|
|
16
17
|
`tests/unit/providers/codex/app_server/test_replay.py` parses every line.
|
|
17
18
|
Re-record them only when the CLI version in
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|