Skip to content
Open
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
4 changes: 2 additions & 2 deletions bindings/typescript/src/generated/openai/ConnectorId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

/**
* Identifier for service connectors, like those available in ChatGPT. One of
* `server_url` or `connector_id` must be provided. Learn more about service
* connectors [here](/docs/guides/tools-remote-mcp#connectors).
* `server_url`, `connector_id`, or `tunnel_id` must be provided. Learn more
* about service connectors [here](/docs/guides/tools-remote-mcp#connectors).
*
* Currently supported `connector_id` values are:
*
Expand Down
13 changes: 9 additions & 4 deletions bindings/typescript/src/generated/openai/InputItemTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ search_context_size: WebSearchContextSize | null, user_location: ApproximateLoca
authorization: string | null,
/**
* Identifier for service connectors, like those available in ChatGPT. One of
* `server_url` or `connector_id` must be provided. Learn more about service
* connectors [here](/docs/guides/tools-remote-mcp#connectors).
* `server_url`, `connector_id`, or `tunnel_id` must be provided. Learn more
* about service connectors [here](/docs/guides/tools-remote-mcp#connectors).
*
* Currently supported `connector_id` values are:
*
Expand All @@ -206,10 +206,15 @@ server_description: string | null,
*/
server_label: string | null,
/**
* The URL for the MCP server. One of `server_url` or `connector_id` must be
* provided.
* The URL for the MCP server. One of `server_url`, `connector_id`, or
* `tunnel_id` must be provided.
*/
server_url: string | null,
/**
* The Secure MCP Tunnel ID to use instead of a direct server URL. One of
* `server_url`, `connector_id`, or `tunnel_id` must be provided.
*/
tunnel_id: string | null,
/**
* The code interpreter container. Can be a container ID or an object that
* specifies uploaded file IDs to make available to your code, along with an
Expand Down
72 changes: 50 additions & 22 deletions crates/lingua/src/providers/openai/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1819,16 +1819,12 @@ pub struct CreateResponseClass {
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt: Option<Prompt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<Reasoning>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<ResponseTextParam>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<CreateResponseToolChoiceParam>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Tool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation: Option<Truncation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context_management: Option<Vec<ContextManagementParam>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation: Option<ConversationParam>,
Expand All @@ -1845,11 +1841,15 @@ pub struct CreateResponseClass {
#[serde(skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<Reasoning>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<ResponseStreamOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation: Option<Truncation>,
}

/// Context management configuration for this request.
Expand Down Expand Up @@ -3742,8 +3742,8 @@ pub struct InputItemTool {
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization: Option<String>,
/// Identifier for service connectors, like those available in ChatGPT. One of
/// `server_url` or `connector_id` must be provided. Learn more about service
/// connectors [here](/docs/guides/tools-remote-mcp#connectors).
/// `server_url`, `connector_id`, or `tunnel_id` must be provided. Learn more
/// about service connectors [here](/docs/guides/tools-remote-mcp#connectors).
///
/// Currently supported `connector_id` values are:
///
Expand All @@ -3767,10 +3767,14 @@ pub struct InputItemTool {
/// A label for this MCP server, used to identify it in tool calls.
#[serde(skip_serializing_if = "Option::is_none")]
pub server_label: Option<String>,
/// The URL for the MCP server. One of `server_url` or `connector_id` must be
/// provided.
/// The URL for the MCP server. One of `server_url`, `connector_id`, or
/// `tunnel_id` must be provided.
#[serde(skip_serializing_if = "Option::is_none")]
pub server_url: Option<String>,
/// The Secure MCP Tunnel ID to use instead of a direct server URL. One of
/// `server_url`, `connector_id`, or `tunnel_id` must be provided.
#[serde(skip_serializing_if = "Option::is_none")]
pub tunnel_id: Option<String>,
/// The code interpreter container. Can be a container ID or an object that
/// specifies uploaded file IDs to make available to your code, along with an
/// optional `memory_limit` setting.
Expand Down Expand Up @@ -3884,8 +3888,8 @@ pub enum Background {
}

/// Identifier for service connectors, like those available in ChatGPT. One of
/// `server_url` or `connector_id` must be provided. Learn more about service
/// connectors [here](/docs/guides/tools-remote-mcp#connectors).
/// `server_url`, `connector_id`, or `tunnel_id` must be provided. Learn more
/// about service connectors [here](/docs/guides/tools-remote-mcp#connectors).
///
/// Currently supported `connector_id` values are:
///
Expand Down Expand Up @@ -4563,6 +4567,8 @@ pub struct Input {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[ts(export_to = "openai/")]
pub struct Reasoning {
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Context>,
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<ReasoningEffort>,
#[serde(skip_serializing_if = "Option::is_none")]
Expand All @@ -4571,6 +4577,20 @@ pub struct Reasoning {
pub summary: Option<Summary>,
}

/// Controls which reasoning items are rendered back to the model on later turns.
/// When returned on a response, this is the effective reasoning context mode
/// used for the response.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "openai/")]
pub enum Context {
#[serde(rename = "all_turns")]
AllTurns,
Auto,
#[serde(rename = "current_turn")]
CurrentTurn,
}

/// **Deprecated:** use `summary` instead.
///
/// A summary of the reasoning performed by the model. This can be
Expand Down Expand Up @@ -4974,8 +4994,8 @@ pub struct MCPTool {
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization: Option<String>,
/// Identifier for service connectors, like those available in ChatGPT. One of
/// `server_url` or `connector_id` must be provided. Learn more about service
/// connectors [here](/docs/guides/tools-remote-mcp#connectors).
/// `server_url`, `connector_id`, or `tunnel_id` must be provided. Learn more
/// about service connectors [here](/docs/guides/tools-remote-mcp#connectors).
///
/// Currently supported `connector_id` values are:
///
Expand Down Expand Up @@ -5003,10 +5023,14 @@ pub struct MCPTool {
pub server_description: Option<String>,
/// A label for this MCP server, used to identify it in tool calls.
pub server_label: String,
/// The URL for the MCP server. One of `server_url` or `connector_id` must be
/// provided.
/// The URL for the MCP server. One of `server_url`, `connector_id`, or
/// `tunnel_id` must be provided.
#[serde(skip_serializing_if = "Option::is_none")]
pub server_url: Option<String>,
/// The Secure MCP Tunnel ID to use instead of a direct server URL. One of
/// `server_url`, `connector_id`, or `tunnel_id` must be provided.
#[serde(skip_serializing_if = "Option::is_none")]
pub tunnel_id: Option<String>,
}

/// A tool that runs Python code to help generate a response to a prompt.
Expand Down Expand Up @@ -5243,14 +5267,10 @@ pub struct TheResponseObject {
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt: Option<Prompt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<Reasoning>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<ResponseTextParam>,
pub tool_choice: TheResponseObjectToolChoiceParam,
pub tools: Vec<Tool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation: Option<Truncation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation: Option<Conversation>,
Expand Down Expand Up @@ -5283,11 +5303,15 @@ pub struct TheResponseObject {
pub output_text: Option<String>,
/// Whether to allow the model to run tool calls in parallel.
pub parallel_tool_calls: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<Reasoning>,
/// The status of the response generation. One of `completed`, `failed`,
/// `in_progress`, `cancelled`, `queued`, or `incomplete`.
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<StatusEnum>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation: Option<Truncation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<ResponseUsage>,
}

Expand Down Expand Up @@ -6515,8 +6539,8 @@ pub struct OutputItemTool {
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization: Option<String>,
/// Identifier for service connectors, like those available in ChatGPT. One of
/// `server_url` or `connector_id` must be provided. Learn more about service
/// connectors [here](/docs/guides/tools-remote-mcp#connectors).
/// `server_url`, `connector_id`, or `tunnel_id` must be provided. Learn more
/// about service connectors [here](/docs/guides/tools-remote-mcp#connectors).
///
/// Currently supported `connector_id` values are:
///
Expand All @@ -6540,10 +6564,14 @@ pub struct OutputItemTool {
/// A label for this MCP server, used to identify it in tool calls.
#[serde(skip_serializing_if = "Option::is_none")]
pub server_label: Option<String>,
/// The URL for the MCP server. One of `server_url` or `connector_id` must be
/// provided.
/// The URL for the MCP server. One of `server_url`, `connector_id`, or
/// `tunnel_id` must be provided.
#[serde(skip_serializing_if = "Option::is_none")]
pub server_url: Option<String>,
/// The Secure MCP Tunnel ID to use instead of a direct server URL. One of
/// `server_url`, `connector_id`, or `tunnel_id` must be provided.
#[serde(skip_serializing_if = "Option::is_none")]
pub tunnel_id: Option<String>,
/// The code interpreter container. Can be a container ID or an object that
/// specifies uploaded file IDs to make available to your code, along with an
/// optional `memory_limit` setting.
Expand Down
42 changes: 41 additions & 1 deletion crates/lingua/src/providers/openai/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ These structs use `#[serde(flatten)]` to automatically capture unknown fields,
eliminating the need for explicit KNOWN_KEYS arrays.
*/

use crate::providers::openai::generated::{ChatCompletionRequestMessage, Instructions, Summary};
use crate::providers::openai::generated::{
ChatCompletionRequestMessage, Context as ReasoningContext, Instructions, Summary,
};
use crate::serde_json::Value;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
Expand Down Expand Up @@ -220,8 +222,13 @@ pub enum OpenAIReasoningEffort {
/// Typed OpenAI Responses reasoning parameter view.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OpenAIReasoning {
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<ReasoningContext>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid enabling reasoning for context-only requests

When a Responses request contains only reasoning.context (allowed by the updated schema because effort, summary, and generate_summary are all optional), this new field makes typed_params.reasoning present, and request_to_universal converts any present OpenAIReasoning into ReasoningConfig { enabled: Some(true), ... } even though context is not represented there. If that universal request is then emitted to Chat Completions, Anthropic, or Google, the provider conversion synthesizes a default reasoning effort/budget, changing a context-selection-only request into one that explicitly enables reasoning; only create canonical reasoning when an effort/summary field is present, or model context separately.

Useful? React with 👍 / 👎.

#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<OpenAIReasoningEffort>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<Summary>,
#[serde(skip_serializing_if = "Option::is_none")]
pub generate_summary: Option<Summary>,
}

Expand Down Expand Up @@ -346,6 +353,39 @@ mod tests {
);
}

#[test]
fn test_reasoning_context_parses() {
use crate::providers::openai::generated::Context as ReasoningContext;

let responses = json!({
"model": "gpt-5.4",
"input": [{"role": "user", "content": "Hello"}],
"reasoning": {"effort": "high", "context": "current_turn"}
});
let params: OpenAIResponsesParams = serde_json::from_value(responses).unwrap();
let reasoning = params.reasoning.expect("reasoning should be present");
assert_eq!(reasoning.effort, Some(OpenAIReasoningEffort::High));
assert_eq!(reasoning.context, Some(ReasoningContext::CurrentTurn));

let all_turns = json!({
"model": "gpt-5.4",
"input": [{"role": "user", "content": "Hello"}],
"reasoning": {"effort": "medium", "context": "all_turns"}
});
let params: OpenAIResponsesParams = serde_json::from_value(all_turns).unwrap();
let reasoning = params.reasoning.expect("reasoning should be present");
assert_eq!(reasoning.context, Some(ReasoningContext::AllTurns));

let auto = json!({
"model": "gpt-5.4",
"input": [{"role": "user", "content": "Hello"}],
"reasoning": {"effort": "high", "context": "auto"}
});
let params: OpenAIResponsesParams = serde_json::from_value(auto).unwrap();
let reasoning = params.reasoning.expect("reasoning should be present");
assert_eq!(reasoning.context, Some(ReasoningContext::Auto));
}

#[test]
fn test_roundtrip_preserves_extras() {
let json = json!({
Expand Down
70 changes: 70 additions & 0 deletions crates/lingua/src/providers/openai/responses_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,12 @@ impl ProviderAdapter for ResponsesAdapter {
if let Some(moderation) = typed_params.moderation {
extras_map.insert("moderation".into(), moderation);
}
if let Some(ref reasoning) = typed_params.reasoning {
let reasoning_val = serde_json::to_value(reasoning).map_err(|e| {
TransformError::SerializationFailed(format!("reasoning extras: {}", e))
})?;
extras_map.insert("reasoning".into(), reasoning_val);
}

if !extras_map.is_empty() {
params.extras.insert(ProviderFormat::Responses, extras_map);
Expand Down Expand Up @@ -2145,6 +2151,70 @@ mod tests {
);
}

#[test]
fn test_responses_reasoning_context_roundtrips() {
let input_json = json!({
"model": "gpt-5-nano",
"input": [{"role": "user", "content": "Hello"}],
"reasoning": {
"effort": "high",
"context": "current_turn"
}
});

let adapter = ResponsesAdapter;
let universal = adapter
.request_to_universal(input_json)
.expect("should parse");

let output_json = adapter
.request_from_universal(&universal)
.expect("should reconstruct");

let reasoning = output_json
.get("reasoning")
.expect("reasoning should be present");
assert_eq!(
reasoning.get("context").and_then(|v| v.as_str()),
Some("current_turn"),
"reasoning.context should roundtrip"
);
assert_eq!(
reasoning.get("effort").and_then(|v| v.as_str()),
Some("high"),
"reasoning.effort should roundtrip"
);
}

#[test]
fn test_responses_reasoning_context_all_turns_roundtrips() {
let input_json = json!({
"model": "gpt-5-nano",
"input": [{"role": "user", "content": "Hello"}],
"reasoning": {
"effort": "medium",
"context": "all_turns"
}
});

let adapter = ResponsesAdapter;
let universal = adapter
.request_to_universal(input_json)
.expect("should parse");

let output_json = adapter
.request_from_universal(&universal)
.expect("should reconstruct");

let reasoning = output_json
.get("reasoning")
.expect("reasoning should be present");
assert_eq!(
reasoning.get("context").and_then(|v| v.as_str()),
Some("all_turns"),
);
}

#[test]
fn test_responses_clamps_synthesized_max_output_tokens_to_provider_minimum() {
use crate::providers::openai::generated::CreateResponseClass;
Expand Down
1 change: 1 addition & 0 deletions crates/lingua/src/universal/reasoning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,7 @@ mod tests {
#[test]
fn test_from_openai_responses_reasoning() {
let reasoning = OpenAIReasoning {
context: None,
effort: Some(OpenAIReasoningEffortParam::High),
summary: Some(OpenAISummary::Detailed),
generate_summary: None,
Expand Down
Loading
Loading