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:
Claude
2026-06-09 00:03:42 +00:00
parent 0f19c395d5
commit a2ae9a1acf
6 changed files with 105 additions and 68 deletions
+1 -4
View File
@@ -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,
},
+4 -5
View File
@@ -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,
};
+5 -1
View File
@@ -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/",
+25 -29
View File
@@ -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())
}
+50 -1
View File
@@ -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
View File
@@ -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(