Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 33 additions & 1 deletion crates/llmtrim-cli/src/reroute/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Value> {
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<Value> {
let mut body = Map::new();
body.insert("model".into(), json!(model));
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -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(
Expand Down
79 changes: 78 additions & 1 deletion crates/llmtrim-cli/src/reroute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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::*;
Expand Down Expand Up @@ -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"));
}
}
Loading