diff --git a/docs/implementation-status.md b/docs/implementation-status.md index fd5eae666..d40551dc3 100644 --- a/docs/implementation-status.md +++ b/docs/implementation-status.md @@ -16,7 +16,7 @@ Update this file before each feature commit. | Account pool and refresh | Partial | `0f97c5c` + `2f25db8` + `4dc0c9a` | `cargo test account_pool_skips_expired_disabled_banned_and_quota_exhausted_accounts`; `cargo test --test account_pool_scheduling_test`; `cargo test --test account_refresh_test`; full `fmt/test/clippy` before commit | Implements base account status filtering, refresh scheduler semantics, max concurrent slots, stale slot cleanup, least-used/round-robin/sticky strategies, tier priority, quota-cache filtering, Cloudflare cooldown skip, request staggering metadata, preferred account selection, exclude IDs, and capacity summary. Still missing settings-backed scheduling config, SQLite repository integration for pool snapshots/release usage, upstream retry/fallback wiring, and admin account CRUD. | | Account repository | Completed | `14fe706` | `cargo test --test account_repository_test`; `cargo fmt --check`; `cargo clippy --all-targets --all-features --locked -- -D warnings`; `cargo test` | Adds encrypted SQLite account token storage, decrypting reads, cursor pagination, status/label updates, release-time usage counter persistence, and refresh-token-preserving token updates. | | Translation | Completed | `eff182e` | `cargo test chat_completion_translates_to_codex_response_request`; full `fmt/test/clippy` before commit | Adds OpenAI Chat request types, Codex Responses request type, translation, and OpenAI-compatible error body helper. | -| HTTP routes | Partial | `ab0ad8f` + `3830b21` | `cargo test v1_requires_client_api_key_not_admin_cookie`; `cargo test models_route_returns_openai_compatible_codex_model_list`; full `fmt/test/clippy` before commit | Adds Axum router, health route, `/v1/models`, `/v1` unauthorized OpenAI error body, global request-id response header, and binary listener. Still missing admin auth middleware extraction, account/auth/API-key management routes, settings routes, diagnostics, usage stats, model catalog/detail/refresh routes, and additional Responses routes. | +| HTTP routes | Partial | `ab0ad8f` + `3830b21` + `pending` | `cargo test v1_requires_client_api_key_not_admin_cookie`; `cargo test models_route_returns_openai_compatible_codex_model_list`; full `fmt/test/clippy` before commit | Adds Axum router, health route, `/v1/models`, `/v1` unauthorized OpenAI error body, global request-id response header, binary listener, and `gpt-5.5` as the default/test model baseline. Still missing admin auth middleware extraction, account/auth/API-key management routes, settings routes, diagnostics, usage stats, model catalog/detail/refresh routes, and additional Responses routes. | | Admin/frontend response contract | Completed | `1c34ea7` + `b0c10f2` | `cargo test admin_envelope_serializes_lower_camel_case_request_id`; `cargo test admin_logs_reject_missing_admin_session_cookie`; `cargo test v1_response_has_request_id_header_without_admin_body_field`; full `fmt/test/clippy` before commit | Adds reusable admin envelopes with body codes, lower camelCase JSON, paginated list shape, requestId bodies, admin session cookie enforcement, and keeps `/v1/*` OpenAI-compatible. | | HTTP auth boundary | Completed | `cec4607` | `cargo test --test http_auth_test`; `cargo test --test routes_responses_test`; `cargo test --test admin_logs_route_test`; `cargo fmt --check`; `cargo clippy --all-targets --all-features --locked -- -D warnings`; `cargo test` | Adds a real `src/http/auth.rs` boundary for client API key and admin session cookie parsing. `/v1/*` accepts only `Bearer cpr_...`, admin session parsing ignores client API keys, and route handlers no longer own ad hoc credential parsing. | | Upstream lifecycle | Partial | `88cad87` + `225fbff` | `cargo test responses_route_rejects_non_codex_provider_models`; `cargo test v1_requires_client_api_key_not_admin_cookie`; `cargo test --test codex_sse_usage_test`; full `fmt/test/clippy` before commit | `/v1` currently enforces client API key shape, rejects non-Codex provider models, returns OpenAI-compatible errors, and includes reusable SSE/usage helpers. It does not yet acquire accounts, forward to Codex, replay/capture Cookies, stream/collect upstream responses, capture rate-limit headers, retry across accounts, record usage, or preserve previous-response/session affinity. | diff --git a/docs/superpowers/plans/2026-06-11-codex-proxy-rs.md b/docs/superpowers/plans/2026-06-11-codex-proxy-rs.md index 9a68286ec..dd17cead9 100644 --- a/docs/superpowers/plans/2026-06-11-codex-proxy-rs.md +++ b/docs/superpowers/plans/2026-06-11-codex-proxy-rs.md @@ -2100,7 +2100,7 @@ fn account_pool_skips_expired_disabled_banned_and_quota_exhausted_accounts() { pool.insert(Account::test("banned", AccountStatus::Banned)); pool.insert(Account::test("quota", AccountStatus::QuotaExhausted)); - let acquired = pool.acquire("gpt-5.4").unwrap(); + let acquired = pool.acquire("gpt-5.5").unwrap(); assert_eq!(acquired.id, "active"); } ``` @@ -2322,7 +2322,7 @@ use codex_proxy_rs::translation::openai_to_codex::{translate_chat_to_codex, Chat #[test] fn chat_completion_translates_to_codex_response_request() { let req = ChatCompletionRequest { - model: "gpt-5.4".to_string(), + model: "gpt-5.5".to_string(), stream: true, messages: vec![ChatMessage { role: "user".to_string(), @@ -2331,7 +2331,7 @@ fn chat_completion_translates_to_codex_response_request() { }; let codex = translate_chat_to_codex(req).unwrap(); - assert_eq!(codex.model, "gpt-5.4"); + assert_eq!(codex.model, "gpt-5.5"); assert!(codex.stream); assert_eq!(codex.input[0]["role"], "user"); } @@ -2738,7 +2738,7 @@ pub async fn responses(headers: HeaderMap, Json(body): Json) -> i Json(openai_error("Missing client API key", "invalid_api_key")), ); } - let model = body.model.unwrap_or_else(|| "gpt-5.4".to_string()); + let model = body.model.unwrap_or_else(|| "gpt-5.5".to_string()); if !(model.starts_with("gpt") || model.starts_with("codex") || model.starts_with("o")) { return ( StatusCode::NOT_FOUND, @@ -2959,6 +2959,7 @@ git commit -m "docs: document codex-proxy-rs scope" - [x] Add a real HTTP auth boundary. `src/http/auth.rs` parses client API keys and admin session cookies as separate credential types, `/v1/*` uses only `Bearer cpr_...`, and admin session parsing ignores client API keys. - [x] Add a tested Codex WebSocket boundary. `src/codex/websocket.rs` classifies HTTP SSE versus WebSocket-only requests, adds `previous_response_id`/`use_websocket` request fields, and returns typed errors instead of silently downgrading WebSocket-required traffic. - [x] Extend account scheduling primitives against the original TypeScript lifecycle. `AccountPool` now covers max concurrent slots, stale slot cleanup, least-used/round-robin/sticky strategies, tier priority, model-plan filtering, cached quota skip, exclude IDs, preferred account selection for session affinity, Cloudflare cooldown skip, request-staggering metadata, and capacity summary. +- [x] Set `gpt-5.5` as the default/test model baseline across code, tests, and plan examples. `/v1/models` now asserts the exact default model instead of only checking for a `gpt` prefix. ## 2026-06-11 Original Project Gap Audit diff --git a/src/http/v1.rs b/src/http/v1.rs index b0e40759c..cd957dc6a 100644 --- a/src/http/v1.rs +++ b/src/http/v1.rs @@ -9,6 +9,8 @@ use serde_json::json; use crate::{http::auth::client_api_key, translation::codex_to_openai::openai_error}; +const DEFAULT_CODEX_MODEL: &str = "gpt-5.5"; + #[derive(Deserialize)] struct ResponsesBody { model: Option, @@ -23,9 +25,11 @@ pub async fn responses(headers: HeaderMap, body: Bytes) -> impl IntoResponse { } let body = serde_json::from_slice::(&body).unwrap_or_else(|_| ResponsesBody { - model: Some("gpt-5.4".to_string()), + model: Some(DEFAULT_CODEX_MODEL.to_string()), }); - let model = body.model.unwrap_or_else(|| "gpt-5.4".to_string()); + let model = body + .model + .unwrap_or_else(|| DEFAULT_CODEX_MODEL.to_string()); if !(model.starts_with("gpt") || model.starts_with("codex") || model.starts_with('o')) { return ( StatusCode::NOT_FOUND, @@ -56,7 +60,7 @@ pub async fn models(headers: HeaderMap) -> impl IntoResponse { "object": "list", "data": [ { - "id": "gpt-5.4", + "id": DEFAULT_CODEX_MODEL, "object": "model", "created": 0, "owned_by": "openai" diff --git a/tests/account_pool_scheduling_test.rs b/tests/account_pool_scheduling_test.rs index 46daf2e95..bc1e2815d 100644 --- a/tests/account_pool_scheduling_test.rs +++ b/tests/account_pool_scheduling_test.rs @@ -15,15 +15,15 @@ fn account_pool_should_respect_max_concurrent_slots_per_account() { pool.insert(Account::test("acct_a", AccountStatus::Active)); pool.insert(Account::test("acct_b", AccountStatus::Active)); - let first = pool.acquire("gpt-5.4").unwrap(); - let second = pool.acquire("gpt-5.4").unwrap(); - let third = pool.acquire("gpt-5.4"); + let first = pool.acquire("gpt-5.5").unwrap(); + let second = pool.acquire("gpt-5.5").unwrap(); + let third = pool.acquire("gpt-5.5"); assert_ne!(first.id, second.id); assert!(third.is_none()); pool.release(&first.id); - assert_eq!(pool.acquire("gpt-5.4").unwrap().id, first.id); + assert_eq!(pool.acquire("gpt-5.5").unwrap().id, first.id); } #[test] @@ -37,10 +37,10 @@ fn account_pool_should_rotate_round_robin_across_candidates() { pool.insert(Account::test("acct_b", AccountStatus::Active)); pool.insert(Account::test("acct_c", AccountStatus::Active)); - assert_eq!(pool.acquire("gpt-5.4").unwrap().id, "acct_a"); - assert_eq!(pool.acquire("gpt-5.4").unwrap().id, "acct_c"); - assert_eq!(pool.acquire("gpt-5.4").unwrap().id, "acct_b"); - assert!(pool.acquire("gpt-5.4").is_none()); + assert_eq!(pool.acquire("gpt-5.5").unwrap().id, "acct_a"); + assert_eq!(pool.acquire("gpt-5.5").unwrap().id, "acct_c"); + assert_eq!(pool.acquire("gpt-5.5").unwrap().id, "acct_b"); + assert!(pool.acquire("gpt-5.5").is_none()); } #[test] @@ -51,7 +51,7 @@ fn account_pool_should_skip_accounts_with_cached_quota_limit() { pool.insert(limited); pool.insert(Account::test("usable", AccountStatus::Active)); - assert_eq!(pool.acquire("gpt-5.4").unwrap().id, "usable"); + assert_eq!(pool.acquire("gpt-5.5").unwrap().id, "usable"); } #[test] @@ -67,13 +67,13 @@ fn account_pool_should_prefer_configured_tier_priority() { pool.insert(free); pool.insert(team); - assert_eq!(pool.acquire("gpt-5.4").unwrap().id, "team"); + assert_eq!(pool.acquire("gpt-5.5").unwrap().id, "team"); } #[test] fn account_pool_should_filter_by_model_plan_allowlist() { let mut model_plans = BTreeMap::new(); - model_plans.insert("gpt-5.4".to_string(), vec!["plus".to_string()]); + model_plans.insert("gpt-5.5".to_string(), vec!["plus".to_string()]); let mut pool = AccountPool::with_options(AccountPoolOptions { model_plan_allowlist: model_plans, ..AccountPoolOptions::default() @@ -85,7 +85,7 @@ fn account_pool_should_filter_by_model_plan_allowlist() { pool.insert(free); pool.insert(plus); - assert_eq!(pool.acquire("gpt-5.4").unwrap().id, "plus"); + assert_eq!(pool.acquire("gpt-5.5").unwrap().id, "plus"); } #[test] @@ -96,7 +96,7 @@ fn account_pool_should_exclude_requested_account_ids() { let acquired = pool .acquire_with( - AccountAcquireRequest::new("gpt-5.4", fixed_time()) + AccountAcquireRequest::new("gpt-5.5", fixed_time()) .with_exclude_account_ids(["acct_a"]), ) .unwrap(); @@ -112,7 +112,7 @@ fn account_pool_should_prefer_session_affinity_account_when_available() { let acquired = pool .acquire_with( - AccountAcquireRequest::new("gpt-5.4", fixed_time()).with_preferred_account_id("acct_b"), + AccountAcquireRequest::new("gpt-5.5", fixed_time()).with_preferred_account_id("acct_b"), ) .unwrap(); @@ -129,7 +129,7 @@ fn account_pool_should_skip_accounts_in_cloudflare_cooldown() { pool.insert(Account::test("usable", AccountStatus::Active)); let acquired = pool - .acquire_with(AccountAcquireRequest::new("gpt-5.4", now)) + .acquire_with(AccountAcquireRequest::new("gpt-5.5", now)) .unwrap(); assert_eq!(acquired.account.id, "usable"); @@ -146,11 +146,11 @@ fn account_pool_should_cleanup_stale_slots_before_acquire() { pool.insert(Account::test("acct_a", AccountStatus::Active)); assert!(pool - .acquire_with(AccountAcquireRequest::new("gpt-5.4", now)) + .acquire_with(AccountAcquireRequest::new("gpt-5.5", now)) .is_some()); let acquired = pool .acquire_with(AccountAcquireRequest::new( - "gpt-5.4", + "gpt-5.5", now + Duration::minutes(5) + Duration::seconds(1), )) .unwrap(); @@ -168,11 +168,11 @@ fn account_pool_should_return_previous_slot_time_for_request_staggering() { pool.insert(Account::test("acct_a", AccountStatus::Active)); let first = pool - .acquire_with(AccountAcquireRequest::new("gpt-5.4", now)) + .acquire_with(AccountAcquireRequest::new("gpt-5.5", now)) .unwrap(); let second = pool .acquire_with(AccountAcquireRequest::new( - "gpt-5.4", + "gpt-5.5", now + Duration::milliseconds(250), )) .unwrap(); @@ -191,7 +191,7 @@ fn account_pool_should_report_capacity_summary() { pool.insert(Account::test("acct_a", AccountStatus::Active)); pool.insert(Account::test("acct_b", AccountStatus::Disabled)); - pool.acquire_with(AccountAcquireRequest::new("gpt-5.4", now)); + pool.acquire_with(AccountAcquireRequest::new("gpt-5.5", now)); let summary = pool.capacity_summary(now); assert_eq!(summary.total_slots, 2); @@ -209,9 +209,9 @@ fn account_pool_should_rotate_tied_least_used_accounts() { pool.insert(Account::test("acct_a", AccountStatus::Active)); pool.insert(Account::test("acct_b", AccountStatus::Active)); - let first = pool.acquire("gpt-5.4").unwrap(); + let first = pool.acquire("gpt-5.5").unwrap(); pool.release(&first.id); - let second = pool.acquire("gpt-5.4").unwrap(); + let second = pool.acquire("gpt-5.5").unwrap(); assert_ne!(first.id, second.id); } diff --git a/tests/codex_websocket_test.rs b/tests/codex_websocket_test.rs index 076ecbce6..445a6d36b 100644 --- a/tests/codex_websocket_test.rs +++ b/tests/codex_websocket_test.rs @@ -28,7 +28,7 @@ fn transport_for_request_should_require_websocket_for_previous_response_id() { fn base_request() -> CodexResponsesRequest { CodexResponsesRequest { - model: "gpt-5.4".to_string(), + model: "gpt-5.5".to_string(), instructions: String::new(), input: Vec::new(), stream: true, diff --git a/tests/refresh_scheduler_test.rs b/tests/refresh_scheduler_test.rs index 1d1f6d238..073d09f2c 100644 --- a/tests/refresh_scheduler_test.rs +++ b/tests/refresh_scheduler_test.rs @@ -12,6 +12,6 @@ fn account_pool_skips_expired_disabled_banned_and_quota_exhausted_accounts() { pool.insert(Account::test("banned", AccountStatus::Banned)); pool.insert(Account::test("quota", AccountStatus::QuotaExhausted)); - let acquired = pool.acquire("gpt-5.4").unwrap(); + let acquired = pool.acquire("gpt-5.5").unwrap(); assert_eq!(acquired.id, "active"); } diff --git a/tests/routes_chat_test.rs b/tests/routes_chat_test.rs index 765ddec1d..6fca46f19 100644 --- a/tests/routes_chat_test.rs +++ b/tests/routes_chat_test.rs @@ -5,7 +5,7 @@ use codex_proxy_rs::translation::openai_to_codex::{ #[test] fn chat_completion_translates_to_codex_response_request() { let req = ChatCompletionRequest { - model: "gpt-5.4".to_string(), + model: "gpt-5.5".to_string(), stream: true, messages: vec![ChatMessage { role: "user".to_string(), @@ -14,7 +14,7 @@ fn chat_completion_translates_to_codex_response_request() { }; let codex = translate_chat_to_codex(req).unwrap(); - assert_eq!(codex.model, "gpt-5.4"); + assert_eq!(codex.model, "gpt-5.5"); assert!(codex.stream); assert_eq!(codex.input[0]["role"], "user"); } diff --git a/tests/routes_responses_test.rs b/tests/routes_responses_test.rs index 1b7b66ff9..f56a647f3 100644 --- a/tests/routes_responses_test.rs +++ b/tests/routes_responses_test.rs @@ -128,5 +128,5 @@ async fn models_route_returns_openai_compatible_codex_model_list() { let body: Value = serde_json::from_slice(&bytes).unwrap(); assert_eq!(body["object"], "list"); assert_eq!(body["data"][0]["object"], "model"); - assert!(body["data"][0]["id"].as_str().unwrap().starts_with("gpt")); + assert_eq!(body["data"][0]["id"], "gpt-5.5"); }