diff --git a/crates/coverage-report/src/requests_expected_differences.json b/crates/coverage-report/src/requests_expected_differences.json
index e7d3f8ad..ef451acd 100644
--- a/crates/coverage-report/src/requests_expected_differences.json
+++ b/crates/coverage-report/src/requests_expected_differences.json
@@ -654,6 +654,28 @@
"skip": true,
"reason": "Google code_execution is provider-specific and is not a lossless equivalent of Anthropic bash"
},
+ {
+ "testCase": "codeInterpreterToolParam",
+ "source": "Google",
+ "target": "ChatCompletions",
+ "fields": [
+ { "pattern": "params.tools", "reason": "Google code_execution has no OpenAI Chat Completions equivalent and is dropped" },
+ { "pattern": "messages[*].content.length", "reason": "Google executableCode/codeExecutionResult followup content does not roundtrip through Chat Completions" }
+ ]
+ },
+ {
+ "testCase": "codeInterpreterToolParam",
+ "source": "Google",
+ "target": "Responses",
+ "fields": [
+ { "pattern": "params.tools[*].builtin_type", "reason": "Google code_execution is canonicalized to the OpenAI Responses code_interpreter builtin type" },
+ { "pattern": "params.tools[*].name", "reason": "Google code_execution is canonicalized to the OpenAI Responses code_interpreter builtin name" },
+ { "pattern": "params.tools[*].provider", "reason": "Google code_execution is canonicalized from a Google builtin to a Responses builtin" },
+ { "pattern": "params.tools[*].config.type", "reason": "Google code_execution is normalized to OpenAI code_interpreter config" },
+ { "pattern": "params.tools[*].config.container", "reason": "Google code_execution is normalized to OpenAI code_interpreter container config" },
+ { "pattern": "messages[*].content[*].provider_options", "reason": "Google code execution metadata is not losslessly representable through Responses message content" }
+ ]
+ },
{
"testCase": "toolChoiceRequiredWithReasoningParam",
"source": "Google",
diff --git a/crates/lingua/src/processing/transform.rs b/crates/lingua/src/processing/transform.rs
index d50f8753..51a23ecd 100644
--- a/crates/lingua/src/processing/transform.rs
+++ b/crates/lingua/src/processing/transform.rs
@@ -758,6 +758,70 @@ mod tests {
assert!(output.get("top_p").is_none(), "Should not have top_p");
}
+ #[test]
+ #[cfg(all(feature = "openai", feature = "google"))]
+ fn test_google_code_execution_maps_to_responses_code_interpreter() {
+ let payload = json!({
+ "contents": [{
+ "role": "user",
+ "parts": [{"text": "Execute Python code to generate a random number"}]
+ }],
+ "tools": [{
+ "codeExecution": {}
+ }]
+ });
+ let input = to_bytes(&payload);
+
+ let result =
+ transform_request(input, ProviderFormat::Responses, Some("gpt-5-nano")).unwrap();
+
+ assert!(!result.is_passthrough());
+ assert_eq!(result.source_format(), Some(ProviderFormat::Google));
+
+ let output: Value = crate::serde_json::from_slice(result.as_bytes()).unwrap();
+ assert_eq!(output.get("model").unwrap().as_str().unwrap(), "gpt-5-nano");
+ assert_eq!(
+ output.get("tools"),
+ Some(&json!([
+ {
+ "type": "code_interpreter",
+ "container": {
+ "type": "auto"
+ }
+ }
+ ])),
+ "Google codeExecution should map to Responses code_interpreter"
+ );
+ }
+
+ #[test]
+ #[cfg(all(feature = "openai", feature = "google"))]
+ fn test_google_code_execution_is_stripped_for_chat_requests() {
+ let payload = json!({
+ "contents": [{
+ "role": "user",
+ "parts": [{"text": "Execute Python code to generate a random number"}]
+ }],
+ "tools": [{
+ "codeExecution": {}
+ }]
+ });
+ let input = to_bytes(&payload);
+
+ let result =
+ transform_request(input, ProviderFormat::ChatCompletions, Some("gpt-5-nano")).unwrap();
+
+ assert!(!result.is_passthrough());
+ assert_eq!(result.source_format(), Some(ProviderFormat::Google));
+
+ let output: Value = crate::serde_json::from_slice(result.as_bytes()).unwrap();
+ assert_eq!(output.get("model").unwrap().as_str().unwrap(), "gpt-5-nano");
+ assert!(
+ output.get("tools").is_none(),
+ "Google codeExecution should be stripped for Chat Completions"
+ );
+ }
+
#[test]
#[cfg(feature = "openai")]
fn test_non_reasoning_model_still_passthroughs() {
diff --git a/crates/lingua/src/universal/tools.rs b/crates/lingua/src/universal/tools.rs
index 2a9120c5..c9d84f6c 100644
--- a/crates/lingua/src/universal/tools.rs
+++ b/crates/lingua/src/universal/tools.rs
@@ -35,8 +35,8 @@ use crate::providers::anthropic::generated::{
};
use crate::providers::google::generated::GoogleSearch;
use crate::providers::openai::generated::{
- ApproximateLocation, Tool as OpenAIResponsesTool, UserLocationType as OpenAIUserLocationType,
- WebSearchTool,
+ ApproximateLocation, CodeInterpreterTool, Tool as OpenAIResponsesTool,
+ UserLocationType as OpenAIUserLocationType, WebSearchTool,
};
use crate::serde_json::{self, json, Map, Value};
@@ -526,6 +526,28 @@ impl UniversalTool {
error: e.to_string(),
})
}
+ BuiltinToolProvider::Google if builtin_type == "code_execution" => {
+ let _google_config =
+ config
+ .clone()
+ .ok_or_else(|| ConvertError::UnsupportedToolType {
+ tool_name: self.name.clone(),
+ tool_type: builtin_type.clone(),
+ target_provider: ProviderFormat::Responses,
+ })?;
+
+ let tool = OpenAIResponsesTool::CodeInterpreter(CodeInterpreterTool {
+ container: json!({ "type": "auto" }),
+ });
+
+ serde_json::to_value(tool).map_err(|e| ConvertError::JsonSerializationFailed {
+ field: format!(
+ "OpenAI responses Google code execution tool conversion for '{}'",
+ self.name
+ ),
+ error: e.to_string(),
+ })
+ }
BuiltinToolProvider::Anthropic
| BuiltinToolProvider::Converse
| BuiltinToolProvider::Google => Err(ConvertError::UnsupportedToolType {
@@ -558,7 +580,9 @@ pub fn tools_to_openai_chat_value(tools: &[UniversalTool]) -> Result