codetether_agent/provider/bedrock/
response.rs1use crate::provider::{CompletionResponse, ContentPart, FinishReason, Message, Role, Usage};
22use anyhow::{Context, Result};
23use serde::Deserialize;
24use serde_json::Value;
25
26#[derive(Debug, Deserialize)]
27#[serde(rename_all = "camelCase")]
28struct ConverseResponse {
29 output: ConverseOutput,
30 #[serde(default)]
31 stop_reason: Option<String>,
32 #[serde(default)]
33 usage: Option<ConverseUsage>,
34}
35
36#[derive(Debug, Deserialize)]
37struct ConverseOutput {
38 message: ConverseMessage,
39}
40
41#[derive(Debug, Deserialize)]
42struct ConverseMessage {
43 #[allow(dead_code)]
44 role: String,
45 content: Vec<ConverseContent>,
46}
47
48#[derive(Debug, Deserialize)]
49#[serde(untagged)]
50enum ConverseContent {
51 ReasoningContent {
52 #[serde(rename = "reasoningContent")]
53 reasoning_content: ReasoningContentBlock,
54 },
55 Text {
56 text: String,
57 },
58 ToolUse {
59 #[serde(rename = "toolUse")]
60 tool_use: ConverseToolUse,
61 },
62}
63
64#[derive(Debug, Deserialize)]
65#[serde(rename_all = "camelCase")]
66struct ReasoningContentBlock {
67 reasoning_text: ReasoningText,
68}
69
70#[derive(Debug, Deserialize)]
71struct ReasoningText {
72 text: String,
73}
74
75#[derive(Debug, Deserialize)]
76#[serde(rename_all = "camelCase")]
77struct ConverseToolUse {
78 tool_use_id: String,
79 name: String,
80 input: Value,
81}
82
83#[derive(Debug, Deserialize)]
84#[serde(rename_all = "camelCase")]
85struct ConverseUsage {
86 #[serde(default)]
87 input_tokens: usize,
88 #[serde(default)]
89 output_tokens: usize,
90 #[serde(default)]
91 total_tokens: usize,
92}
93
94#[derive(Debug, Deserialize)]
96pub struct BedrockError {
97 pub message: String,
99}
100
101pub fn parse_converse_response(text: &str) -> Result<CompletionResponse> {
121 let response: ConverseResponse = serde_json::from_str(text).context(format!(
122 "Failed to parse Bedrock response: {}",
123 crate::util::truncate_bytes_safe(text, 300)
124 ))?;
125
126 let mut content = Vec::new();
127 let mut has_tool_calls = false;
128
129 for part in &response.output.message.content {
130 match part {
131 ConverseContent::ReasoningContent { reasoning_content } => {
132 if !reasoning_content.reasoning_text.text.is_empty() {
133 content.push(ContentPart::Thinking {
134 text: reasoning_content.reasoning_text.text.clone(),
135 });
136 }
137 }
138 ConverseContent::Text { text } => {
139 if !text.is_empty() {
140 content.push(ContentPart::Text { text: text.clone() });
141 }
142 }
143 ConverseContent::ToolUse { tool_use } => {
144 has_tool_calls = true;
145 content.push(ContentPart::ToolCall {
146 id: tool_use.tool_use_id.clone(),
147 name: tool_use.name.clone(),
148 arguments: serde_json::to_string(&tool_use.input).unwrap_or_default(),
149 thought_signature: None,
150 });
151 }
152 }
153 }
154
155 let finish_reason = if has_tool_calls {
156 FinishReason::ToolCalls
157 } else {
158 match response.stop_reason.as_deref() {
159 Some("end_turn") | Some("stop") | Some("stop_sequence") => FinishReason::Stop,
160 Some("max_tokens") => FinishReason::Length,
161 Some("tool_use") => FinishReason::ToolCalls,
162 Some("content_filtered") => FinishReason::ContentFilter,
163 _ => FinishReason::Stop,
164 }
165 };
166
167 let usage = response.usage.as_ref();
168
169 Ok(CompletionResponse {
170 message: Message {
171 role: Role::Assistant,
172 content,
173 },
174 usage: Usage {
175 prompt_tokens: usage.map(|u| u.input_tokens).unwrap_or(0),
176 completion_tokens: usage.map(|u| u.output_tokens).unwrap_or(0),
177 total_tokens: usage.map(|u| u.total_tokens).unwrap_or(0),
178 cache_read_tokens: None,
179 cache_write_tokens: None,
180 },
181 finish_reason,
182 })
183}