@@ -2,14 +2,16 @@ use crate::client::{
2
2
ContentBlock , ConverseRequest , ConverseResponse , ImageFormat , ImageSource as ClientImageSource ,
3
3
InferenceConfig , Message as ClientMessage , Role as ClientRole , StopReason , SystemContentBlock ,
4
4
Tool , ToolChoice , ToolConfig , ToolSpec , ToolResultContentBlock , ToolResultStatus ,
5
+ ImageBlock , ToolUseBlock , ToolResultBlock , ToolInputSchema , ToolChoiceTool ,
5
6
} ;
6
7
use base64:: { engine:: general_purpose, Engine as _} ;
7
8
use golem_llm:: golem:: llm:: llm:: {
8
9
ChatEvent , CompleteResponse , Config , ContentPart , Error , ErrorCode , FinishReason ,
9
10
ImageReference , ImageSource , Message , ResponseMetadata , Role , ToolCall ,
10
11
ToolDefinition , ToolResult , Usage ,
11
12
} ;
12
- use std:: collections:: HashMap ;
13
+ use reqwest:: { Client , Url } ;
14
+ use std:: { collections:: HashMap , fs, path:: Path } ;
13
15
14
16
pub fn messages_to_request (
15
17
messages : Vec < Message > ,
@@ -69,10 +71,14 @@ pub fn messages_to_request(
69
71
70
72
let tool_choice = config. tool_choice . map ( convert_tool_choice) ;
71
73
72
- Some ( ToolConfig {
73
- tools,
74
- tool_choice,
75
- } )
74
+ if tools. is_empty ( ) {
75
+ None
76
+ } else {
77
+ Some ( ToolConfig {
78
+ tools,
79
+ tool_choice,
80
+ } )
81
+ }
76
82
} ;
77
83
78
84
Ok ( ConverseRequest {
@@ -91,11 +97,19 @@ pub fn messages_to_request(
91
97
}
92
98
93
99
fn convert_tool_choice ( tool_name : String ) -> ToolChoice {
100
+ use serde_json:: Value ;
101
+
94
102
match tool_name. as_str ( ) {
95
- "auto" => ToolChoice :: Auto ,
96
- "any" => ToolChoice :: Any ,
103
+ "auto" => ToolChoice :: Auto {
104
+ auto : Value :: Object ( serde_json:: Map :: new ( ) ) ,
105
+ } ,
106
+ "any" => ToolChoice :: Any {
107
+ any : Value :: Object ( serde_json:: Map :: new ( ) ) ,
108
+ } ,
97
109
name => ToolChoice :: Tool {
98
- name : name. to_string ( ) ,
110
+ tool : ToolChoiceTool {
111
+ name : name. to_string ( ) ,
112
+ } ,
99
113
} ,
100
114
}
101
115
}
@@ -107,10 +121,10 @@ pub fn process_response(response: ConverseResponse) -> ChatEvent {
107
121
for content in response. output . message . content {
108
122
match content {
109
123
ContentBlock :: Text { text } => contents. push ( ContentPart :: Text ( text) ) ,
110
- ContentBlock :: Image { format , source } => {
111
- match general_purpose:: STANDARD . decode ( & source. bytes ) {
124
+ ContentBlock :: Image { image } => {
125
+ match general_purpose:: STANDARD . decode ( & image . source . bytes ) {
112
126
Ok ( decoded_data) => {
113
- let mime_type = match format {
127
+ let mime_type = match image . format {
114
128
ImageFormat :: Jpeg => "image/jpeg" ,
115
129
ImageFormat :: Png => "image/png" ,
116
130
ImageFormat :: Gif => "image/gif" ,
@@ -133,14 +147,10 @@ pub fn process_response(response: ConverseResponse) -> ChatEvent {
133
147
}
134
148
}
135
149
}
136
- ContentBlock :: ToolUse {
137
- tool_use_id,
138
- name,
139
- input,
140
- } => tool_calls. push ( ToolCall {
141
- id : tool_use_id,
142
- name,
143
- arguments_json : serde_json:: to_string ( & input) . unwrap ( ) ,
150
+ ContentBlock :: ToolUse { tool_use } => tool_calls. push ( ToolCall {
151
+ id : tool_use. tool_use_id ,
152
+ name : tool_use. name ,
153
+ arguments_json : serde_json:: to_string ( & tool_use. input ) . unwrap ( ) ,
144
154
} ) ,
145
155
ContentBlock :: ToolResult { .. } => { }
146
156
}
@@ -149,7 +159,7 @@ pub fn process_response(response: ConverseResponse) -> ChatEvent {
149
159
if contents. is_empty ( ) && !tool_calls. is_empty ( ) {
150
160
ChatEvent :: ToolRequest ( tool_calls)
151
161
} else {
152
- let request_id = response. response_metadata . request_id . clone ( ) ;
162
+ let request_id = "bedrock- response" . to_string ( ) ;
153
163
154
164
let metadata = ResponseMetadata {
155
165
finish_reason : Some ( stop_reason_to_finish_reason ( response. stop_reason ) ) ,
@@ -176,9 +186,11 @@ pub fn tool_results_to_messages(
176
186
for ( tool_call, tool_result) in tool_results {
177
187
messages. push ( ClientMessage {
178
188
content : vec ! [ ContentBlock :: ToolUse {
179
- tool_use_id: tool_call. id. clone( ) ,
180
- name: tool_call. name,
181
- input: serde_json:: from_str( & tool_call. arguments_json) . unwrap( ) ,
189
+ tool_use: ToolUseBlock {
190
+ tool_use_id: tool_call. id. clone( ) ,
191
+ name: tool_call. name,
192
+ input: serde_json:: from_str( & tool_call. arguments_json) . unwrap( ) ,
193
+ } ,
182
194
} ] ,
183
195
role : ClientRole :: Assistant ,
184
196
} ) ;
@@ -200,9 +212,11 @@ pub fn tool_results_to_messages(
200
212
201
213
messages. push ( ClientMessage {
202
214
content : vec ! [ ContentBlock :: ToolResult {
203
- tool_use_id: tool_call. id,
204
- content,
205
- status,
215
+ tool_result: ToolResultBlock {
216
+ tool_use_id: tool_call. id,
217
+ content,
218
+ status,
219
+ } ,
206
220
} ] ,
207
221
role : ClientRole :: User ,
208
222
} ) ;
@@ -239,13 +253,45 @@ fn message_to_content(message: &Message) -> Result<Vec<ContentBlock>, Error> {
239
253
text : text. clone ( ) ,
240
254
} ) ,
241
255
ContentPart :: Image ( image_reference) => match image_reference {
242
- ImageReference :: Url ( _image_url) => {
243
- return Err ( Error {
244
- code : ErrorCode :: InvalidRequest ,
245
- message : "Bedrock API does not support image URLs, only base64 encoded images" . to_string ( ) ,
246
- provider_error_json : None ,
256
+ ImageReference :: Url ( image_url) => {
257
+ let url = & image_url. url ;
258
+ let mut format = ImageFormat :: Png ;
259
+ let bytes = if Url :: parse ( url) . is_ok ( ) {
260
+ let client = Client :: new ( ) ;
261
+ let response = client. get ( url) . send ( ) . map_err ( |e| Error {
262
+ code : ErrorCode :: InvalidRequest ,
263
+ message : format ! ( "Failed to fetch image from URL: {}" , e) ,
264
+ provider_error_json : None ,
265
+ } ) ;
266
+ response. map ( |r| {
267
+ format = match r. headers ( ) . get ( "Content-Type" ) . unwrap ( ) . to_str ( ) . unwrap ( ) {
268
+ "image/jpeg" => ImageFormat :: Jpeg ,
269
+ "image/png" => ImageFormat :: Png ,
270
+ "image/gif" => ImageFormat :: Gif ,
271
+ "image/webp" => ImageFormat :: Webp ,
272
+ _ => ImageFormat :: Jpeg ,
273
+ } ;
274
+ r. bytes ( ) . unwrap ( ) . to_vec ( )
275
+ } )
276
+ } else {
277
+ let path = Path :: new ( url) ;
278
+ fs:: read ( path) . map_err ( |e| Error {
279
+ code : ErrorCode :: InvalidRequest ,
280
+ message : format ! ( "Failed to read image from path: {}" , e) ,
281
+ provider_error_json : None ,
282
+ } )
283
+ } ;
284
+
285
+ let base64_data = general_purpose:: STANDARD . encode ( & bytes. unwrap ( ) ) ;
286
+ result. push ( ContentBlock :: Image {
287
+ image : ImageBlock {
288
+ format : ImageFormat :: Png ,
289
+ source : ClientImageSource {
290
+ bytes : base64_data,
291
+ } ,
292
+ } ,
247
293
} ) ;
248
- }
294
+ } ,
249
295
ImageReference :: Inline ( image_source) => {
250
296
let base64_data = general_purpose:: STANDARD . encode ( & image_source. data ) ;
251
297
let format = match image_source. mime_type . as_str ( ) {
@@ -257,9 +303,11 @@ fn message_to_content(message: &Message) -> Result<Vec<ContentBlock>, Error> {
257
303
} ;
258
304
259
305
result. push ( ContentBlock :: Image {
260
- format,
261
- source : ClientImageSource {
262
- bytes : base64_data,
306
+ image : ImageBlock {
307
+ format,
308
+ source : ClientImageSource {
309
+ bytes : base64_data,
310
+ } ,
263
311
} ,
264
312
} ) ;
265
313
}
@@ -286,18 +334,34 @@ fn message_to_system_content(message: &Message) -> Vec<SystemContentBlock> {
286
334
}
287
335
288
336
fn tool_definition_to_tool ( tool : & ToolDefinition ) -> Result < Tool , Error > {
289
- match serde_json:: from_str ( & tool. parameters_schema ) {
290
- Ok ( json_schema) => Ok ( Tool {
291
- tool_spec : ToolSpec {
292
- name : tool. name . clone ( ) ,
293
- description : tool. description . clone ( ) . unwrap_or_default ( ) ,
294
- input_schema : json_schema,
337
+ use serde_json:: Value ;
338
+
339
+ let schema_value = if tool. parameters_schema . trim ( ) . is_empty ( ) {
340
+ serde_json:: json!( {
341
+ "type" : "object" ,
342
+ "properties" : { } ,
343
+ "additionalProperties" : false
344
+ } )
345
+ } else {
346
+ match serde_json:: from_str :: < Value > ( & tool. parameters_schema ) {
347
+ Ok ( value) => value,
348
+ Err ( error) => {
349
+ return Err ( Error {
350
+ code : ErrorCode :: InternalError ,
351
+ message : format ! ( "Failed to parse tool parameters for {}: {error}" , tool. name) ,
352
+ provider_error_json : None ,
353
+ } ) ;
354
+ }
355
+ }
356
+ } ;
357
+
358
+ Ok ( Tool {
359
+ tool_spec : ToolSpec {
360
+ name : tool. name . clone ( ) ,
361
+ description : tool. description . clone ( ) . unwrap_or_default ( ) ,
362
+ input_schema : ToolInputSchema {
363
+ json : schema_value,
295
364
} ,
296
- } ) ,
297
- Err ( error) => Err ( Error {
298
- code : ErrorCode :: InternalError ,
299
- message : format ! ( "Failed to parse tool parameters for {}: {error}" , tool. name) ,
300
- provider_error_json : None ,
301
- } ) ,
302
- }
365
+ } ,
366
+ } )
303
367
}
0 commit comments