Skip to content

Commit cfb4bb4

Browse files
committed
add json5 - fallback
1 parent ea92227 commit cfb4bb4

File tree

2 files changed

+58
-20
lines changed

2 files changed

+58
-20
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,4 @@ bytes = "1.10.1"
106106
rand = "0.9.0"
107107
indoc = "2.0.6"
108108
owo-colors = "4.2.0"
109+
json5 = "0.4.1"

src/llm/anthropic.rs

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use async_trait::async_trait;
22
use crate::llm::{LlmGenerationClient, LlmSpec, LlmGenerateRequest, LlmGenerateResponse, ToJsonSchemaOptions, OutputFormat};
33
use anyhow::{Result, bail, Context};
44
use serde_json::Value;
5+
use json5;
6+
57
use crate::api_bail;
68
use 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

Comments
 (0)