From f37bacb934bd816e1664197c32a6fc9ec8a16c50 Mon Sep 17 00:00:00 2001 From: Erin McNulty Date: Mon, 11 May 2026 14:13:10 -0400 Subject: [PATCH 1/2] auto-route /chat/completions to /responses for unsupported chat completion features --- crates/braintrust-llm-router/src/router.rs | 67 ++- crates/braintrust-llm-router/tests/router.rs | 145 ++++++ crates/lingua/src/processing/transform.rs | 168 +++++++ crates/lingua/src/providers/openai/convert.rs | 64 ++- crates/lingua/src/python.rs | 2 + crates/lingua/src/wasm.rs | 3 + payloads/cases/models.ts | 2 + payloads/cases/params.ts | 51 +++ .../__snapshots__/transforms.test.ts.snap | 430 ++++++++++++++++++ .../chat-completions/error.json | 3 + .../chat-completions/request.json | 32 ++ .../responses/followup-request.json | 50 ++ .../followup-response-streaming.json | 367 +++++++++++++++ .../responses/followup-response.json | 93 ++++ .../responses/request.json | 32 ++ .../responses/response-streaming.json | 320 +++++++++++++ .../responses/response.json | 91 ++++ ...functionToolsWithReasoningEffortParam.json | 39 ++ ...functionToolsWithReasoningEffortParam.json | 42 ++ ...functionToolsWithReasoningEffortParam.json | 91 ++++ ...functionToolsWithReasoningEffortParam.json | 39 ++ ...functionToolsWithReasoningEffortParam.json | 42 ++ 22 files changed, 2136 insertions(+), 37 deletions(-) create mode 100644 payloads/snapshots/functionToolsWithReasoningEffortParam/chat-completions/error.json create mode 100644 payloads/snapshots/functionToolsWithReasoningEffortParam/chat-completions/request.json create mode 100644 payloads/snapshots/functionToolsWithReasoningEffortParam/responses/followup-request.json create mode 100644 payloads/snapshots/functionToolsWithReasoningEffortParam/responses/followup-response-streaming.json create mode 100644 payloads/snapshots/functionToolsWithReasoningEffortParam/responses/followup-response.json create mode 100644 payloads/snapshots/functionToolsWithReasoningEffortParam/responses/request.json create mode 100644 payloads/snapshots/functionToolsWithReasoningEffortParam/responses/response-streaming.json create mode 100644 payloads/snapshots/functionToolsWithReasoningEffortParam/responses/response.json create mode 100644 payloads/transforms/chat-completions_to_anthropic/functionToolsWithReasoningEffortParam.json create mode 100644 payloads/transforms/chat-completions_to_google/functionToolsWithReasoningEffortParam.json create mode 100644 payloads/transforms/chat-completions_to_responses/functionToolsWithReasoningEffortParam.json create mode 100644 payloads/transforms/responses_to_anthropic/functionToolsWithReasoningEffortParam.json create mode 100644 payloads/transforms/responses_to_google/functionToolsWithReasoningEffortParam.json diff --git a/crates/braintrust-llm-router/src/router.rs b/crates/braintrust-llm-router/src/router.rs index feecc0cd..f2a9fedc 100644 --- a/crates/braintrust-llm-router/src/router.rs +++ b/crates/braintrust-llm-router/src/router.rs @@ -154,20 +154,21 @@ async fn prepare_provider_request( spec: &ModelSpec, format: ProviderFormat, stream: bool, -) -> Result<(Bytes, Option)> { +) -> Result<(Bytes, Option, ProviderFormat)> { if requires_bedrock_request_preparation(format) { let bytes = prepare_bedrock_request(body, spec, format).await?; - return Ok((bytes, Some(format))); + return Ok((bytes, Some(format), format)); } - let (transformed, detected_format) = + let (transformed, detected_format, actual_format) = match lingua::transform_request(body.clone(), format, Some(&spec.model)) { - Ok(TransformResult::PassThrough(bytes)) => (bytes, None), + Ok(TransformResult::PassThrough(bytes)) => (bytes, None, format), Ok(TransformResult::Transformed { bytes, source_format, - }) => (bytes, Some(source_format)), - Err(TransformError::UnsupportedTargetFormat(_)) => (body, None), + actual_target_format, + }) => (bytes, Some(source_format), actual_target_format), + Err(TransformError::UnsupportedTargetFormat(_)) => (body, None, format), Err(err) => return Err(err.into()), }; @@ -175,11 +176,12 @@ async fn prepare_provider_request( // TODO: Fold streaming intent into `lingua::transform_request` once we // are ready to update its Rust/WASM/Python/TS call sites together. Ok(( - enable_streaming_payload(transformed, format), + enable_streaming_payload(transformed, actual_format), detected_format, + actual_format, )) } else { - Ok((transformed, detected_format)) + Ok((transformed, detected_format, actual_format)) } } @@ -214,14 +216,14 @@ impl Router { .first() .ok_or_else(|| Error::NoProvider(output_format))?; let (provider_alias, provider, auth, spec, format, strategy) = route; - let (payload, detected_format) = + let (payload, detected_format, actual_format) = prepare_provider_request(body, spec.as_ref(), *format, stream).await?; Ok(( PreparedRequestInner { provider: provider.clone(), auth, spec: spec.clone(), - format: *format, + format: actual_format, payload, output_format, strategy: strategy.clone(), @@ -229,7 +231,7 @@ impl Router { RouterMetadata { detected_input_format: detected_format.unwrap_or(*format), provider_alias: provider_alias.clone(), - provider_format: *format, + provider_format: actual_format, }, )) } @@ -872,7 +874,7 @@ mod tests { ); let spec = openai_spec("gpt-5-mini", ModelFlavor::Chat); - let (payload, _) = + let (payload, _, _) = prepare_provider_request(body, &spec, ProviderFormat::ChatCompletions, true) .await .expect("request prepares"); @@ -890,7 +892,7 @@ mod tests { ); let spec = openai_spec("gpt-5-mini", ModelFlavor::Chat); - let (payload, _) = + let (payload, _, _) = prepare_provider_request(body, &spec, ProviderFormat::ChatCompletions, false) .await .expect("request prepares"); @@ -900,6 +902,45 @@ mod tests { assert_eq!(parsed.get("stream_options"), None); } + #[tokio::test] + async fn prepare_provider_request_upgrades_actual_format_to_responses_for_reasoning_plus_tools() + { + // A chat-completions request with reasoning_effort + tools should have its actual_format + // upgraded to Responses so the router sends it to the correct endpoint. + let body = Bytes::from( + serde_json::json!({ + "model": "gpt-5.4-mini", + "messages": [{"role": "user", "content": "Tokyo weather?"}], + "reasoning_effort": "medium", + "tools": [{ + "type": "function", + "function": { + "name": "get_weather", + "description": "Get weather", + "parameters": { + "type": "object", + "properties": {"location": {"type": "string"}}, + "required": ["location"] + } + } + }] + }) + .to_string(), + ); + let spec = openai_spec("gpt-5.4-mini", ModelFlavor::Chat); + + let (_, _, actual_format) = + prepare_provider_request(body, &spec, ProviderFormat::ChatCompletions, false) + .await + .expect("request prepares"); + + assert_eq!( + actual_format, + ProviderFormat::Responses, + "actual_format must be Responses so the router uses the /v1/responses endpoint" + ); + } + fn dummy_auth() -> AuthConfig { AuthConfig::ApiKey { key: "test".into(), diff --git a/crates/braintrust-llm-router/tests/router.rs b/crates/braintrust-llm-router/tests/router.rs index ff3398d6..d8ad9878 100644 --- a/crates/braintrust-llm-router/tests/router.rs +++ b/crates/braintrust-llm-router/tests/router.rs @@ -654,6 +654,151 @@ async fn anthropic_native_catalog_format_resolves_to_anthropic() { ); } +/// Stub provider that supports both ChatCompletions and Responses formats and +/// records the format it was called with. +#[derive(Clone)] +struct CapturingOpenAIStub { + recorded_format: Arc>>, +} + +#[async_trait] +impl Provider for CapturingOpenAIStub { + fn id(&self) -> &'static str { + "openai" + } + + fn provider_formats(&self) -> Vec { + vec![ProviderFormat::ChatCompletions, ProviderFormat::Responses] + } + + async fn complete( + &self, + _payload: Bytes, + _auth: &AuthConfig, + _spec: &ModelSpec, + format: ProviderFormat, + _client_headers: &ClientHeaders, + ) -> braintrust_llm_router::Result { + *self.recorded_format.lock().unwrap() = Some(format); + let response = braintrust_llm_router::serde_json::json!({ + "id": "stub", + "object": "chat.completion", + "model": "gpt-5.4-mini", + "choices": [{"index": 0, "message": {"role": "assistant", "content": "ok"}, "finish_reason": "stop"}], + "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2} + }); + Ok(Bytes::from( + braintrust_llm_router::serde_json::to_vec(&response).unwrap(), + )) + } + + async fn complete_stream( + &self, + _payload: Bytes, + _auth: &AuthConfig, + _spec: &ModelSpec, + _format: ProviderFormat, + _client_headers: &ClientHeaders, + ) -> braintrust_llm_router::Result { + Ok(Box::pin(tokio_stream::empty())) + } + + async fn health_check(&self, _auth: &AuthConfig) -> braintrust_llm_router::Result<()> { + Ok(()) + } +} + +fn openai_router(catalog: Arc) -> (Router, Arc>>) { + let recorded_format = Arc::new(Mutex::new(None)); + let stub = CapturingOpenAIStub { + recorded_format: Arc::clone(&recorded_format), + }; + let router = RouterBuilder::new() + .with_catalog(catalog) + .add_provider( + "openai", + stub, + AuthConfig::ApiKey { + key: "test-key".into(), + header: Some("authorization".into()), + prefix: Some("Bearer".into()), + }, + vec![ProviderFormat::ChatCompletions, ProviderFormat::Responses], + ) + .build() + .expect("router builds"); + (router, recorded_format) +} + +#[tokio::test] +async fn reasoning_effort_with_tools_upgrades_format_to_responses() { + // Use gpt-5.2-mini (minor version 2 < 3) so model_requires_responses_api() returns + // false. Only the body-level detection (reasoning_effort + tools) should trigger the + // upgrade from ChatCompletions to Responses. + let mut catalog = ModelCatalog::empty(); + catalog.insert( + "gpt-5.2-mini".into(), + ModelSpec { + model: "gpt-5.2-mini".into(), + format: ProviderFormat::ChatCompletions, + flavor: ModelFlavor::Chat, + display_name: None, + parent: None, + input_cost_per_mil_tokens: None, + output_cost_per_mil_tokens: None, + input_cache_read_cost_per_mil_tokens: None, + multimodal: None, + reasoning: None, + max_input_tokens: None, + max_output_tokens: None, + supports_streaming: true, + extra: Default::default(), + available_providers: Default::default(), + }, + ); + let (router, recorded_format) = openai_router(Arc::new(catalog)); + + let body = to_body(json!({ + "model": "gpt-5.2-mini", + "messages": [{"role": "user", "content": "Tokyo weather?"}], + "reasoning_effort": "medium", + "tools": [{ + "type": "function", + "function": { + "name": "get_weather", + "description": "Get weather", + "parameters": { + "type": "object", + "properties": {"location": {"type": "string"}}, + "required": ["location"] + } + } + }] + })); + + let (request, metadata) = router + .create_request(body, "gpt-5.2-mini", ProviderFormat::ChatCompletions) + .await + .expect("create request"); + + assert_eq!( + metadata.provider_format, + ProviderFormat::Responses, + "provider_format in metadata should reflect the upgrade to Responses" + ); + + router + .complete(request, &ClientHeaders::default()) + .await + .expect("complete"); + + assert_eq!( + *recorded_format.lock().unwrap(), + Some(ProviderFormat::Responses), + "provider.complete() must be called with Responses so the request hits /v1/responses" + ); +} + fn retry_model_catalog() -> Arc { let mut catalog = ModelCatalog::empty(); catalog.insert( diff --git a/crates/lingua/src/processing/transform.rs b/crates/lingua/src/processing/transform.rs index 6d36ffa6..55cb6c01 100644 --- a/crates/lingua/src/processing/transform.rs +++ b/crates/lingua/src/processing/transform.rs @@ -111,6 +111,12 @@ pub enum TransformResult { bytes: Bytes, /// The detected source format source_format: ProviderFormat, + /// The format the payload was actually transformed into. + /// + /// Usually equal to the `target_format` passed to the transform function, but may + /// differ when the transform function upgrades the target (e.g. `ChatCompletions` → + /// `Responses` when `reasoning_effort` + `tools` are present). + actual_target_format: ProviderFormat, }, } @@ -231,6 +237,26 @@ pub fn extract_model(input: &[u8]) -> Option { /// let result = transform_request(openai_payload, ProviderFormat::Anthropic, None).unwrap(); /// let output_bytes = result.into_bytes(); /// ``` +/// Returns `true` when a Chat Completions request should be redirected to the Responses API. +/// +/// The `/v1/chat/completions` endpoint rejects requests that combine `reasoning_effort` +/// with function `tools` for certain models (e.g. `gpt-5.4-mini`). The Responses API +/// (`/v1/responses`) supports this combination, so we upgrade the target automatically. +#[cfg(feature = "openai")] +fn chat_completions_needs_responses_upgrade(payload: &Value) -> bool { + let has_reasoning_effort = payload + .get("reasoning_effort") + .and_then(Value::as_str) + .is_some_and(|e| e != "none"); + + let has_tools = payload + .get("tools") + .and_then(Value::as_array) + .is_some_and(|t| !t.is_empty()); + + has_reasoning_effort && has_tools +} + pub fn transform_request( input: Bytes, target_format: ProviderFormat, @@ -242,6 +268,18 @@ pub fn transform_request( let source_adapter = detect_adapter(&payload, DetectKind::Request)?; + // Upgrade ChatCompletions → Responses when reasoning_effort + tools are both present. + // The /v1/chat/completions endpoint rejects this combination for certain models; + // /v1/responses handles it correctly. + #[cfg(feature = "openai")] + let target_format = if target_format == ProviderFormat::ChatCompletions + && chat_completions_needs_responses_upgrade(&payload) + { + ProviderFormat::Responses + } else { + target_format + }; + if source_adapter.format() == target_format && !needs_forced_translation(&payload, model, target_format) { @@ -270,6 +308,7 @@ pub fn transform_request( Ok(TransformResult::Transformed { bytes: Bytes::from(bytes), source_format, + actual_target_format: target_format, }) } @@ -316,6 +355,7 @@ pub fn transform_response( Ok(TransformResult::Transformed { bytes: Bytes::from(bytes), source_format, + actual_target_format: target_format, }) } @@ -511,6 +551,7 @@ pub(crate) fn transform_stream_chunk_step( result: TransformResult::Transformed { bytes, source_format, + actual_target_format: target_format, }, source_format, source_is_native_stream, @@ -1276,6 +1317,133 @@ mod tests { assert!(output.get("messages").is_some()); } + // ========================================================================= + // chat_completions_needs_responses_upgrade / format upgrade tests + // ========================================================================= + + #[test] + #[cfg(feature = "openai")] + fn test_upgrade_detection_triggers_with_reasoning_effort_and_tools() { + let payload = json!({ + "reasoning_effort": "medium", + "tools": [{"type": "function", "function": {"name": "get_weather"}}] + }); + assert!(chat_completions_needs_responses_upgrade(&payload)); + } + + #[test] + #[cfg(feature = "openai")] + fn test_upgrade_detection_triggers_with_low_reasoning_effort() { + let payload = json!({ + "reasoning_effort": "low", + "tools": [{"type": "function", "function": {"name": "fn"}}] + }); + assert!(chat_completions_needs_responses_upgrade(&payload)); + } + + #[test] + #[cfg(feature = "openai")] + fn test_upgrade_detection_skips_when_reasoning_effort_none() { + let payload = json!({ + "reasoning_effort": "none", + "tools": [{"type": "function", "function": {"name": "fn"}}] + }); + assert!(!chat_completions_needs_responses_upgrade(&payload)); + } + + #[test] + #[cfg(feature = "openai")] + fn test_upgrade_detection_skips_when_no_reasoning_effort() { + let payload = json!({ + "tools": [{"type": "function", "function": {"name": "fn"}}] + }); + assert!(!chat_completions_needs_responses_upgrade(&payload)); + } + + #[test] + #[cfg(feature = "openai")] + fn test_upgrade_detection_skips_when_no_tools() { + let payload = json!({ "reasoning_effort": "medium" }); + assert!(!chat_completions_needs_responses_upgrade(&payload)); + } + + #[test] + #[cfg(feature = "openai")] + fn test_upgrade_detection_skips_when_tools_empty() { + let payload = json!({ "reasoning_effort": "medium", "tools": [] }); + assert!(!chat_completions_needs_responses_upgrade(&payload)); + } + + #[test] + #[cfg(feature = "openai")] + fn test_transform_request_upgrades_target_to_responses_for_reasoning_plus_tools() { + let payload = json!({ + "model": "gpt-5.4-mini", + "messages": [{"role": "user", "content": "Tokyo weather?"}], + "reasoning_effort": "medium", + "tools": [{ + "type": "function", + "function": { + "name": "get_weather", + "description": "Get weather", + "parameters": { + "type": "object", + "properties": {"location": {"type": "string"}}, + "required": ["location"] + } + } + }] + }); + let input = to_bytes(&payload); + + let result = transform_request(input, ProviderFormat::ChatCompletions, None).unwrap(); + + match result { + TransformResult::Transformed { + actual_target_format, + .. + } => { + assert_eq!( + actual_target_format, + ProviderFormat::Responses, + "Should upgrade to Responses when reasoning_effort + tools are present" + ); + } + TransformResult::PassThrough(_) => { + panic!("Expected transformation, got passthrough"); + } + } + } + + #[test] + #[cfg(feature = "openai")] + fn test_transform_request_does_not_upgrade_without_tools() { + let payload = json!({ + "model": "gpt-5.4-mini", + "messages": [{"role": "user", "content": "Hello"}], + "reasoning_effort": "medium" + }); + let input = to_bytes(&payload); + + let result = transform_request(input, ProviderFormat::ChatCompletions, None).unwrap(); + + match result { + TransformResult::Transformed { + actual_target_format, + .. + } => { + assert_eq!( + actual_target_format, + ProviderFormat::ChatCompletions, + "Should not upgrade without tools" + ); + } + TransformResult::PassThrough(_) => { + // PassThrough is also acceptable here (no upgrade, no forced translation) + } + } + } + #[test] #[cfg(all(feature = "openai", feature = "bedrock"))] fn test_non_anthropic_bedrock_model_uses_converse() { diff --git a/crates/lingua/src/providers/openai/convert.rs b/crates/lingua/src/providers/openai/convert.rs index 0c848330..c93faeb3 100644 --- a/crates/lingua/src/providers/openai/convert.rs +++ b/crates/lingua/src/providers/openai/convert.rs @@ -2333,8 +2333,13 @@ impl TryFromLLM> for Vec { let parts: Vec = match item.output_item_type { Some(openai::OutputItemType::Message) => { - // Extract text content from message output items + // Extract text content from message output items. + // `phase` is a message-level field (not content-level); it is stored in + // the first text part's provider_options under "_output_item_phase" so + // it survives the OutputItem → Message → OutputItem roundtrip. + let item_phase = item.phase.take(); let mut text_parts = Vec::new(); + let mut phase_stored = false; if let Some(content) = item.content { for c in content { if let Some(text) = c.text { @@ -2344,27 +2349,30 @@ impl TryFromLLM> for Vec { // injected by `response_from_universal`, so they are not // stored back to keep the universal representation clean. let non_empty_annotations = c.annotations.filter(|a| !a.is_empty()); - let provider_options = - if non_empty_annotations.is_some() || c.logprobs.is_some() { - let mut options = serde_json::Map::new(); - if let Some(annotations) = non_empty_annotations { - if let Ok(value) = serde_json::to_value(&annotations) { - options.insert("annotations".to_string(), value); - } - } - if let Some(logprobs) = c.logprobs { - if let Ok(value) = serde_json::to_value(&logprobs) { - options.insert("logprobs".to_string(), value); - } - } - if options.is_empty() { - None - } else { - Some(ProviderOptions { options }) + let mut options = serde_json::Map::new(); + if let Some(annotations) = non_empty_annotations { + if let Ok(value) = serde_json::to_value(&annotations) { + options.insert("annotations".to_string(), value); + } + } + if let Some(logprobs) = c.logprobs { + if let Ok(value) = serde_json::to_value(&logprobs) { + options.insert("logprobs".to_string(), value); + } + } + if !phase_stored { + phase_stored = true; + if let Some(ref phase) = item_phase { + if let Ok(value) = serde_json::to_value(phase) { + options.insert("_output_item_phase".to_string(), value); } - } else { - None - }; + } + } + let provider_options = if options.is_empty() { + None + } else { + Some(ProviderOptions { options }) + }; text_parts.push(AssistantContentPart::Text(TextContentPart { text, encrypted_content: None, @@ -2656,7 +2664,7 @@ impl TryFromLLM> for Vec { // Extract annotations and logprobs from provider_options. // Default annotations to Some(vec![]) so the Responses API // output always has the required `annotations` array. - let (annotations, logprobs) = if let Some(ref opts) = + let (annotations, logprobs, phase) = if let Some(ref opts) = text_part.provider_options { let annotations = @@ -2672,9 +2680,16 @@ impl TryFromLLM> for Vec { ) .ok() }); - (annotations.or(Some(vec![])), logprobs) + let phase = + opts.options.get("_output_item_phase").and_then(|v| { + serde_json::from_value::( + v.clone(), + ) + .ok() + }); + (annotations.or(Some(vec![])), logprobs, phase) } else { - (Some(vec![]), None) + (Some(vec![]), None, None) }; result.push(openai::OutputItem { output_item_type: Some(openai::OutputItemType::Message), @@ -2689,6 +2704,7 @@ impl TryFromLLM> for Vec { }]), id: use_id(&mut id_used, &id), status: Some(openai::FunctionCallItemStatus::Completed), + phase, ..Default::default() }); } diff --git a/crates/lingua/src/python.rs b/crates/lingua/src/python.rs index 54c49c05..9f9f1f6a 100644 --- a/crates/lingua/src/python.rs +++ b/crates/lingua/src/python.rs @@ -304,6 +304,7 @@ fn transform_request( TransformResult::Transformed { bytes, source_format, + .. } => { let data: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| { PyErr::new::(format!( @@ -361,6 +362,7 @@ fn transform_response(py: Python, json: &str, target_format: &str) -> PyResult

{ let data: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| { PyErr::new::(format!( diff --git a/crates/lingua/src/wasm.rs b/crates/lingua/src/wasm.rs index d26a379c..4d9ab80e 100644 --- a/crates/lingua/src/wasm.rs +++ b/crates/lingua/src/wasm.rs @@ -373,6 +373,7 @@ pub fn transform_request( TransformResult::Transformed { bytes, source_format, + .. } => (false, bytes, Some(source_format)), }; @@ -421,6 +422,7 @@ pub fn transform_response(input: &str, target_format: &str) -> Result (false, bytes, Some(source_format)), }; @@ -453,6 +455,7 @@ pub fn transform_stream_chunk(input: &str, target_format: &str) -> Result (false, bytes, Some(source_format)), }; diff --git a/payloads/cases/models.ts b/payloads/cases/models.ts index d91afed4..335c9ea0 100644 --- a/payloads/cases/models.ts +++ b/payloads/cases/models.ts @@ -2,6 +2,8 @@ export const OPENAI_CHAT_COMPLETIONS_MODEL = "gpt-5-nano"; export const OPENAI_RESPONSES_MODEL = "gpt-5-nano"; export const OPENAI_REASONING_NONE_MODEL = "gpt-5.2"; +// Mini reasoning model: supports reasoning_effort but requires /v1/responses when combined with function tools +export const OPENAI_MINI_REASONING_MODEL = "gpt-5.4-mini"; // For parameters not supported by reasoning models (temperature, top_p, logprobs) export const OPENAI_NON_REASONING_MODEL = "gpt-4o-mini"; export const ANTHROPIC_MODEL = "claude-sonnet-4-5-20250929"; diff --git a/payloads/cases/params.ts b/payloads/cases/params.ts index bd249345..cf8903fb 100644 --- a/payloads/cases/params.ts +++ b/payloads/cases/params.ts @@ -11,6 +11,7 @@ import { OPENAI_RESPONSES_MODEL, OPENAI_REASONING_NONE_MODEL, OPENAI_NON_REASONING_MODEL, + OPENAI_MINI_REASONING_MODEL, ANTHROPIC_MODEL, ANTHROPIC_OPUS_MODEL, GOOGLE_GEMINI_3_MODEL, @@ -1054,6 +1055,56 @@ export const paramsCases: TestCaseCollection = { bedrock: null, }, + // Reproduces: "Function tools with reasoning_effort are not supported for + // gpt-5.4-mini in /v1/chat/completions. Please use /v1/responses instead." + // The router should detect reasoning_effort + function tools and forward to + // the responses endpoint rather than passing through to chat/completions. + functionToolsWithReasoningEffortParam: { + "chat-completions": { + model: OPENAI_MINI_REASONING_MODEL, + messages: [{ role: "user", content: "Tokyo weather" }], + reasoning_effort: "medium", + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get weather", + strict: true, + parameters: { + type: "object", + properties: { location: { type: "string" } }, + required: ["location"], + additionalProperties: false, + }, + }, + }, + ], + }, + responses: { + model: OPENAI_MINI_REASONING_MODEL, + input: [{ role: "user", content: "Tokyo weather" }], + reasoning: { effort: "medium" }, + tools: [ + { + type: "function", + name: "get_weather", + description: "Get weather", + strict: true, + parameters: { + type: "object", + properties: { location: { type: "string" } }, + required: ["location"], + additionalProperties: false, + }, + }, + ], + }, + anthropic: null, + google: null, + bedrock: null, + }, + parallelToolCallsDisabledParam: { "chat-completions": { model: OPENAI_CHAT_COMPLETIONS_MODEL, diff --git a/payloads/scripts/transforms/__snapshots__/transforms.test.ts.snap b/payloads/scripts/transforms/__snapshots__/transforms.test.ts.snap index 1a0711d4..d75ee639 100644 --- a/payloads/scripts/transforms/__snapshots__/transforms.test.ts.snap +++ b/payloads/scripts/transforms/__snapshots__/transforms.test.ts.snap @@ -6755,6 +6755,81 @@ exports[`chat-completions → anthropic > frequencyPenaltyParam > response 1`] = } `; +exports[`chat-completions → anthropic > functionToolsWithReasoningEffortParam > request 1`] = ` +{ + "max_tokens": 4096, + "messages": [ + { + "content": "Tokyo weather", + "role": "user", + }, + ], + "model": "claude-sonnet-4-5-20250929", + "thinking": { + "budget_tokens": 2048, + "type": "enabled", + }, + "tools": [ + { + "description": "Get weather", + "input_schema": { + "additionalProperties": false, + "properties": { + "location": { + "type": "string", + }, + }, + "required": [ + "location", + ], + "type": "object", + }, + "name": "get_weather", + "strict": true, + }, + ], +} +`; + +exports[`chat-completions → anthropic > functionToolsWithReasoningEffortParam > response 1`] = ` +{ + "choices": [ + { + "finish_reason": "tool_calls", + "index": 0, + "message": { + "annotations": [], + "reasoning": "The user is asking about the weather in Tokyo. I have a function called \`get_weather\` that takes a location as a parameter. I should call this function with "Tokyo" as the location.", + "reasoning_signature": "EvwCCmQIDRgCKkCJMREbp5e2miiIK0hFIPNRvo3++TX0UIQO458y2Uohnt66xyIE2vrHFRru0PGiuzpuOtupXgLk2Djqm0+dWdBsMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAEgxLhf9ngwOWnW8as9oaDDGyvqX4CzkkByZnjiIwWhb/nE7s3LIw89DrbLfaKD9ysU+7Emk0ri23bCBc+xqnzX2CT+J99W0/oCP5y40+KsUBjaxRWkJpOiG7mQgtPec39Q7YeLHo40W7YjTRaHxQEJ5icNagqZb1o8zFy1kfvtJzhfwglw+PykorFRyhK3zNy2i9gMFA9oTjHRHwWkMELMcBRJ2ihtE8uJYBfJIvLIznRMhrmJqlO3rs3RzqGzlqXaFXbLHxKEoJ2MhHemuMCu+PoGrUeE6DeMGn35KAM9323KgUuvKYDaJ9huexKfm6LjPn5M+4YSCQH/lBTb8zgD4Exu1WUzUFxAByXuOQ+2H4AihMT14YAQ==", + "role": "assistant", + "tool_calls": [ + { + "function": { + "arguments": "{"location":"Tokyo"}", + "name": "get_weather", + }, + "id": "toolu_018kcvffT3SCYkT8yiRyrnsk", + "type": "function", + }, + ], + }, + }, + ], + "created": 0, + "id": "chatcmpl-01BHYHXCTAGhXwEeG5n7iivg", + "model": "claude-sonnet-4-5-20250929", + "object": "chat.completion", + "usage": { + "completion_tokens": 102, + "prompt_tokens": 592, + "prompt_tokens_details": { + "cached_tokens": 0, + }, + "total_tokens": 694, + }, +} +`; + exports[`chat-completions → anthropic > imageUrlMimeTypeFallbackParam > request 1`] = ` { "max_tokens": 4096, @@ -9237,6 +9312,96 @@ exports[`chat-completions → google > frequencyPenaltyParam > response 1`] = ` } `; +exports[`chat-completions → google > functionToolsWithReasoningEffortParam > request 1`] = ` +{ + "contents": [ + { + "parts": [ + { + "text": "Tokyo weather", + }, + ], + "role": "user", + }, + ], + "generationConfig": { + "responseSchema": null, + "thinkingConfig": { + "includeThoughts": true, + "thinkingBudget": 2048, + }, + }, + "model": "gemini-2.5-flash", + "tools": [ + { + "functionDeclarations": [ + { + "description": "Get weather", + "name": "get_weather", + "parameters": null, + "parametersJsonSchema": { + "additionalProperties": false, + "properties": { + "location": { + "type": "string", + }, + }, + "required": [ + "location", + ], + "type": "object", + }, + "response": null, + }, + ], + }, + ], +} +`; + +exports[`chat-completions → google > functionToolsWithReasoningEffortParam > response 1`] = ` +{ + "choices": [ + { + "finish_reason": "tool_calls", + "index": 0, + "message": { + "annotations": [], + "reasoning": "Here's my thought process, summarized: + +**Analyzing the Request for Tokyo's Weather** + +Okay, so the user wants to know the weather in Tokyo. That's straightforward enough. I know I have a \`get_weather\` tool at my disposal, and that tool is designed specifically for retrieving weather information based on a given location. Therefore, my next logical step is to utilize this tool. I need to call \`get_weather\` with the input "Tokyo." That should give me the weather data the user is seeking. It's a simple request, and I can fulfill it directly using the appropriate function. +", + "role": "assistant", + "tool_calls": [ + { + "function": { + "arguments": "{"location":"Tokyo"}", + "name": "get_weather", + }, + "id": "call_0", + "type": "function", + }, + ], + }, + }, + ], + "created": 0, + "id": "chatcmpl-transformed", + "model": "gemini-2.5-flash", + "object": "chat.completion", + "usage": { + "completion_tokens": 61, + "completion_tokens_details": { + "reasoning_tokens": 46, + }, + "prompt_tokens": 37, + "total_tokens": 98, + }, +} +`; + exports[`chat-completions → google > imageUrlMimeTypeFallbackParam > request 1`] = ` { "contents": [ @@ -12030,6 +12195,90 @@ exports[`chat-completions → responses > frequencyPenaltyParam > response 1`] = } `; +exports[`chat-completions → responses > functionToolsWithReasoningEffortParam > request 1`] = ` +{ + "input": [ + { + "content": "Tokyo weather", + "role": "user", + }, + ], + "model": "gpt-5.4-mini", + "reasoning": { + "effort": "medium", + }, + "tools": [ + { + "description": "Get weather", + "name": "get_weather", + "parameters": { + "additionalProperties": false, + "properties": { + "location": { + "type": "string", + }, + }, + "required": [ + "location", + ], + "type": "object", + }, + "strict": true, + "type": "function", + }, + ], +} +`; + +exports[`chat-completions → responses > functionToolsWithReasoningEffortParam > response 1`] = ` +{ + "choices": [ + { + "finish_reason": "tool_calls", + "index": 0, + "message": { + "annotations": [], + "reasoning": "", + "role": "assistant", + }, + }, + { + "finish_reason": "tool_calls", + "index": 1, + "message": { + "annotations": [], + "role": "assistant", + "tool_calls": [ + { + "function": { + "arguments": "{"location":"Tokyo"}", + "name": "get_weather", + }, + "id": "call_2QJ5OBuYVgUhPoOTBBDkqeMZ", + "type": "function", + }, + ], + }, + }, + ], + "created": 0, + "id": "chatcmpl-084d44b731628f64006a021408541081938c4193815899068d", + "model": "gpt-5.4-mini-2026-03-17", + "object": "chat.completion", + "usage": { + "completion_tokens": 52, + "completion_tokens_details": { + "reasoning_tokens": 32, + }, + "prompt_tokens": 41, + "prompt_tokens_details": { + "cached_tokens": 0, + }, + "total_tokens": 93, + }, +} +`; + exports[`chat-completions → responses > imageUrlMimeTypeFallbackParam > request 1`] = ` { "input": [ @@ -22450,6 +22699,89 @@ The digits 0 and 1 dominate because they appear frequently in both hours (00-19) } `; +exports[`responses → anthropic > functionToolsWithReasoningEffortParam > request 1`] = ` +{ + "max_tokens": 4096, + "messages": [ + { + "content": "Tokyo weather", + "role": "user", + }, + ], + "model": "claude-sonnet-4-5-20250929", + "thinking": { + "budget_tokens": 2048, + "type": "enabled", + }, + "tools": [ + { + "description": "Get weather", + "input_schema": { + "additionalProperties": false, + "properties": { + "location": { + "type": "string", + }, + }, + "required": [ + "location", + ], + "type": "object", + }, + "name": "get_weather", + "strict": true, + }, + ], +} +`; + +exports[`responses → anthropic > functionToolsWithReasoningEffortParam > response 1`] = ` +{ + "created_at": 0, + "id": "resp_011P5UEbqb8vTBxTJ2g8NtFY", + "incomplete_details": null, + "model": "claude-sonnet-4-5-20250929", + "object": "response", + "output": [ + { + "encrypted_content": "EvcCCmQIDRgCKkCVjyd30PmtYqFiFoEY4TdUIerrZsZOObMOcrI5cNv2qcF0NKaa43ohCeNxKvr1XiXXYrbvUC2P4E1hzLIkcTbGMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAEgw7drsvHRMC++7Cjs4aDEnGp0pzLci6a6ENviIwnz3mHVENgTHTS7ltzPN4L4nJTqR+95VEV6+baqO81xwUaUfDe/oL6Y+DZObbyFbAKsAB+Dl9tjqGAfdO7Au3iARPqEBRwJsrOvcN2CkqXs7zqOFU4nLaGCNyXFVRNVoWpK/fFxxBTSMXgyq/s1vKFdv/MPRk8iREOXGrjK86eQE00QqedreBAPuEcVl58HuHuSbwSVNxv6tqfggpuNXNm13x+rbNZpC883vgqrrfoP3Oyrck5YV6TjOOndHQDUJV0MmkTn/7dLVt5Xug4UOe0Po2CInr5qLZMqk43A+WCBwn+DmucsUjYigKX+r3er1VRJt9GAE=", + "id": "msg_transformed_item_0", + "summary": [ + { + "text": "The user is asking about the weather in Tokyo. I have a function called "get_weather" that takes a location parameter. I should call this function with "Tokyo" as the location.", + "type": "summary_text", + }, + ], + "type": "reasoning", + }, + { + "arguments": "{"location":"Tokyo"}", + "call_id": "toolu_01MkMTYHdPrL7TZi2NCrLc9B", + "id": "msg_transformed_item_1", + "name": "get_weather", + "status": "completed", + "type": "function_call", + }, + ], + "output_text": "", + "parallel_tool_calls": false, + "status": "completed", + "tool_choice": "none", + "tools": [], + "usage": { + "input_tokens": 592, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 100, + "output_tokens_details": { + "reasoning_tokens": 0, + }, + "total_tokens": 692, + }, +} +`; + exports[`responses → anthropic > instructionsParam > request 1`] = ` { "max_tokens": 4096, @@ -25436,6 +25768,104 @@ Total for H2: } `; +exports[`responses → google > functionToolsWithReasoningEffortParam > request 1`] = ` +{ + "contents": [ + { + "parts": [ + { + "text": "Tokyo weather", + }, + ], + "role": "user", + }, + ], + "generationConfig": { + "responseSchema": null, + "thinkingConfig": { + "includeThoughts": true, + "thinkingBudget": 2048, + }, + }, + "model": "gemini-2.5-flash", + "tools": [ + { + "functionDeclarations": [ + { + "description": "Get weather", + "name": "get_weather", + "parameters": null, + "parametersJsonSchema": { + "additionalProperties": false, + "properties": { + "location": { + "type": "string", + }, + }, + "required": [ + "location", + ], + "type": "object", + }, + "response": null, + }, + ], + }, + ], +} +`; + +exports[`responses → google > functionToolsWithReasoningEffortParam > response 1`] = ` +{ + "created_at": 0, + "id": "resp_transformed", + "incomplete_details": null, + "model": "gemini-2.5-flash", + "object": "response", + "output": [ + { + "id": "msg_transformed_item_0", + "summary": [ + { + "text": "Here's my thought process, summarized as you requested: + +**Tokyo Weather Query Processing** + +Okay, so the user wants to know the weather in Tokyo. That's straightforward enough. The request is very clear – I need to determine the current weather conditions for the specified location. Luckily, I've got the \`get_weather\` tool at my disposal, which is specifically designed for retrieving weather information based on a location input. Therefore, my immediate course of action is obvious: I must call the \`get_weather\` tool. The location parameter for this tool is "Tokyo", so that will be the input I feed into it. It's a simple process, really - identify the core request, select the appropriate tool, and supply the necessary input. Let's get that data! +", + "type": "summary_text", + }, + ], + "type": "reasoning", + }, + { + "arguments": "{"location":"Tokyo"}", + "call_id": "call_0", + "id": "msg_transformed_item_1", + "name": "get_weather", + "status": "completed", + "type": "function_call", + }, + ], + "output_text": "", + "parallel_tool_calls": false, + "status": "completed", + "tool_choice": "none", + "tools": [], + "usage": { + "input_tokens": 37, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 61, + "output_tokens_details": { + "reasoning_tokens": 46, + }, + "total_tokens": 98, + }, +} +`; + exports[`responses → google > instructionsParam > request 1`] = ` { "contents": [ diff --git a/payloads/snapshots/functionToolsWithReasoningEffortParam/chat-completions/error.json b/payloads/snapshots/functionToolsWithReasoningEffortParam/chat-completions/error.json new file mode 100644 index 00000000..cd6e96fb --- /dev/null +++ b/payloads/snapshots/functionToolsWithReasoningEffortParam/chat-completions/error.json @@ -0,0 +1,3 @@ +{ + "error": "Error: 400 Function tools with reasoning_effort are not supported for gpt-5.4-mini in /v1/chat/completions. Please use /v1/responses instead." +} \ No newline at end of file diff --git a/payloads/snapshots/functionToolsWithReasoningEffortParam/chat-completions/request.json b/payloads/snapshots/functionToolsWithReasoningEffortParam/chat-completions/request.json new file mode 100644 index 00000000..fd39ef96 --- /dev/null +++ b/payloads/snapshots/functionToolsWithReasoningEffortParam/chat-completions/request.json @@ -0,0 +1,32 @@ +{ + "model": "gpt-5.4-mini", + "messages": [ + { + "role": "user", + "content": "Tokyo weather" + } + ], + "reasoning_effort": "medium", + "tools": [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get weather", + "strict": true, + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + } + } + } + ] +} \ No newline at end of file diff --git a/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/followup-request.json b/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/followup-request.json new file mode 100644 index 00000000..3a533c5b --- /dev/null +++ b/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/followup-request.json @@ -0,0 +1,50 @@ +{ + "model": "gpt-5.4-mini", + "input": [ + { + "role": "user", + "content": "Tokyo weather" + }, + { + "id": "rs_01f5db785982cb14006a02140598e48190ab82bcd53f938e25", + "type": "reasoning", + "summary": [] + }, + { + "id": "fc_01f5db785982cb14006a021405c51081908e7377d96b6d12f5", + "type": "function_call", + "status": "completed", + "arguments": "{\"location\":\"Tokyo\"}", + "call_id": "call_qFboATm7JVJj35UQz8WAACxZ", + "name": "get_weather" + }, + { + "type": "function_call_output", + "call_id": "call_qFboATm7JVJj35UQz8WAACxZ", + "output": "71 degrees" + } + ], + "reasoning": { + "effort": "medium" + }, + "tools": [ + { + "type": "function", + "name": "get_weather", + "description": "Get weather", + "strict": true, + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + } + } + ] +} \ No newline at end of file diff --git a/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/followup-response-streaming.json b/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/followup-response-streaming.json new file mode 100644 index 00000000..3458bc14 --- /dev/null +++ b/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/followup-response-streaming.json @@ -0,0 +1,367 @@ +[ + { + "type": "response.created", + "response": { + "id": "resp_01f5db785982cb14006a0214068b488190ac14a93c8b79984d", + "object": "response", + "created_at": 1778521094, + "status": "in_progress", + "background": false, + "completed_at": null, + "error": null, + "frequency_penalty": 0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-5.4-mini-2026-03-17", + "moderation": null, + "output": [], + "parallel_tool_calls": true, + "presence_penalty": 0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": "in_memory", + "reasoning": { + "effort": "medium", + "summary": null + }, + "safety_identifier": null, + "service_tier": "auto", + "store": true, + "temperature": 1, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get weather", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 0.98, + "truncation": "disabled", + "usage": null, + "user": null, + "metadata": {} + }, + "sequence_number": 0 + }, + { + "type": "response.in_progress", + "response": { + "id": "resp_01f5db785982cb14006a0214068b488190ac14a93c8b79984d", + "object": "response", + "created_at": 1778521094, + "status": "in_progress", + "background": false, + "completed_at": null, + "error": null, + "frequency_penalty": 0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-5.4-mini-2026-03-17", + "moderation": null, + "output": [], + "parallel_tool_calls": true, + "presence_penalty": 0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": "in_memory", + "reasoning": { + "effort": "medium", + "summary": null + }, + "safety_identifier": null, + "service_tier": "auto", + "store": true, + "temperature": 1, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get weather", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 0.98, + "truncation": "disabled", + "usage": null, + "user": null, + "metadata": {} + }, + "sequence_number": 1 + }, + { + "type": "response.output_item.added", + "item": { + "id": "msg_01f5db785982cb14006a021407488081908d7f60947e3de58a", + "type": "message", + "status": "in_progress", + "content": [], + "phase": "final_answer", + "role": "assistant" + }, + "output_index": 0, + "sequence_number": 2 + }, + { + "type": "response.content_part.added", + "content_index": 0, + "item_id": "msg_01f5db785982cb14006a021407488081908d7f60947e3de58a", + "output_index": 0, + "part": { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "" + }, + "sequence_number": 3 + }, + { + "type": "response.output_text.delta", + "content_index": 0, + "delta": "Tokyo", + "item_id": "msg_01f5db785982cb14006a021407488081908d7f60947e3de58a", + "logprobs": [], + "obfuscation": "R26ypQV9HzK", + "output_index": 0, + "sequence_number": 4 + }, + { + "type": "response.output_text.delta", + "content_index": 0, + "delta": " weather", + "item_id": "msg_01f5db785982cb14006a021407488081908d7f60947e3de58a", + "logprobs": [], + "obfuscation": "TCqDbYNM", + "output_index": 0, + "sequence_number": 5 + }, + { + "type": "response.output_text.delta", + "content_index": 0, + "delta": ":", + "item_id": "msg_01f5db785982cb14006a021407488081908d7f60947e3de58a", + "logprobs": [], + "obfuscation": "i1DhcZfxeLBkWPO", + "output_index": 0, + "sequence_number": 6 + }, + { + "type": "response.output_text.delta", + "content_index": 0, + "delta": " ", + "item_id": "msg_01f5db785982cb14006a021407488081908d7f60947e3de58a", + "logprobs": [], + "obfuscation": "1JxrjJB0pLv0zmx", + "output_index": 0, + "sequence_number": 7 + }, + { + "type": "response.output_text.delta", + "content_index": 0, + "delta": "71", + "item_id": "msg_01f5db785982cb14006a021407488081908d7f60947e3de58a", + "logprobs": [], + "obfuscation": "MdTjGLeyqVg2TO", + "output_index": 0, + "sequence_number": 8 + }, + { + "type": "response.output_text.delta", + "content_index": 0, + "delta": " degrees", + "item_id": "msg_01f5db785982cb14006a021407488081908d7f60947e3de58a", + "logprobs": [], + "obfuscation": "E3QKYgFu", + "output_index": 0, + "sequence_number": 9 + }, + { + "type": "response.output_text.delta", + "content_index": 0, + "delta": ".", + "item_id": "msg_01f5db785982cb14006a021407488081908d7f60947e3de58a", + "logprobs": [], + "obfuscation": "mloIFpEBySPASGS", + "output_index": 0, + "sequence_number": 10 + }, + { + "type": "response.output_text.done", + "content_index": 0, + "item_id": "msg_01f5db785982cb14006a021407488081908d7f60947e3de58a", + "logprobs": [], + "output_index": 0, + "sequence_number": 11, + "text": "Tokyo weather: 71 degrees." + }, + { + "type": "response.content_part.done", + "content_index": 0, + "item_id": "msg_01f5db785982cb14006a021407488081908d7f60947e3de58a", + "output_index": 0, + "part": { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Tokyo weather: 71 degrees." + }, + "sequence_number": 12 + }, + { + "type": "response.output_item.done", + "item": { + "id": "msg_01f5db785982cb14006a021407488081908d7f60947e3de58a", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Tokyo weather: 71 degrees." + } + ], + "phase": "final_answer", + "role": "assistant" + }, + "output_index": 0, + "sequence_number": 13 + }, + { + "type": "response.completed", + "response": { + "id": "resp_01f5db785982cb14006a0214068b488190ac14a93c8b79984d", + "object": "response", + "created_at": 1778521094, + "status": "completed", + "background": false, + "completed_at": 1778521095, + "error": null, + "frequency_penalty": 0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-5.4-mini-2026-03-17", + "moderation": null, + "output": [ + { + "id": "msg_01f5db785982cb14006a021407488081908d7f60947e3de58a", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Tokyo weather: 71 degrees." + } + ], + "phase": "final_answer", + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": "in_memory", + "reasoning": { + "effort": "medium", + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get weather", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 0.98, + "truncation": "disabled", + "usage": { + "input_tokens": 96, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 11, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 107 + }, + "user": null, + "metadata": {} + }, + "sequence_number": 14 + } +] \ No newline at end of file diff --git a/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/followup-response.json b/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/followup-response.json new file mode 100644 index 00000000..e7e94fc6 --- /dev/null +++ b/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/followup-response.json @@ -0,0 +1,93 @@ +{ + "id": "resp_01f5db785982cb14006a02140688c88190bbd0e1d18e8a4387", + "object": "response", + "created_at": 1778521094, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1778521095, + "error": null, + "frequency_penalty": 0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-5.4-mini-2026-03-17", + "moderation": null, + "output": [ + { + "id": "msg_01f5db785982cb14006a021406fa34819097f3f5c7d271596e", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Tokyo weather: 71 degrees." + } + ], + "phase": "final_answer", + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": "in_memory", + "reasoning": { + "effort": "medium", + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get weather", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 0.98, + "truncation": "disabled", + "usage": { + "input_tokens": 96, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 11, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 107 + }, + "user": null, + "metadata": {}, + "output_text": "Tokyo weather: 71 degrees." +} \ No newline at end of file diff --git a/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/request.json b/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/request.json new file mode 100644 index 00000000..bbd51b07 --- /dev/null +++ b/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/request.json @@ -0,0 +1,32 @@ +{ + "model": "gpt-5.4-mini", + "input": [ + { + "role": "user", + "content": "Tokyo weather" + } + ], + "reasoning": { + "effort": "medium" + }, + "tools": [ + { + "type": "function", + "name": "get_weather", + "description": "Get weather", + "strict": true, + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + } + } + ] +} \ No newline at end of file diff --git a/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/response-streaming.json b/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/response-streaming.json new file mode 100644 index 00000000..294decbd --- /dev/null +++ b/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/response-streaming.json @@ -0,0 +1,320 @@ +[ + { + "type": "response.created", + "response": { + "id": "resp_0daca400c2cf33c9006a0214043fd88190b76876d3e6446e85", + "object": "response", + "created_at": 1778521092, + "status": "in_progress", + "background": false, + "completed_at": null, + "error": null, + "frequency_penalty": 0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-5.4-mini-2026-03-17", + "moderation": null, + "output": [], + "parallel_tool_calls": true, + "presence_penalty": 0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": "in_memory", + "reasoning": { + "effort": "medium", + "summary": null + }, + "safety_identifier": null, + "service_tier": "auto", + "store": true, + "temperature": 1, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get weather", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 0.98, + "truncation": "disabled", + "usage": null, + "user": null, + "metadata": {} + }, + "sequence_number": 0 + }, + { + "type": "response.in_progress", + "response": { + "id": "resp_0daca400c2cf33c9006a0214043fd88190b76876d3e6446e85", + "object": "response", + "created_at": 1778521092, + "status": "in_progress", + "background": false, + "completed_at": null, + "error": null, + "frequency_penalty": 0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-5.4-mini-2026-03-17", + "moderation": null, + "output": [], + "parallel_tool_calls": true, + "presence_penalty": 0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": "in_memory", + "reasoning": { + "effort": "medium", + "summary": null + }, + "safety_identifier": null, + "service_tier": "auto", + "store": true, + "temperature": 1, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get weather", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 0.98, + "truncation": "disabled", + "usage": null, + "user": null, + "metadata": {} + }, + "sequence_number": 1 + }, + { + "type": "response.output_item.added", + "item": { + "id": "rs_0daca400c2cf33c9006a021404d910819081e56428ba32ac4c", + "type": "reasoning", + "summary": [] + }, + "output_index": 0, + "sequence_number": 2 + }, + { + "type": "response.output_item.done", + "item": { + "id": "rs_0daca400c2cf33c9006a021404d910819081e56428ba32ac4c", + "type": "reasoning", + "summary": [] + }, + "output_index": 0, + "sequence_number": 3 + }, + { + "type": "response.output_item.added", + "item": { + "id": "fc_0daca400c2cf33c9006a02140504208190af3987d8afa0b90f", + "type": "function_call", + "status": "in_progress", + "arguments": "", + "call_id": "call_ahg2yVSnxIlU0OsPkAmndF76", + "name": "get_weather" + }, + "output_index": 1, + "sequence_number": 4 + }, + { + "type": "response.function_call_arguments.delta", + "delta": "{\"", + "item_id": "fc_0daca400c2cf33c9006a02140504208190af3987d8afa0b90f", + "obfuscation": "IivxY6PlKcuvAx", + "output_index": 1, + "sequence_number": 5 + }, + { + "type": "response.function_call_arguments.delta", + "delta": "location", + "item_id": "fc_0daca400c2cf33c9006a02140504208190af3987d8afa0b90f", + "obfuscation": "8N4cHFwq", + "output_index": 1, + "sequence_number": 6 + }, + { + "type": "response.function_call_arguments.delta", + "delta": "\":\"", + "item_id": "fc_0daca400c2cf33c9006a02140504208190af3987d8afa0b90f", + "obfuscation": "d7qqtALaunjkA", + "output_index": 1, + "sequence_number": 7 + }, + { + "type": "response.function_call_arguments.delta", + "delta": "Tokyo", + "item_id": "fc_0daca400c2cf33c9006a02140504208190af3987d8afa0b90f", + "obfuscation": "rjNt9RPoygB", + "output_index": 1, + "sequence_number": 8 + }, + { + "type": "response.function_call_arguments.delta", + "delta": "\"}", + "item_id": "fc_0daca400c2cf33c9006a02140504208190af3987d8afa0b90f", + "obfuscation": "73FO0a7q3DZqfn", + "output_index": 1, + "sequence_number": 9 + }, + { + "type": "response.function_call_arguments.done", + "arguments": "{\"location\":\"Tokyo\"}", + "item_id": "fc_0daca400c2cf33c9006a02140504208190af3987d8afa0b90f", + "output_index": 1, + "sequence_number": 10 + }, + { + "type": "response.output_item.done", + "item": { + "id": "fc_0daca400c2cf33c9006a02140504208190af3987d8afa0b90f", + "type": "function_call", + "status": "completed", + "arguments": "{\"location\":\"Tokyo\"}", + "call_id": "call_ahg2yVSnxIlU0OsPkAmndF76", + "name": "get_weather" + }, + "output_index": 1, + "sequence_number": 11 + }, + { + "type": "response.completed", + "response": { + "id": "resp_0daca400c2cf33c9006a0214043fd88190b76876d3e6446e85", + "object": "response", + "created_at": 1778521092, + "status": "completed", + "background": false, + "completed_at": 1778521093, + "error": null, + "frequency_penalty": 0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-5.4-mini-2026-03-17", + "moderation": null, + "output": [ + { + "id": "rs_0daca400c2cf33c9006a021404d910819081e56428ba32ac4c", + "type": "reasoning", + "summary": [] + }, + { + "id": "fc_0daca400c2cf33c9006a02140504208190af3987d8afa0b90f", + "type": "function_call", + "status": "completed", + "arguments": "{\"location\":\"Tokyo\"}", + "call_id": "call_ahg2yVSnxIlU0OsPkAmndF76", + "name": "get_weather" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": "in_memory", + "reasoning": { + "effort": "medium", + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get weather", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 0.98, + "truncation": "disabled", + "usage": { + "input_tokens": 41, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 43, + "output_tokens_details": { + "reasoning_tokens": 23 + }, + "total_tokens": 84 + }, + "user": null, + "metadata": {} + }, + "sequence_number": 12 + } +] \ No newline at end of file diff --git a/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/response.json b/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/response.json new file mode 100644 index 00000000..1fd4bb7f --- /dev/null +++ b/payloads/snapshots/functionToolsWithReasoningEffortParam/responses/response.json @@ -0,0 +1,91 @@ +{ + "id": "resp_01f5db785982cb14006a0214049d9081908a4f084e46110e92", + "object": "response", + "created_at": 1778521092, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1778521093, + "error": null, + "frequency_penalty": 0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-5.4-mini-2026-03-17", + "moderation": null, + "output": [ + { + "id": "rs_01f5db785982cb14006a02140598e48190ab82bcd53f938e25", + "type": "reasoning", + "summary": [] + }, + { + "id": "fc_01f5db785982cb14006a021405c51081908e7377d96b6d12f5", + "type": "function_call", + "status": "completed", + "arguments": "{\"location\":\"Tokyo\"}", + "call_id": "call_qFboATm7JVJj35UQz8WAACxZ", + "name": "get_weather" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": "in_memory", + "reasoning": { + "effort": "medium", + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get weather", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 0.98, + "truncation": "disabled", + "usage": { + "input_tokens": 41, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 42, + "output_tokens_details": { + "reasoning_tokens": 22 + }, + "total_tokens": 83 + }, + "user": null, + "metadata": {}, + "output_text": "" +} \ No newline at end of file diff --git a/payloads/transforms/chat-completions_to_anthropic/functionToolsWithReasoningEffortParam.json b/payloads/transforms/chat-completions_to_anthropic/functionToolsWithReasoningEffortParam.json new file mode 100644 index 00000000..c39a4f6a --- /dev/null +++ b/payloads/transforms/chat-completions_to_anthropic/functionToolsWithReasoningEffortParam.json @@ -0,0 +1,39 @@ +{ + "model": "claude-sonnet-4-5-20250929", + "id": "msg_01BHYHXCTAGhXwEeG5n7iivg", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "thinking", + "thinking": "The user is asking about the weather in Tokyo. I have a function called `get_weather` that takes a location as a parameter. I should call this function with \"Tokyo\" as the location.", + "signature": "EvwCCmQIDRgCKkCJMREbp5e2miiIK0hFIPNRvo3++TX0UIQO458y2Uohnt66xyIE2vrHFRru0PGiuzpuOtupXgLk2Djqm0+dWdBsMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAEgxLhf9ngwOWnW8as9oaDDGyvqX4CzkkByZnjiIwWhb/nE7s3LIw89DrbLfaKD9ysU+7Emk0ri23bCBc+xqnzX2CT+J99W0/oCP5y40+KsUBjaxRWkJpOiG7mQgtPec39Q7YeLHo40W7YjTRaHxQEJ5icNagqZb1o8zFy1kfvtJzhfwglw+PykorFRyhK3zNy2i9gMFA9oTjHRHwWkMELMcBRJ2ihtE8uJYBfJIvLIznRMhrmJqlO3rs3RzqGzlqXaFXbLHxKEoJ2MhHemuMCu+PoGrUeE6DeMGn35KAM9323KgUuvKYDaJ9huexKfm6LjPn5M+4YSCQH/lBTb8zgD4Exu1WUzUFxAByXuOQ+2H4AihMT14YAQ==" + }, + { + "type": "tool_use", + "id": "toolu_018kcvffT3SCYkT8yiRyrnsk", + "name": "get_weather", + "input": { + "location": "Tokyo" + }, + "caller": { + "type": "direct" + } + } + ], + "stop_reason": "tool_use", + "stop_sequence": null, + "stop_details": null, + "usage": { + "input_tokens": 592, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0 + }, + "output_tokens": 102, + "service_tier": "standard", + "inference_geo": "not_available" + } +} \ No newline at end of file diff --git a/payloads/transforms/chat-completions_to_google/functionToolsWithReasoningEffortParam.json b/payloads/transforms/chat-completions_to_google/functionToolsWithReasoningEffortParam.json new file mode 100644 index 00000000..4b19f62e --- /dev/null +++ b/payloads/transforms/chat-completions_to_google/functionToolsWithReasoningEffortParam.json @@ -0,0 +1,42 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "Here's my thought process, summarized:\n\n**Analyzing the Request for Tokyo's Weather**\n\nOkay, so the user wants to know the weather in Tokyo. That's straightforward enough. I know I have a `get_weather` tool at my disposal, and that tool is designed specifically for retrieving weather information based on a given location. Therefore, my next logical step is to utilize this tool. I need to call `get_weather` with the input \"Tokyo.\" That should give me the weather data the user is seeking. It's a simple request, and I can fulfill it directly using the appropriate function.\n", + "thought": true + }, + { + "functionCall": { + "name": "get_weather", + "args": { + "location": "Tokyo" + } + }, + "thoughtSignature": "CucBAQw51sf3ly4/NPgf7Ek2GW7TFKExYspWKxo1FDh0SEd3bhvkBUsqj/hKTP/t5K52qzd+94aqzwD0qEtQmndHMjheAavUlU8wNTaJlyMxYPLgyr6njdSASQT19OGN6CvTdhikTvvHCeuYoQ5cLTTX/cQmaU35dmDsq+jQww4KejnIzuiA0S//0DXztUy5K+uiGDu/ghNOOePgNJbKNbGGChP7qnCbSgi5lb3GRIJBRhQPbiRHG0Hh6gVrWWHw4jOqBcDOZaYgSslwDeWZQZebPhxPMlDq4GUcR7l+0Kjh38Aoctmx4RVn" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "finishMessage": "Model generated function call(s)." + } + ], + "usageMetadata": { + "promptTokenCount": 37, + "candidatesTokenCount": 15, + "totalTokenCount": 98, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 37 + } + ], + "thoughtsTokenCount": 46, + "serviceTier": "standard" + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "CBQCatimEsHg_uMP7JfvwQ8" +} \ No newline at end of file diff --git a/payloads/transforms/chat-completions_to_responses/functionToolsWithReasoningEffortParam.json b/payloads/transforms/chat-completions_to_responses/functionToolsWithReasoningEffortParam.json new file mode 100644 index 00000000..c002d99b --- /dev/null +++ b/payloads/transforms/chat-completions_to_responses/functionToolsWithReasoningEffortParam.json @@ -0,0 +1,91 @@ +{ + "id": "resp_084d44b731628f64006a021408541081938c4193815899068d", + "object": "response", + "created_at": 1778521096, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1778521097, + "error": null, + "frequency_penalty": 0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-5.4-mini-2026-03-17", + "moderation": null, + "output": [ + { + "id": "rs_084d44b731628f64006a021408c9048193b74f0cd2345c2e6f", + "type": "reasoning", + "summary": [] + }, + { + "id": "fc_084d44b731628f64006a02140982dc819397d1aa5e6d85082d", + "type": "function_call", + "status": "completed", + "arguments": "{\"location\":\"Tokyo\"}", + "call_id": "call_2QJ5OBuYVgUhPoOTBBDkqeMZ", + "name": "get_weather" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": "in_memory", + "reasoning": { + "effort": "medium", + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get weather", + "name": "get_weather", + "parameters": { + "additionalProperties": false, + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "type": "object" + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 0.98, + "truncation": "disabled", + "usage": { + "input_tokens": 41, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 52, + "output_tokens_details": { + "reasoning_tokens": 32 + }, + "total_tokens": 93 + }, + "user": null, + "metadata": {}, + "output_text": "" +} \ No newline at end of file diff --git a/payloads/transforms/responses_to_anthropic/functionToolsWithReasoningEffortParam.json b/payloads/transforms/responses_to_anthropic/functionToolsWithReasoningEffortParam.json new file mode 100644 index 00000000..1ab7d9bb --- /dev/null +++ b/payloads/transforms/responses_to_anthropic/functionToolsWithReasoningEffortParam.json @@ -0,0 +1,39 @@ +{ + "model": "claude-sonnet-4-5-20250929", + "id": "msg_011P5UEbqb8vTBxTJ2g8NtFY", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "thinking", + "thinking": "The user is asking about the weather in Tokyo. I have a function called \"get_weather\" that takes a location parameter. I should call this function with \"Tokyo\" as the location.", + "signature": "EvcCCmQIDRgCKkCVjyd30PmtYqFiFoEY4TdUIerrZsZOObMOcrI5cNv2qcF0NKaa43ohCeNxKvr1XiXXYrbvUC2P4E1hzLIkcTbGMhpjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOTgAEgw7drsvHRMC++7Cjs4aDEnGp0pzLci6a6ENviIwnz3mHVENgTHTS7ltzPN4L4nJTqR+95VEV6+baqO81xwUaUfDe/oL6Y+DZObbyFbAKsAB+Dl9tjqGAfdO7Au3iARPqEBRwJsrOvcN2CkqXs7zqOFU4nLaGCNyXFVRNVoWpK/fFxxBTSMXgyq/s1vKFdv/MPRk8iREOXGrjK86eQE00QqedreBAPuEcVl58HuHuSbwSVNxv6tqfggpuNXNm13x+rbNZpC883vgqrrfoP3Oyrck5YV6TjOOndHQDUJV0MmkTn/7dLVt5Xug4UOe0Po2CInr5qLZMqk43A+WCBwn+DmucsUjYigKX+r3er1VRJt9GAE=" + }, + { + "type": "tool_use", + "id": "toolu_01MkMTYHdPrL7TZi2NCrLc9B", + "name": "get_weather", + "input": { + "location": "Tokyo" + }, + "caller": { + "type": "direct" + } + } + ], + "stop_reason": "tool_use", + "stop_sequence": null, + "stop_details": null, + "usage": { + "input_tokens": 592, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0 + }, + "output_tokens": 100, + "service_tier": "standard", + "inference_geo": "not_available" + } +} \ No newline at end of file diff --git a/payloads/transforms/responses_to_google/functionToolsWithReasoningEffortParam.json b/payloads/transforms/responses_to_google/functionToolsWithReasoningEffortParam.json new file mode 100644 index 00000000..46ee44b4 --- /dev/null +++ b/payloads/transforms/responses_to_google/functionToolsWithReasoningEffortParam.json @@ -0,0 +1,42 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "Here's my thought process, summarized as you requested:\n\n**Tokyo Weather Query Processing**\n\nOkay, so the user wants to know the weather in Tokyo. That's straightforward enough. The request is very clear – I need to determine the current weather conditions for the specified location. Luckily, I've got the `get_weather` tool at my disposal, which is specifically designed for retrieving weather information based on a location input. Therefore, my immediate course of action is obvious: I must call the `get_weather` tool. The location parameter for this tool is \"Tokyo\", so that will be the input I feed into it. It's a simple process, really - identify the core request, select the appropriate tool, and supply the necessary input. Let's get that data!\n", + "thought": true + }, + { + "functionCall": { + "name": "get_weather", + "args": { + "location": "Tokyo" + } + }, + "thoughtSignature": "CucBAQw51sfI3nF6dvNsGQZLXNucpXA50nmkpy3jUKfEgbwVQRyKTTmwwvw3L+JTcCOemo09d1TGbXMIJ5iFc5vdcmuh0hx5rvHiBzrt/mp99kFug2WOsqaj+tBrSP/gma+Znqxgg1MOPtQogszqGOEob55sUaDw258D9wni8Sh4DkHUeDCeSDi32icivpdwrWamBUDjoHUb+A98VNeGT89ErdIWMRphp8N9esX7Ghm6HQwdXCPzIww09BJ7TuU3oLA2L9NFGAqKxQyzMqhfNakwVhvV4jHVD6sjEWmBSg6xgwbUItfiYSAO" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "finishMessage": "Model generated function call(s)." + } + ], + "usageMetadata": { + "promptTokenCount": 37, + "candidatesTokenCount": 15, + "totalTokenCount": 98, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 37 + } + ], + "thoughtsTokenCount": 46, + "serviceTier": "standard" + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "CBQCavzID8fR_uMP_JaW8QM" +} \ No newline at end of file From 7f63111f106601d4296fecafd63cd58a5bff63d0 Mon Sep 17 00:00:00 2001 From: Erin McNulty Date: Mon, 11 May 2026 14:31:42 -0400 Subject: [PATCH 2/2] fix type checks --- crates/lingua/src/providers/openai/convert.rs | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/crates/lingua/src/providers/openai/convert.rs b/crates/lingua/src/providers/openai/convert.rs index c93faeb3..a98eb964 100644 --- a/crates/lingua/src/providers/openai/convert.rs +++ b/crates/lingua/src/providers/openai/convert.rs @@ -2661,36 +2661,32 @@ impl TryFromLLM> for Vec { &mut id_used, &id, ); - // Extract annotations and logprobs from provider_options. - // Default annotations to Some(vec![]) so the Responses API - // output always has the required `annotations` array. - let (annotations, logprobs, phase) = if let Some(ref opts) = - text_part.provider_options - { - let annotations = - opts.options.get("annotations").and_then(|v| { - serde_json::from_value::>( - v.clone(), + // Extract annotations, logprobs, and phase from + // provider_options using a typed struct so we avoid raw + // Value field access. Default annotations to Some(vec![]) + // so the Responses API output always has the required array. + #[derive(serde::Deserialize, Default)] + struct TextPartProviderOpts { + annotations: Option>, + logprobs: Option>, + #[serde(rename = "_output_item_phase")] + phase: Option, + } + let (annotations, logprobs, phase) = + if let Some(ref opts) = text_part.provider_options { + let parsed = + serde_json::from_value::( + serde_json::Value::Object(opts.options.clone()), ) - .ok() - }); - let logprobs = opts.options.get("logprobs").and_then(|v| { - serde_json::from_value::>( - v.clone(), + .unwrap_or_default(); + ( + parsed.annotations.or(Some(vec![])), + parsed.logprobs, + parsed.phase, ) - .ok() - }); - let phase = - opts.options.get("_output_item_phase").and_then(|v| { - serde_json::from_value::( - v.clone(), - ) - .ok() - }); - (annotations.or(Some(vec![])), logprobs, phase) - } else { - (Some(vec![]), None, None) - }; + } else { + (Some(vec![]), None, None) + }; result.push(openai::OutputItem { output_item_type: Some(openai::OutputItemType::Message), role: Some(openai::MessageRole::Assistant),