diff --git a/docs/implementation-status.md b/docs/implementation-status.md index 3625e9cf4..73de2c710 100644 --- a/docs/implementation-status.md +++ b/docs/implementation-status.md @@ -14,7 +14,7 @@ Update this file before each feature commit. | TLS headers and fingerprint | Completed | pending codex headers commit | `cargo test codex_headers_include_desktop_identity_and_turn_state`; `cargo tree \| rg 'reqwest\|rustls'`; full `fmt/test/clippy` before commit | Adds Codex Desktop fingerprint model, exact identity/request headers, and pinned reqwest/rustls client builder. | | Cookie persistence | Completed | pending cookies commit | `cargo test cookie_jar_captures_and_replays_account_scoped_cookies`; full `fmt/test/clippy` before commit | Adds account-scoped in-memory Cookie jar and repository shell; database persistence follows storage integration. | | Account pool and refresh | Completed | pending accounts commit | `cargo test account_pool_skips_expired_disabled_banned_and_quota_exhausted_accounts`; full `fmt/test/clippy` before commit | Adds account statuses, acquisition filter, lifecycle events, token pair, OAuth config, and refresh policy shell. | -| Translation | Planned | | | | +| Translation | Completed | pending translation commit | `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 | Planned | | | | | Upstream lifecycle | Planned | | | | | Fingerprint updates | Planned | | | | 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 6aab6ba5f..7e730d16d 100644 --- a/docs/superpowers/plans/2026-06-11-codex-proxy-rs.md +++ b/docs/superpowers/plans/2026-06-11-codex-proxy-rs.md @@ -2310,7 +2310,7 @@ git commit -m "feat: add account pool foundation" - Modify: `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs` - Test: `/home/zyy/桌面/Codes/codex-proxy-rs/tests/routes_chat_test.rs` -- [ ] **Step 1: Write failing translation test** +- [x] **Step 1: Write failing translation test** Create `/home/zyy/桌面/Codes/codex-proxy-rs/tests/routes_chat_test.rs`: @@ -2335,7 +2335,7 @@ fn chat_completion_translates_to_codex_response_request() { } ``` -- [ ] **Step 2: Run test and verify RED** +- [x] **Step 2: Run test and verify RED** Run: @@ -2345,7 +2345,7 @@ cargo test chat_completion_translates_to_codex_response_request Expected: compile failure because translation module does not exist. -- [ ] **Step 3: Implement minimal translation types** +- [x] **Step 3: Implement minimal translation types** Modify `/home/zyy/桌面/Codes/codex-proxy-rs/src/lib.rs`: @@ -2463,7 +2463,7 @@ pub enum ResponseFormat { } ``` -- [ ] **Step 4: Verify translation** +- [x] **Step 4: Verify translation** Run: @@ -2473,7 +2473,7 @@ cargo test chat_completion_translates_to_codex_response_request Expected: pass. -- [ ] **Step 5: Commit translation layer** +- [x] **Step 5: Commit translation layer** ```bash git add src/translation src/codex/types.rs src/lib.rs tests/routes_chat_test.rs diff --git a/src/codex/types.rs b/src/codex/types.rs index 8b1378917..cbd0c51f0 100644 --- a/src/codex/types.rs +++ b/src/codex/types.rs @@ -1 +1,15 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CodexResponsesRequest { + pub model: String, + pub instructions: String, + pub input: Vec, + pub stream: bool, + pub store: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tools: Option>, +} diff --git a/src/lib.rs b/src/lib.rs index a6d9967c3..03f2c7565 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,3 +10,4 @@ pub mod logs; pub mod pagination; pub mod state; pub mod storage; +pub mod translation; diff --git a/src/translation/codex_to_openai.rs b/src/translation/codex_to_openai.rs new file mode 100644 index 000000000..64f5a6a35 --- /dev/null +++ b/src/translation/codex_to_openai.rs @@ -0,0 +1,12 @@ +use serde_json::{json, Value}; + +pub fn openai_error(message: &str, code: &str) -> Value { + json!({ + "error": { + "message": message, + "type": "server_error", + "param": null, + "code": code + } + }) +} diff --git a/src/translation/mod.rs b/src/translation/mod.rs new file mode 100644 index 000000000..6992ee40a --- /dev/null +++ b/src/translation/mod.rs @@ -0,0 +1,3 @@ +pub mod codex_to_openai; +pub mod openai_to_codex; +pub mod schema; diff --git a/src/translation/openai_to_codex.rs b/src/translation/openai_to_codex.rs new file mode 100644 index 000000000..709f2b5d0 --- /dev/null +++ b/src/translation/openai_to_codex.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use crate::{ + codex::types::CodexResponsesRequest, + error::{AppError, AppResult}, +}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatCompletionRequest { + pub model: String, + #[serde(default)] + pub stream: bool, + pub messages: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatMessage { + pub role: String, + pub content: String, +} + +pub fn translate_chat_to_codex(req: ChatCompletionRequest) -> AppResult { + if req.messages.is_empty() { + return Err(AppError::BadRequest( + "messages must not be empty".to_string(), + )); + } + let input = req + .messages + .into_iter() + .map(|message| json!({ "role": message.role, "content": message.content })) + .collect(); + Ok(CodexResponsesRequest { + model: req.model, + instructions: String::new(), + input, + stream: req.stream, + store: false, + reasoning: None, + tools: None, + }) +} diff --git a/src/translation/schema.rs b/src/translation/schema.rs new file mode 100644 index 000000000..7767e0ada --- /dev/null +++ b/src/translation/schema.rs @@ -0,0 +1,6 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResponseFormat { + Text, + JsonObject, + JsonSchema, +} diff --git a/tests/routes_chat_test.rs b/tests/routes_chat_test.rs new file mode 100644 index 000000000..765ddec1d --- /dev/null +++ b/tests/routes_chat_test.rs @@ -0,0 +1,20 @@ +use codex_proxy_rs::translation::openai_to_codex::{ + translate_chat_to_codex, ChatCompletionRequest, ChatMessage, +}; + +#[test] +fn chat_completion_translates_to_codex_response_request() { + let req = ChatCompletionRequest { + model: "gpt-5.4".to_string(), + stream: true, + messages: vec![ChatMessage { + role: "user".to_string(), + content: "hello".to_string(), + }], + }; + + let codex = translate_chat_to_codex(req).unwrap(); + assert_eq!(codex.model, "gpt-5.4"); + assert!(codex.stream); + assert_eq!(codex.input[0]["role"], "user"); +}