Skip to content
Closed
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
246 changes: 246 additions & 0 deletions crates/goose/src/providers/formats/openai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,79 @@ use serde_json::{json, Value};
use std::borrow::Cow;
use std::ops::Deref;

// ============================================================================
// Responses API Types (for gpt-5.1-codex and similar models)
// ============================================================================

#[derive(Debug, Serialize, Deserialize)]
pub struct ResponsesApiResponse {
pub id: String,
pub object: String,
pub created_at: i64,
pub status: String,
pub model: String,
pub output: Vec<ResponseOutputItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<ResponseReasoningInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<ResponseUsage>,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum ResponseOutputItem {
Reasoning {
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
summary: Option<Vec<String>>,
},
Message {
id: String,
status: String,
role: String,
content: Vec<ResponseContentBlock>,
},
FunctionCall {
id: String,
status: String,
#[serde(skip_serializing_if = "Option::is_none")]
call_id: Option<String>,
name: String,
arguments: String,
},
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum ResponseContentBlock {
OutputText {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<Vec<Value>>,
},
ToolCall {
id: String,
name: String,
input: Value,
},
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ResponseReasoningInfo {
pub effort: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ResponseUsage {
pub input_tokens: i32,
pub output_tokens: i32,
pub total_tokens: i32,
}

#[derive(Serialize, Deserialize, Debug)]
struct DeltaToolCallFunction {
name: Option<String>,
Expand Down Expand Up @@ -604,6 +677,179 @@ where
}
}

// ============================================================================
// Responses API Helper Functions
// ============================================================================

pub fn create_responses_request(
model_config: &ModelConfig,
system: &str,
messages: &[Message],
tools: &[Tool],
) -> anyhow::Result<Value, Error> {
let mut conversation_parts = Vec::new();

for message in messages.iter().filter(|m| m.is_agent_visible()) {
for content in &message.content {
match content {
MessageContent::Text(text) if !text.text.is_empty() => {
let role_str = if message.role == Role::User {
"User"
} else {
"Assistant"
};
conversation_parts.push(format!("{}: {}", role_str, text.text));
}
MessageContent::ToolRequest(request) => {
if let Ok(tool_call) = &request.tool_call {
conversation_parts.push(format!(
"Tool Call: {} with arguments {:?}",
tool_call.name, tool_call.arguments
));
}
}
MessageContent::ToolResponse(response) => {
if let Ok(contents) = &response.tool_result {
let text_content: Vec<String> = contents
.iter()
.filter_map(|c| {
if let RawContent::Text(t) = c.deref() {
Some(t.text.clone())
} else {
None
}
})
.collect();
if !text_content.is_empty() {
conversation_parts
.push(format!("Tool Result: {}", text_content.join(" ")));
}
}
}
_ => {}
}
}
}

let input = if conversation_parts.is_empty() {
"Hello".to_string()
} else {
conversation_parts.join("\n\n")
};

let mut payload = json!({
"model": model_config.model_name,
"input": input,
"instructions": system,
});

if !tools.is_empty() {
let tools_spec: Vec<Value> = tools
.iter()
.map(|tool| {
json!({
"type": "function",
"name": tool.name,
"description": tool.description,
"parameters": tool.input_schema,
})
})
.collect();

payload
.as_object_mut()
.unwrap()
.insert("tools".to_string(), json!(tools_spec));
}

if let Some(temp) = model_config.temperature {
payload
.as_object_mut()
.unwrap()
.insert("temperature".to_string(), json!(temp));
}

if let Some(tokens) = model_config.max_tokens {
payload
.as_object_mut()
.unwrap()
Comment on lines +761 to +775
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .unwrap() calls could panic if the payload is not a valid JSON object. Use proper error handling with ok_or_else() or similar to return an anyhow::Result error.

Suggested change
.unwrap()
.insert("tools".to_string(), json!(tools_spec));
}
if let Some(temp) = model_config.temperature {
payload
.as_object_mut()
.unwrap()
.insert("temperature".to_string(), json!(temp));
}
if let Some(tokens) = model_config.max_tokens {
payload
.as_object_mut()
.unwrap()
.ok_or_else(|| anyhow!("Payload is not a JSON object"))?
.insert("tools".to_string(), json!(tools_spec));
}
if let Some(temp) = model_config.temperature {
payload
.as_object_mut()
.ok_or_else(|| anyhow!("Payload is not a JSON object"))?
.insert("temperature".to_string(), json!(temp));
}
if let Some(tokens) = model_config.max_tokens {
payload
.as_object_mut()
.ok_or_else(|| anyhow!("Payload is not a JSON object"))?

Copilot uses AI. Check for mistakes.
Comment on lines +761 to +775
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .unwrap() calls could panic if the payload is not a valid JSON object. Use proper error handling with ok_or_else() or similar to return an anyhow::Result error.

Suggested change
.unwrap()
.insert("tools".to_string(), json!(tools_spec));
}
if let Some(temp) = model_config.temperature {
payload
.as_object_mut()
.unwrap()
.insert("temperature".to_string(), json!(temp));
}
if let Some(tokens) = model_config.max_tokens {
payload
.as_object_mut()
.unwrap()
.ok_or_else(|| anyhow!("payload is not a JSON object"))?
.insert("tools".to_string(), json!(tools_spec));
}
if let Some(temp) = model_config.temperature {
payload
.as_object_mut()
.ok_or_else(|| anyhow!("payload is not a JSON object"))?
.insert("temperature".to_string(), json!(temp));
}
if let Some(tokens) = model_config.max_tokens {
payload
.as_object_mut()
.ok_or_else(|| anyhow!("payload is not a JSON object"))?

Copilot uses AI. Check for mistakes.
Comment on lines +761 to +775
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .unwrap() calls could panic if the payload is not a valid JSON object. Use proper error handling with ok_or_else() or similar to return an anyhow::Result error.

Suggested change
.unwrap()
.insert("tools".to_string(), json!(tools_spec));
}
if let Some(temp) = model_config.temperature {
payload
.as_object_mut()
.unwrap()
.insert("temperature".to_string(), json!(temp));
}
if let Some(tokens) = model_config.max_tokens {
payload
.as_object_mut()
.unwrap()
.ok_or_else(|| anyhow!("payload is not a JSON object"))?
.insert("tools".to_string(), json!(tools_spec));
}
if let Some(temp) = model_config.temperature {
payload
.as_object_mut()
.ok_or_else(|| anyhow!("payload is not a JSON object"))?
.insert("temperature".to_string(), json!(temp));
}
if let Some(tokens) = model_config.max_tokens {
payload
.as_object_mut()
.ok_or_else(|| anyhow!("payload is not a JSON object"))?

Copilot uses AI. Check for mistakes.
.insert("max_output_tokens".to_string(), json!(tokens));
}

Ok(payload)
}

pub fn responses_api_to_message(response: &ResponsesApiResponse) -> anyhow::Result<Message> {
let mut content = Vec::new();

for item in &response.output {
match item {
ResponseOutputItem::Reasoning { .. } => {
continue;
}
ResponseOutputItem::Message {
content: msg_content,
..
} => {
for block in msg_content {
match block {
ResponseContentBlock::OutputText { text, .. } => {
if !text.is_empty() {
content.push(MessageContent::text(text));
}
}
ResponseContentBlock::ToolCall { id, name, input } => {
content.push(MessageContent::tool_request(
id.clone(),
Ok(CallToolRequestParam {
name: name.clone().into(),
arguments: Some(object(input.clone())),
}),
));
}
}
}
}
ResponseOutputItem::FunctionCall {
id,
name,
arguments,
..
} => {
let parsed_args = if arguments.is_empty() {
json!({})
} else {
serde_json::from_str(arguments).unwrap_or_else(|_| json!({}))
Comment on lines +819 to +822
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .unwrap_or_else() silently converts invalid JSON to an empty object. Consider logging this error or using a more explicit error message.

Copilot generated this review using guidance from repository custom instructions.
};

content.push(MessageContent::tool_request(
id.clone(),
Ok(CallToolRequestParam {
name: name.clone().into(),
arguments: Some(object(parsed_args)),
}),
));
}
}
}

let mut message = Message::new(Role::Assistant, chrono::Utc::now().timestamp(), content);

message = message.with_id(response.id.clone());

Ok(message)
}

pub fn get_responses_usage(response: &ResponsesApiResponse) -> Usage {
response.usage.as_ref().map_or_else(Usage::default, |u| {
Usage::new(
Some(u.input_tokens),
Some(u.output_tokens),
Some(u.total_tokens),
)
})
}

pub fn create_request(
model_config: &ModelConfig,
system: &str,
Expand Down
Loading
Loading