diff --git a/CHANGELOG.md b/CHANGELOG.md index 92584f6b..dd7dc91a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ All notable changes to this project are documented here. The format follows ### Fixed +- **Codex `-fast` models request priority service tier.** Incoming ids like `gpt-5.4-mini-fast` + already stripped to the base model, but the Responses body never set `service_tier`. Codex + upstream requests now send `"service_tier": "priority"` when the client asked for `-fast` + (Grok model ids that literally contain `-fast` are unchanged). + - **Codex hosted web search results reach Claude Code.** After the request-side `allowed_tools` fix, Codex `web_search_call` items were still dropped on the way back, so the client never saw `server_tool_use` / `web_search_tool_result` blocks or `usage.server_tool_use`. The reducer now diff --git a/crates/llmtrim-cli/src/reroute/codex.rs b/crates/llmtrim-cli/src/reroute/codex.rs index b61b90df..c2cba977 100644 --- a/crates/llmtrim-cli/src/reroute/codex.rs +++ b/crates/llmtrim-cli/src/reroute/codex.rs @@ -35,11 +35,22 @@ const WEB_SEARCH_TOOL: &str = "web_search_20250305"; /// /// `model` is already resolved to the upstream id (the caller stripped `[1m]`/`-fast` and mapped /// tiers). `session_id` is the `x-claude-code-session-id` header value if present; it becomes the -/// Responses `prompt_cache_key`. +/// Responses `prompt_cache_key`. Convenience wrapper: no priority service tier. pub fn build_request_body( anthropic: &Value, model: &str, session_id: Option<&str>, +) -> Result { + build_request_body_with_priority(anthropic, model, session_id, false) +} + +/// Like [`build_request_body`], but when `priority` is true sets `service_tier: "priority"` +/// (Codex mapping for an incoming model id ending in `-fast`). +pub(crate) fn build_request_body_with_priority( + anthropic: &Value, + model: &str, + session_id: Option<&str>, + priority: bool, ) -> Result { let mut body = Map::new(); body.insert("model".into(), json!(model)); @@ -78,6 +89,11 @@ pub fn build_request_body( body.insert("prompt_cache_key".into(), json!(sid)); } + // Incoming `-fast` (stripped by the caller) maps to Codex priority service tier, not a model id. + if priority { + body.insert("service_tier".into(), json!("priority")); + } + // text = { verbosity, format? } let mut text = Map::new(); text.insert("verbosity".into(), json!("low")); @@ -1747,6 +1763,22 @@ mod tests { assert!(body.get("include").is_none()); } + #[test] + fn priority_flag_sets_service_tier() { + let body = build_request_body_with_priority( + &json!({ "messages": [] }), + "gpt-5.4-mini", + None, + true, + ) + .expect("build"); + assert_eq!(body["model"], "gpt-5.4-mini"); + assert_eq!(body["service_tier"], "priority"); + let plain = + build_request_body(&json!({ "messages": [] }), "gpt-5.4-mini", None).expect("build"); + assert!(plain.get("service_tier").is_none()); + } + #[test] fn max_tokens_and_sampling_dropped_static_fields_present() { let body = build_request_body( diff --git a/crates/llmtrim-cli/src/reroute/mod.rs b/crates/llmtrim-cli/src/reroute/mod.rs index 735fb41b..e42e301a 100644 --- a/crates/llmtrim-cli/src/reroute/mod.rs +++ b/crates/llmtrim-cli/src/reroute/mod.rs @@ -79,7 +79,15 @@ pub(crate) fn build_upstream_for_model( } let (host, path, body, headers) = match provider { SubProvider::Codex => { - let b = codex::build_request_body(anthropic_body, &model, session_id)?; + // `-fast` is a service-tier hint for Codex (not a model id); Grok ids keep `-fast` + // literally and never take this path. + let priority = incoming_wants_priority_tier(incoming); + let b = codex::build_request_body_with_priority( + anthropic_body, + &model, + session_id, + priority, + )?; let h = codex::request_headers_with_mode( &token.access, token.account_id.as_deref(), @@ -448,6 +456,13 @@ pub fn normalize_incoming(model: &str) -> (String, bool) { } } +/// Whether the incoming Anthropic model id requested Codex priority service tier via a trailing +/// `-fast` (after stripping `[1m]`). Callers must only apply this on the Codex path — Grok model +/// ids can literally contain `-fast` (e.g. `grok-composer-2.5-fast`). +pub fn incoming_wants_priority_tier(incoming: &str) -> bool { + normalize_incoming(incoming).1 +} + #[cfg(test)] mod tests { use super::*; @@ -635,4 +650,66 @@ mod tests { serde_json::json!([{ "type": "web_search" }]) ); } + + #[test] + fn codex_fast_suffix_sets_priority_service_tier() { + let token = auth::TokenSet { + access: "tok".into(), + account_id: None, + }; + let body = serde_json::json!({ + "model": "gpt-5.4-mini-fast", + "messages": [] + }); + let up = build_upstream(SubProvider::Codex, &body, &BTreeMap::new(), &token, None) + .expect("build"); + assert_eq!(up.model, "gpt-5.4-mini"); + let parsed: serde_json::Value = serde_json::from_slice(&up.body).expect("json"); + assert_eq!(parsed["model"], "gpt-5.4-mini"); + assert_eq!(parsed["service_tier"], "priority"); + } + + #[test] + fn codex_non_fast_omits_service_tier() { + let token = auth::TokenSet { + access: "tok".into(), + account_id: None, + }; + let body = serde_json::json!({ + "model": "gpt-5.4-mini", + "messages": [] + }); + let up = build_upstream(SubProvider::Codex, &body, &BTreeMap::new(), &token, None) + .expect("build"); + let parsed: serde_json::Value = serde_json::from_slice(&up.body).expect("json"); + assert_eq!(parsed["model"], "gpt-5.4-mini"); + assert!(parsed.get("service_tier").is_none()); + } + + #[test] + fn grok_fast_model_id_keeps_literal_suffix_without_service_tier() { + let token = auth::TokenSet { + access: "tok".into(), + account_id: None, + }; + let body = serde_json::json!({ + "model": "grok-composer-2.5-fast", + "messages": [] + }); + let up = build_upstream(SubProvider::Grok, &body, &BTreeMap::new(), &token, None) + .expect("build"); + assert_eq!(up.model, "grok-composer-2.5-fast"); + let parsed: serde_json::Value = serde_json::from_slice(&up.body).expect("json"); + assert_eq!(parsed["model"], "grok-composer-2.5-fast"); + assert!(parsed.get("service_tier").is_none()); + } + + #[test] + fn incoming_wants_priority_tier_detects_fast_suffix() { + assert!(incoming_wants_priority_tier("gpt-5.4-mini-fast")); + assert!(incoming_wants_priority_tier("gpt-5.5-fast[1m]")); + assert!(!incoming_wants_priority_tier("gpt-5.4-mini")); + // Grok ids also end in -fast; the helper reports true, but only the Codex path applies it. + assert!(incoming_wants_priority_tier("grok-composer-2.5-fast")); + } }