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
32 changes: 32 additions & 0 deletions crates/coverage-report/src/requests_expected_differences.json
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,38 @@
{ "pattern": "params.reasoning.budget_tokens", "reason": "OpenAI uses effort levels, thinkingBudget gets quantized" }
]
},
{
"testCase": "textFormatJsonSchemaParam",
"source": "Google",
"target": "ChatCompletions",
"fields": [
{ "pattern": "params.response_format.json_schema.schema.additionalProperties", "reason": "Strict OpenAI-style targets require explicit closed object schemas when Google omits additionalProperties" }
]
},
{
"testCase": "textFormatJsonSchemaParam",
"source": "Google",
"target": "Responses",
"fields": [
{ "pattern": "params.response_format.json_schema.schema.additionalProperties", "reason": "Strict OpenAI-style targets require explicit closed object schemas when Google omits additionalProperties" }
]
},
{
"testCase": "textFormatJsonSchemaWithDescriptionParam",
"source": "Google",
"target": "ChatCompletions",
"fields": [
{ "pattern": "params.response_format.json_schema.schema.additionalProperties", "reason": "Strict OpenAI-style targets require explicit closed object schemas when Google omits additionalProperties" }
]
},
{
"testCase": "textFormatJsonSchemaWithDescriptionParam",
"source": "Google",
"target": "Responses",
"fields": [
{ "pattern": "params.response_format.json_schema.schema.additionalProperties", "reason": "Strict OpenAI-style targets require explicit closed object schemas when Google omits additionalProperties" }
]
},
{
"testCase": "thinkingLevelParam",
"source": "Google",
Expand Down
6 changes: 6 additions & 0 deletions crates/lingua/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ pub enum ConvertError {
#[error("Invalid tool schema for '{tool_name}': {reason}")]
InvalidToolSchema { tool_name: String, reason: String },

#[error("Invalid response schema for {target_provider}: {reason}")]
InvalidResponseSchema {
target_provider: ProviderFormat,
reason: String,
},

#[error("Invalid {type_name} value: '{value}'")]
InvalidEnumValue {
type_name: &'static str,
Expand Down
38 changes: 30 additions & 8 deletions crates/lingua/src/providers/anthropic/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::serde_json::{json, Value};
use crate::universal::request::{
JsonSchemaConfig, ResponseFormatConfig, ResponseFormatType, ToolChoiceConfig, ToolChoiceMode,
};
use crate::universal::response_format::normalize_response_schema_for_strict_target;
use crate::universal::tools::{BuiltinToolProvider, UniversalTool, UniversalToolType};
use crate::universal::{
convert::TryFromLLM, message::ProviderOptions, AssistantContent, AssistantContentPart, Message,
Expand Down Expand Up @@ -1339,22 +1340,43 @@ impl From<&JsonOutputFormat> for ResponseFormatConfig {
}

impl TryFrom<&ResponseFormatConfig> for JsonOutputFormat {
type Error = ();
type Error = ConvertError;

fn try_from(config: &ResponseFormatConfig) -> Result<Self, Self::Error> {
match config.format_type.ok_or(())? {
ResponseFormatType::Text => Err(()),
match config
.format_type
.ok_or_else(|| ConvertError::MissingRequiredField {
field: "format_type".to_string(),
})? {
ResponseFormatType::Text => Err(ConvertError::InvalidResponseSchema {
target_provider: ProviderFormat::Anthropic,
reason: "text response format is not supported by Anthropic output_config.format"
.to_string(),
}),
// Anthropic json_object compatibility is handled in adapter.rs via synthetic json tool shim.
// Do not emit output_config.format for json_object here.
ResponseFormatType::JsonObject => Err(()),
ResponseFormatType::JsonObject => Err(ConvertError::InvalidResponseSchema {
target_provider: ProviderFormat::Anthropic,
reason: "json_object response format uses the Anthropic JSON tool shim".to_string(),
}),
ResponseFormatType::JsonSchema => {
let js = config.json_schema.as_ref().ok_or(())?;
match &js.schema {
let js = config.json_schema.as_ref().ok_or_else(|| {
ConvertError::MissingRequiredField {
field: "json_schema".to_string(),
}
})?;
match normalize_response_schema_for_strict_target(
&js.schema,
ProviderFormat::Anthropic,
)? {
Value::Object(m) => Ok(JsonOutputFormat {
schema: m.clone(),
schema: m,
json_output_format_type: JsonOutputFormatType::JsonSchema,
}),
_ => Err(()),
_ => Err(ConvertError::InvalidResponseSchema {
target_provider: ProviderFormat::Anthropic,
reason: "response schema root must be a JSON object".to_string(),
}),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/lingua/src/providers/openai/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ impl ProviderAdapter for OpenAIAdapter {
&mut obj,
"response_format",
req.params
.response_format_for(ProviderFormat::ChatCompletions),
.try_response_format_for(ProviderFormat::ChatCompletions)?,
);
}
insert_opt_i64(&mut obj, "seed", req.params.seed);
Expand Down
5 changes: 4 additions & 1 deletion crates/lingua/src/providers/openai/responses_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,10 @@ impl ProviderAdapter for ResponsesAdapter {
// Convert response_format to Responses API text format using helper method
if let Some(raw_text) = responses_extras_view.text.as_ref() {
obj.insert("text".into(), raw_text.clone());
} else if let Some(text_val) = req.params.response_format_for(ProviderFormat::Responses) {
} else if let Some(text_val) = req
.params
.try_response_format_for(ProviderFormat::Responses)?
{
obj.insert("text".into(), text_val);
}

Expand Down
12 changes: 10 additions & 2 deletions crates/lingua/src/universal/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use ts_rs::TS;

use crate::capabilities::ProviderFormat;
use crate::error::ConvertError;
use crate::processing::transform::TransformError;
use crate::serde_json::{Map, Value};
use crate::universal::message::Message;
use crate::universal::tools::UniversalTool;
Expand Down Expand Up @@ -247,10 +248,17 @@ impl UniversalParams {
/// .flatten()
/// ```
pub fn response_format_for(&self, provider: ProviderFormat) -> Option<Value> {
self.try_response_format_for(provider).ok().flatten()
}

/// Get response_format for a provider and preserve conversion failures.
pub fn try_response_format_for(
&self,
provider: ProviderFormat,
) -> Result<Option<Value>, TransformError> {
self.response_format
.as_ref()
.and_then(|rf| rf.to_provider(provider).ok())
.flatten()
.map_or(Ok(None), |rf| rf.to_provider(provider))
}
}

Expand Down
Loading
Loading