diff --git a/codex-rs/config.md b/codex-rs/config.md index 1af963a1..a9d01dbc 100644 --- a/codex-rs/config.md +++ b/codex-rs/config.md @@ -243,6 +243,25 @@ To disable reasoning summaries, set `model_reasoning_summary` to `"none"` in you model_reasoning_summary = "none" # disable reasoning summaries ``` +## model_verbosity + +Controls output length/detail on GPT‑5 family models when using the Responses API. Supported values: + +- `"low"` +- `"medium"` (default when omitted) +- `"high"` + +When set, Codex includes a `text` object in the request payload with the configured verbosity, for example: `"text": { "verbosity": "low" }`. + +Example: + +```toml +model = "gpt-5" +model_verbosity = "low" +``` + +Note: This applies only to providers using the Responses API. Chat Completions providers are unaffected. + ## model_supports_reasoning_summaries By default, `reasoning` is only set on requests to OpenAI models that are known to support them. To force `reasoning` to set on requests to the current model, you can force this behavior by setting the following in `config.toml`: diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 5534e11f..471312d3 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -28,6 +28,7 @@ use crate::client_common::ResponseEvent; use crate::client_common::ResponseStream; use crate::client_common::ResponsesApiRequest; use crate::client_common::create_reasoning_param_for_request; +use crate::client_common::create_text_param_for_request; use crate::config::Config; use crate::error::CodexErr; use crate::error::Result; @@ -164,6 +165,19 @@ impl ModelClient { let input_with_instructions = prompt.get_formatted_input(); + // Only include `text.verbosity` for GPT-5 family models + let text = if self.config.model_family.family == "gpt-5" { + create_text_param_for_request(self.config.model_verbosity) + } else { + if self.config.model_verbosity.is_some() { + warn!( + "model_verbosity is set but ignored for non-gpt-5 model family: {}", + self.config.model_family.family + ); + } + None + }; + let payload = ResponsesApiRequest { model: &self.config.model, instructions: &full_instructions, @@ -176,6 +190,7 @@ impl ModelClient { stream: true, include, prompt_cache_key: Some(self.session_id.to_string()), + text, }; let mut attempt = 0; diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index bf7bc133..f99fe945 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -1,3 +1,4 @@ +use crate::config_types::Verbosity as VerbosityConfig; use crate::error::Result; use crate::model_family::ModelFamily; use crate::models::ContentItem; @@ -99,6 +100,32 @@ pub(crate) struct Reasoning { pub(crate) summary: ReasoningSummaryConfig, } +/// Controls under the `text` field in the Responses API for GPT-5. +#[derive(Debug, Serialize, Default, Clone, Copy)] +pub(crate) struct TextControls { + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) verbosity: Option, +} + +#[derive(Debug, Serialize, Default, Clone, Copy)] +#[serde(rename_all = "lowercase")] +pub(crate) enum OpenAiVerbosity { + Low, + #[default] + Medium, + High, +} + +impl From for OpenAiVerbosity { + fn from(v: VerbosityConfig) -> Self { + match v { + VerbosityConfig::Low => OpenAiVerbosity::Low, + VerbosityConfig::Medium => OpenAiVerbosity::Medium, + VerbosityConfig::High => OpenAiVerbosity::High, + } + } +} + /// Request object that is serialized as JSON and POST'ed when using the /// Responses API. #[derive(Debug, Serialize)] @@ -119,6 +146,8 @@ pub(crate) struct ResponsesApiRequest<'a> { pub(crate) include: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) prompt_cache_key: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) text: Option, } pub(crate) fn create_reasoning_param_for_request( @@ -133,6 +162,14 @@ pub(crate) fn create_reasoning_param_for_request( } } +pub(crate) fn create_text_param_for_request( + verbosity: Option, +) -> Option { + verbosity.map(|v| TextControls { + verbosity: Some(v.into()), + }) +} + pub(crate) struct ResponseStream { pub(crate) rx_event: mpsc::Receiver>, } @@ -161,4 +198,57 @@ mod tests { let full = prompt.get_full_instructions(&model_family); assert_eq!(full, expected); } + + #[test] + fn serializes_text_verbosity_when_set() { + let input: Vec = vec![]; + let tools: Vec = vec![]; + let req = ResponsesApiRequest { + model: "gpt-5", + instructions: "i", + input: &input, + tools: &tools, + tool_choice: "auto", + parallel_tool_calls: false, + reasoning: None, + store: true, + stream: true, + include: vec![], + prompt_cache_key: None, + text: Some(TextControls { + verbosity: Some(OpenAiVerbosity::Low), + }), + }; + + let v = serde_json::to_value(&req).expect("json"); + assert_eq!( + v.get("text") + .and_then(|t| t.get("verbosity")) + .and_then(|s| s.as_str()), + Some("low") + ); + } + + #[test] + fn omits_text_when_not_set() { + let input: Vec = vec![]; + let tools: Vec = vec![]; + let req = ResponsesApiRequest { + model: "gpt-5", + instructions: "i", + input: &input, + tools: &tools, + tool_choice: "auto", + parallel_tool_calls: false, + reasoning: None, + store: true, + stream: true, + include: vec![], + prompt_cache_key: None, + text: None, + }; + + let v = serde_json::to_value(&req).expect("json"); + assert!(v.get("text").is_none()); + } } diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 5b6a1ed2..31ff5f10 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -6,6 +6,7 @@ use crate::config_types::ShellEnvironmentPolicy; use crate::config_types::ShellEnvironmentPolicyToml; use crate::config_types::Tui; use crate::config_types::UriBasedFileOpener; +use crate::config_types::Verbosity; use crate::model_family::ModelFamily; use crate::model_family::find_family_for_model; use crate::model_provider_info::ModelProviderInfo; @@ -150,6 +151,9 @@ pub struct Config { /// request using the Responses API. pub model_reasoning_summary: ReasoningSummary, + /// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`). + pub model_verbosity: Option, + /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). pub chatgpt_base_url: String, @@ -441,6 +445,8 @@ pub struct ConfigToml { pub model_reasoning_effort: Option, pub model_reasoning_summary: Option, + /// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`). + pub model_verbosity: Option, /// Override to force-enable reasoning summaries for the configured model. pub model_supports_reasoning_summaries: Option, @@ -718,7 +724,7 @@ impl Config { .model_reasoning_summary .or(cfg.model_reasoning_summary) .unwrap_or_default(), - + model_verbosity: config_profile.model_verbosity.or(cfg.model_verbosity), chatgpt_base_url: config_profile .chatgpt_base_url .or(cfg.chatgpt_base_url) @@ -1087,6 +1093,7 @@ disable_response_storage = true show_raw_agent_reasoning: false, model_reasoning_effort: ReasoningEffort::High, model_reasoning_summary: ReasoningSummary::Detailed, + model_verbosity: None, chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), experimental_resume: None, base_instructions: None, @@ -1140,6 +1147,7 @@ disable_response_storage = true show_raw_agent_reasoning: false, model_reasoning_effort: ReasoningEffort::default(), model_reasoning_summary: ReasoningSummary::default(), + model_verbosity: None, chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), experimental_resume: None, base_instructions: None, @@ -1208,6 +1216,7 @@ disable_response_storage = true show_raw_agent_reasoning: false, model_reasoning_effort: ReasoningEffort::default(), model_reasoning_summary: ReasoningSummary::default(), + model_verbosity: None, chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), experimental_resume: None, base_instructions: None, diff --git a/codex-rs/core/src/config_profile.rs b/codex-rs/core/src/config_profile.rs index 8bb07387..54869919 100644 --- a/codex-rs/core/src/config_profile.rs +++ b/codex-rs/core/src/config_profile.rs @@ -1,6 +1,7 @@ use serde::Deserialize; use std::path::PathBuf; +use crate::config_types::Verbosity; use crate::protocol::AskForApproval; use codex_protocol::config_types::ReasoningEffort; use codex_protocol::config_types::ReasoningSummary; @@ -17,6 +18,7 @@ pub struct ConfigProfile { pub disable_response_storage: Option, pub model_reasoning_effort: Option, pub model_reasoning_summary: Option, + pub model_verbosity: Option, pub chatgpt_base_url: Option, pub experimental_instructions_file: Option, } diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index d6661699..88be151f 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -8,6 +8,8 @@ use std::path::PathBuf; use wildmatch::WildMatchPattern; use serde::Deserialize; +use serde::Serialize; +use strum_macros::Display; #[derive(Deserialize, Debug, Clone, PartialEq)] pub struct McpServerConfig { @@ -183,3 +185,43 @@ impl From for ShellEnvironmentPolicy { } } } + +/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning +#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum ReasoningEffort { + Low, + #[default] + Medium, + High, + /// Option to disable reasoning. + None, +} + +/// A summary of the reasoning performed by the model. This can be useful for +/// debugging and understanding the model's reasoning process. +/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries +#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum ReasoningSummary { + #[default] + Auto, + Concise, + Detailed, + /// Option to disable reasoning summaries. + None, +} + +/// Controls output length/detail on GPT-5 models via the Responses API. +/// Serialized with lowercase values to match the OpenAI API. +#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum Verbosity { + Low, + #[default] + Medium, + High, +}