mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
feat: default codex model to gpt-5.5
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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<ResponsesBody>) -> 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
|
||||
|
||||
|
||||
+7
-3
@@ -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<String>,
|
||||
@@ -23,9 +25,11 @@ pub async fn responses(headers: HeaderMap, body: Bytes) -> impl IntoResponse {
|
||||
}
|
||||
|
||||
let body = serde_json::from_slice::<ResponsesBody>(&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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user