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
21 changes: 1 addition & 20 deletions src/crates/ai-adapters/src/providers/openai/codex_chatgpt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,6 @@ pub(crate) fn is_codex_chatgpt_endpoint(request_url: &str) -> bool {
request_url.contains("chatgpt.com/backend-api/codex")
}

/// Convert a `ToolDefinition` list to the *flat* Responses tool schema that
/// Codex backend (and OpenAI's public Responses API) expects:
/// `{ "type": "function", "name": ..., "description": ..., "parameters": ..., "strict": false }`.
fn convert_tools_flat(tools: Option<Vec<ToolDefinition>>) -> Option<Vec<Value>> {
tools.map(|defs| {
defs.into_iter()
.map(|tool| {
json!({
"type": "function",
"name": tool.name,
"description": tool.description,
"parameters": tool.parameters,
"strict": false,
})
})
.collect()
})
}

fn attach_tools(request_body: &mut Value, tools: Option<Vec<Value>>) {
if let Some(tools) = tools {
let names: Vec<String> = tools
Expand Down Expand Up @@ -173,7 +154,7 @@ pub(crate) async fn send_stream(

let (instructions, response_input) =
OpenAIMessageConverter::convert_messages_to_responses_input(messages);
let tools_flat = convert_tools_flat(tools);
let tools_flat = common::convert_tools_flat(tools);
let request_body =
build_request_body(client, instructions, response_input, tools_flat, extra_body);
let idle_timeout = client.stream_options.idle_timeout;
Expand Down
21 changes: 20 additions & 1 deletion src/crates/ai-adapters/src/providers/openai/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::client::quirks::apply_openai_compatible_reasoning_fields;
use crate::client::utils::{dedupe_remote_models, normalize_base_url_for_discovery};
use crate::client::AIClient;
use crate::providers::shared;
use crate::types::RemoteModelInfo;
use crate::types::{RemoteModelInfo, ToolDefinition};
use anyhow::Result;
use log::warn;
use reqwest::RequestBuilder;
Expand Down Expand Up @@ -335,6 +335,7 @@ pub(crate) fn extract_tool_name(tool: &serde_json::Value) -> String {
tool.get("function")
.and_then(|function| function.get("name"))
.and_then(|name| name.as_str())
.or_else(|| tool.get("name").and_then(|name| name.as_str()))
.unwrap_or("unknown")
.to_string()
}
Expand All @@ -358,3 +359,21 @@ pub(crate) fn attach_tools(
}
}
}

pub(crate) fn convert_tools_flat(
tools: Option<Vec<ToolDefinition>>,
) -> Option<Vec<serde_json::Value>> {
tools.map(|defs| {
defs.into_iter()
.map(|tool| {
serde_json::json!({
"type": "function",
"name": tool.name,
"description": tool.description,
"parameters": tool.parameters,
"strict": false,
})
})
.collect()
})
}
63 changes: 62 additions & 1 deletion src/crates/ai-adapters/src/providers/openai/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ pub(crate) async fn send_stream(

let (instructions, response_input) =
OpenAIMessageConverter::convert_messages_to_responses_input(messages);
let openai_tools = OpenAIMessageConverter::convert_tools(tools);
let openai_tools = common::convert_tools_flat(tools);
let request_body = build_request_body(
client,
instructions,
Expand All @@ -133,3 +133,64 @@ pub(crate) async fn send_stream(
)
.await
}

#[cfg(test)]
mod tests {
use super::build_request_body;
use crate::types::{ReasoningMode, ToolDefinition};
use crate::{client::AIClient, types::AIConfig};
use serde_json::json;

fn test_client() -> AIClient {
AIClient::new(AIConfig {
name: "test".to_string(),
base_url: "https://api.openai.com/v1".to_string(),
request_url: "https://api.openai.com/v1/responses".to_string(),
api_key: "test-key".to_string(),
model: "gpt-5.4".to_string(),
format: "responses".to_string(),
context_window: 128_000,
max_tokens: None,
temperature: None,
top_p: None,
reasoning_mode: ReasoningMode::Default,
inline_think_in_text: false,
custom_headers: None,
custom_headers_mode: None,
skip_ssl_verify: false,
reasoning_effort: None,
thinking_budget_tokens: None,
custom_request_body: None,
custom_request_body_mode: None,
})
}

#[test]
fn attaches_flat_tool_schema_for_responses_api() {
let client = test_client();
let request_body = build_request_body(
&client,
None,
vec![json!({
"type": "message",
"role": "user",
"content": [{ "type": "input_text", "text": "hello" }]
})],
crate::providers::openai::common::convert_tools_flat(Some(vec![ToolDefinition {
name: "get_weather".to_string(),
description: "Get weather".to_string(),
parameters: json!({
"type": "object",
"properties": {
"city": { "type": "string" }
}
}),
}])),
None,
);

assert_eq!(request_body["tools"][0]["name"], json!("get_weather"));
assert_eq!(request_body["tools"][0]["type"], json!("function"));
assert!(request_body["tools"][0].get("function").is_none());
}
}
Loading