Skip to content

Commit bafcfd6

Browse files
authored
Merge pull request #3 from bitloops/fix-claude-json-schema
Fix structured output for claude
2 parents 7b9b850 + b18c4dd commit bafcfd6

4 files changed

Lines changed: 60 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## [0.1.7] - 2026-05-18
2+
3+
### Fixed
4+
5+
- Support for structured output on claude response
6+
17
## [0.1.6] - 2026-05-14
28

39
### Added

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ members = [
66
resolver = "2"
77

88
[workspace.package]
9-
version = "0.1.6"
9+
version = "0.1.7"
1010
edition = "2024"
1111
license = "Apache-2.0"
1212

crates/bitloops-inference/src/provider/cli_agent.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,21 @@ fn parse_cli_json(text: &str) -> Result<Value, ProviderError> {
391391

392392
fn parse_claude_json(text: &str) -> Result<Value, ProviderError> {
393393
let outer = parse_cli_json(text)?;
394+
if let Some(structured_output) = outer.get("structured_output") {
395+
if structured_output.is_object() {
396+
return Ok(structured_output.clone());
397+
}
398+
399+
return Err(ProviderError::invalid_provider_response(
400+
"Claude CLI structured_output field was not a JSON object",
401+
Some(json!({
402+
"field": "structured_output",
403+
"value": structured_output.clone(),
404+
"outer": outer.clone(),
405+
})),
406+
));
407+
}
408+
394409
if let Some(result) = outer.get("result") {
395410
if let Some(result_text) = result.as_str() {
396411
return parse_cli_json(result_text);
@@ -722,4 +737,40 @@ mod tests {
722737

723738
assert_eq!(parsed["summary"], "ok");
724739
}
740+
741+
#[test]
742+
fn claude_structured_output_field_is_preferred() {
743+
let parsed = parse_claude_json(
744+
r#"{"result":"","structured_output":{"roles":[{"canonical_key":"http_route_handler"}]}}"#,
745+
)
746+
.expect("claude structured output");
747+
748+
assert_eq!(parsed["roles"][0]["canonical_key"], "http_route_handler");
749+
}
750+
751+
#[test]
752+
fn claude_structured_output_wins_over_prose_result() {
753+
let parsed = parse_claude_json(
754+
r#"{"result":"I've inferred roles...","structured_output":{"roles":[]}}"#,
755+
)
756+
.expect("claude structured output");
757+
758+
assert_eq!(parsed["roles"].as_array().unwrap().len(), 0);
759+
}
760+
761+
#[test]
762+
fn claude_structured_output_must_be_object() {
763+
let error = parse_claude_json(r#"{"result":"{}","structured_output":"not-object"}"#)
764+
.expect_err("non-object structured output should fail");
765+
766+
assert_eq!(error.code, "invalid_provider_response");
767+
assert!(
768+
error.message.contains("structured_output"),
769+
"error should identify the malformed structured_output field: {}",
770+
error.message
771+
);
772+
let details = error.details.expect("details");
773+
assert_eq!(details["field"], "structured_output");
774+
assert_eq!(details["value"], "not-object");
775+
}
725776
}

0 commit comments

Comments
 (0)