fix(codex): satisfy clippy/fmt gate and default to gpt-5.5 for Codex
Tightens the experimental OpenAI Codex (ChatGPT) provider so the v0.8.55 gate is green. - clippy: collapse 5 nested if/if-let blocks flagged by clippy::collapsible_if into let-chains (oauth.rs env-override resolution, responses.rs SSE delta handling). cargo clippy --workspace --all-targets -- -D warnings is now clean. - fmt: cargo fmt --all over the Codex/Together changes (the gate's --check was failing, incl. a mangled "| ApiProvider::Ollama"). - default model: Config::default_model() now resolves to the Codex default (gpt-5.5) for the Codex provider instead of leaking a DeepSeek default_text_model the Responses backend rejects. The carve-out sits after the explicit provider-scoped model block (so [providers.openai_codex] model still wins) and before the DeepSeek-validating path, which is unchanged. Adds a behavior test. https://claude.ai/code/session_013cHWv5sR6XPnVWzfMP8uma
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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<Value> {
|
||||
}],
|
||||
}));
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
+20
-28
@@ -155,22 +155,16 @@ fn refresh_access_token(refresh_token: &str) -> Result<CodexCredentials> {
|
||||
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<CodexCredentials> {
|
||||
// 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(
|
||||
|
||||
Reference in New Issue
Block a user