codex-as-api 0.9.2__tar.gz → 0.9.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 (51) hide show
  1. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/PKG-INFO +1 -1
  2. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/RELEASES.md +16 -0
  3. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/pyproject.toml +1 -1
  4. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/src/codex_as_api/__init__.py +1 -1
  5. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/src/codex_as_api/provider.py +25 -0
  6. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/src/codex_as_api/server.py +1 -1
  7. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/test_provider.py +62 -3
  8. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/test_server.py +1 -1
  9. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/.gitignore +0 -0
  10. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/LICENSE +0 -0
  11. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/README.md +0 -0
  12. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/THIRD_PARTY_NOTICES.md +0 -0
  13. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/config/codex-upstream-contract.json +0 -0
  14. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/config/o200k_base.tiktoken +0 -0
  15. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/config/verified-hidden-models.json +0 -0
  16. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/requirements.txt +0 -0
  17. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/rust/THIRD_PARTY_NOTICES.md +0 -0
  18. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/scripts/check_no_static_model_catalog.py +0 -0
  19. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/scripts/check_package_versions.py +0 -0
  20. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/scripts/check_registry_release.py +0 -0
  21. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/scripts/check_release_artifacts.py +0 -0
  22. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/scripts/generate_rust_third_party_notices.py +0 -0
  23. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/scripts/package_rust_release.py +0 -0
  24. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/src/codex_as_api/anthropic_adapter.py +0 -0
  25. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/src/codex_as_api/auth.py +0 -0
  26. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/src/codex_as_api/codex_config.py +0 -0
  27. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/src/codex_as_api/codex_version.py +0 -0
  28. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/src/codex_as_api/messages.py +0 -0
  29. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/src/codex_as_api/model_capabilities.py +0 -0
  30. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/src/codex_as_api/o200k_tokenizer.py +0 -0
  31. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/src/codex_as_api/protocol.py +0 -0
  32. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/src/codex_as_api/reasoning_signature.py +0 -0
  33. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/src/codex_as_api/strict_json.py +0 -0
  34. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/__init__.py +0 -0
  35. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/conftest.py +0 -0
  36. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/fixtures/o200k_base_encode_ordinary.json +0 -0
  37. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/test_anthropic_adapter.py +0 -0
  38. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/test_auth.py +0 -0
  39. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/test_codex_config.py +0 -0
  40. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/test_codex_version.py +0 -0
  41. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/test_messages.py +0 -0
  42. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/test_model_capabilities.py +0 -0
  43. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/test_no_static_model_catalog.py +0 -0
  44. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/test_o200k_tokenizer.py +0 -0
  45. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/test_package_versions.py +0 -0
  46. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/test_protocol.py +0 -0
  47. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/test_registry_release.py +0 -0
  48. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/test_release_artifacts.py +0 -0
  49. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/test_rust_third_party_notices.py +0 -0
  50. {codex_as_api-0.9.2 → codex_as_api-0.9.3}/tests/test_strict_json.py +0 -0
  51. {codex_as_api-0.9.2 → codex_as_api-0.9.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.9.2
3
+ Version: 0.9.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
@@ -1,5 +1,21 @@
1
1
  # Release Notes
2
2
 
3
+ ## v0.9.3
4
+
5
+ ### Prompt cache routing headers
6
+
7
+ - Send the session headers the official Codex CLI sends on every Responses
8
+ request: `session-id`, `thread-id` and `x-client-request-id`, derived from
9
+ `client_metadata.session_id` / `thread_id` or, failing those, from
10
+ `prompt_cache_key`. Applies to streamed `/responses` and `/responses/compact`
11
+ requests in Python, TypeScript and Rust. Values containing CR or LF are not sent.
12
+ - Measured against the ChatGPT Codex backend: an identical 7k-token prefix sent
13
+ with `prompt_cache_key` alone read 0 cached tokens on every request; with the
14
+ headers, repeated requests read 7,296 cached tokens. The official Codex CLI on
15
+ the same account read ~30k cached tokens per turn.
16
+ - No change when a request carries neither `prompt_cache_key` nor
17
+ `client_metadata.session_id`.
18
+
3
19
  ## v0.9.2
4
20
 
5
21
  ### TypeScript version lookup deadline
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codex-as-api"
7
- version = "0.9.2"
7
+ version = "0.9.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.9.2"
3
+ __version__ = "0.9.3"
@@ -409,6 +409,29 @@ def _redact_failure_event(
409
409
  return value
410
410
 
411
411
 
412
+ def codex_session_headers(payload: dict[str, Any]) -> dict[str, str]:
413
+ """Session headers the official Codex CLI sends on every Responses request.
414
+
415
+ The ChatGPT Codex backend does not reuse its prompt cache from
416
+ ``prompt_cache_key`` alone: measured 2026-10-27, an identical 7k-token prefix
417
+ read 0 cached tokens on every request without these headers and 7,296 with them.
418
+ """
419
+ metadata = payload.get("client_metadata")
420
+ if not isinstance(metadata, dict):
421
+ metadata = {}
422
+
423
+ def non_empty(value: Any) -> str | None:
424
+ return value if isinstance(value, str) and value.strip() else None
425
+
426
+ session_id = non_empty(metadata.get("session_id")) or non_empty(payload.get("prompt_cache_key"))
427
+ if session_id is None:
428
+ return {}
429
+ thread_id = non_empty(metadata.get("thread_id")) or session_id
430
+ if any(ch in value for value in (session_id, thread_id) for ch in ("\r", "\n")):
431
+ return {}
432
+ return {"session-id": session_id, "thread-id": thread_id, "x-client-request-id": thread_id}
433
+
434
+
412
435
  class ChatGPTOAuthProvider:
413
436
  name: str = "chatgpt_oauth"
414
437
  provider_namespace: str = "agent.provider.chatgpt_oauth"
@@ -1558,6 +1581,7 @@ class ChatGPTOAuthProvider:
1558
1581
  _require_catalog_account(token.account_id, catalog_key)
1559
1582
  headers = self._headers(token, client_version=resolved_client_version)
1560
1583
  headers["Accept"] = "text/event-stream"
1584
+ headers.update(codex_session_headers(payload))
1561
1585
  if extra_headers:
1562
1586
  headers.update(extra_headers)
1563
1587
  token_values = (token.access_token, token.refresh_token, token.id_token, token.account_id)
@@ -1649,6 +1673,7 @@ class ChatGPTOAuthProvider:
1649
1673
  token = token_for_request(self.auth_json_path)
1650
1674
  _require_catalog_account(token.account_id, catalog_key)
1651
1675
  headers = self._headers(token, client_version=resolved_client_version)
1676
+ headers.update(codex_session_headers(payload))
1652
1677
  if extra_headers:
1653
1678
  headers.update(extra_headers)
1654
1679
  token_values = (token.access_token, token.refresh_token, token.id_token, token.account_id)
@@ -465,7 +465,7 @@ def _health_error_message(exc: BaseException) -> str:
465
465
  app = FastAPI(
466
466
  title="codex-as-api",
467
467
  description="Local OpenAI-compatible API server backed by ChatGPT/Codex OAuth.",
468
- version="0.9.2",
468
+ version="0.9.3",
469
469
  )
470
470
 
471
471
 
@@ -53,6 +53,7 @@ from codex_as_api.provider import (
53
53
  _validate_response_event,
54
54
  _web_search_event_from_response_item,
55
55
  codex_cli_headers_for_version,
56
+ codex_session_headers,
56
57
  resolve_codex_cli_version,
57
58
  resolve_model_reasoning_effort,
58
59
  )
@@ -2192,7 +2193,7 @@ def test_codex_cli_headers_include_official_originator_and_versioned_user_agent(
2192
2193
 
2193
2194
  assert headers["originator"] == "codex_cli_rs"
2194
2195
  assert headers["User-Agent"].startswith("codex_cli_rs/0.164.0 (")
2195
- assert headers["User-Agent"].endswith(") codex-as-api/0.9.2")
2196
+ assert headers["User-Agent"].endswith(") codex-as-api/0.9.3")
2196
2197
 
2197
2198
 
2198
2199
  def test_codex_cli_headers_reject_unpinned_version():
@@ -2932,7 +2933,7 @@ def test_provider_headers_include_codex_cli_headers(auth_json_factory):
2932
2933
 
2933
2934
  assert headers["originator"] == "codex_cli_rs"
2934
2935
  assert headers["User-Agent"].startswith("codex_cli_rs/0.164.0 (")
2935
- assert headers["User-Agent"].endswith(") codex-as-api/0.9.2")
2936
+ assert headers["User-Agent"].endswith(") codex-as-api/0.9.3")
2936
2937
  assert headers["Authorization"].startswith("Bearer ")
2937
2938
 
2938
2939
 
@@ -4533,7 +4534,7 @@ def test_provider_injects_dynamic_npm_version_across_catalog_responses_and_401_r
4533
4534
  for full_url, headers in observed_requests:
4534
4535
  assert full_url == "https://example.test/codex/models?client_version=0.175.2"
4535
4536
  assert headers["user-agent"].startswith("codex_cli_rs/0.175.2 (")
4536
- assert headers["user-agent"].endswith(") codex-as-api/0.9.2")
4537
+ assert headers["user-agent"].endswith(") codex-as-api/0.9.3")
4537
4538
 
4538
4539
 
4539
4540
  def test_provider_cold_version_resolver_failure_raises_typed_503(auth_json_factory, monkeypatch):
@@ -4682,3 +4683,61 @@ def test_responses_payload_replays_all_25_compatible_model_pairs(
4682
4683
  "encrypted_content": "enc_payload",
4683
4684
  "summary": [{"type": "summary_text", "text": "summary"}],
4684
4685
  }
4686
+
4687
+
4688
+ def test_codex_session_headers_derive_from_cache_key_and_metadata():
4689
+ assert codex_session_headers({"prompt_cache_key": "key-1"}) == {
4690
+ "session-id": "key-1",
4691
+ "thread-id": "key-1",
4692
+ "x-client-request-id": "key-1",
4693
+ }
4694
+ assert codex_session_headers(
4695
+ {"prompt_cache_key": "key-1", "client_metadata": {"session_id": "s-1", "thread_id": "t-1"}}
4696
+ ) == {"session-id": "s-1", "thread-id": "t-1", "x-client-request-id": "t-1"}
4697
+ assert codex_session_headers({"client_metadata": {"session_id": "s-2"}}) == {
4698
+ "session-id": "s-2",
4699
+ "thread-id": "s-2",
4700
+ "x-client-request-id": "s-2",
4701
+ }
4702
+ assert codex_session_headers({}) == {}
4703
+ assert codex_session_headers({"prompt_cache_key": " "}) == {}
4704
+ assert codex_session_headers({"prompt_cache_key": "a\r\nInjected: 1"}) == {}
4705
+ assert codex_session_headers({"client_metadata": {"session_id": "s", "thread_id": "t\nx"}}) == {}
4706
+
4707
+
4708
+ def test_responses_requests_carry_codex_session_headers_only_with_a_key(monkeypatch):
4709
+ seen: list[tuple[str, dict[str, str]]] = []
4710
+
4711
+ class Handler(BaseHTTPRequestHandler):
4712
+ def do_POST(self) -> None: # noqa: N802
4713
+ seen.append((self.path, {k.lower(): v for k, v in self.headers.items()}))
4714
+ self.send_response(400)
4715
+ self.end_headers()
4716
+
4717
+ def log_message(self, _format, *args): # noqa: ANN001
4718
+ del args
4719
+
4720
+ server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
4721
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
4722
+ thread.start()
4723
+ try:
4724
+ token = _test_token()
4725
+ provider = ChatGPTOAuthProvider(base_url=f"http://127.0.0.1:{server.server_port}")
4726
+ monkeypatch.setattr(provider_module, "token_for_request", lambda _path: token)
4727
+ with pytest.raises(ChatGPTOAuthUpstreamError):
4728
+ list(provider._post_sse("/responses", {"prompt_cache_key": "cache-key-7"})) # noqa: SLF001
4729
+ with pytest.raises(ChatGPTOAuthUpstreamError):
4730
+ provider._request_json("/responses/compact", {"prompt_cache_key": "cache-key-7"}) # noqa: SLF001
4731
+ with pytest.raises(ChatGPTOAuthUpstreamError):
4732
+ list(provider._post_sse("/responses", {})) # noqa: SLF001
4733
+ finally:
4734
+ server.shutdown()
4735
+ server.server_close()
4736
+ thread.join(timeout=2)
4737
+ assert [path for path, _ in seen] == ["/responses", "/responses/compact", "/responses"]
4738
+ for _, headers in seen[:2]:
4739
+ assert headers["session-id"] == "cache-key-7"
4740
+ assert headers["thread-id"] == "cache-key-7"
4741
+ assert headers["x-client-request-id"] == "cache-key-7"
4742
+ assert "session-id" not in seen[2][1]
4743
+ assert "thread-id" not in seen[2][1]
@@ -6648,7 +6648,7 @@ def test_dynamic_npm_version_resolver_updates_models_query_user_agent_and_cold_f
6648
6648
  responses_req_1 = recording_backend.requests.get(timeout=1)
6649
6649
  assert catalog_req_1["path"] == "/models?client_version=0.175.2"
6650
6650
  assert catalog_req_1["headers"]["user-agent"].startswith("codex_cli_rs/0.175.2 (")
6651
- assert catalog_req_1["headers"]["user-agent"].endswith(") codex-as-api/0.9.2")
6651
+ assert catalog_req_1["headers"]["user-agent"].endswith(") codex-as-api/0.9.3")
6652
6652
  assert responses_req_1["headers"]["user-agent"].startswith("codex_cli_rs/0.175.2 (")
6653
6653
 
6654
6654
  active_version = "0.176.0"
File without changes
File without changes
File without changes