XciD HF Staff commited on
Commit
0ff308b
·
1 Parent(s): e31dca6

Add OpenAI Responses API support (/v1/responses endpoint)

Browse files
src/adapter/convert_responses.rs ADDED
@@ -0,0 +1,593 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use serde_json::value::RawValue;
2
+ use crate::types::openrouter::{
3
+ CreateChatCompletionRequest, ChatCompletionMessage, MessageContent, ContentPart,
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,
11
+ };
12
+
13
+ /// Convert Responses API request to Chat Completions request
14
+ pub fn convert_responses_to_openrouter(req: &CreateResponseRequest) -> CreateChatCompletionRequest {
15
+ let mut messages = Vec::new();
16
+
17
+ // Add system message if instructions provided
18
+ if let Some(instructions) = &req.instructions {
19
+ messages.push(ChatCompletionMessage {
20
+ role: Role::System,
21
+ content: Some(MessageContent::Text(instructions.clone())),
22
+ tool_call_id: None,
23
+ refusal: None,
24
+ tool_calls: None,
25
+ reasoning: None,
26
+ reasoning_details: None,
27
+ });
28
+ }
29
+
30
+ // Convert input to messages
31
+ match &req.input {
32
+ Input::Text(text) => {
33
+ messages.push(ChatCompletionMessage {
34
+ role: Role::User,
35
+ content: Some(MessageContent::Text(text.clone())),
36
+ tool_call_id: None,
37
+ refusal: None,
38
+ tool_calls: None,
39
+ reasoning: None,
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
+ }
49
+ }
50
+ }
51
+
52
+ // Convert tools
53
+ let tools = req.tools.as_ref().map(|tools| {
54
+ tools.iter().filter_map(|tool| {
55
+ match tool {
56
+ ResponsesTool::Function { name, description, parameters, strict } => {
57
+ Some(Tool {
58
+ tool_type: ToolCallType::Function,
59
+ function: Function {
60
+ name: name.clone(),
61
+ description: description.clone(),
62
+ parameters: parameters.as_ref().and_then(|p| {
63
+ RawValue::from_string(p.to_string()).ok()
64
+ }),
65
+ strict: *strict,
66
+ },
67
+ })
68
+ }
69
+ }
70
+ }).collect()
71
+ });
72
+
73
+ // Convert tool_choice
74
+ let tool_choice = req.tool_choice.as_ref().and_then(|tc| {
75
+ match tc {
76
+ ResponsesToolChoice::Auto(s) => {
77
+ match s.as_str() {
78
+ "auto" => Some(crate::types::openrouter::ToolChoice::Mode(crate::types::openrouter::ToolChoiceMode::Auto)),
79
+ "none" => Some(crate::types::openrouter::ToolChoice::Mode(crate::types::openrouter::ToolChoiceMode::None)),
80
+ "required" => Some(crate::types::openrouter::ToolChoice::Mode(crate::types::openrouter::ToolChoiceMode::Required)),
81
+ _ => None,
82
+ }
83
+ }
84
+ ResponsesToolChoice::Function { name, .. } => {
85
+ Some(crate::types::openrouter::ToolChoice::Tool(Tool {
86
+ tool_type: ToolCallType::Function,
87
+ function: Function {
88
+ name: name.clone(),
89
+ description: None,
90
+ parameters: None,
91
+ strict: None,
92
+ },
93
+ }))
94
+ }
95
+ }
96
+ });
97
+
98
+ CreateChatCompletionRequest {
99
+ model: req.model.clone(),
100
+ messages,
101
+ stream: true,
102
+ max_tokens: req.max_output_tokens.map(|t| t as i32),
103
+ temperature: req.temperature,
104
+ top_p: req.top_p,
105
+ tools,
106
+ tool_choice,
107
+ ..Default::default()
108
+ }
109
+ }
110
+
111
+ fn convert_input_message(msg: &InputMessage) -> Option<ChatCompletionMessage> {
112
+ let role = match msg.role.as_str() {
113
+ "user" => Role::User,
114
+ "assistant" => Role::Assistant,
115
+ "system" => Role::System,
116
+ "developer" => Role::Developer,
117
+ "tool" => Role::Tool,
118
+ _ => Role::User,
119
+ };
120
+
121
+ let (content, tool_call_id) = match &msg.content {
122
+ InputMessageContent::Text { content } => {
123
+ (Some(MessageContent::Text(content.clone())), None)
124
+ }
125
+ InputMessageContent::Array { content } => {
126
+ let parts: Vec<ContentPart> = content.iter().filter_map(|part| {
127
+ match part {
128
+ ResponsesContentPart::InputText { text } => {
129
+ Some(ContentPart {
130
+ part_type: ContentPartType::Text,
131
+ text: Some(text.clone()),
132
+ refusal: None,
133
+ image_url: None,
134
+ cache_control: None,
135
+ })
136
+ }
137
+ ResponsesContentPart::InputImage { image_url } => {
138
+ Some(ContentPart {
139
+ part_type: ContentPartType::ImageUrl,
140
+ text: None,
141
+ refusal: None,
142
+ image_url: Some(ImageUrl {
143
+ url: image_url.clone(),
144
+ detail: None,
145
+ }),
146
+ cache_control: None,
147
+ })
148
+ }
149
+ ResponsesContentPart::OutputText { text } => {
150
+ Some(ContentPart {
151
+ part_type: ContentPartType::Text,
152
+ text: Some(text.clone()),
153
+ refusal: None,
154
+ image_url: None,
155
+ cache_control: None,
156
+ })
157
+ }
158
+ }
159
+ }).collect();
160
+ (Some(MessageContent::Parts(parts)), None)
161
+ }
162
+ InputMessageContent::FunctionCallOutput { call_id, output, .. } => {
163
+ return Some(ChatCompletionMessage {
164
+ role: Role::Tool,
165
+ content: Some(MessageContent::Text(output.clone())),
166
+ tool_call_id: Some(call_id.clone()),
167
+ refusal: None,
168
+ tool_calls: None,
169
+ reasoning: None,
170
+ reasoning_details: None,
171
+ });
172
+ }
173
+ InputMessageContent::FunctionCall { .. } => {
174
+ return None;
175
+ }
176
+ };
177
+
178
+ Some(ChatCompletionMessage {
179
+ role,
180
+ content,
181
+ tool_call_id,
182
+ refusal: None,
183
+ tool_calls: None,
184
+ reasoning: None,
185
+ reasoning_details: None,
186
+ })
187
+ }
188
+
189
+ /// Stream converter for Chat Completions → Responses API
190
+ #[derive(Debug, Default)]
191
+ pub struct ResponsesStreamConverter {
192
+ response_id: String,
193
+ model: String,
194
+ created_at: i64,
195
+ sequence_number: i64,
196
+ // Current output state
197
+ current_message_id: Option<String>,
198
+ current_reasoning_id: Option<String>,
199
+ current_function_call_id: Option<String>,
200
+ // Accumulated content
201
+ full_text: String,
202
+ full_reasoning: String,
203
+ full_arguments: String,
204
+ // Indices
205
+ output_index: i64,
206
+ // Usage tracking
207
+ input_tokens: i64,
208
+ output_tokens: i64,
209
+ // State
210
+ started: bool,
211
+ in_reasoning: bool,
212
+ }
213
+
214
+ impl ResponsesStreamConverter {
215
+ pub fn new() -> Self {
216
+ Self {
217
+ response_id: format!("resp_{}", uuid::Uuid::new_v4().to_string().replace("-", "")[..24].to_string()),
218
+ created_at: std::time::SystemTime::now()
219
+ .duration_since(std::time::UNIX_EPOCH)
220
+ .unwrap()
221
+ .as_secs() as i64,
222
+ ..Default::default()
223
+ }
224
+ }
225
+
226
+ pub fn convert_chunk(&mut self, chunk: &ChatCompletionChunk) -> Vec<StreamEvent> {
227
+ let mut events = Vec::new();
228
+
229
+ // Set model from first chunk
230
+ if self.model.is_empty() {
231
+ self.model = chunk.model.clone();
232
+ }
233
+
234
+ // Update usage
235
+ if let Some(usage) = &chunk.usage {
236
+ self.input_tokens = usage.prompt_tokens;
237
+ self.output_tokens = usage.completion_tokens;
238
+ }
239
+
240
+ // Initial response.created event
241
+ if !self.started {
242
+ self.started = true;
243
+ events.push(self.create_response_event(StreamEventType::Created));
244
+ events.push(self.create_response_event(StreamEventType::InProgress));
245
+ }
246
+
247
+ // Process choices
248
+ if let Some(choice) = chunk.choices.first() {
249
+ if let Some(delta) = &choice.delta {
250
+ // Handle reasoning content
251
+ if let Some(reasoning) = &delta.reasoning {
252
+ if !reasoning.is_empty() {
253
+ events.extend(self.handle_reasoning(reasoning));
254
+ }
255
+ }
256
+ if let Some(reasoning_content) = &delta.reasoning_content {
257
+ if !reasoning_content.is_empty() {
258
+ events.extend(self.handle_reasoning(reasoning_content));
259
+ }
260
+ }
261
+
262
+ // Handle text content
263
+ if let Some(content) = &delta.content {
264
+ if !content.is_empty() {
265
+ events.extend(self.handle_text(content));
266
+ }
267
+ }
268
+
269
+ // Handle tool calls
270
+ if let Some(tool_calls) = &delta.tool_calls {
271
+ if let Some(tool_call) = tool_calls.first() {
272
+ events.extend(self.handle_tool_call(tool_call));
273
+ }
274
+ }
275
+ }
276
+ }
277
+
278
+ events
279
+ }
280
+
281
+ fn handle_reasoning(&mut self, content: &str) -> Vec<StreamEvent> {
282
+ let mut events = Vec::new();
283
+
284
+ // Close text message if we were in text mode
285
+ if !self.in_reasoning && self.current_message_id.is_some() {
286
+ events.extend(self.close_current_message());
287
+ }
288
+
289
+ // Start reasoning if needed
290
+ if self.current_reasoning_id.is_none() {
291
+ self.in_reasoning = true;
292
+ let reasoning_id = format!("rs_{}", &uuid::Uuid::new_v4().to_string().replace("-", "")[..24]);
293
+ self.current_reasoning_id = Some(reasoning_id.clone());
294
+
295
+ events.push(StreamEvent::OutputItemAdded {
296
+ output_index: self.output_index,
297
+ item: OutputItem::Reasoning {
298
+ id: reasoning_id.clone(),
299
+ status: OutputStatus::InProgress,
300
+ content: vec![],
301
+ },
302
+ sequence_number: self.next_sequence(),
303
+ });
304
+
305
+ events.push(StreamEvent::ContentPartAdded {
306
+ item_id: reasoning_id.clone(),
307
+ output_index: self.output_index,
308
+ content_index: 0,
309
+ part: OutputContent::Text { text: String::new() },
310
+ sequence_number: self.next_sequence(),
311
+ });
312
+ }
313
+
314
+ // Emit reasoning delta
315
+ self.full_reasoning.push_str(content);
316
+ events.push(StreamEvent::ReasoningTextDelta {
317
+ item_id: self.current_reasoning_id.clone().unwrap(),
318
+ output_index: self.output_index,
319
+ content_index: 0,
320
+ delta: content.to_string(),
321
+ sequence_number: self.next_sequence(),
322
+ });
323
+
324
+ events
325
+ }
326
+
327
+ fn handle_text(&mut self, content: &str) -> Vec<StreamEvent> {
328
+ let mut events = Vec::new();
329
+
330
+ // Close reasoning if we were in reasoning mode
331
+ if self.in_reasoning && self.current_reasoning_id.is_some() {
332
+ events.extend(self.close_current_reasoning());
333
+ }
334
+
335
+ // Start message if needed
336
+ if self.current_message_id.is_none() {
337
+ self.in_reasoning = false;
338
+ let message_id = format!("msg_{}", &uuid::Uuid::new_v4().to_string().replace("-", "")[..24]);
339
+ self.current_message_id = Some(message_id.clone());
340
+
341
+ events.push(StreamEvent::OutputItemAdded {
342
+ output_index: self.output_index,
343
+ item: OutputItem::Message {
344
+ id: message_id.clone(),
345
+ role: "assistant".to_string(),
346
+ status: OutputStatus::InProgress,
347
+ content: vec![],
348
+ },
349
+ sequence_number: self.next_sequence(),
350
+ });
351
+
352
+ events.push(StreamEvent::ContentPartAdded {
353
+ item_id: message_id.clone(),
354
+ output_index: self.output_index,
355
+ content_index: 0,
356
+ part: OutputContent::Text { text: String::new() },
357
+ sequence_number: self.next_sequence(),
358
+ });
359
+ }
360
+
361
+ // Emit text delta
362
+ self.full_text.push_str(content);
363
+ events.push(StreamEvent::OutputTextDelta {
364
+ item_id: self.current_message_id.clone().unwrap(),
365
+ output_index: self.output_index,
366
+ content_index: 0,
367
+ delta: content.to_string(),
368
+ sequence_number: self.next_sequence(),
369
+ });
370
+
371
+ events
372
+ }
373
+
374
+ fn handle_tool_call(&mut self, tool_call: &crate::types::openrouter::ToolCall) -> Vec<StreamEvent> {
375
+ let mut events = Vec::new();
376
+
377
+ // Close message/reasoning if needed
378
+ if self.current_message_id.is_some() {
379
+ events.extend(self.close_current_message());
380
+ }
381
+ if self.current_reasoning_id.is_some() {
382
+ events.extend(self.close_current_reasoning());
383
+ }
384
+
385
+ // Start function call if this is a new one
386
+ let is_new = !tool_call.id.is_empty() &&
387
+ self.current_function_call_id.as_ref() != Some(&tool_call.id);
388
+
389
+ if is_new || self.current_function_call_id.is_none() {
390
+ if self.current_function_call_id.is_some() {
391
+ events.extend(self.close_current_function_call());
392
+ }
393
+
394
+ let fc_id = format!("fc_{}", &uuid::Uuid::new_v4().to_string().replace("-", "")[..24]);
395
+ self.current_function_call_id = Some(tool_call.id.clone());
396
+ self.full_arguments.clear();
397
+
398
+ events.push(StreamEvent::OutputItemAdded {
399
+ output_index: self.output_index,
400
+ item: OutputItem::FunctionCall {
401
+ id: fc_id,
402
+ call_id: tool_call.id.clone(),
403
+ name: tool_call.function.name.clone(),
404
+ arguments: String::new(),
405
+ status: Some(OutputStatus::InProgress),
406
+ },
407
+ sequence_number: self.next_sequence(),
408
+ });
409
+ }
410
+
411
+ // Emit arguments delta
412
+ if !tool_call.function.arguments.is_empty() {
413
+ self.full_arguments.push_str(&tool_call.function.arguments);
414
+ events.push(StreamEvent::FunctionCallArgumentsDelta {
415
+ item_id: self.current_function_call_id.clone().unwrap_or_default(),
416
+ output_index: self.output_index,
417
+ delta: tool_call.function.arguments.clone(),
418
+ sequence_number: self.next_sequence(),
419
+ });
420
+ }
421
+
422
+ events
423
+ }
424
+
425
+ fn close_current_message(&mut self) -> Vec<StreamEvent> {
426
+ let mut events = Vec::new();
427
+
428
+ if let Some(message_id) = self.current_message_id.take() {
429
+ events.push(StreamEvent::OutputTextDone {
430
+ item_id: message_id.clone(),
431
+ output_index: self.output_index,
432
+ content_index: 0,
433
+ text: self.full_text.clone(),
434
+ sequence_number: self.next_sequence(),
435
+ });
436
+
437
+ events.push(StreamEvent::ContentPartDone {
438
+ item_id: message_id.clone(),
439
+ output_index: self.output_index,
440
+ content_index: 0,
441
+ part: OutputContent::Text { text: self.full_text.clone() },
442
+ sequence_number: self.next_sequence(),
443
+ });
444
+
445
+ events.push(StreamEvent::OutputItemDone {
446
+ output_index: self.output_index,
447
+ item: OutputItem::Message {
448
+ id: message_id,
449
+ role: "assistant".to_string(),
450
+ status: OutputStatus::Completed,
451
+ content: vec![OutputContent::Text { text: self.full_text.clone() }],
452
+ },
453
+ sequence_number: self.next_sequence(),
454
+ });
455
+
456
+ self.output_index += 1;
457
+ self.full_text.clear();
458
+ }
459
+
460
+ events
461
+ }
462
+
463
+ fn close_current_reasoning(&mut self) -> Vec<StreamEvent> {
464
+ let mut events = Vec::new();
465
+
466
+ if let Some(reasoning_id) = self.current_reasoning_id.take() {
467
+ events.push(StreamEvent::ReasoningTextDone {
468
+ item_id: reasoning_id.clone(),
469
+ output_index: self.output_index,
470
+ content_index: 0,
471
+ text: self.full_reasoning.clone(),
472
+ sequence_number: self.next_sequence(),
473
+ });
474
+
475
+ events.push(StreamEvent::OutputItemDone {
476
+ output_index: self.output_index,
477
+ item: OutputItem::Reasoning {
478
+ id: reasoning_id,
479
+ status: OutputStatus::Completed,
480
+ content: vec![ReasoningContent::Text { text: self.full_reasoning.clone() }],
481
+ },
482
+ sequence_number: self.next_sequence(),
483
+ });
484
+
485
+ self.output_index += 1;
486
+ self.full_reasoning.clear();
487
+ self.in_reasoning = false;
488
+ }
489
+
490
+ events
491
+ }
492
+
493
+ fn close_current_function_call(&mut self) -> Vec<StreamEvent> {
494
+ let mut events = Vec::new();
495
+
496
+ if let Some(call_id) = self.current_function_call_id.take() {
497
+ events.push(StreamEvent::FunctionCallArgumentsDone {
498
+ item_id: call_id.clone(),
499
+ output_index: self.output_index,
500
+ arguments: self.full_arguments.clone(),
501
+ sequence_number: self.next_sequence(),
502
+ });
503
+
504
+ events.push(StreamEvent::OutputItemDone {
505
+ output_index: self.output_index,
506
+ item: OutputItem::FunctionCall {
507
+ id: format!("fc_{}", &uuid::Uuid::new_v4().to_string().replace("-", "")[..24]),
508
+ call_id,
509
+ name: String::new(), // Would need to track this
510
+ arguments: self.full_arguments.clone(),
511
+ status: Some(OutputStatus::Completed),
512
+ },
513
+ sequence_number: self.next_sequence(),
514
+ });
515
+
516
+ self.output_index += 1;
517
+ self.full_arguments.clear();
518
+ }
519
+
520
+ events
521
+ }
522
+
523
+ pub fn finish(&mut self) -> Vec<StreamEvent> {
524
+ let mut events = Vec::new();
525
+
526
+ // Close any open items
527
+ if self.current_message_id.is_some() {
528
+ events.extend(self.close_current_message());
529
+ }
530
+ if self.current_reasoning_id.is_some() {
531
+ events.extend(self.close_current_reasoning());
532
+ }
533
+ if self.current_function_call_id.is_some() {
534
+ events.extend(self.close_current_function_call());
535
+ }
536
+
537
+ // Emit response.completed
538
+ events.push(self.create_response_event(StreamEventType::Completed));
539
+
540
+ events
541
+ }
542
+
543
+ fn next_sequence(&mut self) -> i64 {
544
+ self.sequence_number += 1;
545
+ self.sequence_number
546
+ }
547
+
548
+ fn create_response_event(&mut self, event_type: StreamEventType) -> StreamEvent {
549
+ let response = Response {
550
+ id: self.response_id.clone(),
551
+ object: "response".to_string(),
552
+ created_at: self.created_at,
553
+ model: self.model.clone(),
554
+ status: match event_type {
555
+ StreamEventType::Created | StreamEventType::InProgress => ResponseStatus::InProgress,
556
+ StreamEventType::Completed => ResponseStatus::Completed,
557
+ },
558
+ error: None,
559
+ instructions: None,
560
+ max_output_tokens: None,
561
+ temperature: None,
562
+ top_p: None,
563
+ output: vec![],
564
+ usage: Some(ResponseUsage {
565
+ input_tokens: self.input_tokens,
566
+ output_tokens: self.output_tokens,
567
+ total_tokens: self.input_tokens + self.output_tokens,
568
+ }),
569
+ };
570
+
571
+ match event_type {
572
+ StreamEventType::Created => StreamEvent::ResponseCreated {
573
+ response,
574
+ sequence_number: self.next_sequence(),
575
+ },
576
+ StreamEventType::InProgress => StreamEvent::ResponseInProgress {
577
+ response,
578
+ sequence_number: self.next_sequence(),
579
+ },
580
+ StreamEventType::Completed => StreamEvent::ResponseCompleted {
581
+ response,
582
+ sequence_number: self.next_sequence(),
583
+ },
584
+ }
585
+ }
586
+ }
587
+
588
+ #[derive(Debug, Clone, Copy)]
589
+ enum StreamEventType {
590
+ Created,
591
+ InProgress,
592
+ Completed,
593
+ }
src/adapter/mod.rs CHANGED
@@ -1,5 +1,7 @@
1
  pub mod convert_request;
 
2
  pub mod convert_stream;
3
 
4
  pub use convert_request::convert_anthropic_to_openrouter;
 
5
  pub use convert_stream::StreamConverter;
 
1
  pub mod convert_request;
2
+ pub mod convert_responses;
3
  pub mod convert_stream;
4
 
5
  pub use convert_request::convert_anthropic_to_openrouter;
6
+ pub use convert_responses::{convert_responses_to_openrouter, ResponsesStreamConverter};
7
  pub use convert_stream::StreamConverter;
src/handlers/mod.rs CHANGED
@@ -1,5 +1,7 @@
1
  pub mod count_tokens;
2
  pub mod messages;
 
3
 
4
  pub use count_tokens::count_tokens;
5
  pub use messages::messages;
 
 
1
  pub mod count_tokens;
2
  pub mod messages;
3
+ pub mod responses;
4
 
5
  pub use count_tokens::count_tokens;
6
  pub use messages::messages;
7
+ pub use responses::responses;
src/handlers/responses.rs ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use axum::{
2
+ body::Body,
3
+ extract::State,
4
+ http::{header, HeaderMap, StatusCode},
5
+ response::Response,
6
+ Json,
7
+ };
8
+ use futures::StreamExt;
9
+ use tokio_stream::wrappers::ReceiverStream;
10
+
11
+ use crate::adapter::{convert_responses_to_openrouter, ResponsesStreamConverter};
12
+ use crate::error::AppError;
13
+ use crate::types::openrouter::ChatCompletionChunk;
14
+ use crate::types::responses::CreateResponseRequest;
15
+ use crate::AppState;
16
+
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")
25
+ .and_then(|v| v.to_str().ok())
26
+ .map(|s| s.to_string())
27
+ .or_else(|| {
28
+ headers
29
+ .get(header::AUTHORIZATION)
30
+ .and_then(|v| v.to_str().ok())
31
+ .and_then(|s| s.strip_prefix("Bearer "))
32
+ .map(|s| s.to_string())
33
+ })
34
+ .or_else(|| {
35
+ if state.openrouter_api_key.is_empty() {
36
+ None
37
+ } else {
38
+ Some(state.openrouter_api_key.clone())
39
+ }
40
+ })
41
+ .ok_or_else(|| AppError::InvalidRequest("Missing API key".to_string()))?;
42
+
43
+ let request_id = uuid::Uuid::new_v4().to_string()[..8].to_string();
44
+ tracing::info!(req = %request_id, model = %request.model, "POST /v1/responses");
45
+
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
+
52
+ let response = state
53
+ .http_client
54
+ .post(&url)
55
+ .header("Authorization", format!("Bearer {}", api_key))
56
+ .header("Content-Type", "application/json")
57
+ .header("Accept", "text/event-stream")
58
+ .json(&openrouter_request)
59
+ .send()
60
+ .await
61
+ .map_err(|e| AppError::RequestError(e.to_string()))?;
62
+
63
+ if !response.status().is_success() {
64
+ let status = response.status();
65
+ let body = response.text().await.unwrap_or_default();
66
+ return Err(AppError::UpstreamError(status.as_u16(), body));
67
+ }
68
+
69
+ // Create SSE stream
70
+ let (tx, rx) = tokio::sync::mpsc::channel::<Result<String, std::io::Error>>(100);
71
+
72
+ // Spawn task to handle SSE conversion
73
+ let req_id = request_id.clone();
74
+ tokio::spawn(async move {
75
+ let mut converter = ResponsesStreamConverter::new();
76
+ let mut stream = response.bytes_stream();
77
+ let mut buffer = String::new();
78
+
79
+ while let Some(chunk_result) = stream.next().await {
80
+ match chunk_result {
81
+ Ok(bytes) => {
82
+ let text = match String::from_utf8(bytes.to_vec()) {
83
+ Ok(t) => t,
84
+ Err(_) => continue,
85
+ };
86
+ buffer.push_str(&text);
87
+
88
+ // Process complete SSE events from buffer
89
+ while let Some(event_end) = buffer.find("\n\n") {
90
+ let event_str = buffer[..event_end].to_string();
91
+ buffer = buffer[event_end + 2..].to_string();
92
+
93
+ // Parse SSE event
94
+ let mut data = String::new();
95
+ for line in event_str.lines() {
96
+ if let Some(rest) = line.strip_prefix("data:") {
97
+ data = rest.trim().to_string();
98
+ } else if line.starts_with("data:") {
99
+ data = line[5..].trim().to_string();
100
+ }
101
+ }
102
+
103
+ if data == "[DONE]" {
104
+ tracing::debug!(req = %req_id, "Stream completed");
105
+ let finish_events = converter.finish();
106
+ for event in finish_events {
107
+ if tx.send(Ok(event.to_sse_string())).await.is_err() {
108
+ return;
109
+ }
110
+ }
111
+ return;
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
+ }
125
+ }
126
+ Err(_) => {
127
+ break;
128
+ }
129
+ }
130
+ }
131
+
132
+ // If we exit the loop without [DONE], still finish
133
+ let finish_events = converter.finish();
134
+ for event in finish_events {
135
+ let _ = tx.send(Ok(event.to_sse_string())).await;
136
+ }
137
+ });
138
+
139
+ // Create response stream
140
+ let stream = ReceiverStream::new(rx);
141
+ let body = Body::from_stream(stream);
142
+
143
+ Ok(Response::builder()
144
+ .status(StatusCode::OK)
145
+ .header(header::CONTENT_TYPE, "text/event-stream")
146
+ .header(header::CACHE_CONTROL, "no-cache")
147
+ .header(header::CONNECTION, "keep-alive")
148
+ .body(body)
149
+ .unwrap())
150
+ }
src/server.rs CHANGED
@@ -12,6 +12,7 @@ pub fn create_router(state: AppState) -> Router {
12
  .route("/", get(health_check))
13
  .route("/v1/messages", post(handlers::messages))
14
  .route("/v1/messages/count_tokens", post(handlers::count_tokens))
 
15
  .layer(TraceLayer::new_for_http())
16
  .with_state(state)
17
  }
 
12
  .route("/", get(health_check))
13
  .route("/v1/messages", post(handlers::messages))
14
  .route("/v1/messages/count_tokens", post(handlers::count_tokens))
15
+ .route("/v1/responses", post(handlers::responses))
16
  .layer(TraceLayer::new_for_http())
17
  .with_state(state)
18
  }
src/types/mod.rs CHANGED
@@ -1,2 +1,3 @@
1
  pub mod anthropic;
2
  pub mod openrouter;
 
 
1
  pub mod anthropic;
2
  pub mod openrouter;
3
+ pub mod responses;
src/types/openrouter.rs CHANGED
@@ -2,7 +2,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
2
  use serde_json::value::RawValue;
3
 
4
  // Request types
5
- #[derive(Debug, Clone, Serialize, Deserialize)]
6
  pub struct CreateChatCompletionRequest {
7
  pub messages: Vec<ChatCompletionMessage>,
8
  pub model: String,
 
2
  use serde_json::value::RawValue;
3
 
4
  // Request types
5
+ #[derive(Debug, Clone, Default, Serialize, Deserialize)]
6
  pub struct CreateChatCompletionRequest {
7
  pub messages: Vec<ChatCompletionMessage>,
8
  pub model: String,
src/types/responses.rs ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use serde::{Deserialize, Serialize};
2
+ use serde_json::Value;
3
+
4
+ // ============== Request Types ==============
5
+
6
+ #[derive(Debug, Clone, Serialize, Deserialize)]
7
+ pub struct CreateResponseRequest {
8
+ pub model: String,
9
+ pub input: Input,
10
+ #[serde(default)]
11
+ pub stream: bool,
12
+ #[serde(skip_serializing_if = "Option::is_none")]
13
+ pub instructions: Option<String>,
14
+ #[serde(skip_serializing_if = "Option::is_none")]
15
+ pub max_output_tokens: Option<i64>,
16
+ #[serde(skip_serializing_if = "Option::is_none")]
17
+ pub temperature: Option<f64>,
18
+ #[serde(skip_serializing_if = "Option::is_none")]
19
+ pub top_p: Option<f64>,
20
+ #[serde(skip_serializing_if = "Option::is_none")]
21
+ pub tools: Option<Vec<Tool>>,
22
+ #[serde(skip_serializing_if = "Option::is_none")]
23
+ pub tool_choice: Option<ToolChoice>,
24
+ #[serde(skip_serializing_if = "Option::is_none")]
25
+ pub metadata: Option<std::collections::HashMap<String, String>>,
26
+ }
27
+
28
+ #[derive(Debug, Clone, Serialize, Deserialize)]
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)]
63
+ #[serde(tag = "type")]
64
+ pub enum ContentPart {
65
+ #[serde(rename = "input_text")]
66
+ InputText { text: String },
67
+ #[serde(rename = "input_image")]
68
+ InputImage { image_url: String },
69
+ #[serde(rename = "output_text")]
70
+ OutputText { text: String },
71
+ }
72
+
73
+ #[derive(Debug, Clone, Serialize, Deserialize)]
74
+ #[serde(tag = "type")]
75
+ pub enum Tool {
76
+ #[serde(rename = "function")]
77
+ Function {
78
+ name: String,
79
+ #[serde(skip_serializing_if = "Option::is_none")]
80
+ description: Option<String>,
81
+ #[serde(skip_serializing_if = "Option::is_none")]
82
+ parameters: Option<Value>,
83
+ #[serde(skip_serializing_if = "Option::is_none")]
84
+ strict: Option<bool>,
85
+ },
86
+ }
87
+
88
+ #[derive(Debug, Clone, Serialize, Deserialize)]
89
+ #[serde(untagged)]
90
+ pub enum ToolChoice {
91
+ Auto(String), // "auto", "none", "required"
92
+ Function {
93
+ #[serde(rename = "type")]
94
+ choice_type: String,
95
+ name: String
96
+ },
97
+ }
98
+
99
+ // ============== Response Types ==============
100
+
101
+ #[derive(Debug, Clone, Serialize, Deserialize)]
102
+ pub struct Response {
103
+ pub id: String,
104
+ pub object: String, // "response"
105
+ pub created_at: i64,
106
+ pub model: String,
107
+ pub status: ResponseStatus,
108
+ #[serde(skip_serializing_if = "Option::is_none")]
109
+ pub error: Option<ResponseError>,
110
+ #[serde(skip_serializing_if = "Option::is_none")]
111
+ pub instructions: Option<String>,
112
+ #[serde(skip_serializing_if = "Option::is_none")]
113
+ pub max_output_tokens: Option<i64>,
114
+ #[serde(skip_serializing_if = "Option::is_none")]
115
+ pub temperature: Option<f64>,
116
+ #[serde(skip_serializing_if = "Option::is_none")]
117
+ pub top_p: Option<f64>,
118
+ pub output: Vec<OutputItem>,
119
+ #[serde(skip_serializing_if = "Option::is_none")]
120
+ pub usage: Option<ResponseUsage>,
121
+ }
122
+
123
+ #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
124
+ #[serde(rename_all = "snake_case")]
125
+ pub enum ResponseStatus {
126
+ InProgress,
127
+ Completed,
128
+ Failed,
129
+ }
130
+
131
+ #[derive(Debug, Clone, Serialize, Deserialize)]
132
+ pub struct ResponseError {
133
+ pub code: String,
134
+ pub message: String,
135
+ }
136
+
137
+ #[derive(Debug, Clone, Serialize, Deserialize)]
138
+ pub struct ResponseUsage {
139
+ pub input_tokens: i64,
140
+ pub output_tokens: i64,
141
+ pub total_tokens: i64,
142
+ }
143
+
144
+ // ============== Output Items ==============
145
+
146
+ #[derive(Debug, Clone, Serialize, Deserialize)]
147
+ #[serde(tag = "type")]
148
+ pub enum OutputItem {
149
+ #[serde(rename = "message")]
150
+ Message {
151
+ id: String,
152
+ role: String,
153
+ status: OutputStatus,
154
+ content: Vec<OutputContent>,
155
+ },
156
+ #[serde(rename = "reasoning")]
157
+ Reasoning {
158
+ id: String,
159
+ status: OutputStatus,
160
+ content: Vec<ReasoningContent>,
161
+ },
162
+ #[serde(rename = "function_call")]
163
+ FunctionCall {
164
+ id: String,
165
+ call_id: String,
166
+ name: String,
167
+ arguments: String,
168
+ #[serde(skip_serializing_if = "Option::is_none")]
169
+ status: Option<OutputStatus>,
170
+ },
171
+ }
172
+
173
+ #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
174
+ #[serde(rename_all = "snake_case")]
175
+ pub enum OutputStatus {
176
+ InProgress,
177
+ Completed,
178
+ Incomplete,
179
+ }
180
+
181
+ #[derive(Debug, Clone, Serialize, Deserialize)]
182
+ #[serde(tag = "type")]
183
+ pub enum OutputContent {
184
+ #[serde(rename = "output_text")]
185
+ Text { text: String },
186
+ #[serde(rename = "refusal")]
187
+ Refusal { refusal: String },
188
+ }
189
+
190
+ #[derive(Debug, Clone, Serialize, Deserialize)]
191
+ #[serde(tag = "type")]
192
+ pub enum ReasoningContent {
193
+ #[serde(rename = "reasoning_text")]
194
+ Text { text: String },
195
+ }
196
+
197
+ // ============== Streaming Events ==============
198
+
199
+ #[derive(Debug, Clone, Serialize, Deserialize)]
200
+ #[serde(tag = "type")]
201
+ pub enum StreamEvent {
202
+ #[serde(rename = "response.created")]
203
+ ResponseCreated {
204
+ response: Response,
205
+ sequence_number: i64,
206
+ },
207
+ #[serde(rename = "response.in_progress")]
208
+ ResponseInProgress {
209
+ response: Response,
210
+ sequence_number: i64,
211
+ },
212
+ #[serde(rename = "response.completed")]
213
+ ResponseCompleted {
214
+ response: Response,
215
+ sequence_number: i64,
216
+ },
217
+ #[serde(rename = "response.failed")]
218
+ ResponseFailed {
219
+ response: Response,
220
+ sequence_number: i64,
221
+ },
222
+ #[serde(rename = "response.output_item.added")]
223
+ OutputItemAdded {
224
+ output_index: i64,
225
+ item: OutputItem,
226
+ sequence_number: i64,
227
+ },
228
+ #[serde(rename = "response.output_item.done")]
229
+ OutputItemDone {
230
+ output_index: i64,
231
+ item: OutputItem,
232
+ sequence_number: i64,
233
+ },
234
+ #[serde(rename = "response.content_part.added")]
235
+ ContentPartAdded {
236
+ item_id: String,
237
+ output_index: i64,
238
+ content_index: i64,
239
+ part: OutputContent,
240
+ sequence_number: i64,
241
+ },
242
+ #[serde(rename = "response.content_part.done")]
243
+ ContentPartDone {
244
+ item_id: String,
245
+ output_index: i64,
246
+ content_index: i64,
247
+ part: OutputContent,
248
+ sequence_number: i64,
249
+ },
250
+ #[serde(rename = "response.output_text.delta")]
251
+ OutputTextDelta {
252
+ item_id: String,
253
+ output_index: i64,
254
+ content_index: i64,
255
+ delta: String,
256
+ sequence_number: i64,
257
+ },
258
+ #[serde(rename = "response.output_text.done")]
259
+ OutputTextDone {
260
+ item_id: String,
261
+ output_index: i64,
262
+ content_index: i64,
263
+ text: String,
264
+ sequence_number: i64,
265
+ },
266
+ #[serde(rename = "response.reasoning_text.delta")]
267
+ ReasoningTextDelta {
268
+ item_id: String,
269
+ output_index: i64,
270
+ content_index: i64,
271
+ delta: String,
272
+ sequence_number: i64,
273
+ },
274
+ #[serde(rename = "response.reasoning_text.done")]
275
+ ReasoningTextDone {
276
+ item_id: String,
277
+ output_index: i64,
278
+ content_index: i64,
279
+ text: String,
280
+ sequence_number: i64,
281
+ },
282
+ #[serde(rename = "response.function_call_arguments.delta")]
283
+ FunctionCallArgumentsDelta {
284
+ item_id: String,
285
+ output_index: i64,
286
+ delta: String,
287
+ sequence_number: i64,
288
+ },
289
+ #[serde(rename = "response.function_call_arguments.done")]
290
+ FunctionCallArgumentsDone {
291
+ item_id: String,
292
+ output_index: i64,
293
+ arguments: String,
294
+ sequence_number: i64,
295
+ },
296
+ }
297
+
298
+ impl StreamEvent {
299
+ pub fn to_sse_string(&self) -> String {
300
+ let json = serde_json::to_string(self).unwrap_or_default();
301
+ format!("data: {}\n\n", json)
302
+ }
303
+ }