XciD HF Staff commited on
Commit
330cf0c
·
1 Parent(s): ede9df2

Fix streaming tool calls and input parsing for Responses API

Browse files

- Add DeltaToolCall struct with optional fields for streaming chunks
(first chunk has id/type/name, subsequent chunks only have arguments)
- Restructure InputItem as enum with FunctionCall, FunctionCallOutput, Message variants
- Extract tool name from call_id patterns (e.g., "functions.shell:0" -> "shell")
- Track current_function_name across streaming chunks for proper close handling
- Add debug logging for tool call chunks and request conversion

src/adapter/convert_responses.rs CHANGED
@@ -4,7 +4,7 @@ use crate::types::openrouter::{
4
  ContentPartType, ImageUrl, ChatCompletionChunk, Tool, Function, Role, ToolCallType,
5
  };
6
  use crate::types::responses::{
7
- CreateResponseRequest, Input, InputMessage, InputMessageContent,
8
  ContentPart as ResponsesContentPart, Tool as ResponsesTool, ToolChoice as ResponsesToolChoice,
9
  Response, ResponseStatus, ResponseUsage, OutputItem, OutputStatus,
10
  OutputContent, ReasoningContent, StreamEvent,
@@ -40,9 +40,9 @@ pub fn convert_responses_to_openrouter(req: &CreateResponseRequest) -> CreateCha
40
  reasoning_details: None,
41
  });
42
  }
43
- Input::Messages(input_messages) => {
44
- for msg in input_messages {
45
- if let Some(chat_msg) = convert_input_message(msg) {
46
  messages.push(chat_msg);
47
  }
48
  }
@@ -110,22 +110,83 @@ pub fn convert_responses_to_openrouter(req: &CreateResponseRequest) -> CreateCha
110
  }
111
  }
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  fn convert_input_message(msg: &InputMessage) -> Option<ChatCompletionMessage> {
 
114
  let role = match msg.role.as_str() {
115
  "user" => Role::User,
116
  "assistant" => Role::Assistant,
117
- "system" => Role::System,
118
- "developer" => Role::Developer,
119
  "tool" => Role::Tool,
120
  _ => Role::User,
121
  };
122
 
123
- let (content, tool_call_id) = match &msg.content {
124
- InputMessageContent::Text { content } => {
125
- (Some(MessageContent::Text(content.clone())), None)
126
  }
127
- InputMessageContent::Array { content } => {
128
- let parts: Vec<ContentPart> = content.iter().filter_map(|part| {
129
  match part {
130
  ResponsesContentPart::InputText { text } => {
131
  Some(ContentPart {
@@ -159,28 +220,14 @@ fn convert_input_message(msg: &InputMessage) -> Option<ChatCompletionMessage> {
159
  }
160
  }
161
  }).collect();
162
- (Some(MessageContent::Parts(parts)), None)
163
- }
164
- InputMessageContent::FunctionCallOutput { call_id, output, .. } => {
165
- return Some(ChatCompletionMessage {
166
- role: Role::Tool,
167
- content: Some(MessageContent::Text(output.clone())),
168
- tool_call_id: Some(call_id.clone()),
169
- refusal: None,
170
- tool_calls: None,
171
- reasoning: None,
172
- reasoning_details: None,
173
- });
174
- }
175
- InputMessageContent::FunctionCall { .. } => {
176
- return None;
177
  }
178
  };
179
 
180
  Some(ChatCompletionMessage {
181
  role,
182
  content,
183
- tool_call_id,
184
  refusal: None,
185
  tool_calls: None,
186
  reasoning: None,
@@ -199,6 +246,7 @@ pub struct ResponsesStreamConverter {
199
  current_message_id: Option<String>,
200
  current_reasoning_id: Option<String>,
201
  current_function_call_id: Option<String>,
 
202
  // Accumulated content
203
  full_text: String,
204
  full_reasoning: String,
@@ -373,7 +421,7 @@ impl ResponsesStreamConverter {
373
  events
374
  }
375
 
376
- fn handle_tool_call(&mut self, tool_call: &crate::types::openrouter::ToolCall) -> Vec<StreamEvent> {
377
  let mut events = Vec::new();
378
 
379
  // Close message/reasoning if needed
@@ -384,9 +432,12 @@ impl ResponsesStreamConverter {
384
  events.extend(self.close_current_reasoning());
385
  }
386
 
387
- // Start function call if this is a new one
388
- let is_new = !tool_call.id.is_empty() &&
389
- self.current_function_call_id.as_ref() != Some(&tool_call.id);
 
 
 
390
 
391
  if is_new || self.current_function_call_id.is_none() {
392
  if self.current_function_call_id.is_some() {
@@ -394,15 +445,16 @@ impl ResponsesStreamConverter {
394
  }
395
 
396
  let fc_id = format!("fc_{}", &uuid::Uuid::new_v4().to_string().replace("-", "")[..24]);
397
- self.current_function_call_id = Some(tool_call.id.clone());
 
398
  self.full_arguments.clear();
399
 
400
  events.push(StreamEvent::OutputItemAdded {
401
  output_index: self.output_index,
402
  item: OutputItem::FunctionCall {
403
  id: fc_id,
404
- call_id: tool_call.id.clone(),
405
- name: tool_call.function.name.clone(),
406
  arguments: String::new(),
407
  status: Some(OutputStatus::InProgress),
408
  },
@@ -411,14 +463,16 @@ impl ResponsesStreamConverter {
411
  }
412
 
413
  // Emit arguments delta
414
- if !tool_call.function.arguments.is_empty() {
415
- self.full_arguments.push_str(&tool_call.function.arguments);
416
- events.push(StreamEvent::FunctionCallArgumentsDelta {
417
- item_id: self.current_function_call_id.clone().unwrap_or_default(),
418
- output_index: self.output_index,
419
- delta: tool_call.function.arguments.clone(),
420
- sequence_number: self.next_sequence(),
421
- });
 
 
422
  }
423
 
424
  events
@@ -496,6 +550,8 @@ impl ResponsesStreamConverter {
496
  let mut events = Vec::new();
497
 
498
  if let Some(call_id) = self.current_function_call_id.take() {
 
 
499
  events.push(StreamEvent::FunctionCallArgumentsDone {
500
  item_id: call_id.clone(),
501
  output_index: self.output_index,
@@ -508,7 +564,7 @@ impl ResponsesStreamConverter {
508
  item: OutputItem::FunctionCall {
509
  id: format!("fc_{}", &uuid::Uuid::new_v4().to_string().replace("-", "")[..24]),
510
  call_id,
511
- name: String::new(), // Would need to track this
512
  arguments: self.full_arguments.clone(),
513
  status: Some(OutputStatus::Completed),
514
  },
 
4
  ContentPartType, ImageUrl, ChatCompletionChunk, Tool, Function, Role, ToolCallType,
5
  };
6
  use crate::types::responses::{
7
+ CreateResponseRequest, Input, InputItem, InputMessage, InputMessageContent,
8
  ContentPart as ResponsesContentPart, Tool as ResponsesTool, ToolChoice as ResponsesToolChoice,
9
  Response, ResponseStatus, ResponseUsage, OutputItem, OutputStatus,
10
  OutputContent, ReasoningContent, StreamEvent,
 
40
  reasoning_details: None,
41
  });
42
  }
43
+ Input::Messages(input_items) => {
44
+ for item in input_items {
45
+ if let Some(chat_msg) = convert_input_item(item) {
46
  messages.push(chat_msg);
47
  }
48
  }
 
110
  }
111
  }
112
 
113
+ fn convert_input_item(item: &InputItem) -> Option<ChatCompletionMessage> {
114
+ match item {
115
+ InputItem::FunctionCall { call_id, name, arguments, .. } => {
116
+ // Function calls need to be converted to assistant messages with tool_calls
117
+ // Extract name from call_id if name is empty (format: "functions.<name>:<index>")
118
+ let tool_name = if name.is_empty() {
119
+ extract_tool_name_from_call_id(call_id)
120
+ } else {
121
+ name.clone()
122
+ };
123
+ Some(ChatCompletionMessage {
124
+ role: Role::Assistant,
125
+ content: None,
126
+ tool_call_id: None,
127
+ refusal: None,
128
+ tool_calls: Some(vec![crate::types::openrouter::ToolCall {
129
+ index: 0,
130
+ id: call_id.clone(),
131
+ call_type: ToolCallType::Function,
132
+ function: crate::types::openrouter::ToolCallFunction {
133
+ name: tool_name,
134
+ arguments: arguments.clone(),
135
+ },
136
+ }]),
137
+ reasoning: None,
138
+ reasoning_details: None,
139
+ })
140
+ }
141
+ InputItem::FunctionCallOutput { call_id, output, .. } => {
142
+ Some(ChatCompletionMessage {
143
+ role: Role::Tool,
144
+ content: Some(MessageContent::Text(output.clone())),
145
+ tool_call_id: Some(call_id.clone()),
146
+ refusal: None,
147
+ tool_calls: None,
148
+ reasoning: None,
149
+ reasoning_details: None,
150
+ })
151
+ }
152
+ InputItem::Message(msg) => convert_input_message(msg),
153
+ }
154
+ }
155
+
156
+ /// Extract tool name from call_id format like "functions.shell:0" -> "shell"
157
+ fn extract_tool_name_from_call_id(call_id: &str) -> String {
158
+ // Try format "functions.<name>:<index>"
159
+ if let Some(rest) = call_id.strip_prefix("functions.") {
160
+ if let Some(name) = rest.split(':').next() {
161
+ return name.to_string();
162
+ }
163
+ }
164
+ // Try format "<name>:<index>"
165
+ if let Some(name) = call_id.split(':').next() {
166
+ if !name.is_empty() {
167
+ return name.to_string();
168
+ }
169
+ }
170
+ // Fallback: use the whole call_id
171
+ call_id.to_string()
172
+ }
173
+
174
  fn convert_input_message(msg: &InputMessage) -> Option<ChatCompletionMessage> {
175
+ // Convert developer to system (not all backends support developer role)
176
  let role = match msg.role.as_str() {
177
  "user" => Role::User,
178
  "assistant" => Role::Assistant,
179
+ "system" | "developer" => Role::System,
 
180
  "tool" => Role::Tool,
181
  _ => Role::User,
182
  };
183
 
184
+ let content = match &msg.content {
185
+ InputMessageContent::Text(text) => {
186
+ Some(MessageContent::Text(text.clone()))
187
  }
188
+ InputMessageContent::Array(parts) => {
189
+ let converted_parts: Vec<ContentPart> = parts.iter().filter_map(|part| {
190
  match part {
191
  ResponsesContentPart::InputText { text } => {
192
  Some(ContentPart {
 
220
  }
221
  }
222
  }).collect();
223
+ Some(MessageContent::Parts(converted_parts))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  }
225
  };
226
 
227
  Some(ChatCompletionMessage {
228
  role,
229
  content,
230
+ tool_call_id: None,
231
  refusal: None,
232
  tool_calls: None,
233
  reasoning: None,
 
246
  current_message_id: Option<String>,
247
  current_reasoning_id: Option<String>,
248
  current_function_call_id: Option<String>,
249
+ current_function_name: Option<String>,
250
  // Accumulated content
251
  full_text: String,
252
  full_reasoning: String,
 
421
  events
422
  }
423
 
424
+ fn handle_tool_call(&mut self, tool_call: &crate::types::openrouter::DeltaToolCall) -> Vec<StreamEvent> {
425
  let mut events = Vec::new();
426
 
427
  // Close message/reasoning if needed
 
432
  events.extend(self.close_current_reasoning());
433
  }
434
 
435
+ // Get the id if present
436
+ let tool_call_id = tool_call.id.clone().unwrap_or_default();
437
+
438
+ // Start function call if this is a new one (has id) or we don't have one yet
439
+ let is_new = !tool_call_id.is_empty() &&
440
+ self.current_function_call_id.as_ref() != Some(&tool_call_id);
441
 
442
  if is_new || self.current_function_call_id.is_none() {
443
  if self.current_function_call_id.is_some() {
 
445
  }
446
 
447
  let fc_id = format!("fc_{}", &uuid::Uuid::new_v4().to_string().replace("-", "")[..24]);
448
+ self.current_function_call_id = Some(tool_call_id.clone());
449
+ self.current_function_name = tool_call.function.name.clone();
450
  self.full_arguments.clear();
451
 
452
  events.push(StreamEvent::OutputItemAdded {
453
  output_index: self.output_index,
454
  item: OutputItem::FunctionCall {
455
  id: fc_id,
456
+ call_id: tool_call_id,
457
+ name: tool_call.function.name.clone().unwrap_or_default(),
458
  arguments: String::new(),
459
  status: Some(OutputStatus::InProgress),
460
  },
 
463
  }
464
 
465
  // Emit arguments delta
466
+ if let Some(ref arguments) = tool_call.function.arguments {
467
+ if !arguments.is_empty() {
468
+ self.full_arguments.push_str(arguments);
469
+ events.push(StreamEvent::FunctionCallArgumentsDelta {
470
+ item_id: self.current_function_call_id.clone().unwrap_or_default(),
471
+ output_index: self.output_index,
472
+ delta: arguments.clone(),
473
+ sequence_number: self.next_sequence(),
474
+ });
475
+ }
476
  }
477
 
478
  events
 
550
  let mut events = Vec::new();
551
 
552
  if let Some(call_id) = self.current_function_call_id.take() {
553
+ let name = self.current_function_name.take().unwrap_or_default();
554
+
555
  events.push(StreamEvent::FunctionCallArgumentsDone {
556
  item_id: call_id.clone(),
557
  output_index: self.output_index,
 
564
  item: OutputItem::FunctionCall {
565
  id: format!("fc_{}", &uuid::Uuid::new_v4().to_string().replace("-", "")[..24]),
566
  call_id,
567
+ name,
568
  arguments: self.full_arguments.clone(),
569
  status: Some(OutputStatus::Completed),
570
  },
src/adapter/convert_stream.rs CHANGED
@@ -94,39 +94,40 @@ impl StreamConverter {
94
  // Handle tool calls
95
  if let Some(tool_calls) = &delta.tool_calls {
96
  if let Some(tool_call) = tool_calls.first() {
97
- if let Some(function) = Some(&tool_call.function) {
98
- let new_tool = !tool_call.id.is_empty()
99
- && self.current_tool_call_id.as_ref() != Some(&tool_call.id);
100
-
101
- if self.current_delta_type != Some(MessageContentDeltaType::InputJsonDelta)
102
- || new_tool
103
- {
104
- events.extend(self.close_current_block());
105
- self.current_delta_type = Some(MessageContentDeltaType::InputJsonDelta);
106
- self.current_tool_call_id = Some(tool_call.id.clone());
107
-
108
- events.push(Event::ContentBlockStart {
109
- index: self.block_index,
110
- content_block: MessageContent {
111
- content_type: MessageContentType::ToolUse,
112
- id: Some(tool_call.id.clone()),
113
- name: Some(function.name.clone()),
114
- input: Some(
115
- serde_json::value::RawValue::from_string("{}".to_string())
116
- .unwrap(),
117
- ),
118
- ..Default::default()
119
- },
120
- });
121
- }
122
 
123
- // Emit partial JSON if present
124
- if !function.arguments.is_empty() {
 
125
  events.push(Event::ContentBlockDelta {
126
  index: self.block_index,
127
  delta: MessageContentDelta {
128
  delta_type: MessageContentDeltaType::InputJsonDelta,
129
- partial_json: Some(function.arguments.clone()),
130
  text: None,
131
  thinking: None,
132
  signature: None,
 
94
  // Handle tool calls
95
  if let Some(tool_calls) = &delta.tool_calls {
96
  if let Some(tool_call) = tool_calls.first() {
97
+ let tool_call_id = tool_call.id.clone().unwrap_or_default();
98
+ let new_tool = !tool_call_id.is_empty()
99
+ && self.current_tool_call_id.as_ref() != Some(&tool_call_id);
100
+
101
+ if self.current_delta_type != Some(MessageContentDeltaType::InputJsonDelta)
102
+ || new_tool
103
+ {
104
+ events.extend(self.close_current_block());
105
+ self.current_delta_type = Some(MessageContentDeltaType::InputJsonDelta);
106
+ self.current_tool_call_id = Some(tool_call_id.clone());
107
+
108
+ events.push(Event::ContentBlockStart {
109
+ index: self.block_index,
110
+ content_block: MessageContent {
111
+ content_type: MessageContentType::ToolUse,
112
+ id: Some(tool_call_id),
113
+ name: tool_call.function.name.clone(),
114
+ input: Some(
115
+ serde_json::value::RawValue::from_string("{}".to_string())
116
+ .unwrap(),
117
+ ),
118
+ ..Default::default()
119
+ },
120
+ });
121
+ }
122
 
123
+ // Emit partial JSON if present
124
+ if let Some(ref arguments) = tool_call.function.arguments {
125
+ if !arguments.is_empty() {
126
  events.push(Event::ContentBlockDelta {
127
  index: self.block_index,
128
  delta: MessageContentDelta {
129
  delta_type: MessageContentDeltaType::InputJsonDelta,
130
+ partial_json: Some(arguments.clone()),
131
  text: None,
132
  thinking: None,
133
  signature: None,
src/handlers/responses.rs CHANGED
@@ -3,7 +3,6 @@ use axum::{
3
  extract::State,
4
  http::{header, HeaderMap, StatusCode},
5
  response::Response,
6
- Json,
7
  };
8
  use futures::StreamExt;
9
  use tokio_stream::wrappers::ReceiverStream;
@@ -17,8 +16,20 @@ use crate::AppState;
17
  pub async fn responses(
18
  State(state): State<AppState>,
19
  headers: HeaderMap,
20
- Json(request): Json<CreateResponseRequest>,
21
  ) -> Result<Response, AppError> {
 
 
 
 
 
 
 
 
 
 
 
 
22
  // Extract API key from incoming request headers
23
  let api_key = headers
24
  .get("x-api-key")
@@ -46,6 +57,10 @@ pub async fn responses(
46
  // Convert Responses API request to OpenRouter/Chat Completions format
47
  let openrouter_request = convert_responses_to_openrouter(&request);
48
 
 
 
 
 
49
  // Create request to backend
50
  let url = format!("{}/v1/chat/completions", state.openrouter_base_url);
51
 
@@ -112,13 +127,26 @@ pub async fn responses(
112
  }
113
 
114
  if !data.is_empty() {
115
- if let Ok(chunk) = serde_json::from_str::<ChatCompletionChunk>(&data) {
116
- let events = converter.convert_chunk(&chunk);
117
- for event in events {
118
- if tx.send(Ok(event.to_sse_string())).await.is_err() {
119
- return;
 
 
 
 
 
 
 
 
 
 
120
  }
121
  }
 
 
 
122
  }
123
  }
124
  }
 
3
  extract::State,
4
  http::{header, HeaderMap, StatusCode},
5
  response::Response,
 
6
  };
7
  use futures::StreamExt;
8
  use tokio_stream::wrappers::ReceiverStream;
 
16
  pub async fn responses(
17
  State(state): State<AppState>,
18
  headers: HeaderMap,
19
+ body: String,
20
  ) -> Result<Response, AppError> {
21
+ // Log raw body for debugging (first 8000 chars)
22
+ eprintln!("=== RAW BODY (first 8000 chars) ===\n{}\n=== END ===", &body[..body.len().min(8000)]);
23
+
24
+ let request: CreateResponseRequest = serde_json::from_str(&body)
25
+ .map_err(|e| AppError::InvalidRequest(format!("Failed to parse request: {}", e)))?;
26
+
27
+ // Debug: log parsed input items
28
+ if let crate::types::responses::Input::Messages(items) = &request.input {
29
+ for (i, item) in items.iter().enumerate() {
30
+ eprintln!("=== INPUT ITEM {} ===\n{:?}\n=== END ===", i, item);
31
+ }
32
+ }
33
  // Extract API key from incoming request headers
34
  let api_key = headers
35
  .get("x-api-key")
 
57
  // Convert Responses API request to OpenRouter/Chat Completions format
58
  let openrouter_request = convert_responses_to_openrouter(&request);
59
 
60
+ // Debug: log the converted request
61
+ eprintln!("=== OPENROUTER REQUEST ===\n{}\n=== END ===",
62
+ serde_json::to_string_pretty(&openrouter_request).unwrap_or_default());
63
+
64
  // Create request to backend
65
  let url = format!("{}/v1/chat/completions", state.openrouter_base_url);
66
 
 
127
  }
128
 
129
  if !data.is_empty() {
130
+ match serde_json::from_str::<ChatCompletionChunk>(&data) {
131
+ Ok(chunk) => {
132
+ // Debug: log tool calls if present
133
+ if let Some(choice) = chunk.choices.first() {
134
+ if let Some(delta) = &choice.delta {
135
+ if delta.tool_calls.is_some() {
136
+ eprintln!("=== TOOL CALL CHUNK ===\n{}\n=== END ===", &data);
137
+ }
138
+ }
139
+ }
140
+ let events = converter.convert_chunk(&chunk);
141
+ for event in events {
142
+ if tx.send(Ok(event.to_sse_string())).await.is_err() {
143
+ return;
144
+ }
145
  }
146
  }
147
+ Err(e) => {
148
+ eprintln!("Failed to parse chunk: {} - data: {}", e, &data[..data.len().min(500)]);
149
+ }
150
  }
151
  }
152
  }
src/types/openrouter.rs CHANGED
@@ -224,6 +224,28 @@ pub struct ToolCall {
224
  pub function: ToolCallFunction,
225
  }
226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
228
  #[serde(rename_all = "snake_case")]
229
  pub enum ToolCallType {
@@ -246,6 +268,15 @@ where
246
  Ok(opt.unwrap_or_default())
247
  }
248
 
 
 
 
 
 
 
 
 
 
249
  #[derive(Debug, Clone, Serialize, Deserialize)]
250
  pub struct ReasoningDetail {
251
  #[serde(rename = "type")]
@@ -401,7 +432,7 @@ pub struct ChunkDelta {
401
  #[serde(skip_serializing_if = "Option::is_none")]
402
  pub refusal: Option<String>,
403
  #[serde(skip_serializing_if = "Option::is_none")]
404
- pub tool_calls: Option<Vec<ToolCall>>,
405
  #[serde(skip_serializing_if = "Option::is_none")]
406
  pub reasoning: Option<String>,
407
  #[serde(skip_serializing_if = "Option::is_none")]
 
224
  pub function: ToolCallFunction,
225
  }
226
 
227
+ /// Tool call in streaming delta - all fields optional except index
228
+ /// First chunk has: index, id, type, function.name
229
+ /// Subsequent chunks have: index, function.arguments
230
+ #[derive(Debug, Clone, Serialize, Deserialize)]
231
+ pub struct DeltaToolCall {
232
+ #[serde(default)]
233
+ pub index: i32,
234
+ #[serde(default, deserialize_with = "deserialize_option_string")]
235
+ pub id: Option<String>,
236
+ #[serde(rename = "type")]
237
+ pub call_type: Option<ToolCallType>,
238
+ pub function: DeltaToolCallFunction,
239
+ }
240
+
241
+ #[derive(Debug, Clone, Serialize, Deserialize)]
242
+ pub struct DeltaToolCallFunction {
243
+ #[serde(default, deserialize_with = "deserialize_option_string")]
244
+ pub name: Option<String>,
245
+ #[serde(default, deserialize_with = "deserialize_option_string")]
246
+ pub arguments: Option<String>,
247
+ }
248
+
249
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
250
  #[serde(rename_all = "snake_case")]
251
  pub enum ToolCallType {
 
268
  Ok(opt.unwrap_or_default())
269
  }
270
 
271
+ fn deserialize_option_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
272
+ where
273
+ D: Deserializer<'de>,
274
+ {
275
+ let opt: Option<String> = Option::deserialize(deserializer)?;
276
+ // Filter out empty strings
277
+ Ok(opt.filter(|s| !s.is_empty()))
278
+ }
279
+
280
  #[derive(Debug, Clone, Serialize, Deserialize)]
281
  pub struct ReasoningDetail {
282
  #[serde(rename = "type")]
 
432
  #[serde(skip_serializing_if = "Option::is_none")]
433
  pub refusal: Option<String>,
434
  #[serde(skip_serializing_if = "Option::is_none")]
435
+ pub tool_calls: Option<Vec<DeltaToolCall>>,
436
  #[serde(skip_serializing_if = "Option::is_none")]
437
  pub reasoning: Option<String>,
438
  #[serde(skip_serializing_if = "Option::is_none")]
src/types/responses.rs CHANGED
@@ -29,34 +29,46 @@ pub struct CreateResponseRequest {
29
  #[serde(untagged)]
30
  pub enum Input {
31
  Text(String),
32
- Messages(Vec<InputMessage>),
33
- }
34
-
35
- #[derive(Debug, Clone, Serialize, Deserialize)]
36
- pub struct InputMessage {
37
- pub role: String,
38
- #[serde(flatten)]
39
- pub content: InputMessageContent,
40
  }
41
 
 
42
  #[derive(Debug, Clone, Serialize, Deserialize)]
43
  #[serde(untagged)]
44
- pub enum InputMessageContent {
45
- Text { content: String },
46
- Array { content: Vec<ContentPart> },
47
  FunctionCall {
48
  #[serde(rename = "type")]
49
- content_type: String, // "function_call"
 
 
50
  call_id: String,
 
51
  name: String,
52
  arguments: String,
53
  },
 
54
  FunctionCallOutput {
55
  #[serde(rename = "type")]
56
- content_type: String, // "function_call_output"
57
  call_id: String,
58
  output: String,
59
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
 
62
  #[derive(Debug, Clone, Serialize, Deserialize)]
 
29
  #[serde(untagged)]
30
  pub enum Input {
31
  Text(String),
32
+ Messages(Vec<InputItem>),
 
 
 
 
 
 
 
33
  }
34
 
35
+ /// Input items can be either regular messages (with role) or function call items (with type)
36
  #[derive(Debug, Clone, Serialize, Deserialize)]
37
  #[serde(untagged)]
38
+ pub enum InputItem {
39
+ /// Function call: { type: "function_call", call_id, name, arguments }
 
40
  FunctionCall {
41
  #[serde(rename = "type")]
42
+ item_type: String,
43
+ #[serde(default)]
44
+ id: Option<String>,
45
  call_id: String,
46
+ #[serde(default)]
47
  name: String,
48
  arguments: String,
49
  },
50
+ /// Function call output: { type: "function_call_output", call_id, output }
51
  FunctionCallOutput {
52
  #[serde(rename = "type")]
53
+ item_type: String,
54
  call_id: String,
55
  output: String,
56
  },
57
+ /// Regular message: { role, content }
58
+ Message(InputMessage),
59
+ }
60
+
61
+ #[derive(Debug, Clone, Serialize, Deserialize)]
62
+ pub struct InputMessage {
63
+ pub role: String,
64
+ pub content: InputMessageContent,
65
+ }
66
+
67
+ #[derive(Debug, Clone, Serialize, Deserialize)]
68
+ #[serde(untagged)]
69
+ pub enum InputMessageContent {
70
+ Text(String),
71
+ Array(Vec<ContentPart>),
72
  }
73
 
74
  #[derive(Debug, Clone, Serialize, Deserialize)]