codex-as-api 0.7.0__tar.gz → 0.8.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.
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/PKG-INFO +67 -4
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/README.md +65 -3
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/RELEASES.md +43 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/pyproject.toml +2 -2
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/src/codex_as_api/__init__.py +1 -1
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/src/codex_as_api/codex_config.py +5 -4
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/src/codex_as_api/server.py +5 -5
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/test_model_capabilities.py +18 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/test_provider.py +2 -2
- codex_as_api-0.8.1/tests/test_ultrafast.py +159 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/.gitignore +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/LICENSE +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/THIRD_PARTY_NOTICES.md +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/config/codex-upstream-contract.json +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/config/o200k_base.tiktoken +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/requirements.txt +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/rust/THIRD_PARTY_NOTICES.md +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/scripts/check_no_static_model_catalog.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/scripts/check_package_versions.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/scripts/check_registry_release.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/scripts/check_release_artifacts.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/scripts/generate_rust_third_party_notices.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/scripts/package_rust_release.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/src/codex_as_api/anthropic_adapter.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/src/codex_as_api/auth.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/src/codex_as_api/messages.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/src/codex_as_api/model_capabilities.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/src/codex_as_api/o200k_tokenizer.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/src/codex_as_api/protocol.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/src/codex_as_api/provider.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/src/codex_as_api/strict_json.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/__init__.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/conftest.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/fixtures/o200k_base_encode_ordinary.json +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/test_anthropic_adapter.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/test_auth.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/test_codex_config.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/test_messages.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/test_no_static_model_catalog.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/test_o200k_tokenizer.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/test_package_versions.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/test_protocol.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/test_registry_release.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/test_release_artifacts.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/test_rust_third_party_notices.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/test_server.py +0 -0
- {codex_as_api-0.7.0 → codex_as_api-0.8.1}/tests/test_strict_json.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: codex-as-api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.1
|
|
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
|
|
@@ -28,6 +28,7 @@ Requires-Dist: httpx; extra == 'dev'
|
|
|
28
28
|
Requires-Dist: mypy; extra == 'dev'
|
|
29
29
|
Requires-Dist: pytest; extra == 'dev'
|
|
30
30
|
Requires-Dist: ruff; extra == 'dev'
|
|
31
|
+
Requires-Dist: tomli>=2.0.1; extra == 'dev'
|
|
31
32
|
Requires-Dist: types-regex; extra == 'dev'
|
|
32
33
|
Description-Content-Type: text/markdown
|
|
33
34
|
|
|
@@ -463,6 +464,36 @@ Readiness check. It returns `200` only when OAuth is available, a fresh catalog
|
|
|
463
464
|
curl -i http://localhost:18080/health
|
|
464
465
|
```
|
|
465
466
|
|
|
467
|
+
### `POST /v1/realtime/calls` (TypeScript)
|
|
468
|
+
|
|
469
|
+
Creates a ChatGPT Live (realtime voice) call on the OAuth-authenticated ChatGPT
|
|
470
|
+
backend. The client owns the media plane: it sends its own WebRTC SDP offer and
|
|
471
|
+
applies the returned answer, so no audio is relayed through this proxy.
|
|
472
|
+
|
|
473
|
+
The body is `{"sdp": "<offer>", "session": {...}}`. `session` may carry
|
|
474
|
+
`instructions`, a `voice` (default `cove`), and any additional ChatGPT Live
|
|
475
|
+
session field; `model` defaults to `gpt-live-1-codex`, and `delegation` defaults
|
|
476
|
+
to `{"type": "client"}`. The route rejects unknown top-level fields and any
|
|
477
|
+
client-chosen voice-session id (`id`, `session_id`, `voice_session_id`), because
|
|
478
|
+
the backend denies calls that set those itself. On success it returns `201` with
|
|
479
|
+
the SDP answer as `text/plain` and a mirror `Location: /v1/realtime/calls/{call_id}`.
|
|
480
|
+
|
|
481
|
+
```bash
|
|
482
|
+
curl -i http://localhost:18080/v1/realtime/calls \
|
|
483
|
+
-H "Content-Type: application/json" \
|
|
484
|
+
-d '{"sdp": "v=0\r\n...", "session": {"instructions": "Reply briefly."}}'
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### `WS /v1/realtime/sideband/{call_id}` (TypeScript)
|
|
488
|
+
|
|
489
|
+
Relays a WebSocket to the OpenAI realtime sideband of an already-created call
|
|
490
|
+
(`wss://api.openai.com/v1/live/{call_id}`) with the account's OAuth headers, so a
|
|
491
|
+
headless client — a bot, a server-side agent, or an observer attached to the same
|
|
492
|
+
call — can read and write realtime events without running the media plane.
|
|
493
|
+
Frames, text and binary, are forwarded verbatim; the proxy does not rewrite or
|
|
494
|
+
invent session events. Browser clients that already hold the `oai-events` data
|
|
495
|
+
channel do not need this route.
|
|
496
|
+
|
|
466
497
|
## Codex-Specific Features
|
|
467
498
|
|
|
468
499
|
These features are extensions beyond the standard OpenAI API, designed for Codex CLI compatibility.
|
|
@@ -540,6 +571,23 @@ The mapping is intentionally transport-aware:
|
|
|
540
571
|
| `previous_response_id` | Resolve locally and replay complete Response history over HTTP |
|
|
541
572
|
| `service_tier: "fast"` | Send `service_tier: "priority"` |
|
|
542
573
|
| `service_tier: "default"` | Omit the field |
|
|
574
|
+
| `service_tier: "ultrafast"` | Send `service_tier: "ultrafast"` only when the live model advertises it |
|
|
575
|
+
| Messages/compact `speed: "ultrafast"` (proxy extension) | Resolve to `service_tier: "ultrafast"`, with the same catalog check |
|
|
576
|
+
|
|
577
|
+
Ultra Fast is distinct from Fast (`priority`), not a reasoning effort. For OpenAI
|
|
578
|
+
Chat requests use `"service_tier": "ultrafast"`; for `/v1/messages` and
|
|
579
|
+
`/v1/messages/compact` either that field or the proxy extension
|
|
580
|
+
`"speed": "ultrafast"` is accepted. If both fields are supplied they must agree.
|
|
581
|
+
Unsupported tiers return HTTP 400 before an inference request, including streaming
|
|
582
|
+
requests; the proxy never silently downgrades Ultra Fast to priority. Check the
|
|
583
|
+
account-specific `service_tiers` returned by `GET /v1/models`. An official feature
|
|
584
|
+
flag does not grant account access or change pricing. `speed: "ultrafast"` is not
|
|
585
|
+
claimed as an official Anthropic field, and existing `speed: "fast"` behavior is
|
|
586
|
+
unchanged. The proxy does not inherit speed defaults from Codex config.
|
|
587
|
+
|
|
588
|
+
Official behavior reference: Codex [0.164.0 service-tier resolution](https://github.com/openai/codex/blob/rust-v0.164.0/codex-rs/tui/src/service_tier_resolution.rs)
|
|
589
|
+
and [stable, enabled-by-default ultrafast feature](https://github.com/openai/codex/blob/rust-v0.164.0/codex-rs/features/src/lib.rs).
|
|
590
|
+
The rest of the private HTTP contract remains pinned to 0.153.3.
|
|
543
591
|
|
|
544
592
|
Authenticated tests on July 10, 2026 verified both official continuation paths with `gpt-5.6-sol`: a direct private Responses WebSocket completed a second delta request using the exact prior `response.id`, and the private HTTP endpoint completed the same continuation when the full prior input/output history was replayed. The proxy implements the HTTP replay strategy. Direct HTTP forwarding of `previous_response_id`, Pro, public cache controls, and `safety_identifier` was rejected and is not used. See OpenAI's [reasoning mode documentation](https://developers.openai.com/api/docs/guides/reasoning#reasoning-mode) and the Codex 0.153.3 [HTTP/WebSocket request structures](https://github.com/openai/codex/blob/b1a547b1f73ce86205d9222ac19cff334b3b7a2e/codex-rs/codex-api/src/common.rs#L275-L350).
|
|
545
593
|
|
|
@@ -812,6 +860,7 @@ The provider handles:
|
|
|
812
860
|
- Bounded, account-scoped `previous_response_id` history replay over private HTTP
|
|
813
861
|
- Multimodal Chat input, image generation/inspection, and capability-gated classic `original` detail
|
|
814
862
|
- Remote conversation compaction
|
|
863
|
+
- ChatGPT Live realtime call creation and a sideband event relay, in the TypeScript runtime only
|
|
815
864
|
|
|
816
865
|
## Release & package publishing
|
|
817
866
|
|
|
@@ -841,9 +890,9 @@ registry setup is still required:
|
|
|
841
890
|
After bumping every version and passing the local gate, push the matching tag:
|
|
842
891
|
|
|
843
892
|
```bash
|
|
844
|
-
python scripts/check_package_versions.py --tag v0.
|
|
845
|
-
git tag v0.
|
|
846
|
-
git push origin v0.
|
|
893
|
+
python scripts/check_package_versions.py --tag v0.8.0
|
|
894
|
+
git tag v0.8.0
|
|
895
|
+
git push origin v0.8.0
|
|
847
896
|
```
|
|
848
897
|
|
|
849
898
|
## Tests
|
|
@@ -874,6 +923,20 @@ npm test
|
|
|
874
923
|
|
|
875
924
|
## Release Notes
|
|
876
925
|
|
|
926
|
+
### v0.8.0
|
|
927
|
+
|
|
928
|
+
- Add ChatGPT Live realtime voice to the TypeScript runtime: `POST
|
|
929
|
+
/v1/realtime/calls` creates an OAuth-authorized call from a client-owned WebRTC
|
|
930
|
+
SDP offer and returns the SDP answer, and `WS /v1/realtime/sideband/{call_id}`
|
|
931
|
+
relays the OpenAI realtime event sideband for headless clients. Audio stays on
|
|
932
|
+
the client's media plane.
|
|
933
|
+
- Mirror the pinned Codex call contract exactly: one identity in
|
|
934
|
+
`x-session-id`/`session-id`/`thread-id`, the `openai-alpha: quicksilver=v2`
|
|
935
|
+
handshake, `intent=quicksilver&architecture=avas`, and a session payload that
|
|
936
|
+
never chooses its own voice-session id.
|
|
937
|
+
- Document realtime as a TypeScript-only capability; Python and Rust remain
|
|
938
|
+
HTTP-only in this release instead of claiming cross-runtime parity.
|
|
939
|
+
|
|
877
940
|
### v0.7.0
|
|
878
941
|
|
|
879
942
|
- Replace bundled model capability tables with a fresh authenticated Codex
|
|
@@ -430,6 +430,36 @@ Readiness check. It returns `200` only when OAuth is available, a fresh catalog
|
|
|
430
430
|
curl -i http://localhost:18080/health
|
|
431
431
|
```
|
|
432
432
|
|
|
433
|
+
### `POST /v1/realtime/calls` (TypeScript)
|
|
434
|
+
|
|
435
|
+
Creates a ChatGPT Live (realtime voice) call on the OAuth-authenticated ChatGPT
|
|
436
|
+
backend. The client owns the media plane: it sends its own WebRTC SDP offer and
|
|
437
|
+
applies the returned answer, so no audio is relayed through this proxy.
|
|
438
|
+
|
|
439
|
+
The body is `{"sdp": "<offer>", "session": {...}}`. `session` may carry
|
|
440
|
+
`instructions`, a `voice` (default `cove`), and any additional ChatGPT Live
|
|
441
|
+
session field; `model` defaults to `gpt-live-1-codex`, and `delegation` defaults
|
|
442
|
+
to `{"type": "client"}`. The route rejects unknown top-level fields and any
|
|
443
|
+
client-chosen voice-session id (`id`, `session_id`, `voice_session_id`), because
|
|
444
|
+
the backend denies calls that set those itself. On success it returns `201` with
|
|
445
|
+
the SDP answer as `text/plain` and a mirror `Location: /v1/realtime/calls/{call_id}`.
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
curl -i http://localhost:18080/v1/realtime/calls \
|
|
449
|
+
-H "Content-Type: application/json" \
|
|
450
|
+
-d '{"sdp": "v=0\r\n...", "session": {"instructions": "Reply briefly."}}'
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### `WS /v1/realtime/sideband/{call_id}` (TypeScript)
|
|
454
|
+
|
|
455
|
+
Relays a WebSocket to the OpenAI realtime sideband of an already-created call
|
|
456
|
+
(`wss://api.openai.com/v1/live/{call_id}`) with the account's OAuth headers, so a
|
|
457
|
+
headless client — a bot, a server-side agent, or an observer attached to the same
|
|
458
|
+
call — can read and write realtime events without running the media plane.
|
|
459
|
+
Frames, text and binary, are forwarded verbatim; the proxy does not rewrite or
|
|
460
|
+
invent session events. Browser clients that already hold the `oai-events` data
|
|
461
|
+
channel do not need this route.
|
|
462
|
+
|
|
433
463
|
## Codex-Specific Features
|
|
434
464
|
|
|
435
465
|
These features are extensions beyond the standard OpenAI API, designed for Codex CLI compatibility.
|
|
@@ -507,6 +537,23 @@ The mapping is intentionally transport-aware:
|
|
|
507
537
|
| `previous_response_id` | Resolve locally and replay complete Response history over HTTP |
|
|
508
538
|
| `service_tier: "fast"` | Send `service_tier: "priority"` |
|
|
509
539
|
| `service_tier: "default"` | Omit the field |
|
|
540
|
+
| `service_tier: "ultrafast"` | Send `service_tier: "ultrafast"` only when the live model advertises it |
|
|
541
|
+
| Messages/compact `speed: "ultrafast"` (proxy extension) | Resolve to `service_tier: "ultrafast"`, with the same catalog check |
|
|
542
|
+
|
|
543
|
+
Ultra Fast is distinct from Fast (`priority`), not a reasoning effort. For OpenAI
|
|
544
|
+
Chat requests use `"service_tier": "ultrafast"`; for `/v1/messages` and
|
|
545
|
+
`/v1/messages/compact` either that field or the proxy extension
|
|
546
|
+
`"speed": "ultrafast"` is accepted. If both fields are supplied they must agree.
|
|
547
|
+
Unsupported tiers return HTTP 400 before an inference request, including streaming
|
|
548
|
+
requests; the proxy never silently downgrades Ultra Fast to priority. Check the
|
|
549
|
+
account-specific `service_tiers` returned by `GET /v1/models`. An official feature
|
|
550
|
+
flag does not grant account access or change pricing. `speed: "ultrafast"` is not
|
|
551
|
+
claimed as an official Anthropic field, and existing `speed: "fast"` behavior is
|
|
552
|
+
unchanged. The proxy does not inherit speed defaults from Codex config.
|
|
553
|
+
|
|
554
|
+
Official behavior reference: Codex [0.164.0 service-tier resolution](https://github.com/openai/codex/blob/rust-v0.164.0/codex-rs/tui/src/service_tier_resolution.rs)
|
|
555
|
+
and [stable, enabled-by-default ultrafast feature](https://github.com/openai/codex/blob/rust-v0.164.0/codex-rs/features/src/lib.rs).
|
|
556
|
+
The rest of the private HTTP contract remains pinned to 0.153.3.
|
|
510
557
|
|
|
511
558
|
Authenticated tests on July 10, 2026 verified both official continuation paths with `gpt-5.6-sol`: a direct private Responses WebSocket completed a second delta request using the exact prior `response.id`, and the private HTTP endpoint completed the same continuation when the full prior input/output history was replayed. The proxy implements the HTTP replay strategy. Direct HTTP forwarding of `previous_response_id`, Pro, public cache controls, and `safety_identifier` was rejected and is not used. See OpenAI's [reasoning mode documentation](https://developers.openai.com/api/docs/guides/reasoning#reasoning-mode) and the Codex 0.153.3 [HTTP/WebSocket request structures](https://github.com/openai/codex/blob/b1a547b1f73ce86205d9222ac19cff334b3b7a2e/codex-rs/codex-api/src/common.rs#L275-L350).
|
|
512
559
|
|
|
@@ -779,6 +826,7 @@ The provider handles:
|
|
|
779
826
|
- Bounded, account-scoped `previous_response_id` history replay over private HTTP
|
|
780
827
|
- Multimodal Chat input, image generation/inspection, and capability-gated classic `original` detail
|
|
781
828
|
- Remote conversation compaction
|
|
829
|
+
- ChatGPT Live realtime call creation and a sideband event relay, in the TypeScript runtime only
|
|
782
830
|
|
|
783
831
|
## Release & package publishing
|
|
784
832
|
|
|
@@ -808,9 +856,9 @@ registry setup is still required:
|
|
|
808
856
|
After bumping every version and passing the local gate, push the matching tag:
|
|
809
857
|
|
|
810
858
|
```bash
|
|
811
|
-
python scripts/check_package_versions.py --tag v0.
|
|
812
|
-
git tag v0.
|
|
813
|
-
git push origin v0.
|
|
859
|
+
python scripts/check_package_versions.py --tag v0.8.0
|
|
860
|
+
git tag v0.8.0
|
|
861
|
+
git push origin v0.8.0
|
|
814
862
|
```
|
|
815
863
|
|
|
816
864
|
## Tests
|
|
@@ -841,6 +889,20 @@ npm test
|
|
|
841
889
|
|
|
842
890
|
## Release Notes
|
|
843
891
|
|
|
892
|
+
### v0.8.0
|
|
893
|
+
|
|
894
|
+
- Add ChatGPT Live realtime voice to the TypeScript runtime: `POST
|
|
895
|
+
/v1/realtime/calls` creates an OAuth-authorized call from a client-owned WebRTC
|
|
896
|
+
SDP offer and returns the SDP answer, and `WS /v1/realtime/sideband/{call_id}`
|
|
897
|
+
relays the OpenAI realtime event sideband for headless clients. Audio stays on
|
|
898
|
+
the client's media plane.
|
|
899
|
+
- Mirror the pinned Codex call contract exactly: one identity in
|
|
900
|
+
`x-session-id`/`session-id`/`thread-id`, the `openai-alpha: quicksilver=v2`
|
|
901
|
+
handshake, `intent=quicksilver&architecture=avas`, and a session payload that
|
|
902
|
+
never chooses its own voice-session id.
|
|
903
|
+
- Document realtime as a TypeScript-only capability; Python and Rust remain
|
|
904
|
+
HTTP-only in this release instead of claiming cross-runtime parity.
|
|
905
|
+
|
|
844
906
|
### v0.7.0
|
|
845
907
|
|
|
846
908
|
- Replace bundled model capability tables with a fresh authenticated Codex
|
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Release Notes
|
|
2
2
|
|
|
3
|
+
## v0.8.1
|
|
4
|
+
|
|
5
|
+
### Explicit Ultra Fast selection (all runtimes)
|
|
6
|
+
|
|
7
|
+
- Accept `speed: "ultrafast"` as a proxy extension on Anthropic Messages and
|
|
8
|
+
compact routes and forward the literal Codex `service_tier: "ultrafast"`.
|
|
9
|
+
Direct `service_tier` selection remains supported on Chat and Messages.
|
|
10
|
+
- Preserve live account/model tier validation: unavailable Ultra Fast is HTTP
|
|
11
|
+
400 before inference/streaming headers, never a silent downgrade to priority.
|
|
12
|
+
Reject conflicting speed/tier inputs; keep Fast and standard behavior unchanged.
|
|
13
|
+
- Add cross-runtime mapping and capability regression tests, including HTTP
|
|
14
|
+
recording-backend tests. Malformed Python speed objects/arrays now return
|
|
15
|
+
invalid-request errors instead of causing an unhashable-type failure.
|
|
16
|
+
- Keep TOML config imports type-checkable on current Python while preserving
|
|
17
|
+
Python 3.10 support; include `tomli` in dev dependencies for the 3.10 analysis target.
|
|
18
|
+
- Document Ultra Fast separately from priority and reasoning effort. No model
|
|
19
|
+
support, account entitlement, pricing or config speed default is invented.
|
|
20
|
+
|
|
21
|
+
## v0.8.0
|
|
22
|
+
|
|
23
|
+
### ChatGPT Live realtime voice (TypeScript)
|
|
24
|
+
|
|
25
|
+
- Add `POST /v1/realtime/calls` to the TypeScript runtime. A client-owned WebRTC
|
|
26
|
+
SDP offer plus an optional `session` object (`instructions`, `voice`, ...) is
|
|
27
|
+
exchanged for the ChatGPT backend's SDP answer; the call id is returned both in
|
|
28
|
+
the mirrored `Location: /v1/realtime/calls/{call_id}` header and as
|
|
29
|
+
`x-realtime-call-id`, and the answer body is `text/plain`, never JSON.
|
|
30
|
+
- Add `WS /v1/realtime/sideband/{call_id}`, which relays text and binary realtime
|
|
31
|
+
frames to `wss://api.openai.com/v1/live/{call_id}` with the account's OAuth
|
|
32
|
+
headers so headless clients (bots, server-side agents, observers) can attach to
|
|
33
|
+
an existing call. The proxy does not proxy RTP; the media plane stays client-side.
|
|
34
|
+
- Send the exact call contract the backend authorizes for OAuth: `version`,
|
|
35
|
+
`openai-alpha: quicksilver=v2`, `session-id`/`x-session-id`/`thread-id` carrying
|
|
36
|
+
one minted identity, and `intent=quicksilver&architecture=avas`. Reject unknown
|
|
37
|
+
top-level request fields and any client-chosen voice-session id (`id`,
|
|
38
|
+
`session_id`, `voice_session_id`) instead of forwarding a call the backend
|
|
39
|
+
denies.
|
|
40
|
+
- Default `model` to `gpt-live-1-codex`, `voice` to `cove`, and `delegation` to
|
|
41
|
+
`{"type": "client"}`.
|
|
42
|
+
- Realtime is TypeScript-only in this release. The Python and Rust runtimes keep
|
|
43
|
+
their existing HTTP surface, and the runtime difference is stated in
|
|
44
|
+
`README.md` and `ts/README.md` rather than presented as parity.
|
|
45
|
+
|
|
3
46
|
## v0.7.0
|
|
4
47
|
|
|
5
48
|
### Live authenticated model catalog
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codex-as-api"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.1"
|
|
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" }
|
|
@@ -33,7 +33,7 @@ dependencies = [
|
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
[project.optional-dependencies]
|
|
36
|
-
dev = ["pytest", "ruff", "mypy", "httpx", "types-regex"]
|
|
36
|
+
dev = ["pytest", "ruff", "mypy", "httpx", "types-regex", "tomli>=2.0.1"]
|
|
37
37
|
|
|
38
38
|
[project.scripts]
|
|
39
39
|
codex-as-api = "codex_as_api.server:main"
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import sys
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
import tomllib
|
|
10
|
-
|
|
11
|
-
import tomli as tomllib
|
|
9
|
+
if sys.version_info >= (3, 11):
|
|
10
|
+
import tomllib
|
|
11
|
+
else: # pragma: no cover - Python 3.10 compatibility
|
|
12
|
+
import tomli as tomllib
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
_JS_SAFE_INTEGER = 9_007_199_254_740_991
|
|
@@ -271,10 +271,10 @@ def _anthropic_service_tier(body: dict[str, Any]) -> str | None:
|
|
|
271
271
|
speed = body.get("speed")
|
|
272
272
|
if speed is None:
|
|
273
273
|
return cast(str | None, service_tier)
|
|
274
|
-
if speed not in {"fast", "standard"}:
|
|
275
|
-
raise ChatGPTOAuthInvalidRequestError("speed must be one of: fast, standard")
|
|
276
|
-
speed_tier = "
|
|
277
|
-
equivalent_tiers = {"fast", "priority"} if speed == "fast" else {
|
|
274
|
+
if not isinstance(speed, str) or speed not in {"fast", "standard", "ultrafast"}:
|
|
275
|
+
raise ChatGPTOAuthInvalidRequestError("speed must be one of: fast, standard, ultrafast")
|
|
276
|
+
speed_tier = "default" if speed == "standard" else speed
|
|
277
|
+
equivalent_tiers = {"fast", "priority"} if speed == "fast" else {speed_tier}
|
|
278
278
|
if service_tier is not None and service_tier not in equivalent_tiers:
|
|
279
279
|
raise ChatGPTOAuthInvalidRequestError("speed conflicts with service_tier")
|
|
280
280
|
return speed_tier
|
|
@@ -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.
|
|
500
|
+
version="0.8.1",
|
|
501
501
|
)
|
|
502
502
|
|
|
503
503
|
|
|
@@ -89,6 +89,24 @@ def test_catalog_parser_diagnostics_do_not_reflect_slug_or_modality_values(
|
|
|
89
89
|
assert parsed.models[0].input_modalities == ("audio", "text")
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
def test_ultrafast_preserves_live_tier_and_rejects_unavailable(model_catalog_document) -> None:
|
|
93
|
+
document = deepcopy(model_catalog_document)
|
|
94
|
+
document["models"][0]["service_tiers"] = [
|
|
95
|
+
{"id": "ultrafast", "name": "Ultra Fast", "description": "Ultra Fast"},
|
|
96
|
+
]
|
|
97
|
+
capability = _parse(document).models[0]
|
|
98
|
+
payload = {}
|
|
99
|
+
apply_model_capability_fields(
|
|
100
|
+
payload, capability=capability, text=None, service_tier="ultrafast",
|
|
101
|
+
)
|
|
102
|
+
assert payload["service_tier"] == "ultrafast"
|
|
103
|
+
unavailable = _parse(model_catalog_document).models[0]
|
|
104
|
+
with pytest.raises(ValueError, match="service_tier"):
|
|
105
|
+
apply_model_capability_fields(
|
|
106
|
+
{}, capability=unavailable, text=None, service_tier="ultrafast",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
92
110
|
def test_invalid_model_control_diagnostics_do_not_reflect_values(
|
|
93
111
|
model_catalog_document,
|
|
94
112
|
) -> None:
|
|
@@ -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.
|
|
2161
|
+
assert headers["User-Agent"].endswith(") codex-as-api/0.8.1")
|
|
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.
|
|
2884
|
+
assert headers["User-Agent"].endswith(") codex-as-api/0.8.1")
|
|
2885
2885
|
assert headers["Authorization"].startswith("Bearer ")
|
|
2886
2886
|
|
|
2887
2887
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""The proxy speed extension must not alias Ultra Fast to ordinary priority."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from codex_as_api.auth import ChatGPTOAuthInvalidRequestError
|
|
6
|
+
from codex_as_api.server import _anthropic_service_tier
|
|
7
|
+
|
|
8
|
+
# Reuse the real provider + loopback HTTP backend, rather than mocking the
|
|
9
|
+
# provider methods (which would bypass live-catalog tier validation).
|
|
10
|
+
from tests.test_server import client, recording_backend # noqa: F401
|
|
11
|
+
|
|
12
|
+
# Imported pytest fixtures are intentionally injected by argument name.
|
|
13
|
+
# ruff: noqa: F811
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(autouse=True)
|
|
17
|
+
def _isolate_ultrafast_http_environment(monkeypatch):
|
|
18
|
+
monkeypatch.setenv("CODEX_AS_API_RESPONSES_LITE", "auto")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture()
|
|
22
|
+
def ultrafast_catalog(model_catalog_document):
|
|
23
|
+
model = next(
|
|
24
|
+
model for model in model_catalog_document["models"]
|
|
25
|
+
if model["slug"] == "gpt-5.6-sol"
|
|
26
|
+
)
|
|
27
|
+
model["service_tiers"].append({
|
|
28
|
+
"id": "ultrafast",
|
|
29
|
+
"name": "Ultra Fast",
|
|
30
|
+
"description": "Test-only advertised Ultra Fast tier",
|
|
31
|
+
})
|
|
32
|
+
return model_catalog_document
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.parametrize("fields", [
|
|
36
|
+
{"speed": "ultrafast"},
|
|
37
|
+
{"speed": "ultrafast", "service_tier": "ultrafast"},
|
|
38
|
+
{"service_tier": "ultrafast"},
|
|
39
|
+
])
|
|
40
|
+
def test_ultrafast_speed_resolves_without_downgrade(fields):
|
|
41
|
+
assert _anthropic_service_tier(fields) == "ultrafast"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.parametrize("fields", [
|
|
45
|
+
{"speed": "ultrafast", "service_tier": "priority"},
|
|
46
|
+
{"speed": "fast", "service_tier": "ultrafast"},
|
|
47
|
+
{"speed": "standard", "service_tier": "ultrafast"},
|
|
48
|
+
{"speed": {}},
|
|
49
|
+
{"speed": []},
|
|
50
|
+
])
|
|
51
|
+
def test_ultrafast_conflicts_and_malformed_speed_are_invalid_requests(fields):
|
|
52
|
+
with pytest.raises(ChatGPTOAuthInvalidRequestError):
|
|
53
|
+
_anthropic_service_tier(fields)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.parametrize("stream", [False, True], ids=["nonstream", "stream"])
|
|
57
|
+
@pytest.mark.parametrize("fields", [
|
|
58
|
+
{"speed": "ultrafast"},
|
|
59
|
+
{"service_tier": "ultrafast"},
|
|
60
|
+
{"speed": "ultrafast", "service_tier": "ultrafast"},
|
|
61
|
+
], ids=["speed-alias", "direct-tier", "matching-controls"])
|
|
62
|
+
def test_messages_ultrafast_reaches_recording_upstream(
|
|
63
|
+
ultrafast_catalog, client, recording_backend, stream, fields,
|
|
64
|
+
):
|
|
65
|
+
response = client.post("/v1/messages", json={
|
|
66
|
+
"model": "gpt-5.6-sol",
|
|
67
|
+
"max_tokens": 64,
|
|
68
|
+
"messages": [{"role": "user", "content": "hello"}],
|
|
69
|
+
"stream": stream,
|
|
70
|
+
**fields,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
assert response.status_code == 200, response.text
|
|
74
|
+
if stream:
|
|
75
|
+
assert response.headers["content-type"].startswith("text/event-stream")
|
|
76
|
+
assert "event: message_stop" in response.text
|
|
77
|
+
assert "backend-ok" in response.text
|
|
78
|
+
assert "event: error" not in response.text
|
|
79
|
+
else:
|
|
80
|
+
assert response.json()["content"][0]["type"] == "text"
|
|
81
|
+
assert response.json()["content"][0]["text"] == "backend-ok"
|
|
82
|
+
recorded = recording_backend.requests.get(timeout=1)
|
|
83
|
+
assert recorded["path"] == "/responses"
|
|
84
|
+
assert recorded["body"]["service_tier"] == "ultrafast"
|
|
85
|
+
assert "speed" not in recorded["body"]
|
|
86
|
+
assert recording_backend.requests.empty()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.parametrize("route, fields", [
|
|
90
|
+
("/v1/messages/compact", {"speed": "ultrafast"}),
|
|
91
|
+
("/v1/messages/compact", {"service_tier": "ultrafast"}),
|
|
92
|
+
("/v1/messages/compact", {"speed": "ultrafast", "service_tier": "ultrafast"}),
|
|
93
|
+
("/v1/compact", {"service_tier": "ultrafast"}),
|
|
94
|
+
], ids=["speed-alias", "direct-tier", "matching-controls", "openai-compact"])
|
|
95
|
+
def test_compact_ultrafast_reaches_recording_upstream(
|
|
96
|
+
ultrafast_catalog, client, recording_backend, route, fields,
|
|
97
|
+
):
|
|
98
|
+
body = {
|
|
99
|
+
"model": "gpt-5.6-sol",
|
|
100
|
+
"messages": [{"role": "user", "content": "hello"}],
|
|
101
|
+
**fields,
|
|
102
|
+
}
|
|
103
|
+
if route == "/v1/messages/compact":
|
|
104
|
+
body["max_tokens"] = 64
|
|
105
|
+
response = client.post(route, json=body)
|
|
106
|
+
|
|
107
|
+
assert response.status_code == 200, response.text
|
|
108
|
+
recorded = recording_backend.requests.get(timeout=1)
|
|
109
|
+
assert recorded["path"] == "/responses/compact"
|
|
110
|
+
assert recorded["body"]["service_tier"] == "ultrafast"
|
|
111
|
+
assert "speed" not in recorded["body"]
|
|
112
|
+
assert recording_backend.requests.empty()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@pytest.mark.parametrize("route, stream", [
|
|
116
|
+
("/v1/messages", False),
|
|
117
|
+
("/v1/messages", True),
|
|
118
|
+
("/v1/messages/compact", False),
|
|
119
|
+
])
|
|
120
|
+
@pytest.mark.parametrize("fields", [
|
|
121
|
+
{"speed": "ultrafast"},
|
|
122
|
+
{"service_tier": "ultrafast"},
|
|
123
|
+
], ids=["speed-alias", "direct-tier"])
|
|
124
|
+
def test_unadvertised_ultrafast_rejected_before_recording_upstream(
|
|
125
|
+
client, recording_backend, route, stream, fields,
|
|
126
|
+
):
|
|
127
|
+
# Default catalog advertises priority only: do not globally grant Ultra Fast
|
|
128
|
+
# in the common fixture, or this rejection guard would become untestable.
|
|
129
|
+
body = {
|
|
130
|
+
"model": "gpt-5.6-sol",
|
|
131
|
+
"max_tokens": 64,
|
|
132
|
+
"messages": [{"role": "user", "content": "hello"}],
|
|
133
|
+
**fields,
|
|
134
|
+
}
|
|
135
|
+
if route == "/v1/messages":
|
|
136
|
+
body["stream"] = stream
|
|
137
|
+
response = client.post(route, json=body)
|
|
138
|
+
|
|
139
|
+
assert response.status_code == 400, response.text
|
|
140
|
+
assert response.json()["error"]["type"] == "invalid_request_error"
|
|
141
|
+
assert "service_tier" in response.json()["error"]["message"]
|
|
142
|
+
assert recording_backend.requests.empty()
|
|
143
|
+
# Catalog discovery is permitted; no generation/compact POST may occur.
|
|
144
|
+
assert not recording_backend.catalog_requests.empty()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_openai_compact_unadvertised_ultrafast_rejected_before_upstream(
|
|
148
|
+
client, recording_backend,
|
|
149
|
+
):
|
|
150
|
+
response = client.post("/v1/compact", json={
|
|
151
|
+
"model": "gpt-5.6-sol",
|
|
152
|
+
"messages": [{"role": "user", "content": "hello"}],
|
|
153
|
+
"service_tier": "ultrafast",
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
assert response.status_code == 400, response.text
|
|
157
|
+
assert "service_tier" in response.json()["error"]["message"]
|
|
158
|
+
assert recording_backend.requests.empty()
|
|
159
|
+
assert not recording_backend.catalog_requests.empty()
|
|
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
|