fix(core): align tool output caps with model policy

This commit is contained in:
James Peter
2026-01-17 15:09:15 +10:00
parent cb7750f055
commit 63dcaeda43
5 changed files with 66 additions and 8 deletions
+9
View File
@@ -279,6 +279,8 @@ pub(super) async fn submission_loop(
.unwrap_or(false); .unwrap_or(false);
let old_model_family = updated_config.model_family.clone(); let old_model_family = updated_config.model_family.clone();
let old_tool_output_max_bytes = updated_config.tool_output_max_bytes;
let old_default_tool_output_max_bytes = old_model_family.tool_output_max_bytes();
updated_config.model = model.clone(); updated_config.model = model.clone();
updated_config.model_explicit = model_explicit; updated_config.model_explicit = model_explicit;
@@ -300,6 +302,9 @@ pub(super) async fn submission_loop(
updated_config.model_family = find_family_for_model(&updated_config.model) updated_config.model_family = find_family_for_model(&updated_config.model)
.unwrap_or_else(|| derive_default_model_family(&updated_config.model)); .unwrap_or_else(|| derive_default_model_family(&updated_config.model));
let new_default_tool_output_max_bytes =
updated_config.model_family.tool_output_max_bytes();
let old_context_window = old_model_family.context_window; let old_context_window = old_model_family.context_window;
let new_context_window = updated_config.model_family.context_window; let new_context_window = updated_config.model_family.context_window;
let old_max_tokens = old_model_family.max_output_tokens; let old_max_tokens = old_model_family.max_output_tokens;
@@ -323,6 +328,10 @@ pub(super) async fn submission_loop(
new_auto_compact, new_auto_compact,
); );
if old_tool_output_max_bytes == old_default_tool_output_max_bytes {
updated_config.tool_output_max_bytes = new_default_tool_output_max_bytes;
}
let skills_outcome = let skills_outcome =
updated_config.skills_enabled.then(|| load_skills(&updated_config)); updated_config.skills_enabled.then(|| load_skills(&updated_config));
if let Some(outcome) = &skills_outcome { if let Some(outcome) = &skills_outcome {
+6 -5
View File
@@ -109,9 +109,6 @@ pub const GPT_5_CODEX_MEDIUM_MODEL: &str = "gpt-5.2-codex";
/// files are *silently truncated* to this size so we do not take up too much of /// files are *silently truncated* to this size so we do not take up too much of
/// the context window. /// the context window.
pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB
/// Maximum number of bytes of tool output included in a model request.
pub(crate) const DEFAULT_TOOL_OUTPUT_MAX_BYTES: usize = 32 * 1024; // 32 KiB
pub(crate) const CONFIG_TOML_FILE: &str = "config.toml"; pub(crate) const CONFIG_TOML_FILE: &str = "config.toml";
const DEFAULT_RESPONSES_ORIGINATOR_HEADER: &str = "code_cli_rs"; const DEFAULT_RESPONSES_ORIGINATOR_HEADER: &str = "code_cli_rs";
@@ -1081,6 +1078,7 @@ impl Config {
let model_family = let model_family =
find_family_for_model(&model).unwrap_or_else(|| derive_default_model_family(&model)); find_family_for_model(&model).unwrap_or_else(|| derive_default_model_family(&model));
let default_tool_output_max_bytes = model_family.tool_output_max_bytes();
// Chat model reasoning effort (used when other flows follow the chat model). // Chat model reasoning effort (used when other flows follow the chat model).
let preferred_model_reasoning_effort = config_profile let preferred_model_reasoning_effort = config_profile
@@ -1373,7 +1371,7 @@ impl Config {
project_doc_max_bytes: cfg.project_doc_max_bytes.unwrap_or(PROJECT_DOC_MAX_BYTES), project_doc_max_bytes: cfg.project_doc_max_bytes.unwrap_or(PROJECT_DOC_MAX_BYTES),
tool_output_max_bytes: cfg tool_output_max_bytes: cfg
.tool_output_max_bytes .tool_output_max_bytes
.unwrap_or(DEFAULT_TOOL_OUTPUT_MAX_BYTES), .unwrap_or(default_tool_output_max_bytes),
project_doc_fallback_filenames: cfg project_doc_fallback_filenames: cfg
.project_doc_fallback_filenames .project_doc_fallback_filenames
.unwrap_or_default() .unwrap_or_default()
@@ -1664,7 +1662,10 @@ persistence = "none"
ConfigOverrides::default(), ConfigOverrides::default(),
code_home.path().to_path_buf(), code_home.path().to_path_buf(),
)?; )?;
assert_eq!(default_config.tool_output_max_bytes, DEFAULT_TOOL_OUTPUT_MAX_BYTES); assert_eq!(
default_config.tool_output_max_bytes,
default_config.model_family.tool_output_max_bytes()
);
let cfg = toml::from_str::<ConfigToml>("tool_output_max_bytes = 65536") let cfg = toml::from_str::<ConfigToml>("tool_output_max_bytes = 65536")
.expect("TOML should deserialize"); .expect("TOML should deserialize");
@@ -23,6 +23,8 @@ use crate::exec_command::exec_command_session::ExecCommandSession;
use crate::exec_command::session_id::SessionId; use crate::exec_command::session_id::SessionId;
use code_protocol::models::FunctionCallOutputPayload; use code_protocol::models::FunctionCallOutputPayload;
const EXEC_COMMAND_OUTPUT_MAX_BYTES: u64 = 1024 * 1024;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct SessionManager { pub struct SessionManager {
next_session_id: AtomicU32, next_session_id: AtomicU32,
@@ -271,7 +273,10 @@ impl SessionManager {
// Collect output until either timeout expires or process exits. // Collect output until either timeout expires or process exits.
// Enforce the byte cap incrementally so runaway commands cannot exhaust memory. // Enforce the byte cap incrementally so runaway commands cannot exhaust memory.
let cap_bytes_u64 = params.max_output_tokens.saturating_mul(4); let cap_bytes_u64 = params
.max_output_tokens
.saturating_mul(4)
.min(EXEC_COMMAND_OUTPUT_MAX_BYTES);
let cap_bytes: usize = cap_bytes_u64.min(usize::MAX as u64) as usize; let cap_bytes: usize = cap_bytes_u64.min(usize::MAX as u64) as usize;
let mut collector = TruncatingCollector::new(cap_bytes); let mut collector = TruncatingCollector::new(cap_bytes);
@@ -367,7 +372,9 @@ impl SessionManager {
return Err("failed to write to stdin".to_string()); return Err("failed to write to stdin".to_string());
} }
let cap_bytes_u64 = max_output_tokens.saturating_mul(4); let cap_bytes_u64 = max_output_tokens
.saturating_mul(4)
.min(EXEC_COMMAND_OUTPUT_MAX_BYTES);
let cap_bytes: usize = cap_bytes_u64.min(usize::MAX as u64) as usize; let cap_bytes: usize = cap_bytes_u64.min(usize::MAX as u64) as usize;
// Collect output up to yield_time_ms, truncating to max_output_tokens bytes. // Collect output up to yield_time_ms, truncating to max_output_tokens bytes.
+29 -1
View File
@@ -1,5 +1,6 @@
use crate::config_types::ReasoningEffort; use crate::config_types::ReasoningEffort;
use crate::tool_apply_patch::ApplyPatchToolType; use crate::tool_apply_patch::ApplyPatchToolType;
use code_protocol::protocol::TruncationPolicy;
/// The `instructions` field in the payload sent to a model should always start /// The `instructions` field in the payload sent to a model should always start
/// with this content. /// with this content.
@@ -21,7 +22,7 @@ const CONTEXT_WINDOW_1M: u64 = 1_047_576;
const MAX_OUTPUT_DEFAULT: u64 = 128_000; const MAX_OUTPUT_DEFAULT: u64 = 128_000;
/// A model family is a group of models that share certain characteristics. /// A model family is a group of models that share certain characteristics.
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelFamily { pub struct ModelFamily {
/// The full model slug used to derive this model family, e.g. /// The full model slug used to derive this model family, e.g.
/// "gpt-4.1-2025-04-14". /// "gpt-4.1-2025-04-14".
@@ -40,6 +41,9 @@ pub struct ModelFamily {
/// Maximum number of output tokens that can be generated for the model. /// Maximum number of output tokens that can be generated for the model.
pub max_output_tokens: Option<u64>, pub max_output_tokens: Option<u64>,
/// Truncation policy to apply when recording tool outputs in the model context.
pub truncation_policy: TruncationPolicy,
/// Token threshold where we should automatically compact history. /// Token threshold where we should automatically compact history.
auto_compact_token_limit: Option<i64>, auto_compact_token_limit: Option<i64>,
@@ -80,6 +84,7 @@ macro_rules! model_family {
needs_special_apply_patch_instructions: false, needs_special_apply_patch_instructions: false,
context_window: Some(CONTEXT_WINDOW_272K), context_window: Some(CONTEXT_WINDOW_272K),
max_output_tokens: Some(MAX_OUTPUT_DEFAULT), max_output_tokens: Some(MAX_OUTPUT_DEFAULT),
truncation_policy: TruncationPolicy::Bytes(10_000),
auto_compact_token_limit: None, auto_compact_token_limit: None,
supports_reasoning_summaries: false, supports_reasoning_summaries: false,
default_reasoning_effort: None, default_reasoning_effort: None,
@@ -158,6 +163,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
supports_parallel_tool_calls: true, supports_parallel_tool_calls: true,
default_reasoning_effort: Some(ReasoningEffort::Medium), default_reasoning_effort: Some(ReasoningEffort::Medium),
truncation_policy: TruncationPolicy::Tokens(10_000),
) )
} else if slug.starts_with("exp-codex") || slug.starts_with("codex-1p") { } else if slug.starts_with("exp-codex") || slug.starts_with("codex-1p") {
// Same defaults as gpt-5.2-codex. // Same defaults as gpt-5.2-codex.
@@ -167,6 +173,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(), base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(),
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
supports_parallel_tool_calls: true, supports_parallel_tool_calls: true,
truncation_policy: TruncationPolicy::Tokens(10_000),
) )
} else if slug.starts_with("exp-") { } else if slug.starts_with("exp-") {
model_family!( model_family!(
@@ -175,6 +182,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
supports_parallel_tool_calls: true, supports_parallel_tool_calls: true,
default_reasoning_effort: Some(ReasoningEffort::Medium), default_reasoning_effort: Some(ReasoningEffort::Medium),
truncation_policy: TruncationPolicy::Bytes(10_000),
) )
} else if slug.starts_with("gpt-5.1-codex-max") { } else if slug.starts_with("gpt-5.1-codex-max") {
model_family!( model_family!(
@@ -184,6 +192,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
context_window: Some(CONTEXT_WINDOW_272K), context_window: Some(CONTEXT_WINDOW_272K),
max_output_tokens: Some(MAX_OUTPUT_DEFAULT), max_output_tokens: Some(MAX_OUTPUT_DEFAULT),
truncation_policy: TruncationPolicy::Tokens(10_000),
) )
} else if slug.starts_with("codex-") } else if slug.starts_with("codex-")
|| slug.starts_with("gpt-5-codex") || slug.starts_with("gpt-5-codex")
@@ -196,6 +205,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
context_window: Some(CONTEXT_WINDOW_272K), context_window: Some(CONTEXT_WINDOW_272K),
max_output_tokens: Some(MAX_OUTPUT_DEFAULT), max_output_tokens: Some(MAX_OUTPUT_DEFAULT),
truncation_policy: TruncationPolicy::Tokens(10_000),
) )
} else if slug.starts_with("gpt-5.2-codex") { } else if slug.starts_with("gpt-5.2-codex") {
// Same defaults as gpt-5.1-codex-max. // Same defaults as gpt-5.1-codex-max.
@@ -207,6 +217,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
supports_parallel_tool_calls: true, supports_parallel_tool_calls: true,
context_window: Some(CONTEXT_WINDOW_272K), context_window: Some(CONTEXT_WINDOW_272K),
max_output_tokens: Some(MAX_OUTPUT_DEFAULT), max_output_tokens: Some(MAX_OUTPUT_DEFAULT),
truncation_policy: TruncationPolicy::Tokens(10_000),
) )
} else if slug.starts_with("bengalfox") { } else if slug.starts_with("bengalfox") {
model_family!( model_family!(
@@ -217,6 +228,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
supports_parallel_tool_calls: true, supports_parallel_tool_calls: true,
context_window: Some(CONTEXT_WINDOW_272K), context_window: Some(CONTEXT_WINDOW_272K),
max_output_tokens: Some(MAX_OUTPUT_DEFAULT), max_output_tokens: Some(MAX_OUTPUT_DEFAULT),
truncation_policy: TruncationPolicy::Tokens(10_000),
) )
} else if slug.starts_with("gpt-5.2") { } else if slug.starts_with("gpt-5.2") {
model_family!( model_family!(
@@ -228,6 +240,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
supports_parallel_tool_calls: true, supports_parallel_tool_calls: true,
context_window: Some(CONTEXT_WINDOW_272K), context_window: Some(CONTEXT_WINDOW_272K),
max_output_tokens: Some(MAX_OUTPUT_DEFAULT), max_output_tokens: Some(MAX_OUTPUT_DEFAULT),
truncation_policy: TruncationPolicy::Bytes(10_000),
) )
} else if slug.starts_with("boomslang") { } else if slug.starts_with("boomslang") {
model_family!( model_family!(
@@ -239,6 +252,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
supports_parallel_tool_calls: true, supports_parallel_tool_calls: true,
context_window: Some(CONTEXT_WINDOW_272K), context_window: Some(CONTEXT_WINDOW_272K),
max_output_tokens: Some(MAX_OUTPUT_DEFAULT), max_output_tokens: Some(MAX_OUTPUT_DEFAULT),
truncation_policy: TruncationPolicy::Bytes(10_000),
) )
} else if slug.starts_with("gpt-5.1") { } else if slug.starts_with("gpt-5.1") {
model_family!( model_family!(
@@ -250,6 +264,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
supports_parallel_tool_calls: true, supports_parallel_tool_calls: true,
context_window: Some(CONTEXT_WINDOW_272K), context_window: Some(CONTEXT_WINDOW_272K),
max_output_tokens: Some(MAX_OUTPUT_DEFAULT), max_output_tokens: Some(MAX_OUTPUT_DEFAULT),
truncation_policy: TruncationPolicy::Bytes(10_000),
) )
} else if slug.starts_with("gpt-5") { } else if slug.starts_with("gpt-5") {
model_family!( model_family!(
@@ -259,6 +274,7 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(), base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(),
context_window: Some(CONTEXT_WINDOW_272K), context_window: Some(CONTEXT_WINDOW_272K),
max_output_tokens: Some(MAX_OUTPUT_DEFAULT), max_output_tokens: Some(MAX_OUTPUT_DEFAULT),
truncation_policy: TruncationPolicy::Bytes(10_000),
) )
} else { } else {
None None
@@ -272,6 +288,7 @@ pub fn derive_default_model_family(model: &str) -> ModelFamily {
needs_special_apply_patch_instructions: false, needs_special_apply_patch_instructions: false,
context_window: None, context_window: None,
max_output_tokens: None, max_output_tokens: None,
truncation_policy: TruncationPolicy::Bytes(10_000),
auto_compact_token_limit: None, auto_compact_token_limit: None,
supports_reasoning_summaries: false, supports_reasoning_summaries: false,
default_reasoning_effort: None, default_reasoning_effort: None,
@@ -293,6 +310,17 @@ impl ModelFamily {
self.auto_compact_token_limit = limit; self.auto_compact_token_limit = limit;
} }
pub fn tool_output_max_bytes(&self) -> usize {
match self.truncation_policy {
TruncationPolicy::Bytes(limit) => limit,
TruncationPolicy::Tokens(limit) => limit.saturating_mul(4),
}
}
pub fn set_truncation_policy(&mut self, policy: TruncationPolicy) {
self.truncation_policy = policy;
}
const fn default_auto_compact_limit(context_window: u64) -> i64 { const fn default_auto_compact_limit(context_window: u64) -> i64 {
// Match upstream behaviour: 90% of the context window. // Match upstream behaviour: 90% of the context window.
((context_window as i64) * 9) / 10 ((context_window as i64) * 9) / 10
+13
View File
@@ -8,6 +8,7 @@ use code_protocol::openai_models::ApplyPatchToolType as ProtocolApplyPatchToolTy
use code_protocol::openai_models::ModelInfo; use code_protocol::openai_models::ModelInfo;
use code_protocol::openai_models::ModelsResponse; use code_protocol::openai_models::ModelsResponse;
use code_protocol::openai_models::ReasoningEffort as ProtocolReasoningEffort; use code_protocol::openai_models::ReasoningEffort as ProtocolReasoningEffort;
use code_protocol::openai_models::TruncationMode as ProtocolTruncationMode;
use reqwest::header; use reqwest::header;
use reqwest::Method; use reqwest::Method;
use reqwest::Url; use reqwest::Url;
@@ -352,6 +353,8 @@ pub fn apply_model_info_overrides(info: &ModelInfo, mut family: ModelFamily) ->
family.set_auto_compact_token_limit(Some(limit)); family.set_auto_compact_token_limit(Some(limit));
} }
family.set_truncation_policy(map_truncation_policy(&info.truncation_policy));
family.supports_reasoning_summaries = info.supports_reasoning_summaries; family.supports_reasoning_summaries = info.supports_reasoning_summaries;
family.supports_parallel_tool_calls = info.supports_parallel_tool_calls; family.supports_parallel_tool_calls = info.supports_parallel_tool_calls;
if let Some(effort) = info.default_reasoning_level { if let Some(effort) = info.default_reasoning_level {
@@ -380,6 +383,16 @@ fn map_reasoning_effort(effort: ProtocolReasoningEffort) -> crate::config_types:
} }
} }
fn map_truncation_policy(
policy: &code_protocol::openai_models::TruncationPolicyConfig,
) -> code_protocol::protocol::TruncationPolicy {
let limit = usize::try_from(policy.limit).unwrap_or(usize::MAX);
match policy.mode {
ProtocolTruncationMode::Bytes => code_protocol::protocol::TruncationPolicy::Bytes(limit),
ProtocolTruncationMode::Tokens => code_protocol::protocol::TruncationPolicy::Tokens(limit),
}
}
/// Convert the build's version triple into a whole semver string. /// Convert the build's version triple into a whole semver string.
fn format_client_version_to_whole() -> String { fn format_client_version_to_whole() -> String {
format!( format!(