feat: enhance OpenAI Responses API handling by stripping unsupported tool fields and improving error handling for routing conversion

This commit is contained in:
Musa 2026-02-25 10:18:38 -08:00
parent fe43cf5ecb
commit eed196bc81
No known key found for this signature in database
2 changed files with 50 additions and 5 deletions

View file

@ -4,7 +4,7 @@ use common::consts::{
ARCH_IS_STREAMING_HEADER, ARCH_PROVIDER_HINT_HEADER, REQUEST_ID_HEADER, TRACE_PARENT_HEADER, ARCH_IS_STREAMING_HEADER, ARCH_PROVIDER_HINT_HEADER, REQUEST_ID_HEADER, TRACE_PARENT_HEADER,
}; };
use common::llm_providers::LlmProviders; use common::llm_providers::LlmProviders;
use hermesllm::apis::openai_responses::InputParam; use hermesllm::apis::openai_responses::{InputParam, Tool as ResponsesTool};
use hermesllm::clients::{SupportedAPIsFromClient, SupportedUpstreamAPIs}; use hermesllm::clients::{SupportedAPIsFromClient, SupportedUpstreamAPIs};
use hermesllm::{ProviderRequest, ProviderRequestType}; use hermesllm::{ProviderRequest, ProviderRequestType};
use http_body_util::combinators::BoxBody; use http_body_util::combinators::BoxBody;
@ -19,7 +19,7 @@ use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tracing::{debug, info, info_span, warn, Instrument}; use tracing::{debug, info, info_span, warn, Instrument};
use crate::handlers::router_chat::router_chat_get_upstream_model; use crate::handlers::router_chat::{router_chat_get_upstream_model, RoutingResult};
use crate::handlers::utils::{ use crate::handlers::utils::{
create_streaming_response, truncate_message, ObservableStreamProcessor, create_streaming_response, truncate_message, ObservableStreamProcessor,
}; };
@ -314,6 +314,33 @@ async fn llm_chat_inner(
} }
} }
// OpenAI Responses API rejects some tool fields that Codex may emit (e.g. domains on web_search).
// Strip those unsupported fields before serializing.
if matches!(
client_api,
Some(SupportedAPIsFromClient::OpenAIResponsesAPI(_))
) {
if let ProviderRequestType::ResponsesAPIRequest(ref mut responses_req) = client_request {
let mut stripped_domains_fields = 0usize;
if let Some(tools) = responses_req.tools.as_mut() {
for tool in tools.iter_mut() {
if let ResponsesTool::WebSearchPreview { domains, .. } = tool {
if domains.is_some() {
*domains = None;
stripped_domains_fields += 1;
}
}
}
}
if stripped_domains_fields > 0 {
debug!(
stripped_domains_fields = stripped_domains_fields,
"removed unsupported web_search domains fields for OpenAI Responses API"
);
}
}
}
// Serialize request for upstream BEFORE router consumes it // Serialize request for upstream BEFORE router consumes it
let client_request_bytes_for_upstream = ProviderRequestType::to_bytes(&client_request).unwrap(); let client_request_bytes_for_upstream = ProviderRequestType::to_bytes(&client_request).unwrap();
@ -345,9 +372,23 @@ async fn llm_chat_inner(
{ {
Ok(result) => result, Ok(result) => result,
Err(err) => { Err(err) => {
let mut internal_error = Response::new(full(err.message)); // Codex /v1/responses can include tools (e.g. web_search) that cannot be
*internal_error.status_mut() = err.status_code; // converted to ChatCompletions for routing. Fall back to alias-resolved model
return Ok(internal_error); // instead of failing the full request.
if request_path == "/v1/responses" && err.message.contains("Unsupported conversion") {
warn!(
request_id = %request_id,
error = %err.message,
"routing conversion unsupported for responses request; falling back to validated model"
);
RoutingResult {
model_name: "none".to_string(),
}
} else {
let mut internal_error = Response::new(full(err.message));
*internal_error.status_mut() = err.status_code;
return Ok(internal_error);
}
} }
}; };

View file

@ -137,6 +137,8 @@ pub enum InputItem {
call_id: String, call_id: String,
output: String, output: String,
}, },
/// Unknown input item type (forward-compatible passthrough)
Unknown(serde_json::Value),
} }
/// Input message with role and content /// Input message with role and content
@ -285,7 +287,9 @@ pub enum Tool {
filters: Option<serde_json::Value>, filters: Option<serde_json::Value>,
}, },
/// Web search tool /// Web search tool
#[serde(alias = "web_search")]
WebSearchPreview { WebSearchPreview {
#[serde(skip_serializing_if = "Option::is_none")]
domains: Option<Vec<String>>, domains: Option<Vec<String>>,
search_context_size: Option<String>, search_context_size: Option<String>,
user_location: Option<UserLocation>, user_location: Option<UserLocation>,