SMOODEV-1472: Structured output (JSON-schema constrained responses) in the LLM client#6
Merged
Merged
Conversation
…s) to LLM client
The general-agent brain must emit a typed JSON object every turn; this lands
the keystone capability on the Rust engine's LLM client.
API:
- ResponseFormat::JsonSchema { name, schema, strict } + json_schema() ctor
- LlmClient::chat_structured / chat_with_format; chat delegates with None
- LlmResponse::structured_json() / deserialize_json::<T>() — clear error on
empty/non-JSON content, never a silent empty value
- LlmProvider::chat_structured trait method; MockLlmClient records the
requested format on RecordedCall.response_format
Provider handling:
- OpenAI-compat: response_format { type: json_schema, json_schema {...} } on
/chat/completions (LiteLLM gateway shape)
- Anthropic-native: forced single tool call whose input_schema IS the schema,
forced via tool_choice; tool input surfaced back as the JSON content
Streaming structured output and agent-level wiring deferred as follow-ups.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 1b6c25e The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Implements structured output in the smooth-operator engine's LLM client — the keystone Phase 5 parity gap (SMOODEV-1472). The general-agent brain must emit a typed JSON object every turn; this gives the client a way to request a schema-constrained JSON response and parse it back in a typed way.
API added
ResponseFormat(public enum, re-exported from the crate root):LlmClient:chat_structured(&self, messages: &[&Message], format: &ResponseFormat) -> Result<LlmResponse>chat_with_format(&self, messages: &[&Message], tools: &[ToolSchema], format: Option<&ResponseFormat>) -> Result<LlmResponse>(core;chatnow delegates withNone)LlmResponseparse helpers:structured_json(&self) -> Result<serde_json::Value>deserialize_json<T: DeserializeOwned>(&self) -> Result<T>LlmProvidertrait gainschat_structured;MockLlmClientrecords the requestedResponseFormatonRecordedCall.response_format.OpenAI vs Anthropic handling
ApiFormat::OpenAiCompat, the LiteLLM gateway path): serialized on/chat/completionsasresponse_format: { type: "json_schema", json_schema: { name, schema, strict } }.ApiFormat::Anthropic,/v1/messages): Anthropic has noresponse_format, so structured output is done via a forced single tool call — a synthetic tool whoseinput_schemaIS the requested schema, forced withtool_choice: { type: "tool", name }. The forced tool'sinputis surfaced back as the JSONcontentstring, so both paths return the same shape. Schema names are sanitized to valid Anthropic tool names.Tests (TDD, via
MockLlmClient)response_formatjson_schema object (and is omitted when absent).tool_choice+ toolinput_schema).chat_structuredrecords the requested format; plainchatrecordsNone.structured_json()and deserializes into a typedT.sanitize_tool_nameedge cases.All 353 crate tests pass;
cargo fmt --check,cargo clippy --all-targets -D warnings, release build, andcargo publish --dry-run --lockedare green. Changeset added (minor). No new deps (Cargo.lock unchanged).Follow-ups (deferred, noted in code + changeset)
chat_streamsendsresponse_format: Nonetoday).Agent::runschema-constrained final response) — the agent tool loop is large; the client + provider + mock surface is landed here so it's unblocking.🤖 Generated with Claude Code