Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/2.0.0.
`None`) for parallel-dispatch resource-conflict detection, plus the
`FnTool::with_resource_key` builder.
- `MockTool::with_delay(Duration)` builder for timing-sensitive tests.
- `StructuredOutput` trait, `ResponseFormat`, `RequestOptions`, and
`StructuredError` (`structured` module): request guaranteed-schema JSON
responses from the model. Includes a lenient JSON extraction helper (handles
markdown fences/prose prefixes) and a `request_structured::<T>()`
convenience function.
- `ApiClient::stream_messages_with_options` and
`create_message_with_options` default methods (additive — existing impls
compile unchanged). `OpenAiClient`, `AnthropicClient`, and `GeminiClient`
override both to inject the schema (OpenAI via native `response_format`,
Anthropic via forced-tool tool-forcing, Gemini via `generationConfig`
`responseMimeType` + `responseSchema`).
- `ToolOutput::structured<T>`, `structured_value()`, and
`structured_as::<T>()` for typed tool results that round-trip through JSON.

### Changed

Expand Down
68 changes: 68 additions & 0 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,74 @@ pub trait ApiClient: Send + Sync {
system: Option<String>,
tools: Option<Vec<ToolSchema>>,
) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, ApiError>> + Send + '_>>;

/// Streaming variant that honors [`RequestOptions`](crate::structured::RequestOptions).
///
/// When `options` is empty (the default), delegates to
/// [`stream_messages`](ApiClient::stream_messages). When `options`
/// contains fields the client does not support (e.g. `response_format`
/// on a client that has not overridden this method), yields an
/// [`ApiError::config`] error as the first stream item.
/// `OpenAiClient`, `AnthropicClient`, and `GeminiClient` override
/// this to inject the schema.
fn stream_messages_with_options(
&self,
messages: Vec<Message>,
system: Option<String>,
tools: Option<Vec<ToolSchema>>,
options: crate::structured::RequestOptions,
) -> Pin<Box<dyn Stream<Item = Result<StreamEvent, ApiError>> + Send + 'static>> {
if options.response_format.is_none() {
return self.stream_messages(messages, system, tools);
}
Box::pin(futures::stream::once(async {
Err(ApiError::config(
"this client does not support structured output (response_format)",
))
}))
}

/// Non-streaming variant that honors [`RequestOptions`](crate::structured::RequestOptions).
///
/// When `options` is empty (the default), delegates to
/// [`create_message`](ApiClient::create_message). When `options`
/// contains fields the client does not support (e.g. `response_format`
/// on a client that has not overridden this method), returns an
/// [`ApiError::config`] error. This is the primary path for structured
/// output — a complete JSON document must be present before
/// deserialization, so callers that need a typed `T` should use this
/// method and then
/// [`StructuredOutput::from_value`](crate::structured::StructuredOutput::from_value)
/// on the extracted payload.
fn create_message_with_options(
&self,
messages: Vec<Message>,
system: Option<String>,
tools: Option<Vec<ToolSchema>>,
options: crate::structured::RequestOptions,
) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, ApiError>> + Send + '_>> {
if options.response_format.is_none() {
return self.create_message(messages, system, tools);
}
Box::pin(async {
Err(ApiError::config(
"this client does not support structured output (response_format)",
))
})
}

/// Extract the structured-output payload from a provider response.
///
/// Each provider knows its own response envelope shape. This method pulls
/// the inner JSON value that should be fed to
/// [`StructuredOutput::from_value`](crate::structured::StructuredOutput::from_value).
/// The default implementation returns the raw value as-is (for mock
/// clients and custom providers that already return the structured value
/// without an envelope). `OpenAiClient`, `AnthropicClient`, and
/// `GeminiClient` override this to navigate their respective envelopes.
fn extract_structured(&self, raw: &serde_json::Value) -> serde_json::Value {
raw.clone()
}
}

/// Owned, single-threaded API client handle.
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pub mod provider;
pub mod reflection;
pub mod runtime;
pub mod stream;
pub mod structured;
#[cfg(feature = "testing")]
pub mod testing;
pub mod tool;
Loading