codex-as-api 0.8.1__tar.gz → 0.8.2__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.2}/PKG-INFO +1 -1
  2. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/RELEASES.md +16 -0
  3. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/pyproject.toml +1 -1
  4. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/src/codex_as_api/__init__.py +1 -1
  5. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/src/codex_as_api/anthropic_adapter.py +12 -2
  6. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/src/codex_as_api/server.py +1 -1
  7. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/test_anthropic_adapter.py +34 -1
  8. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/test_provider.py +2 -2
  9. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/.gitignore +0 -0
  10. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/LICENSE +0 -0
  11. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/README.md +0 -0
  12. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/THIRD_PARTY_NOTICES.md +0 -0
  13. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/config/codex-upstream-contract.json +0 -0
  14. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/config/o200k_base.tiktoken +0 -0
  15. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/requirements.txt +0 -0
  16. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/rust/THIRD_PARTY_NOTICES.md +0 -0
  17. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/scripts/check_no_static_model_catalog.py +0 -0
  18. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/scripts/check_package_versions.py +0 -0
  19. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/scripts/check_registry_release.py +0 -0
  20. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/scripts/check_release_artifacts.py +0 -0
  21. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/scripts/generate_rust_third_party_notices.py +0 -0
  22. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/scripts/package_rust_release.py +0 -0
  23. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/src/codex_as_api/auth.py +0 -0
  24. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/src/codex_as_api/codex_config.py +0 -0
  25. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/src/codex_as_api/messages.py +0 -0
  26. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/src/codex_as_api/model_capabilities.py +0 -0
  27. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/src/codex_as_api/o200k_tokenizer.py +0 -0
  28. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/src/codex_as_api/protocol.py +0 -0
  29. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/src/codex_as_api/provider.py +0 -0
  30. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/src/codex_as_api/strict_json.py +0 -0
  31. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/__init__.py +0 -0
  32. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/conftest.py +0 -0
  33. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/fixtures/o200k_base_encode_ordinary.json +0 -0
  34. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/test_auth.py +0 -0
  35. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/test_codex_config.py +0 -0
  36. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/test_messages.py +0 -0
  37. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/test_model_capabilities.py +0 -0
  38. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/test_no_static_model_catalog.py +0 -0
  39. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/test_o200k_tokenizer.py +0 -0
  40. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/test_package_versions.py +0 -0
  41. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/test_protocol.py +0 -0
  42. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/test_registry_release.py +0 -0
  43. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/test_release_artifacts.py +0 -0
  44. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/test_rust_third_party_notices.py +0 -0
  45. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/test_server.py +0 -0
  46. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/tests/test_strict_json.py +0 -0
  47. {codex_as_api-0.8.1 → codex_as_api-0.8.2}/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.2
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.8.2
4
+
5
+ ### Hotfix: disjoint Anthropic cache usage (all runtimes)
6
+
7
+ - Correct Anthropic Messages usage in streaming and non-streaming responses:
8
+ `input_tokens` now excludes both cache reads and reported cache creation,
9
+ which remain in their separate `cache_read_input_tokens` and
10
+ `cache_creation_input_tokens` counters. Previously, consumers adding those
11
+ counters counted cached input twice.
12
+ - Keep OpenAI/Responses totals unchanged. Preserve unknown cache counters as
13
+ null; subtract only reported cache buckets. Reject impossible cache sums
14
+ exceeding the upstream input total instead of emitting negative/clamped usage.
15
+ - Add no-cache, partial/full-cache, cache-write, invalid-total and HTTP pipeline
16
+ regressions across TypeScript, Python and Rust. Example: 100 total input,
17
+ 50 cache read and 25 cache write becomes 25 uncached + 50 read + 25 write.
18
+
3
19
  ## v0.8.1
4
20
 
5
21
  ### 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.2"
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.2"
@@ -718,13 +718,23 @@ def _reject_unrepresentable_response_events(raw: dict | None) -> None:
718
718
  )
719
719
 
720
720
 
721
+ def _uncached_input_tokens(total: int, cache_read: int | None, cache_write: int | None) -> int:
722
+ # OpenAI totals include cache buckets; Anthropic exposes disjoint counters.
723
+ cached = (cache_read or 0) + (cache_write or 0)
724
+ if cached > total:
725
+ raise ChatGPTOAuthProtocolError("usage cache tokens must not exceed input_tokens")
726
+ return total - cached
727
+
728
+
721
729
  def _anthropic_usage_from_internal(usage: Usage) -> dict[str, Any]:
722
730
  return {
723
731
  "cache_creation": None,
724
732
  "cache_creation_input_tokens": usage.cache_write_tokens,
725
733
  "cache_read_input_tokens": usage.cached_tokens,
726
734
  "inference_geo": None,
727
- "input_tokens": usage.prompt_tokens,
735
+ "input_tokens": _uncached_input_tokens(
736
+ usage.prompt_tokens, usage.cached_tokens, usage.cache_write_tokens
737
+ ),
728
738
  "iterations": None,
729
739
  "output_tokens": usage.completion_tokens,
730
740
  "server_tool_use": None,
@@ -910,7 +920,7 @@ def _anthropic_usage_from_provider(usage: Any) -> dict[str, Any]:
910
920
  out: dict[str, Any] = {
911
921
  "cache_creation_input_tokens": cache_write,
912
922
  "cache_read_input_tokens": cache_read,
913
- "input_tokens": input_tokens,
923
+ "input_tokens": _uncached_input_tokens(input_tokens, cache_read, cache_write),
914
924
  "iterations": None,
915
925
  "output_tokens": output_tokens,
916
926
  "server_tool_use": server_tool_use,
@@ -497,7 +497,7 @@ def _health_error_message(exc: BaseException) -> str:
497
497
  app = FastAPI(
498
498
  title="codex-as-api",
499
499
  description="Local OpenAI-compatible API server backed by ChatGPT/Codex OAuth.",
500
- version="0.8.1",
500
+ version="0.8.2",
501
501
  )
502
502
 
503
503
 
@@ -1206,6 +1206,23 @@ class TestInternalResponseToAnthropic:
1206
1206
  result = internal_response_to_anthropic(resp, "m", "msg_1")
1207
1207
  assert result["usage"]["cache_read_input_tokens"] == 50
1208
1208
  assert result["usage"]["cache_creation_input_tokens"] == 25
1209
+ assert result["usage"]["input_tokens"] == 25
1210
+
1211
+ @pytest.mark.parametrize("read,write", [(0, 0), (40, 0), (100, 0), (0, 100), (40, 60), (101, 0), (60, 41)])
1212
+ def test_disjoint_cache_buckets(self, read, write):
1213
+ source = Usage(prompt_tokens=100, completion_tokens=7, total_tokens=107,
1214
+ cached_tokens=read, cache_write_tokens=write)
1215
+ response = AssistantResponse(content="hi", tool_calls=(), finish_reason="stop",
1216
+ usage=source, reasoning_content=None, raw=None)
1217
+ if read + write > 100:
1218
+ with pytest.raises(ChatGPTOAuthProtocolError, match="cache tokens must not exceed"):
1219
+ internal_response_to_anthropic(response, "m", "msg")
1220
+ else:
1221
+ usage = internal_response_to_anthropic(response, "m", "msg")["usage"]
1222
+ assert usage["input_tokens"] == 100 - read - write
1223
+ buckets = ("input_tokens", "cache_read_input_tokens", "cache_creation_input_tokens")
1224
+ assert sum(usage[k] for k in buckets) == 100
1225
+ assert source.prompt_tokens == 100
1209
1226
 
1210
1227
  def test_web_search_provider_output_is_rejected(self):
1211
1228
  resp = AssistantResponse(
@@ -1460,7 +1477,7 @@ class TestAnthropicStreamAdapter:
1460
1477
  assert msg_delta["usage"] == {
1461
1478
  "cache_creation_input_tokens": 11,
1462
1479
  "cache_read_input_tokens": 13,
1463
- "input_tokens": 123,
1480
+ "input_tokens": 99,
1464
1481
  "iterations": None,
1465
1482
  "output_tokens": 7,
1466
1483
  "server_tool_use": {"web_search_requests": 2, "web_fetch_requests": 1},
@@ -1516,6 +1533,22 @@ class TestAnthropicStreamAdapter:
1516
1533
  with pytest.raises(ChatGPTOAuthProtocolError, match="provider (finish )?usage"):
1517
1534
  self._collect_events(events)
1518
1535
 
1536
+ @pytest.mark.parametrize("read,write", [(0, 0), (40, 0), (100, 0), (0, 100), (40, 60), (101, 0), (60, 41)])
1537
+ def test_stream_disjoint_cache_buckets(self, read, write):
1538
+ source = {"input_tokens": 100, "output_tokens": 7, "total_tokens": 107,
1539
+ "input_tokens_details": {"cached_tokens": read, "cache_write_tokens": write}}
1540
+ events = [{"type": "finish", "finish_reason": "stop", "usage": source}]
1541
+ if read + write > 100:
1542
+ with pytest.raises(ChatGPTOAuthProtocolError, match="cache tokens must not exceed"):
1543
+ self._collect_events(events)
1544
+ else:
1545
+ result = self._collect_events(events)
1546
+ usage = next(e["usage"] for e in result if e["type"] == "message_delta")
1547
+ assert usage["input_tokens"] == 100 - read - write
1548
+ buckets = ("input_tokens", "cache_read_input_tokens", "cache_creation_input_tokens")
1549
+ assert sum(usage[k] for k in buckets) == 100
1550
+ assert source["input_tokens"] == 100
1551
+
1519
1552
  def test_maps_responses_cache_write_details_into_message_delta(self):
1520
1553
  result = self._collect_events(
1521
1554
  [
@@ -2158,7 +2158,7 @@ def test_codex_cli_headers_include_official_originator_and_versioned_user_agent(
2158
2158
 
2159
2159
  assert headers["originator"] == "codex_cli_rs"
2160
2160
  assert headers["User-Agent"].startswith("codex_cli_rs/0.153.3 (")
2161
- assert headers["User-Agent"].endswith(") codex-as-api/0.8.1")
2161
+ assert headers["User-Agent"].endswith(") codex-as-api/0.8.2")
2162
2162
 
2163
2163
 
2164
2164
  def test_codex_cli_headers_reject_unpinned_version():
@@ -2881,7 +2881,7 @@ def test_provider_headers_include_codex_cli_headers(auth_json_factory):
2881
2881
 
2882
2882
  assert headers["originator"] == "codex_cli_rs"
2883
2883
  assert headers["User-Agent"].startswith("codex_cli_rs/0.153.3 (")
2884
- assert headers["User-Agent"].endswith(") codex-as-api/0.8.1")
2884
+ assert headers["User-Agent"].endswith(") codex-as-api/0.8.2")
2885
2885
  assert headers["Authorization"].startswith("Bearer ")
2886
2886
 
2887
2887
 
File without changes
File without changes
File without changes