codex-as-api 0.8.1__tar.gz → 0.8.3__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.
Files changed (47) hide show
  1. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/PKG-INFO +15 -22
  2. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/README.md +14 -21
  3. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/RELEASES.md +45 -0
  4. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/pyproject.toml +1 -1
  5. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/src/codex_as_api/__init__.py +1 -1
  6. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/src/codex_as_api/anthropic_adapter.py +123 -14
  7. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/src/codex_as_api/model_capabilities.py +6 -5
  8. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/src/codex_as_api/provider.py +62 -6
  9. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/src/codex_as_api/server.py +71 -63
  10. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/conftest.py +1 -0
  11. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/test_anthropic_adapter.py +306 -63
  12. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/test_auth.py +64 -32
  13. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/test_codex_config.py +15 -7
  14. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/test_messages.py +14 -7
  15. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/test_model_capabilities.py +19 -37
  16. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/test_package_versions.py +8 -4
  17. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/test_protocol.py +18 -9
  18. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/test_provider.py +167 -83
  19. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/test_release_artifacts.py +14 -10
  20. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/test_rust_third_party_notices.py +8 -4
  21. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/test_server.py +337 -55
  22. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/.gitignore +0 -0
  23. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/LICENSE +0 -0
  24. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/THIRD_PARTY_NOTICES.md +0 -0
  25. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/config/codex-upstream-contract.json +0 -0
  26. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/config/o200k_base.tiktoken +0 -0
  27. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/requirements.txt +0 -0
  28. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/rust/THIRD_PARTY_NOTICES.md +0 -0
  29. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/scripts/check_no_static_model_catalog.py +0 -0
  30. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/scripts/check_package_versions.py +0 -0
  31. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/scripts/check_registry_release.py +0 -0
  32. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/scripts/check_release_artifacts.py +0 -0
  33. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/scripts/generate_rust_third_party_notices.py +0 -0
  34. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/scripts/package_rust_release.py +0 -0
  35. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/src/codex_as_api/auth.py +0 -0
  36. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/src/codex_as_api/codex_config.py +0 -0
  37. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/src/codex_as_api/messages.py +0 -0
  38. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/src/codex_as_api/o200k_tokenizer.py +0 -0
  39. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/src/codex_as_api/protocol.py +0 -0
  40. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/src/codex_as_api/strict_json.py +0 -0
  41. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/__init__.py +0 -0
  42. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/fixtures/o200k_base_encode_ordinary.json +0 -0
  43. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/test_no_static_model_catalog.py +0 -0
  44. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/test_o200k_tokenizer.py +0 -0
  45. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/test_registry_release.py +0 -0
  46. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/test_strict_json.py +0 -0
  47. {codex_as_api-0.8.1 → codex_as_api-0.8.3}/tests/test_ultrafast.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codex-as-api
3
- Version: 0.8.1
3
+ Version: 0.8.3
4
4
  Summary: Use ChatGPT/Codex OAuth as a local OpenAI-compatible API server.
5
5
  Project-URL: Homepage, https://github.com/Eunho-J/codex-as-api
6
6
  Project-URL: Repository, https://github.com/Eunho-J/codex-as-api
@@ -284,11 +284,9 @@ it; continuation uses the complete provider Response items saved behind
284
284
 
285
285
  ### `POST /v1/messages`
286
286
 
287
- Claude Code compatibility endpoint using the Anthropic Messages shape. It supports streaming (`stream: true`) and non-streaming only for requests carrying one valid `x-claude-code-session-id` header. A live Codex model ID routes directly. A recognized `claude-*` facade ID requires an explicit `CODEX_AS_API_MODEL` or Codex config model that is present in the same fresh catalog; it is not silently routed to a default. Other unknown IDs fail. The response preserves the client-supplied model name.
287
+ Anthropic Messages compatibility endpoint for any client, including Claude Code and Scheherazade. It supports streaming (`stream: true`) and non-streaming without a client-identification header. A live Codex model ID routes directly. A `claude-*` facade ID uses the configured Codex backend model, or the authenticated catalog default when no backend model is configured. Other unknown IDs fail. The response preserves the client-supplied model name.
288
288
 
289
- This compatibility endpoint is scoped to Claude Code requests. The proxy accepts
290
- and strips the non-forwarded `max_tokens` and `cache_control` compatibility
291
- fields only when one valid `x-claude-code-session-id` header is present.
289
+ `max_tokens` is a required positive compatibility hint: the private Codex OAuth transport does not expose a matching output-token cap. Valid ephemeral `cache_control` hints are accepted and stripped for every client; their TTL and breakpoint semantics are not forwarded. An optional `x-claude-code-session-id` provides stable hashed cache affinity, not permission to use the endpoint. `metadata.user_id` is accepted as client metadata and is not forwarded.
292
290
 
293
291
  ```bash
294
292
  curl http://localhost:18080/v1/messages \
@@ -325,7 +323,7 @@ curl -N http://localhost:18080/v1/messages \
325
323
  }'
326
324
  ```
327
325
 
328
- Codex reasoning summaries are not Anthropic signed-thinking blocks. A non-streaming response exposes real upstream reasoning only through the top-level `codex_reasoning` proxy extension. A stream emits `event: codex_reasoning_delta` with `type: "codex_reasoning_delta"`. The proxy never fabricates `thinking`, `thinking_delta`, or `signature_delta` data.
326
+ With `thinking.display: "summarized"` (the enabled/adaptive default), real Codex reasoning summaries are returned as unsigned Anthropic `thinking` blocks and streamed `thinking_delta` content, alongside the existing `codex_reasoning` / `codex_reasoning_delta` extensions. `display: "omitted"` and disabled thinking suppress reasoning output. No Anthropic signature is fabricated. Valid thinking blocks in replayed assistant history are checked and stripped before Codex input; ordinary text and tool calls are preserved.
329
327
 
330
328
  Hosted Anthropic `web_search` declarations are rejected with HTTP 400 on
331
329
  Messages, token-counting, and compact requests. OpenAI Responses cannot supply
@@ -345,10 +343,7 @@ the completion is nonterminal or final usage is absent; a stream that has
345
343
  already started emits an Anthropic `error` event and never emits a successful
346
344
  `message_stop`. The final response includes every required usage field, using
347
345
  `null` only where the Anthropic type permits it, plus required container,
348
- citation, context-management, and direct-caller fields. To preserve Claude
349
- Code's realtime behavior, `message_start` is sent immediately without guessed
350
- input-token counts; this narrow stream-start shape is Claude Code compatibility
351
- behavior, not generic Anthropic API parity.
346
+ citation, context-management, and direct-caller fields. `message_start` is sent immediately with numeric zero counters in `message.usage`; these are provisional, not measured counts. The final `message_delta.usage` replaces them with authoritative cumulative totals, including disjoint cache buckets.
352
347
 
353
348
  Generated `tool_use` blocks with `caller: {"type":"direct"}` and empty
354
349
  assistant content arrays can be replayed through the request adapter. Malformed,
@@ -364,9 +359,9 @@ different jobs:
364
359
  | `client_metadata.session_id` | Forwarded to Codex, used as the default `prompt_cache_key`, and required when Codex metadata mode is enabled | Not used or forwarded |
365
360
  | `client_metadata.thread_id` | Codex metadata thread identity; defaults to `session_id` for a root request and preserves an explicit child thread | Not used or synthesized |
366
361
  | `prompt_cache_key` | Explicit value wins; otherwise a non-empty `client_metadata.session_id` is used; otherwise omitted | Optional proxy extension; explicit value wins, otherwise the Claude Code session header is hashed |
367
- | `x-claude-code-session-id` | Not used | Required for the non-forwarded `max_tokens` and `cache_control` compatibility fields; its exact value also supplies hashed cache affinity |
362
+ | `x-claude-code-session-id` | Not used | Optional cache-affinity header; its exact value supplies a hashed default key |
368
363
  | `previous_response_id` | Resolves a known entry from the 256-chain process-local history and replays full input/output over HTTP; never forwarded | Non-null values return Anthropic-style HTTP 400 because normal Messages is stateless |
369
- | `cache_control` | Not an OpenAI Chat control | Accepted only with the Claude Code session header, validated, then stripped before Codex transport |
364
+ | `cache_control` | Not an OpenAI Chat control | Accepted for every client, validated, then stripped before Codex transport |
370
365
 
371
366
  Anthropic `cache_control` is a cache-boundary annotation, not an identifier.
372
367
  This proxy accepts only `{"type":"ephemeral"}` with optional `ttl: "5m"` or
@@ -378,15 +373,13 @@ responses do not expose the Chat-only `response_id`. The custom
378
373
  `previous_response_id` support.
379
374
 
380
375
  The Anthropic type permits `cache_control: null`; the proxy treats that exact
381
- value as omission. A non-null cache-control object is validated in full and is
382
- accepted only on a Claude Code request.
376
+ value as omission. A non-null cache-control object is validated in full for every client.
383
377
 
384
378
  ### `POST /v1/messages/count_tokens`
385
379
 
386
- Anthropic-compatible token counting helper. Codex OAuth does not expose a count-only endpoint equivalent to Anthropic's native API, so this route counts text locally and returns context-window metadata for the effective backend model. Normalized model-visible messages, tool calls, tool-result metadata, reasoning, and tool schemas are counted once. Request-envelope fields and the approved Claude Code `max_tokens` compatibility hint are excluded; other unsupported generation controls fail instead of becoming no-ops. Image inputs use a separate fixed estimate so inline base64 data is not counted as text.
380
+ Anthropic-compatible token counting helper. Codex OAuth does not expose a count-only endpoint equivalent to Anthropic's native API, so this route counts text locally and returns context-window metadata for the effective backend model. Normalized model-visible messages, tool calls, tool-result metadata, reasoning, and tool schemas are counted once. Request-envelope fields and the `max_tokens` compatibility hint are excluded; other unsupported generation controls fail instead of becoming no-ops. Image inputs use a separate fixed estimate so inline base64 data is not counted as text.
387
381
 
388
- `max_tokens` and `cache_control` remain Claude Code-only compatibility hints on
389
- this route and require `x-claude-code-session-id` when present.
382
+ `max_tokens` and `cache_control` are optional client-neutral compatibility hints on this route; no Claude Code header is required.
390
383
 
391
384
  Text estimation uses a bundled port of official `tiktoken` `o200k_base` `encode_ordinary`: the same Unicode pre-tokenization regex, byte-pair merge algorithm, and merge-rank data are implemented in Python, TypeScript, and Rust. It is a local estimator, not model metadata and not a substitute for a provider count API. The project does not depend on a `tiktoken` package or download encoding data at runtime. The last upstream synchronization check was **2026-07-14**, against [`tiktoken` 0.13.0 at `08a5f3b`](https://github.com/openai/tiktoken/tree/08a5f3b2c987ada4fc5aa1f16c643c203fa8acaa); the bundled rank file SHA-256 is `446a9538cb6c348e3516120d7c08b09f57c36495e2acfffe59a5bf8b0cfb1a2d`.
392
385
 
@@ -405,7 +398,7 @@ curl http://localhost:18080/v1/messages/count_tokens \
405
398
 
406
399
  ### `POST /v1/messages/compact`
407
400
 
408
- Anthropic-compatible alias for remote conversation compaction. Accepts Anthropic Messages-shaped bodies and returns compacted checkpoint content. Live GPT model IDs select the matching backend model, and Claude Code effort and Fast Mode controls use the same mappings as `/v1/messages`. Its required non-forwarded `max_tokens` and any `cache_control` hints require one valid `x-claude-code-session-id` header.
401
+ Anthropic-compatible alias for remote conversation compaction. Accepts Anthropic Messages-shaped bodies and returns compacted checkpoint content. Live GPT model IDs select the matching backend model, and Claude Code effort and Fast Mode controls use the same mappings as `/v1/messages`. Its required non-forwarded `max_tokens` and any valid `cache_control` hints are accepted for every client; the optional Claude Code session header only supplies cache affinity.
409
402
 
410
403
  ### `POST /v1/images/generations`
411
404
 
@@ -545,7 +538,7 @@ Requests can instead use the Responses-shaped effort/context object:
545
538
  - Responses Lite uses `all_turns` as the Codex wire default. An explicitly different context is rejected instead of silently overwritten; use `responses_lite: false` when the backend route supports classic Responses and another context is required.
546
539
  - Remote compact keeps its existing private Codex `reasoning_effort` field but does not accept public `reasoning.mode` or `reasoning.context`.
547
540
 
548
- Anthropic `thinking` values map as `enabled → high`, `adaptive → medium`, and `disabled → none`. Claude Code's `output_config.effort` takes precedence over adaptive or enabled thinking and supports `low`, `medium`, `high`, `xhigh`, and `max`. Call-level `thinking.disabled` takes precedence over ambient `output_config.effort` for representable client-side auxiliary calls such as WebFetch; hosted Anthropic WebSearch is rejected before transport because its lifecycle cannot be translated losslessly. The supported reasoning context and verbosity extensions are also available on `/v1/messages`, image generation, and inspection requests; Pro, non-null public cache policy/breakpoints, and any supplied `safety_identifier` fail explicitly on the private Codex provider.
541
+ Anthropic `thinking` values map as `enabled → high` and `adaptive → medium`. Disabled thinking selects `none` when available, otherwise the lowest supported catalog effort, and suppresses reasoning output; it does not claim to eliminate a model's internal reasoning. `output_config.effort` takes precedence over adaptive or enabled thinking and supports `low`, `medium`, `high`, `xhigh`, and `max`. Call-level disabled thinking takes precedence over ambient `output_config.effort` for auxiliary calls such as WebFetch. Explicit unsupported effort requests still fail. Hosted Anthropic WebSearch is rejected because its lifecycle cannot be translated losslessly. The supported reasoning context and verbosity extensions are also available on `/v1/messages`, image generation, and inspection requests; Pro, non-null public cache policy/breakpoints, and any supplied `safety_identifier` fail explicitly on the private Codex provider.
549
542
 
550
543
  Anthropic's `JSONOutputFormat` carries a schema but no name. When Claude Code
551
544
  sends that official shape, the proxy follows pinned Codex 0.153.3 and supplies
@@ -819,9 +812,9 @@ ANTHROPIC_BASE_URL=http://127.0.0.1:18080 ANTHROPIC_AUTH_TOKEN=unused \
819
812
  claude --model gpt-5.6-luna --effort medium
820
813
  ```
821
814
 
822
- Live Codex IDs are sent to the matching backend model. Built-in Claude/Fable/Opus/Sonnet/Haiku IDs are not Codex models, so recognized `claude-*` IDs require the explicitly configured `CODEX_AS_API_MODEL`; the proxy preserves the selected client name in the Anthropic response.
815
+ Live Codex IDs are sent to the matching backend model. Built-in Claude/Fable/Opus/Sonnet/Haiku IDs are not Codex models, so recognized `claude-*` IDs use the configured Codex model or authenticated catalog default; the proxy preserves the selected client name in the Anthropic response.
823
816
 
824
- `ANTHROPIC_BASE_URL` applies to the whole Claude Code process. Therefore the built-in rows remain available in the picker, but they also pass through codex-as-api and use the explicitly configured Codex backend; the rows do not call Anthropic models in that process. To use an actual Anthropic model and GPT in parallel, keep the gateway variables out of global settings, start the GPT process with the inline command above, and start a second process normally:
817
+ `ANTHROPIC_BASE_URL` applies to the whole Claude Code process. Therefore the built-in rows remain available in the picker, but they also pass through codex-as-api and use the configured or catalog-default Codex backend; the rows do not call Anthropic models in that process. To use an actual Anthropic model and GPT in parallel, keep the gateway variables out of global settings, start the GPT process with the inline command above, and start a second process normally:
825
818
 
826
819
  ```bash
827
820
  claude --model sonnet
@@ -831,7 +824,7 @@ If the gateway variables are already in `~/.claude/settings.json`, shell-level `
831
824
 
832
825
  Claude Code gateway discovery cannot expose raw `gpt-*` IDs because Claude Code filters discovered IDs to `claude*` and `anthropic*`. The official single custom-model option avoids a misleading facade ID. This integration is an Anthropic Messages-compatible bridge; Anthropic explicitly does not support routing Claude Code to non-Claude models through third-party gateways. See the official [model configuration](https://code.claude.com/docs/en/model-config), [gateway connection](https://code.claude.com/docs/en/llm-gateway-connect), and [gateway protocol](https://code.claude.com/docs/en/llm-gateway-protocol) references.
833
826
 
834
- The current bridge accepts Claude Code's exact no-op thinking cleanup (`clear_thinking_20251015` with `keep: "all"`). Context edits that would change history, task budgets, enabled beta tool fields such as `strict` or `defer_loading`, malformed output formats, and unsupported image source types return HTTP 400 because the Codex OAuth transport has no lossless equivalent. Valid base64 and URL image blocks plus `tool_result.is_error` are preserved during translation.
827
+ The bridge accepts Claude Code's no-op thinking cleanup (`clear_thinking_20251015` with `keep: "all"`), validates and ignores boolean `eager_input_streaming`, and accepts `defer_loading: false`. Context edits that would change history, task budgets, enabled strict/deferred tool execution, malformed output formats, and unsupported image source types return HTTP 400. Valid base64 and URL image blocks plus `tool_result.is_error` are preserved during translation.
835
828
 
836
829
  ## Architecture
837
830
 
@@ -250,11 +250,9 @@ it; continuation uses the complete provider Response items saved behind
250
250
 
251
251
  ### `POST /v1/messages`
252
252
 
253
- Claude Code compatibility endpoint using the Anthropic Messages shape. It supports streaming (`stream: true`) and non-streaming only for requests carrying one valid `x-claude-code-session-id` header. A live Codex model ID routes directly. A recognized `claude-*` facade ID requires an explicit `CODEX_AS_API_MODEL` or Codex config model that is present in the same fresh catalog; it is not silently routed to a default. Other unknown IDs fail. The response preserves the client-supplied model name.
253
+ Anthropic Messages compatibility endpoint for any client, including Claude Code and Scheherazade. It supports streaming (`stream: true`) and non-streaming without a client-identification header. A live Codex model ID routes directly. A `claude-*` facade ID uses the configured Codex backend model, or the authenticated catalog default when no backend model is configured. Other unknown IDs fail. The response preserves the client-supplied model name.
254
254
 
255
- This compatibility endpoint is scoped to Claude Code requests. The proxy accepts
256
- and strips the non-forwarded `max_tokens` and `cache_control` compatibility
257
- fields only when one valid `x-claude-code-session-id` header is present.
255
+ `max_tokens` is a required positive compatibility hint: the private Codex OAuth transport does not expose a matching output-token cap. Valid ephemeral `cache_control` hints are accepted and stripped for every client; their TTL and breakpoint semantics are not forwarded. An optional `x-claude-code-session-id` provides stable hashed cache affinity, not permission to use the endpoint. `metadata.user_id` is accepted as client metadata and is not forwarded.
258
256
 
259
257
  ```bash
260
258
  curl http://localhost:18080/v1/messages \
@@ -291,7 +289,7 @@ curl -N http://localhost:18080/v1/messages \
291
289
  }'
292
290
  ```
293
291
 
294
- Codex reasoning summaries are not Anthropic signed-thinking blocks. A non-streaming response exposes real upstream reasoning only through the top-level `codex_reasoning` proxy extension. A stream emits `event: codex_reasoning_delta` with `type: "codex_reasoning_delta"`. The proxy never fabricates `thinking`, `thinking_delta`, or `signature_delta` data.
292
+ With `thinking.display: "summarized"` (the enabled/adaptive default), real Codex reasoning summaries are returned as unsigned Anthropic `thinking` blocks and streamed `thinking_delta` content, alongside the existing `codex_reasoning` / `codex_reasoning_delta` extensions. `display: "omitted"` and disabled thinking suppress reasoning output. No Anthropic signature is fabricated. Valid thinking blocks in replayed assistant history are checked and stripped before Codex input; ordinary text and tool calls are preserved.
295
293
 
296
294
  Hosted Anthropic `web_search` declarations are rejected with HTTP 400 on
297
295
  Messages, token-counting, and compact requests. OpenAI Responses cannot supply
@@ -311,10 +309,7 @@ the completion is nonterminal or final usage is absent; a stream that has
311
309
  already started emits an Anthropic `error` event and never emits a successful
312
310
  `message_stop`. The final response includes every required usage field, using
313
311
  `null` only where the Anthropic type permits it, plus required container,
314
- citation, context-management, and direct-caller fields. To preserve Claude
315
- Code's realtime behavior, `message_start` is sent immediately without guessed
316
- input-token counts; this narrow stream-start shape is Claude Code compatibility
317
- behavior, not generic Anthropic API parity.
312
+ citation, context-management, and direct-caller fields. `message_start` is sent immediately with numeric zero counters in `message.usage`; these are provisional, not measured counts. The final `message_delta.usage` replaces them with authoritative cumulative totals, including disjoint cache buckets.
318
313
 
319
314
  Generated `tool_use` blocks with `caller: {"type":"direct"}` and empty
320
315
  assistant content arrays can be replayed through the request adapter. Malformed,
@@ -330,9 +325,9 @@ different jobs:
330
325
  | `client_metadata.session_id` | Forwarded to Codex, used as the default `prompt_cache_key`, and required when Codex metadata mode is enabled | Not used or forwarded |
331
326
  | `client_metadata.thread_id` | Codex metadata thread identity; defaults to `session_id` for a root request and preserves an explicit child thread | Not used or synthesized |
332
327
  | `prompt_cache_key` | Explicit value wins; otherwise a non-empty `client_metadata.session_id` is used; otherwise omitted | Optional proxy extension; explicit value wins, otherwise the Claude Code session header is hashed |
333
- | `x-claude-code-session-id` | Not used | Required for the non-forwarded `max_tokens` and `cache_control` compatibility fields; its exact value also supplies hashed cache affinity |
328
+ | `x-claude-code-session-id` | Not used | Optional cache-affinity header; its exact value supplies a hashed default key |
334
329
  | `previous_response_id` | Resolves a known entry from the 256-chain process-local history and replays full input/output over HTTP; never forwarded | Non-null values return Anthropic-style HTTP 400 because normal Messages is stateless |
335
- | `cache_control` | Not an OpenAI Chat control | Accepted only with the Claude Code session header, validated, then stripped before Codex transport |
330
+ | `cache_control` | Not an OpenAI Chat control | Accepted for every client, validated, then stripped before Codex transport |
336
331
 
337
332
  Anthropic `cache_control` is a cache-boundary annotation, not an identifier.
338
333
  This proxy accepts only `{"type":"ephemeral"}` with optional `ttl: "5m"` or
@@ -344,15 +339,13 @@ responses do not expose the Chat-only `response_id`. The custom
344
339
  `previous_response_id` support.
345
340
 
346
341
  The Anthropic type permits `cache_control: null`; the proxy treats that exact
347
- value as omission. A non-null cache-control object is validated in full and is
348
- accepted only on a Claude Code request.
342
+ value as omission. A non-null cache-control object is validated in full for every client.
349
343
 
350
344
  ### `POST /v1/messages/count_tokens`
351
345
 
352
- Anthropic-compatible token counting helper. Codex OAuth does not expose a count-only endpoint equivalent to Anthropic's native API, so this route counts text locally and returns context-window metadata for the effective backend model. Normalized model-visible messages, tool calls, tool-result metadata, reasoning, and tool schemas are counted once. Request-envelope fields and the approved Claude Code `max_tokens` compatibility hint are excluded; other unsupported generation controls fail instead of becoming no-ops. Image inputs use a separate fixed estimate so inline base64 data is not counted as text.
346
+ Anthropic-compatible token counting helper. Codex OAuth does not expose a count-only endpoint equivalent to Anthropic's native API, so this route counts text locally and returns context-window metadata for the effective backend model. Normalized model-visible messages, tool calls, tool-result metadata, reasoning, and tool schemas are counted once. Request-envelope fields and the `max_tokens` compatibility hint are excluded; other unsupported generation controls fail instead of becoming no-ops. Image inputs use a separate fixed estimate so inline base64 data is not counted as text.
353
347
 
354
- `max_tokens` and `cache_control` remain Claude Code-only compatibility hints on
355
- this route and require `x-claude-code-session-id` when present.
348
+ `max_tokens` and `cache_control` are optional client-neutral compatibility hints on this route; no Claude Code header is required.
356
349
 
357
350
  Text estimation uses a bundled port of official `tiktoken` `o200k_base` `encode_ordinary`: the same Unicode pre-tokenization regex, byte-pair merge algorithm, and merge-rank data are implemented in Python, TypeScript, and Rust. It is a local estimator, not model metadata and not a substitute for a provider count API. The project does not depend on a `tiktoken` package or download encoding data at runtime. The last upstream synchronization check was **2026-07-14**, against [`tiktoken` 0.13.0 at `08a5f3b`](https://github.com/openai/tiktoken/tree/08a5f3b2c987ada4fc5aa1f16c643c203fa8acaa); the bundled rank file SHA-256 is `446a9538cb6c348e3516120d7c08b09f57c36495e2acfffe59a5bf8b0cfb1a2d`.
358
351
 
@@ -371,7 +364,7 @@ curl http://localhost:18080/v1/messages/count_tokens \
371
364
 
372
365
  ### `POST /v1/messages/compact`
373
366
 
374
- Anthropic-compatible alias for remote conversation compaction. Accepts Anthropic Messages-shaped bodies and returns compacted checkpoint content. Live GPT model IDs select the matching backend model, and Claude Code effort and Fast Mode controls use the same mappings as `/v1/messages`. Its required non-forwarded `max_tokens` and any `cache_control` hints require one valid `x-claude-code-session-id` header.
367
+ Anthropic-compatible alias for remote conversation compaction. Accepts Anthropic Messages-shaped bodies and returns compacted checkpoint content. Live GPT model IDs select the matching backend model, and Claude Code effort and Fast Mode controls use the same mappings as `/v1/messages`. Its required non-forwarded `max_tokens` and any valid `cache_control` hints are accepted for every client; the optional Claude Code session header only supplies cache affinity.
375
368
 
376
369
  ### `POST /v1/images/generations`
377
370
 
@@ -511,7 +504,7 @@ Requests can instead use the Responses-shaped effort/context object:
511
504
  - Responses Lite uses `all_turns` as the Codex wire default. An explicitly different context is rejected instead of silently overwritten; use `responses_lite: false` when the backend route supports classic Responses and another context is required.
512
505
  - Remote compact keeps its existing private Codex `reasoning_effort` field but does not accept public `reasoning.mode` or `reasoning.context`.
513
506
 
514
- Anthropic `thinking` values map as `enabled → high`, `adaptive → medium`, and `disabled → none`. Claude Code's `output_config.effort` takes precedence over adaptive or enabled thinking and supports `low`, `medium`, `high`, `xhigh`, and `max`. Call-level `thinking.disabled` takes precedence over ambient `output_config.effort` for representable client-side auxiliary calls such as WebFetch; hosted Anthropic WebSearch is rejected before transport because its lifecycle cannot be translated losslessly. The supported reasoning context and verbosity extensions are also available on `/v1/messages`, image generation, and inspection requests; Pro, non-null public cache policy/breakpoints, and any supplied `safety_identifier` fail explicitly on the private Codex provider.
507
+ Anthropic `thinking` values map as `enabled → high` and `adaptive → medium`. Disabled thinking selects `none` when available, otherwise the lowest supported catalog effort, and suppresses reasoning output; it does not claim to eliminate a model's internal reasoning. `output_config.effort` takes precedence over adaptive or enabled thinking and supports `low`, `medium`, `high`, `xhigh`, and `max`. Call-level disabled thinking takes precedence over ambient `output_config.effort` for auxiliary calls such as WebFetch. Explicit unsupported effort requests still fail. Hosted Anthropic WebSearch is rejected because its lifecycle cannot be translated losslessly. The supported reasoning context and verbosity extensions are also available on `/v1/messages`, image generation, and inspection requests; Pro, non-null public cache policy/breakpoints, and any supplied `safety_identifier` fail explicitly on the private Codex provider.
515
508
 
516
509
  Anthropic's `JSONOutputFormat` carries a schema but no name. When Claude Code
517
510
  sends that official shape, the proxy follows pinned Codex 0.153.3 and supplies
@@ -785,9 +778,9 @@ ANTHROPIC_BASE_URL=http://127.0.0.1:18080 ANTHROPIC_AUTH_TOKEN=unused \
785
778
  claude --model gpt-5.6-luna --effort medium
786
779
  ```
787
780
 
788
- Live Codex IDs are sent to the matching backend model. Built-in Claude/Fable/Opus/Sonnet/Haiku IDs are not Codex models, so recognized `claude-*` IDs require the explicitly configured `CODEX_AS_API_MODEL`; the proxy preserves the selected client name in the Anthropic response.
781
+ Live Codex IDs are sent to the matching backend model. Built-in Claude/Fable/Opus/Sonnet/Haiku IDs are not Codex models, so recognized `claude-*` IDs use the configured Codex model or authenticated catalog default; the proxy preserves the selected client name in the Anthropic response.
789
782
 
790
- `ANTHROPIC_BASE_URL` applies to the whole Claude Code process. Therefore the built-in rows remain available in the picker, but they also pass through codex-as-api and use the explicitly configured Codex backend; the rows do not call Anthropic models in that process. To use an actual Anthropic model and GPT in parallel, keep the gateway variables out of global settings, start the GPT process with the inline command above, and start a second process normally:
783
+ `ANTHROPIC_BASE_URL` applies to the whole Claude Code process. Therefore the built-in rows remain available in the picker, but they also pass through codex-as-api and use the configured or catalog-default Codex backend; the rows do not call Anthropic models in that process. To use an actual Anthropic model and GPT in parallel, keep the gateway variables out of global settings, start the GPT process with the inline command above, and start a second process normally:
791
784
 
792
785
  ```bash
793
786
  claude --model sonnet
@@ -797,7 +790,7 @@ If the gateway variables are already in `~/.claude/settings.json`, shell-level `
797
790
 
798
791
  Claude Code gateway discovery cannot expose raw `gpt-*` IDs because Claude Code filters discovered IDs to `claude*` and `anthropic*`. The official single custom-model option avoids a misleading facade ID. This integration is an Anthropic Messages-compatible bridge; Anthropic explicitly does not support routing Claude Code to non-Claude models through third-party gateways. See the official [model configuration](https://code.claude.com/docs/en/model-config), [gateway connection](https://code.claude.com/docs/en/llm-gateway-connect), and [gateway protocol](https://code.claude.com/docs/en/llm-gateway-protocol) references.
799
792
 
800
- The current bridge accepts Claude Code's exact no-op thinking cleanup (`clear_thinking_20251015` with `keep: "all"`). Context edits that would change history, task budgets, enabled beta tool fields such as `strict` or `defer_loading`, malformed output formats, and unsupported image source types return HTTP 400 because the Codex OAuth transport has no lossless equivalent. Valid base64 and URL image blocks plus `tool_result.is_error` are preserved during translation.
793
+ The bridge accepts Claude Code's no-op thinking cleanup (`clear_thinking_20251015` with `keep: "all"`), validates and ignores boolean `eager_input_streaming`, and accepts `defer_loading: false`. Context edits that would change history, task budgets, enabled strict/deferred tool execution, malformed output formats, and unsupported image source types return HTTP 400. Valid base64 and URL image blocks plus `tool_result.is_error` are preserved during translation.
801
794
 
802
795
  ## Architecture
803
796
 
@@ -1,5 +1,50 @@
1
1
  # Release Notes
2
2
 
3
+ ## v0.8.3
4
+
5
+ ### Client-neutral Anthropic compatibility (all runtimes)
6
+
7
+ - Remove Claude Code identity gates around valid required `max_tokens` and
8
+ ephemeral `cache_control` hints on Messages, counting and compact routes.
9
+ These are compatibility hints, not native Codex output caps or cache TTLs;
10
+ optional Claude session cache affinity remains available.
11
+ - Include provisional zero usage in every initial streaming `message_start`,
12
+ followed by authoritative final usage with the 0.8.2 disjoint cache accounting.
13
+ - Support summarized thinking as real unsigned thinking blocks/deltas, retaining
14
+ existing Codex reasoning extensions. Omitted and disabled thinking suppress
15
+ reasoning output; never fabricate an Anthropic signature.
16
+ - Resolve disabled-derived effort to the model's lowest supported level when
17
+ `none` is unavailable; explicit effort selection remains capability-checked.
18
+ - Validate and strip replayed assistant thinking history for multi-turn clients.
19
+ Accept standard `metadata.user_id` and boolean tool streaming hints without
20
+ inventing backend semantics, and use the live catalog default for unconfigured
21
+ `claude-*` facade routing.
22
+
23
+ ### Official test quality
24
+
25
+ - Replace regex diagnostic assertions with literal returned-error checks and
26
+ remove source-presence, filesystem-permission and worker-liveness oracles.
27
+ Retain actual input/output, persistence-content and cancellation-state cases.
28
+ - Repair stale client-gate expectations and positive controls, including valid
29
+ null cache hints, live-default Claude facade routing and mixed thinking/tool
30
+ replay. Validate all three runtimes with their standard regression runners.
31
+
32
+ ## v0.8.2
33
+
34
+ ### Hotfix: disjoint Anthropic cache usage (all runtimes)
35
+
36
+ - Correct Anthropic Messages usage in streaming and non-streaming responses:
37
+ `input_tokens` now excludes both cache reads and reported cache creation,
38
+ which remain in their separate `cache_read_input_tokens` and
39
+ `cache_creation_input_tokens` counters. Previously, consumers adding those
40
+ counters counted cached input twice.
41
+ - Keep OpenAI/Responses totals unchanged. Preserve unknown cache counters as
42
+ null; subtract only reported cache buckets. Reject impossible cache sums
43
+ exceeding the upstream input total instead of emitting negative/clamped usage.
44
+ - Add no-cache, partial/full-cache, cache-write, invalid-total and HTTP pipeline
45
+ regressions across TypeScript, Python and Rust. Example: 100 total input,
46
+ 50 cache read and 25 cache write becomes 25 uncached + 50 read + 25 write.
47
+
3
48
  ## v0.8.1
4
49
 
5
50
  ### Explicit Ultra Fast selection (all runtimes)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codex-as-api"
7
- version = "0.8.1"
7
+ version = "0.8.3"
8
8
  description = "Use ChatGPT/Codex OAuth as a local OpenAI-compatible API server."
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -1,3 +1,3 @@
1
1
  """codex-as-api: Use ChatGPT/Codex OAuth as a local OpenAI-compatible API server."""
2
2
 
3
- __version__ = "0.8.1"
3
+ __version__ = "0.8.3"
@@ -26,6 +26,7 @@ def anthropic_request_to_internal(
26
26
  max_tokens: int | None = None,
27
27
  output_format: dict[str, Any] | None = None,
28
28
  output_config: object = None,
29
+ metadata: object = None,
29
30
  ) -> tuple[
30
31
  list[Message], list[ToolSchema] | None, str | dict | None, list[str] | None, str | None, dict[str, Any] | None
31
32
  ]:
@@ -37,6 +38,7 @@ def anthropic_request_to_internal(
37
38
  raise ValueError("model must be a non-empty string")
38
39
  if not isinstance(messages, list) or not messages:
39
40
  raise ValueError("messages must be a non-empty array")
41
+ validate_anthropic_metadata(metadata)
40
42
  if stop_sequences is not None and (
41
43
  not isinstance(stop_sequences, list) or any(not isinstance(value, str) or not value for value in stop_sequences)
42
44
  ):
@@ -274,9 +276,28 @@ def _convert_assistant_message(content: object, out: list[Message]) -> None:
274
276
  arguments=json.dumps(arguments, ensure_ascii=False, separators=(",", ":")),
275
277
  )
276
278
  )
279
+ elif block_type == "thinking":
280
+ _reject_unknown_fields(
281
+ block,
282
+ {"type", "thinking", "signature"},
283
+ f"Anthropic assistant content block {index}",
284
+ )
285
+ thinking_text = block.get("thinking")
286
+ if not isinstance(thinking_text, str):
287
+ raise ValueError(f"Anthropic assistant thinking block {index} requires string thinking")
288
+ signature = block.get("signature")
289
+ if signature is not None and not isinstance(signature, str):
290
+ raise ValueError(f"Anthropic assistant thinking block {index} signature must be a string")
291
+ elif block_type == "redacted_thinking":
292
+ _reject_unknown_fields(
293
+ block,
294
+ {"type", "data"},
295
+ f"Anthropic assistant content block {index}",
296
+ )
297
+ data = block.get("data")
298
+ if not isinstance(data, str):
299
+ raise ValueError(f"Anthropic assistant redacted_thinking block {index} requires string data")
277
300
  elif block_type in {
278
- "thinking",
279
- "redacted_thinking",
280
301
  "server_tool_use",
281
302
  "web_search_tool_result",
282
303
  }:
@@ -309,11 +330,17 @@ def _convert_tools(tools: object) -> list[ToolSchema]:
309
330
  if "allowed_callers" in tool or "output_schema" in tool:
310
331
  raise ValueError(f"Anthropic tool {index} uses Programmatic Tool Calling fields that cannot be preserved")
311
332
  if "defer_loading" in tool:
312
- raise ValueError(f"Anthropic tool {index} field defer_loading is not supported by the Codex OAuth backend")
333
+ defer_loading = tool.get("defer_loading")
334
+ if defer_loading is not None and not isinstance(defer_loading, bool):
335
+ raise ValueError(f"Anthropic tool {index} defer_loading must be a boolean")
336
+ if defer_loading is True:
337
+ raise ValueError(
338
+ f"Anthropic tool {index} field defer_loading is not supported by the Codex OAuth backend"
339
+ )
313
340
  for field in ("eager_input_streaming",):
314
341
  value = tool.get(field)
315
- if value is not None:
316
- raise ValueError(f"Anthropic tool {index} field {field} is not supported by the Codex OAuth backend")
342
+ if value is not None and not isinstance(value, bool):
343
+ raise ValueError(f"Anthropic tool {index} {field} must be a boolean")
317
344
  name = tool.get("name")
318
345
  if not isinstance(name, str) or not name:
319
346
  raise ValueError(f"Anthropic tool {index} requires a non-empty name")
@@ -449,6 +476,33 @@ def anthropic_parallel_tool_calls(
449
476
  return not value
450
477
 
451
478
 
479
+ def validate_anthropic_metadata(metadata: object) -> None:
480
+ if metadata is None:
481
+ return
482
+ if not isinstance(metadata, dict):
483
+ raise ValueError("metadata must be an object")
484
+ _reject_unknown_fields(metadata, {"user_id"}, "metadata")
485
+ user_id = metadata.get("user_id")
486
+ if user_id is not None and not isinstance(user_id, str):
487
+ raise ValueError("metadata.user_id must be a string")
488
+
489
+
490
+ def resolve_anthropic_thinking_options(
491
+ thinking: dict[str, Any] | None,
492
+ max_tokens: int | None = None,
493
+ ) -> tuple[str, str | None, bool]:
494
+ """Return (thinking_display, reasoning_summary, thinking_disabled) for a thinking object."""
495
+ if thinking is None:
496
+ return "summarized", None, False
497
+ _convert_thinking(thinking, max_tokens)
498
+ thinking_type = thinking.get("type")
499
+ if thinking_type == "disabled":
500
+ return "omitted", "none", True
501
+ display = "omitted" if thinking.get("display") == "omitted" else "summarized"
502
+ reasoning_summary = "none" if display == "omitted" else "auto"
503
+ return display, reasoning_summary, False
504
+
505
+
452
506
  def _convert_thinking(thinking: dict[str, Any] | None, max_tokens: int | None) -> str | None:
453
507
  if thinking is None:
454
508
  return None
@@ -458,8 +512,8 @@ def _convert_thinking(thinking: dict[str, Any] | None, max_tokens: int | None) -
458
512
  if thinking_type == "enabled":
459
513
  _reject_unknown_fields(thinking, {"type", "budget_tokens", "display"}, "thinking")
460
514
  display = thinking.get("display")
461
- if display is not None and display != "omitted":
462
- raise ValueError("thinking.display must be 'omitted' when provided")
515
+ if display is not None and (not isinstance(display, str) or display not in {"summarized", "omitted"}):
516
+ raise ValueError("thinking.display must be 'summarized' or 'omitted' when provided")
463
517
  budget_tokens = thinking.get("budget_tokens")
464
518
  parsed_budget_tokens = as_js_safe_integer(budget_tokens)
465
519
  if parsed_budget_tokens is None or parsed_budget_tokens < 1024:
@@ -470,8 +524,8 @@ def _convert_thinking(thinking: dict[str, Any] | None, max_tokens: int | None) -
470
524
  if thinking_type == "adaptive":
471
525
  _reject_unknown_fields(thinking, {"type", "display"}, "thinking")
472
526
  display = thinking.get("display")
473
- if display is not None and display != "omitted":
474
- raise ValueError("thinking.display must be 'omitted' when provided")
527
+ if display is not None and (not isinstance(display, str) or display not in {"summarized", "omitted"}):
528
+ raise ValueError("thinking.display must be 'summarized' or 'omitted' when provided")
475
529
  return "medium"
476
530
  if thinking_type == "disabled":
477
531
  _reject_unknown_fields(thinking, {"type"}, "thinking")
@@ -648,11 +702,18 @@ def internal_response_to_anthropic(
648
702
  response: AssistantResponse,
649
703
  model: str,
650
704
  request_id: str,
705
+ *,
706
+ thinking_display: str = "summarized",
651
707
  ) -> dict[str, Any]:
708
+ if thinking_display not in {"summarized", "omitted"}:
709
+ raise ValueError("thinking_display must be 'summarized' or 'omitted'")
652
710
  content: list[dict[str, Any]] = []
653
711
 
654
712
  _reject_unrepresentable_response_events(response.raw)
655
713
 
714
+ if thinking_display == "summarized" and response.reasoning_content:
715
+ content.append({"type": "thinking", "thinking": response.reasoning_content})
716
+
656
717
  if response.content:
657
718
  content.append({"type": "text", "text": response.content, "citations": None})
658
719
 
@@ -688,7 +749,7 @@ def internal_response_to_anthropic(
688
749
  "stop_sequence": None,
689
750
  "usage": usage_dict,
690
751
  }
691
- if response.reasoning_content:
752
+ if thinking_display == "summarized" and response.reasoning_content:
692
753
  result["codex_reasoning"] = response.reasoning_content
693
754
  return result
694
755
 
@@ -718,13 +779,23 @@ def _reject_unrepresentable_response_events(raw: dict | None) -> None:
718
779
  )
719
780
 
720
781
 
782
+ def _uncached_input_tokens(total: int, cache_read: int | None, cache_write: int | None) -> int:
783
+ # OpenAI totals include cache buckets; Anthropic exposes disjoint counters.
784
+ cached = (cache_read or 0) + (cache_write or 0)
785
+ if cached > total:
786
+ raise ChatGPTOAuthProtocolError("usage cache tokens must not exceed input_tokens")
787
+ return total - cached
788
+
789
+
721
790
  def _anthropic_usage_from_internal(usage: Usage) -> dict[str, Any]:
722
791
  return {
723
792
  "cache_creation": None,
724
793
  "cache_creation_input_tokens": usage.cache_write_tokens,
725
794
  "cache_read_input_tokens": usage.cached_tokens,
726
795
  "inference_geo": None,
727
- "input_tokens": usage.prompt_tokens,
796
+ "input_tokens": _uncached_input_tokens(
797
+ usage.prompt_tokens, usage.cached_tokens, usage.cache_write_tokens
798
+ ),
728
799
  "iterations": None,
729
800
  "output_tokens": usage.completion_tokens,
730
801
  "server_tool_use": None,
@@ -790,10 +861,14 @@ def anthropic_stream_adapter(
790
861
  event_stream: Iterator[dict[str, Any]],
791
862
  model: str,
792
863
  request_id: str,
864
+ *,
865
+ thinking_display: str = "summarized",
793
866
  ) -> Iterator[str]:
794
867
  """Convert provider chat_stream events into Anthropic SSE strings."""
868
+ if thinking_display not in {"summarized", "omitted"}:
869
+ raise ValueError("thinking_display must be 'summarized' or 'omitted'")
795
870
  yield _message_start_sse(model, request_id)
796
- yield from _render_anthropic_stream_events(event_stream)
871
+ yield from _render_anthropic_stream_events(event_stream, thinking_display=thinking_display)
797
872
 
798
873
 
799
874
  def _message_start_sse(model: str, request_id: str) -> str:
@@ -811,6 +886,13 @@ def _message_start_sse(model: str, request_id: str) -> str:
811
886
  "context_management": None,
812
887
  "stop_reason": None,
813
888
  "stop_sequence": None,
889
+ # Provisional; message_delta carries authoritative cumulative usage.
890
+ "usage": {
891
+ "input_tokens": 0,
892
+ "output_tokens": 0,
893
+ "cache_read_input_tokens": 0,
894
+ "cache_creation_input_tokens": 0,
895
+ },
814
896
  },
815
897
  },
816
898
  )
@@ -910,7 +992,7 @@ def _anthropic_usage_from_provider(usage: Any) -> dict[str, Any]:
910
992
  out: dict[str, Any] = {
911
993
  "cache_creation_input_tokens": cache_write,
912
994
  "cache_read_input_tokens": cache_read,
913
- "input_tokens": input_tokens,
995
+ "input_tokens": _uncached_input_tokens(input_tokens, cache_read, cache_write),
914
996
  "iterations": None,
915
997
  "output_tokens": output_tokens,
916
998
  "server_tool_use": server_tool_use,
@@ -934,7 +1016,11 @@ def _validate_usage_counter_object(
934
1016
  raise ChatGPTOAuthProtocolError(f"provider usage {field}.{name} must be a non-negative integer")
935
1017
 
936
1018
 
937
- def _render_anthropic_stream_events(events: Iterator[dict[str, Any]]) -> Iterator[str]:
1019
+ def _render_anthropic_stream_events(
1020
+ events: Iterator[dict[str, Any]],
1021
+ *,
1022
+ thinking_display: str = "summarized",
1023
+ ) -> Iterator[str]:
938
1024
  block_index = 0
939
1025
  current_block: str | None = None
940
1026
  has_tool_calls = False
@@ -948,6 +1034,29 @@ def _render_anthropic_stream_events(events: Iterator[dict[str, Any]]) -> Iterato
948
1034
  text = event.get("text")
949
1035
  if not isinstance(text, str):
950
1036
  raise ChatGPTOAuthProtocolError(f"provider {typ} event requires string text")
1037
+ if thinking_display == "omitted":
1038
+ continue
1039
+ if current_block != "thinking":
1040
+ if current_block is not None:
1041
+ yield _sse("content_block_stop", {"type": "content_block_stop", "index": block_index})
1042
+ block_index += 1
1043
+ yield _sse(
1044
+ "content_block_start",
1045
+ {
1046
+ "type": "content_block_start",
1047
+ "index": block_index,
1048
+ "content_block": {"type": "thinking", "thinking": ""},
1049
+ },
1050
+ )
1051
+ current_block = "thinking"
1052
+ yield _sse(
1053
+ "content_block_delta",
1054
+ {
1055
+ "type": "content_block_delta",
1056
+ "index": block_index,
1057
+ "delta": {"type": "thinking_delta", "thinking": text},
1058
+ },
1059
+ )
951
1060
  yield _sse(
952
1061
  "codex_reasoning_delta",
953
1062
  {
@@ -289,11 +289,12 @@ def resolve_model(
289
289
  except ChatGPTOAuthInvalidRequestError as exc:
290
290
  raise ChatGPTOAuthCatalogUnavailableError("configured model is unusable") from exc
291
291
  if candidate is not None and anthropic_facade and candidate.startswith("claude-"):
292
- if configured_model is None:
293
- raise ChatGPTOAuthInvalidRequestError(
294
- "claude-* facade models require CODEX_AS_API_MODEL or config.toml model"
295
- )
296
- return configured_capability(configured_model)
292
+ if configured_model is not None:
293
+ return configured_capability(configured_model)
294
+ capability = snapshot.default_model()
295
+ if not capability.slug.strip() or capability.slug != capability.slug.strip():
296
+ raise ChatGPTOAuthCatalogUnavailableError("default model publishes an unusable slug")
297
+ return capability
297
298
  if candidate is not None:
298
299
  return snapshot.model(candidate)
299
300
  if configured_model is not None: