1use std::fmt;
2use serde::{Deserialize, Serialize};
3
4
5
6#[derive(Serialize, Deserialize, Debug)]
7pub struct OpenAIResponse {
8 pub id: String,
9 pub object: String,
10 pub created: i64,
11 pub model: String,
12 pub choices: Vec<OpenAIChoice>,
13 pub usage: OpenAIUsage,
14}
15
16#[derive(Serialize, Deserialize, Debug, Default)]
17pub(crate) struct OpenAIUsage {
18 pub prompt_tokens: usize,
19 pub completion_tokens: usize,
20 pub total_tokens: usize,
21}
22#[derive(Serialize, Deserialize, Debug)]
23pub struct AnthropicResponse {
24 pub id: String,
25 pub role: String,
26 pub content: Vec<AnthropicContentBlock>,
27 pub model: String,
28 pub stop_reason: String,
29 pub stop_sequence: Option<String>,
30 pub usage: AnthropicUsage,
31}
32
33#[derive(Serialize, Deserialize, Debug)]
35#[serde(untagged)]
36pub enum AnthropicContentBlock {
37 Text {
39 text: String,
41 #[serde(rename = "type")]
43 block_type: String,
44 },
45 ToolUse {
48 #[serde(rename = "type")]
50 block_type: String,
51 id: String,
53 name: String,
55 input: serde_json::Value,
58 },
59}
60
61#[derive(Serialize, Deserialize, Debug)]
66#[serde(untagged)]
67pub enum ResponseMessage {
68 Anthropic(AnthropicResponse),
69 OpenAI(OpenAIResponse),
70}
71
72impl ResponseMessage {
73 pub fn first_message(&self) -> String {
92 match self {
93 ResponseMessage::Anthropic(response) => {
94 if let Some(content) = response.content.first() {
95 match content {
96 AnthropicContentBlock::Text { text, .. } => text.clone(),
97 AnthropicContentBlock::ToolUse { .. } => String::new(), }
99 } else {
100 String::new()
101 }
102 }
103 ResponseMessage::OpenAI(response) => {
104 if let Some(choice) = response.choices.first() {
105 choice.message.content.clone().unwrap_or_else(|| {
106 if let Some(tool_calls) = &choice.message.tool_calls {
108 if let Some(first_tool) = tool_calls.first() {
109 format!("Function call: {}", first_tool.function.name)
110 } else {
111 String::new()
112 }
113 } else {
114 String::new()
115 }
116 })
117 } else {
118 String::new()
119 }
120 }
121 }
122 }
123
124 pub fn tools(&self) -> Option<Vec<ToolResponse>> {
125 match self {
126 ResponseMessage::Anthropic(response) => {
127 let tool_uses: Vec<ToolResponse> = response.content.iter()
128 .filter_map(|block| {
129 if let AnthropicContentBlock::ToolUse { id, name, input, .. } = block {
130 Some(ToolResponse {
131 id: id.clone(),
132 name: name.clone(),
133 input: input.clone(),
134 })
135 } else {
136 None
137 }
138 })
139 .collect();
140 if tool_uses.is_empty() { None } else { Some(tool_uses) }
141 },
142 ResponseMessage::OpenAI(response) => {
143 let tool_calls: Vec<ToolResponse> = response.choices.iter()
144 .filter_map(|choice| choice.message.tool_calls.as_ref())
145 .flatten()
146 .map(|tool_call| ToolResponse {
147 id: tool_call.id.clone(),
148 name: tool_call.function.name.clone(),
149 input: serde_json::from_str(&tool_call.function.arguments).unwrap_or(serde_json::Value::Null),
150 })
151 .collect();
152 if tool_calls.is_empty() { None } else { Some(tool_calls) }
153 },
154 }
155 }
156
157 pub fn role(&self) -> &str {
176 match self {
177 ResponseMessage::Anthropic(response) => &response.role,
178 ResponseMessage::OpenAI(response) => {
179 if let Some(choice) = response.choices.first() {
180 &choice.message.role
181 } else {
182 ""
183 }
184 }
185 }
186 }
187
188 pub fn model(&self) -> &str {
207 match self {
208 ResponseMessage::Anthropic(response) => &response.model,
209 ResponseMessage::OpenAI(response) => &response.model,
210 }
211 }
212
213 pub fn stop_reason(&self) -> &str {
232 match self {
233 ResponseMessage::Anthropic(response) => &response.stop_reason,
234 ResponseMessage::OpenAI(response) => {
235 if let Some(choice) = response.choices.first() {
236 &choice.finish_reason
237 } else {
238 ""
239 }
240 }
241 }
242 }
243
244 pub fn usage(&self) -> CommonUsage {
264 match self {
265 ResponseMessage::Anthropic(response) => CommonUsage {
266 input_tokens: response.usage.input_tokens,
267 output_tokens: response.usage.output_tokens,
268 },
269 ResponseMessage::OpenAI(response) => CommonUsage {
270 input_tokens: response.usage.prompt_tokens,
271 output_tokens: response.usage.completion_tokens,
272 },
273 }
274 }
275}
276
277impl fmt::Display for ResponseMessage {
278 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
279 match self {
280 ResponseMessage::Anthropic(response) => {
281 write!(
282 f,
283 "ResponseMessage {{ id: {}, role: {}, content: {:?} }}",
284 response.id, response.role, response.content
285 )
286 }
287 ResponseMessage::OpenAI(response) => {
288 write!(
289 f,
290 "ResponseMessage {{ id: {}, object: {}, model: {}, choices: {:?} }}",
291 response.id, response.object, response.model, response.choices
292 )
293 }
294 }
295 }
296}
297
298
299#[derive(Serialize, Deserialize, Debug, Default)]
301pub struct AnthropicUsage {
302 pub input_tokens: usize,
303 pub output_tokens: usize,
304}
305
306#[derive(Serialize, Deserialize, Debug, Default)]
307pub struct CommonUsage {
308 pub input_tokens: usize,
309 pub output_tokens: usize,
310}
311
312#[derive(Serialize, Deserialize, Debug)]
313pub struct OpenAIChoice {
314 pub index: usize,
315 pub message: OpenAIMessage,
316 pub finish_reason: String,
317}
318
319#[derive(Serialize, Deserialize, Debug)]
320pub struct OpenAIMessage {
321 pub role: String,
322 pub content: Option<String>,
323 pub tool_calls: Option<Vec<OpenAIToolCall>>,
324}
325
326#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
327pub struct ToolResponse {
328 pub id: String,
329 pub name: String,
330 pub input: serde_json::Value,
331}
332
333
334#[derive(Serialize, Deserialize, Debug)]
335pub struct OpenAIToolCall {
336 pub id: String,
337 #[serde(rename = "type")]
338 pub call_type: String,
339 pub function: OpenAIFunction,
340}
341
342#[derive(Serialize, Deserialize, Debug)]
343pub struct OpenAIFunction {
344 pub name: String,
345 pub arguments: String,
346}
347
348#[cfg(test)]
349
350mod tests {
351 use super::*;
352 use serde_json::json;
353 use crate::response::{AnthropicContentBlock, AnthropicResponse};
354
355 #[test]
356 fn test_anthropic_response_deserialization() {
357 let json_response = json!({
358 "id": "msg_01KGgxCr7Lm9gi1kfaZWWJUs",
359 "type": "message",
360 "role": "assistant",
361 "model": "claude-3-haiku-20240307",
362 "content": [
363 {
364 "type": "tool_use",
365 "id": "toolu_01RQ6pzGpMxBBCirxUcSBokz",
366 "name": "get_weather",
367 "input": {
368 "location": "San Francisco, CA",
369 "unit": "celsius"
370 }
371 }
372 ],
373 "stop_reason": "tool_use",
374 "stop_sequence": null,
375 "usage": {
376 "input_tokens": 406,
377 "output_tokens": 73
378 }
379 });
380
381 let response: AnthropicResponse = serde_json::from_value(json_response).unwrap();
382
383 assert_eq!(response.id, "msg_01KGgxCr7Lm9gi1kfaZWWJUs");
384 assert_eq!(response.role, "assistant");
385 assert_eq!(response.model, "claude-3-haiku-20240307");
386 assert_eq!(response.stop_reason, "tool_use");
387 assert_eq!(response.stop_sequence, None);
388 assert_eq!(response.usage.input_tokens, 406);
389 assert_eq!(response.usage.output_tokens, 73);
390
391 assert_eq!(response.content.len(), 1);
392 if let AnthropicContentBlock::ToolUse { id, name, input, .. } = &response.content[0] {
393 assert_eq!(id, "toolu_01RQ6pzGpMxBBCirxUcSBokz");
394 assert_eq!(name, "get_weather");
395 assert_eq!(input["location"], "San Francisco, CA");
396 assert_eq!(input["unit"], "celsius");
397 } else {
398 panic!("Expected ToolUse content block");
399 }
400 }
401
402 #[test]
403 fn test_anthropic_response_text_content() {
404 let json_response = json!({
405 "id": "msg_text_example",
406 "type": "message",
407 "role": "assistant",
408 "model": "claude-3-haiku-20240307",
409 "content": [
410 {
411 "type": "text",
412 "text": "This is a text response."
413 }
414 ],
415 "stop_reason": "end_turn",
416 "stop_sequence": null,
417 "usage": {
418 "input_tokens": 10,
419 "output_tokens": 20
420 }
421 });
422
423 let response: AnthropicResponse = serde_json::from_value(json_response).unwrap();
424
425 assert_eq!(response.id, "msg_text_example");
426 assert_eq!(response.role, "assistant");
427 assert_eq!(response.model, "claude-3-haiku-20240307");
428 assert_eq!(response.stop_reason, "end_turn");
429
430 assert_eq!(response.content.len(), 1);
431 if let AnthropicContentBlock::Text { text, .. } = &response.content[0] {
432 assert_eq!(text, "This is a text response.");
433 } else {
434 panic!("Expected Text content block");
435 }
436 }
437
438 #[test]
439 fn test_anthropic_response_mixed_content() {
440 let json_response = json!({
441 "id": "msg_mixed_example",
442 "type": "message",
443 "role": "assistant",
444 "model": "claude-3-haiku-20240307",
445 "content": [
446 {
447 "type": "text",
448 "text": "Here's the weather information:"
449 },
450 {
451 "type": "tool_use",
452 "id": "toolu_mixed_example",
453 "name": "get_weather",
454 "input": {
455 "location": "New York, NY",
456 "unit": "fahrenheit"
457 }
458 }
459 ],
460 "stop_reason": "end_turn",
461 "stop_sequence": null,
462 "usage": {
463 "input_tokens": 50,
464 "output_tokens": 60
465 }
466 });
467
468 let response: AnthropicResponse = serde_json::from_value(json_response).unwrap();
469
470 assert_eq!(response.content.len(), 2);
471
472 match &response.content[0] {
473 AnthropicContentBlock::Text { text, .. } => {
474 assert_eq!(text, "Here's the weather information:");
475 },
476 _ => panic!("Expected Text content block"),
477 }
478
479 match &response.content[1] {
480 AnthropicContentBlock::ToolUse { id, name, input, .. } => {
481 assert_eq!(id, "toolu_mixed_example");
482 assert_eq!(name, "get_weather");
483 assert_eq!(input["location"], "New York, NY");
484 assert_eq!(input["unit"], "fahrenheit");
485 },
486 _ => panic!("Expected ToolUse content block"),
487 }
488 }
489
490 #[test]
491 fn test_openai_response_deserialization() {
492 let json_response = json!({
493 "id": "chatcmpl-9p5LSmflVqlG0Gk6ryp14XHKbNah8",
494 "object": "chat.completion",
495 "created": 1721962302,
496 "model": "gpt-4o-2024-05-13",
497 "choices": [
498 {
499 "index": 0,
500 "message": {
501 "role": "assistant",
502 "content": null,
503 "tool_calls": [
504 {
505 "id": "call_5dENonKES2CcyWt6yGAXPDtz",
506 "type": "function",
507 "function": {
508 "name": "get_weather",
509 "arguments": "{\"location\":\"San Francisco, CA\"}"
510 }
511 }
512 ]
513 },
514 "logprobs": null,
515 "finish_reason": "tool_calls"
516 }
517 ],
518 "usage": {
519 "prompt_tokens": 106,
520 "completion_tokens": 17,
521 "total_tokens": 123
522 },
523 "system_fingerprint": "fp_400f27fa1f"
524 });
525
526 let response: OpenAIResponse = serde_json::from_value(json_response).unwrap();
527
528 assert_eq!(response.id, "chatcmpl-9p5LSmflVqlG0Gk6ryp14XHKbNah8");
529 assert_eq!(response.object, "chat.completion");
530 assert_eq!(response.created, 1721962302);
531 assert_eq!(response.model, "gpt-4o-2024-05-13");
532
533 assert_eq!(response.choices.len(), 1);
534 let choice = &response.choices[0];
535 assert_eq!(choice.index, 0);
536 assert_eq!(choice.finish_reason, "tool_calls");
537
538 let message = &choice.message;
539 assert_eq!(message.role, "assistant");
540 assert_eq!(message.content, None);
541
542 assert!(message.tool_calls.is_some());
543 let tool_calls = message.tool_calls.as_ref().unwrap();
544 assert_eq!(tool_calls.len(), 1);
545 let tool_call = &tool_calls[0];
546 assert_eq!(tool_call.id, "call_5dENonKES2CcyWt6yGAXPDtz");
547 assert_eq!(tool_call.call_type, "function");
548 assert_eq!(tool_call.function.name, "get_weather");
549 assert_eq!(tool_call.function.arguments, "{\"location\":\"San Francisco, CA\"}");
550
551 assert_eq!(response.usage.prompt_tokens, 106);
552 assert_eq!(response.usage.completion_tokens, 17);
553 assert_eq!(response.usage.total_tokens, 123);
554
555 }
556
557 #[test]
558 fn test_openai_response_tool_calls() {
559 let json_response = json!({
560 "id": "chatcmpl-9p5LSmflVqlG0Gk6ryp14XHKbNah8",
561 "object": "chat.completion",
562 "created": 1721962302,
563 "model": "gpt-4o-2024-05-13",
564 "choices": [
565 {
566 "index": 0,
567 "message": {
568 "role": "assistant",
569 "content": null,
570 "tool_calls": [
571 {
572 "id": "call_5dENonKES2CcyWt6yGAXPDtz",
573 "type": "function",
574 "function": {
575 "name": "get_weather",
576 "arguments": "{\"location\":\"San Francisco, CA\"}"
577 }
578 }
579 ]
580 },
581 "finish_reason": "tool_calls"
582 }
583 ],
584 "usage": {
585 "prompt_tokens": 106,
586 "completion_tokens": 17,
587 "total_tokens": 123
588 }
589 });
590
591 let response: OpenAIResponse = serde_json::from_value(json_response).unwrap();
592 let response_message = ResponseMessage::OpenAI(response);
593
594 if let Some(tools) = response_message.tools() {
595 assert_eq!(tools.len(), 1);
596 assert_eq!(tools[0].name, "get_weather");
597 assert_eq!(tools[0].id, "call_5dENonKES2CcyWt6yGAXPDtz");
598
599 let input: serde_json::Value = serde_json::from_str(&tools[0].input.to_string()).unwrap();
600 assert_eq!(input["location"], "San Francisco, CA");
601 } else {
602 panic!("Expected tool calls, but none were found");
603 }
604
605 assert_eq!(response_message.stop_reason(), "tool_calls");
606 }
607
608 #[test]
609 fn test_openai_response_no_tool_calls() {
610 let json_response = json!({
611 "id": "chatcmpl-123",
612 "object": "chat.completion",
613 "created": 1721962302,
614 "model": "gpt-4o-2024-05-13",
615 "choices": [
616 {
617 "index": 0,
618 "message": {
619 "role": "assistant",
620 "content": "This is a regular response without tool calls."
621 },
622 "finish_reason": "stop"
623 }
624 ],
625 "usage": {
626 "prompt_tokens": 10,
627 "completion_tokens": 10,
628 "total_tokens": 20
629 }
630 });
631
632 let response: OpenAIResponse = serde_json::from_value(json_response).unwrap();
633 let response_message = ResponseMessage::OpenAI(response);
634 assert_eq!(response_message.tools(), None);
635 assert_eq!(response_message.stop_reason(), "stop");
636 assert_eq!(response_message.first_message(), "This is a regular response without tool calls.");
637 }
638}