diff --git a/crates/agent/src/lib.rs b/crates/agent/src/lib.rs index 71631047..e987294d 100644 --- a/crates/agent/src/lib.rs +++ b/crates/agent/src/lib.rs @@ -603,10 +603,7 @@ impl Default for ModelRegistry { ModelInfo { id: "gpt-5.5".to_string(), provider: ProviderKind::OpenaiCodex, - aliases: vec![ - "codex-gpt-5.5".to_string(), - "chatgpt-gpt-5.5".to_string(), - ], + aliases: vec!["codex-gpt-5.5".to_string(), "chatgpt-gpt-5.5".to_string()], supports_tools: true, supports_reasoning: true, }, diff --git a/crates/config/src/provider.rs b/crates/config/src/provider.rs index 61e751f3..5ce814b4 100644 --- a/crates/config/src/provider.rs +++ b/crates/config/src/provider.rs @@ -12,11 +12,10 @@ use super::{ DEFAULT_NOVITA_BASE_URL, DEFAULT_NOVITA_MODEL, DEFAULT_NVIDIA_NIM_BASE_URL, DEFAULT_NVIDIA_NIM_MODEL, DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_MODEL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_CODEX_BASE_URL, DEFAULT_OPENAI_CODEX_MODEL, - DEFAULT_OPENAI_MODEL, DEFAULT_OPENROUTER_BASE_URL, - DEFAULT_OPENROUTER_MODEL, DEFAULT_SGLANG_BASE_URL, DEFAULT_SGLANG_MODEL, - DEFAULT_SILICONFLOW_BASE_URL, DEFAULT_SILICONFLOW_CN_BASE_URL, DEFAULT_SILICONFLOW_MODEL, - DEFAULT_TOGETHER_BASE_URL, DEFAULT_TOGETHER_MODEL, - DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL, DEFAULT_VOLCENGINE_BASE_URL, + DEFAULT_OPENAI_MODEL, DEFAULT_OPENROUTER_BASE_URL, DEFAULT_OPENROUTER_MODEL, + DEFAULT_SGLANG_BASE_URL, DEFAULT_SGLANG_MODEL, DEFAULT_SILICONFLOW_BASE_URL, + DEFAULT_SILICONFLOW_CN_BASE_URL, DEFAULT_SILICONFLOW_MODEL, DEFAULT_TOGETHER_BASE_URL, + DEFAULT_TOGETHER_MODEL, DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL, DEFAULT_VOLCENGINE_BASE_URL, DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL, DEFAULT_WANJIE_ARK_MODEL, DEFAULT_XIAOMI_MIMO_BASE_URL, DEFAULT_XIAOMI_MIMO_MODEL, ProviderKind, }; diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index f324794a..76ca7340 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -649,7 +649,11 @@ impl DeepSeekClient { // only admits the Codex CLI's user agent; present a codex_cli_rs UA on // that path so the request is handled like the official client. let user_agent: &str = if api_provider == ApiProvider::OpenaiCodex { - concat!("codex_cli_rs/0.137.0 (CodeWhale ", env!("CARGO_PKG_VERSION"), ")") + concat!( + "codex_cli_rs/0.137.0 (CodeWhale ", + env!("CARGO_PKG_VERSION"), + ")" + ) } else { concat!( "Mozilla/5.0 (compatible; codewhale/", diff --git a/crates/tui/src/client/responses.rs b/crates/tui/src/client/responses.rs index a3e7fba2..7a01435f 100644 --- a/crates/tui/src/client/responses.rs +++ b/crates/tui/src/client/responses.rs @@ -269,44 +269,41 @@ impl DeepSeekClient { "response.output_text.delta" => { if let Some(delta_text) = event.get("delta").and_then(|d| d.as_str()) + && let Some(idx) = current_block_index { - if let Some(idx) = current_block_index { - yield Ok(StreamEvent::ContentBlockDelta { - index: idx, - delta: Delta::TextDelta { - text: delta_text.to_string(), - }, - }); - } + yield Ok(StreamEvent::ContentBlockDelta { + index: idx, + delta: Delta::TextDelta { + text: delta_text.to_string(), + }, + }); } } "response.function_call_arguments.delta" => { if let Some(delta_text) = event.get("delta").and_then(|d| d.as_str()) + && let Some(idx) = current_block_index { - if let Some(idx) = current_block_index { - yield Ok(StreamEvent::ContentBlockDelta { - index: idx, - delta: Delta::InputJsonDelta { - partial_json: delta_text.to_string(), - }, - }); - } + yield Ok(StreamEvent::ContentBlockDelta { + index: idx, + delta: Delta::InputJsonDelta { + partial_json: delta_text.to_string(), + }, + }); } } "response.reasoning_summary_text.delta" | "response.reasoning_text.delta" => { if let Some(delta_text) = event.get("delta").and_then(|d| d.as_str()) + && let Some(idx) = current_block_index { - if let Some(idx) = current_block_index { - yield Ok(StreamEvent::ContentBlockDelta { - index: idx, - delta: Delta::ThinkingDelta { - thinking: delta_text.to_string(), - }, - }); - } + yield Ok(StreamEvent::ContentBlockDelta { + index: idx, + delta: Delta::ThinkingDelta { + thinking: delta_text.to_string(), + }, + }); } } "response.output_item.done" => { @@ -535,7 +532,9 @@ fn convert_messages_to_responses_input(request: &MessageRequest) -> Vec { }], })); } - ContentBlock::ToolUse { id, name, input, .. } => { + ContentBlock::ToolUse { + id, name, input, .. + } => { let (call_id, _item_id) = parse_tool_use_id(id); items.push(json!({ "type": "function_call", @@ -596,10 +595,7 @@ fn tool_to_responses_function(tool: &Tool) -> Value { /// Composite format: "call_id|item_id" fn parse_tool_use_id(id: &str) -> (String, String) { if let Some(pipe_pos) = id.find('|') { - ( - id[..pipe_pos].to_string(), - id[pipe_pos + 1..].to_string(), - ) + (id[..pipe_pos].to_string(), id[pipe_pos + 1..].to_string()) } else { (id.to_string(), String::new()) } diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index 57dd3f09..cde1cae1 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -2363,6 +2363,16 @@ impl Config { } } } + // The Codex Responses backend only serves its own model family, and a + // global `default_text_model` is constrained to DeepSeek IDs or "auto" + // by validation — so it can never name a Codex-compatible model. Fall + // back to the Codex default here instead of letting a DeepSeek default + // leak through and be rejected by the backend. An explicit + // `[providers.openai_codex] model` is honored by the block above. + if provider == ApiProvider::OpenaiCodex { + return DEFAULT_OPENAI_CODEX_MODEL.to_string(); + } + let moonshot_config = (provider == ApiProvider::Moonshot) .then(|| self.provider_config()) .flatten(); @@ -2457,7 +2467,7 @@ impl Config { | ApiProvider::Moonshot | ApiProvider::Sglang | ApiProvider::Vllm - | ApiProvider::Ollama + | ApiProvider::Ollama | ApiProvider::Volcengine | ApiProvider::Huggingface | ApiProvider::Together @@ -8475,6 +8485,45 @@ http_headers = { "X-Model-Provider-Id" = "from-file" } Ok(()) } + #[test] + fn openai_codex_default_model_falls_back_to_codex_model() { + // The Codex Responses backend only accepts its own model family, and a + // global `default_text_model` is validated to DeepSeek IDs (or "auto"), + // so with the Codex provider it must resolve to the Codex default + // instead of leaking a DeepSeek id the backend rejects. + let with_deepseek_default = Config { + provider: Some("openai-codex".to_string()), + default_text_model: Some(DEFAULT_TEXT_MODEL.to_string()), + ..Default::default() + }; + assert_eq!( + with_deepseek_default.api_provider(), + ApiProvider::OpenaiCodex + ); + assert_eq!( + with_deepseek_default.default_model(), + DEFAULT_OPENAI_CODEX_MODEL + ); + + // No global default resolves the same way. + let bare = Config { + provider: Some("openai-codex".to_string()), + ..Default::default() + }; + assert_eq!(bare.default_model(), DEFAULT_OPENAI_CODEX_MODEL); + + // An explicit provider-scoped model still wins over the fallback. + let mut providers = ProvidersConfig::default(); + providers.openai_codex.model = Some("gpt-5.5-codex-preview".to_string()); + let pinned = Config { + provider: Some("openai-codex".to_string()), + default_text_model: Some(DEFAULT_TEXT_MODEL.to_string()), + providers: Some(providers), + ..Default::default() + }; + assert_eq!(pinned.default_model(), "gpt-5.5-codex-preview"); + } + #[test] fn insecure_skip_tls_verify_is_scoped_to_active_provider() { let mut providers = ProvidersConfig::default(); diff --git a/crates/tui/src/oauth.rs b/crates/tui/src/oauth.rs index d1384143..68e3d868 100644 --- a/crates/tui/src/oauth.rs +++ b/crates/tui/src/oauth.rs @@ -155,22 +155,16 @@ fn refresh_access_token(refresh_token: &str) -> Result { let body = response.text().unwrap_or_default(); bail!("Token refresh failed (HTTP {status}): {body}"); } - let body: serde_json::Value = response - .json() - .context("parsing token refresh response")?; + let body: serde_json::Value = response.json().context("parsing token refresh response")?; let new_access = body["access_token"] .as_str() .context("missing access_token in refresh response")? .to_string(); - let new_refresh = body["refresh_token"] - .as_str() - .map(ToOwned::to_owned); + let new_refresh = body["refresh_token"].as_str().map(ToOwned::to_owned); let new_id = body["id_token"].as_str().map(ToOwned::to_owned); // Extract account_id from id_token if available. - let account_id = new_id - .as_deref() - .and_then(extract_account_id_from_id_token); + let account_id = new_id.as_deref().and_then(extract_account_id_from_id_token); let creds = CodexCredentials { access_token: new_access, @@ -216,9 +210,7 @@ fn save_credentials(creds: &CodexCredentials, id_token: Option<&str>) -> Result< id_token: id_token.map(ToOwned::to_owned), account_id: creds.account_id.clone(), }), - last_refresh: Some( - chrono_humanize_if_available() - ), + last_refresh: Some(chrono_humanize_if_available()), }; let json = serde_json::to_string_pretty(&auth).context("serializing credentials")?; @@ -258,23 +250,23 @@ fn chrono_humanize_if_available() -> String { /// resolution path (mirrors the Kimi OAuth flow). pub fn get_credentials() -> Result { // Env override takes priority. - if let Ok(token) = std::env::var("OPENAI_CODEX_ACCESS_TOKEN") { - if !token.trim().is_empty() { - return Ok(CodexCredentials { - access_token: token, - refresh_token: None, - account_id: codex_account_id_env(), - }); - } + if let Ok(token) = std::env::var("OPENAI_CODEX_ACCESS_TOKEN") + && !token.trim().is_empty() + { + return Ok(CodexCredentials { + access_token: token, + refresh_token: None, + account_id: codex_account_id_env(), + }); } - if let Ok(token) = std::env::var("CODEX_ACCESS_TOKEN") { - if !token.trim().is_empty() { - return Ok(CodexCredentials { - access_token: token, - refresh_token: None, - account_id: codex_account_id_env(), - }); - } + if let Ok(token) = std::env::var("CODEX_ACCESS_TOKEN") + && !token.trim().is_empty() + { + return Ok(CodexCredentials { + access_token: token, + refresh_token: None, + account_id: codex_account_id_env(), + }); } let creds = load_credentials()?.context(