@@ -2,6 +2,8 @@ use async_trait::async_trait;
22use crate :: llm:: { LlmGenerationClient , LlmSpec , LlmGenerateRequest , LlmGenerateResponse , ToJsonSchemaOptions , OutputFormat } ;
33use anyhow:: { Result , bail, Context } ;
44use serde_json:: Value ;
5+ use json5;
6+
57use crate :: api_bail;
68use urlencoding:: encode;
79
@@ -31,52 +33,87 @@ impl LlmGenerationClient for Client {
3133 & self ,
3234 request : LlmGenerateRequest < ' req > ,
3335 ) -> Result < LlmGenerateResponse > {
34- // Compose the prompt/messages
35- let mut messages = vec ! [ serde_json:: json!( {
36+ let messages = vec ! [ serde_json:: json!( {
3637 "role" : "user" ,
3738 "content" : request. user_prompt
3839 } ) ] ;
39- if let Some ( system) = request. system_prompt {
40- messages. insert ( 0 , serde_json:: json!( {
41- "role" : "system" ,
42- "content" : system
43- } ) ) ;
44- }
4540
4641 let mut payload = serde_json:: json!( {
4742 "model" : self . model,
4843 "messages" : messages,
4944 "max_tokens" : 4096
5045 } ) ;
5146
52- // If structured output is requested, add schema
53- if let Some ( OutputFormat :: JsonSchema { schema, .. } ) = & request. output_format {
54- let schema_json = serde_json:: to_value ( schema) ?;
55- payload[ "tools" ] = serde_json:: json!( [
56- { "type" : "json_object" , "parameters" : schema_json }
57- ] ) ;
47+ // Add system prompt as top-level field if present (required)
48+ if let Some ( system) = request. system_prompt {
49+ payload[ "system" ] = serde_json:: json!( system) ;
5850 }
5951
52+ let OutputFormat :: JsonSchema { schema, .. } = request. output_format . as_ref ( ) . expect ( "Anthropic client expects OutputFormat::JsonSchema for all requests" ) ;
53+ let schema_json = serde_json:: to_value ( schema) ?;
54+ payload[ "tools" ] = serde_json:: json!( [
55+ { "type" : "custom" , "name" : "extraction" , "input_schema" : schema_json }
56+ ] ) ;
57+
6058 let url = "https://api.anthropic.com/v1/messages" ;
6159
6260 let encoded_api_key = encode ( & self . api_key ) ;
61+
6362 let resp = self . client
6463 . post ( url)
6564 . header ( "x-api-key" , encoded_api_key. as_ref ( ) )
65+ . header ( "anthropic-version" , "2023-06-01" )
6666 . json ( & payload)
6767 . send ( )
6868 . await
6969 . context ( "HTTP error" ) ?;
70-
7170 let resp_json: Value = resp. json ( ) . await . context ( "Invalid JSON" ) ?;
72-
7371 if let Some ( error) = resp_json. get ( "error" ) {
7472 bail ! ( "Anthropic API error: {:?}" , error) ;
7573 }
76- let mut resp_json = resp_json;
77- let text = match & mut resp_json[ "content" ] [ 0 ] [ "text" ] {
78- Value :: String ( s) => std:: mem:: take ( s) ,
79- _ => bail ! ( "No text in response" ) ,
74+
75+ // Debug print full response
76+ // println!("Anthropic API full response: {resp_json:?}");
77+
78+ let resp_content = & resp_json[ "content" ] ;
79+ let tool_name = "extraction" ;
80+ let mut extracted_json: Option < Value > = None ;
81+ if let Some ( array) = resp_content. as_array ( ) {
82+ for item in array {
83+ if item. get ( "type" ) == Some ( & Value :: String ( "tool_use" . to_string ( ) ) )
84+ && item. get ( "name" ) == Some ( & Value :: String ( tool_name. to_string ( ) ) )
85+ {
86+ if let Some ( input) = item. get ( "input" ) {
87+ extracted_json = Some ( input. clone ( ) ) ;
88+ break ;
89+ }
90+ }
91+ }
92+ }
93+ let text = if let Some ( json) = extracted_json {
94+ // Try strict JSON serialization first
95+ serde_json:: to_string ( & json) ?
96+ } else {
97+ // Fallback: try text if no tool output found
98+ match & resp_json[ "content" ] [ 0 ] [ "text" ] {
99+ Value :: String ( s) => {
100+ // Try strict JSON parsing first
101+ match serde_json:: from_str :: < serde_json:: Value > ( s) {
102+ Ok ( _) => s. clone ( ) ,
103+ Err ( e) => {
104+ // Try permissive json5 parsing as fallback
105+ match json5:: from_str :: < serde_json:: Value > ( s) {
106+ Ok ( _) => {
107+ println ! ( "[Anthropic] Used permissive JSON5 parser for output" ) ;
108+ s. clone ( )
109+ } ,
110+ Err ( e2) => return Err ( anyhow:: anyhow!( format!( "No structured tool output or text found in response, and permissive JSON5 parsing also failed: {e}; {e2}" ) ) )
111+ }
112+ }
113+ }
114+ } ,
115+ _ => return Err ( anyhow:: anyhow!( "No structured tool output or text found in response" ) ) ,
116+ }
80117 } ;
81118
82119 Ok ( LlmGenerateResponse {
0 commit comments