mirror of
https://github.com/openai/codex.git
synced 2026-11-06 00:30:29 +00:00
Support per-tool MCP output limits (#41421)
## What changed - Add a positive `output_token_limit` setting to each entry under an MCP server's `tools` configuration. - Apply the most restrictive limit when plugin and user policies overlap, while keeping approval policy independent. - Carry the effective MCP output budget in conversation history so tool output, post-tool hook responses, and resumed sessions use the same truncation limit. ## Testing - Cover configuration parsing, serialization, schema validation, and plugin policy merging. - Cover MCP output below and above the configured limit, post-tool hook responses, and session resume. GitOrigin-RevId: d0beb4fca9ba6055d9e1d31c137373b465d50d61
This commit is contained in:
1 parent
03147407e3
commit
f742dabc6f
38 files changed
+487
-224
No files matched your search
BIN
Binary file not shown.
@@ -268,6 +268,18 @@ impl PreparedMcpCall {
|
|||||||
.tool_approval_mode(&self.tool_info.tool.name)
|
.tool_approval_mode(&self.tool_info.tool.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the explicit output budget captured with this call's effective server config.
|
||||||
|
pub fn output_token_limit(&self) -> Option<usize> {
|
||||||
|
self.config
|
||||||
|
.mcp_server_catalog
|
||||||
|
.server(&self.server_name)?
|
||||||
|
.config()
|
||||||
|
.tools
|
||||||
|
.get(self.tool_info.tool.name.as_ref())?
|
||||||
|
.output_token_limit
|
||||||
|
.map(std::num::NonZeroUsize::get)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn plugin_id(&self) -> Option<&str> {
|
pub fn plugin_id(&self) -> Option<&str> {
|
||||||
self.plugin_id.as_deref()
|
self.plugin_id.as_deref()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ fn server(url: &str) -> McpServerConfig {
|
|||||||
"read".to_string(),
|
"read".to_string(),
|
||||||
McpServerToolConfig {
|
McpServerToolConfig {
|
||||||
approval_mode: Some(AppToolApproval::Approve),
|
approval_mode: Some(AppToolApproval::Approve),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
)]),
|
)]),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3966,6 +3966,7 @@ fn server_metadata_preserves_tool_approval_policy() {
|
|||||||
"search".to_string(),
|
"search".to_string(),
|
||||||
McpServerToolConfig {
|
McpServerToolConfig {
|
||||||
approval_mode: Some(AppToolApproval::Approve),
|
approval_mode: Some(AppToolApproval::Approve),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let metadata = McpServerMetadata::from(&EffectiveMcpServer::configured(config));
|
let metadata = McpServerMetadata::from(&EffectiveMcpServer::configured(config));
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
@@ -71,13 +72,24 @@ impl fmt::Display for McpServerDisabledReason {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-tool approval settings for a single MCP server tool.
|
/// Per-tool settings for a single MCP server tool.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||||
#[schemars(deny_unknown_fields)]
|
#[schemars(deny_unknown_fields)]
|
||||||
pub struct McpServerToolConfig {
|
pub struct McpServerToolConfig {
|
||||||
/// Approval mode for this tool.
|
/// Approval mode for this tool.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub approval_mode: Option<AppToolApproval>,
|
pub approval_mode: Option<AppToolApproval>,
|
||||||
|
|
||||||
|
/// Token budget for this tool's output, before the standard 20% serialization allowance.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub output_token_limit: Option<NonZeroUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl McpServerToolConfig {
|
||||||
|
/// Applies the stricter explicit output budget without changing approval policy.
|
||||||
|
pub fn restrict_output_token_limit(&mut self, limit: Option<NonZeroUsize>) {
|
||||||
|
self.output_token_limit = self.output_token_limit.into_iter().chain(limit).min();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
|
||||||
@@ -250,7 +262,7 @@ pub struct McpServerConfig {
|
|||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub oauth_resource: Option<String>,
|
pub oauth_resource: Option<String>,
|
||||||
|
|
||||||
/// Per-tool approval settings keyed by tool name.
|
/// Per-tool settings keyed by tool name.
|
||||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||||
pub tools: HashMap<String, McpServerToolConfig>,
|
pub tools: HashMap<String, McpServerToolConfig>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -480,6 +480,7 @@ fn deserialize_server_config_with_default_tool_approval_mode() {
|
|||||||
|
|
||||||
[tools.search]
|
[tools.search]
|
||||||
approval_mode = "prompt"
|
approval_mode = "prompt"
|
||||||
|
output_token_limit = 30000
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.expect("should deserialize default tool approval mode");
|
.expect("should deserialize default tool approval mode");
|
||||||
@@ -492,17 +493,31 @@ fn deserialize_server_config_with_default_tool_approval_mode() {
|
|||||||
cfg.tools.get("search"),
|
cfg.tools.get("search"),
|
||||||
Some(&McpServerToolConfig {
|
Some(&McpServerToolConfig {
|
||||||
approval_mode: Some(AppToolApproval::Prompt),
|
approval_mode: Some(AppToolApproval::Prompt),
|
||||||
|
output_token_limit: std::num::NonZeroUsize::new(30_000),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
let serialized = toml::to_string(&cfg).expect("should serialize MCP config");
|
let serialized = toml::to_string(&cfg).expect("should serialize MCP config");
|
||||||
assert!(serialized.contains("default_tools_approval_mode = \"approve\""));
|
assert!(serialized.contains("default_tools_approval_mode = \"approve\""));
|
||||||
|
assert!(serialized.contains("output_token_limit = 30000"));
|
||||||
|
|
||||||
let round_tripped: McpServerConfig =
|
let round_tripped: McpServerConfig =
|
||||||
toml::from_str(&serialized).expect("should deserialize serialized MCP config");
|
toml::from_str(&serialized).expect("should deserialize serialized MCP config");
|
||||||
assert_eq!(round_tripped, cfg);
|
assert_eq!(round_tripped, cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_rejects_nonpositive_mcp_tool_output_limits() {
|
||||||
|
for output_token_limit in [0, -1] {
|
||||||
|
let config = format!(
|
||||||
|
"command = \"echo\"\n[tools.search]\noutput_token_limit = {output_token_limit}\n"
|
||||||
|
);
|
||||||
|
let error = toml::from_str::<McpServerConfig>(&config)
|
||||||
|
.expect_err("MCP tool output limit must be positive");
|
||||||
|
assert!(error.to_string().contains("output_token_limit"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serialize_round_trips_server_config_with_parallel_tool_calls() {
|
fn serialize_round_trips_server_config_with_parallel_tool_calls() {
|
||||||
let cfg: McpServerConfig = toml::from_str(
|
let cfg: McpServerConfig = toml::from_str(
|
||||||
|
|||||||
@@ -879,7 +879,7 @@ pub struct PluginMcpServerConfig {
|
|||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub disabled_tools: Option<Vec<String>>,
|
pub disabled_tools: Option<Vec<String>>,
|
||||||
|
|
||||||
/// Per-tool approval settings keyed by tool name.
|
/// Per-tool policy settings keyed by tool name.
|
||||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||||
pub tools: HashMap<String, McpServerToolConfig>,
|
pub tools: HashMap<String, McpServerToolConfig>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -974,6 +974,7 @@ fn apply_plugin_mcp_server_policy(config: &mut McpServerConfig, policy: &PluginM
|
|||||||
if let Some(approval_mode) = tool_policy.approval_mode {
|
if let Some(approval_mode) = tool_policy.approval_mode {
|
||||||
tool_config.approval_mode = Some(approval_mode);
|
tool_config.approval_mode = Some(approval_mode);
|
||||||
}
|
}
|
||||||
|
tool_config.restrict_output_token_limit(tool_policy.output_token_limit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1449,7 +1450,7 @@ pub fn apply_configured_plugin_mcp_server_policies(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (tool_name, tool_policy) in &policy.tools {
|
for (tool_name, tool_policy) in &policy.tools {
|
||||||
if tool_policy.approval_mode.is_some() {
|
if tool_policy.approval_mode.is_some() || tool_policy.output_token_limit.is_some() {
|
||||||
server.tools.entry(tool_name.clone()).or_default();
|
server.tools.entry(tool_name.clone()).or_default();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1467,6 +1468,12 @@ pub fn apply_configured_plugin_mcp_server_policies(
|
|||||||
.restrict_to(approval_mode),
|
.restrict_to(approval_mode),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
tool_config.restrict_output_token_limit(
|
||||||
|
policy
|
||||||
|
.tools
|
||||||
|
.get(tool_name)
|
||||||
|
.and_then(|tool_policy| tool_policy.output_token_limit),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1113,7 +1113,7 @@ async fn load_plugins_applies_plugin_mcp_server_policy() {
|
|||||||
"default_tools_approval_mode": "prompt",
|
"default_tools_approval_mode": "prompt",
|
||||||
"enabled_tools": ["read", "search"],
|
"enabled_tools": ["read", "search"],
|
||||||
"tools": {
|
"tools": {
|
||||||
"search": { "approval_mode": "prompt" }
|
"search": { "approval_mode": "prompt", "output_token_limit": 8000 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1134,6 +1134,7 @@ disabled_tools = ["delete"]
|
|||||||
|
|
||||||
[plugins."sample@test".mcp_servers.sample.tools.search]
|
[plugins."sample@test".mcp_servers.sample.tools.search]
|
||||||
approval_mode = "approve"
|
approval_mode = "approve"
|
||||||
|
output_token_limit = 12000
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let outcome =
|
let outcome =
|
||||||
@@ -1154,6 +1155,7 @@ approval_mode = "approve"
|
|||||||
server.tools.get("search"),
|
server.tools.get("search"),
|
||||||
Some(&McpServerToolConfig {
|
Some(&McpServerToolConfig {
|
||||||
approval_mode: Some(AppToolApproval::Approve),
|
approval_mode: Some(AppToolApproval::Approve),
|
||||||
|
output_token_limit: std::num::NonZeroUsize::new(8_000),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1966,7 +1966,7 @@
|
|||||||
},
|
},
|
||||||
"McpServerToolConfig": {
|
"McpServerToolConfig": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"description": "Per-tool approval settings for a single MCP server tool.",
|
"description": "Per-tool settings for a single MCP server tool.",
|
||||||
"properties": {
|
"properties": {
|
||||||
"approval_mode": {
|
"approval_mode": {
|
||||||
"allOf": [
|
"allOf": [
|
||||||
@@ -1975,6 +1975,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Approval mode for this tool."
|
"description": "Approval mode for this tool."
|
||||||
|
},
|
||||||
|
"output_token_limit": {
|
||||||
|
"description": "Token budget for this tool's output, before the standard 20% serialization allowance.",
|
||||||
|
"format": "uint",
|
||||||
|
"minimum": 1.0,
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
@@ -2990,7 +2996,7 @@
|
|||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"$ref": "#/definitions/McpServerToolConfig"
|
"$ref": "#/definitions/McpServerToolConfig"
|
||||||
},
|
},
|
||||||
"description": "Per-tool approval settings keyed by tool name.",
|
"description": "Per-tool policy settings keyed by tool name.",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -882,6 +882,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
Some(CodexHarnessMetadata {
|
Some(CodexHarnessMetadata {
|
||||||
client_authored: true,
|
client_authored: true,
|
||||||
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
Some(CodexHarnessMetadata::default()),
|
Some(CodexHarnessMetadata::default()),
|
||||||
None,
|
None,
|
||||||
@@ -905,6 +906,7 @@ mod tests {
|
|||||||
item: client.clone(),
|
item: client.clone(),
|
||||||
metadata: Some(CodexHarnessMetadata {
|
metadata: Some(CodexHarnessMetadata {
|
||||||
client_authored: true,
|
client_authored: true,
|
||||||
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ fn image_treatment_preserves_client_developer_boundary_behavior() {
|
|||||||
item,
|
item,
|
||||||
metadata: Some(CodexHarnessMetadata {
|
metadata: Some(CodexHarnessMetadata {
|
||||||
client_authored: true,
|
client_authored: true,
|
||||||
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
for max_tokens in [1871, 1879, 1880, 1950, 2200] {
|
for max_tokens in [1871, 1879, 1880, 1950, 2200] {
|
||||||
|
|||||||
@@ -6773,6 +6773,7 @@ approval_mode = "approve"
|
|||||||
server.tools.get("search"),
|
server.tools.get("search"),
|
||||||
Some(&McpServerToolConfig {
|
Some(&McpServerToolConfig {
|
||||||
approval_mode: Some(AppToolApproval::Approve),
|
approval_mode: Some(AppToolApproval::Approve),
|
||||||
|
..Default::default()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -6816,6 +6817,7 @@ approval_mode = "approve"
|
|||||||
tool,
|
tool,
|
||||||
&McpServerToolConfig {
|
&McpServerToolConfig {
|
||||||
approval_mode: Some(AppToolApproval::Approve),
|
approval_mode: Some(AppToolApproval::Approve),
|
||||||
|
..Default::default()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ impl ConfigDocument {
|
|||||||
&[NOTICE_TABLE_KEY, "model_migrations", from.as_str()],
|
&[NOTICE_TABLE_KEY, "model_migrations", from.as_str()],
|
||||||
value(to.clone()),
|
value(to.clone()),
|
||||||
)),
|
)),
|
||||||
ConfigEdit::ReplaceMcpServers(servers) => Ok(self.replace_mcp_servers(servers)),
|
ConfigEdit::ReplaceMcpServers(servers) => self.replace_mcp_servers(servers),
|
||||||
ConfigEdit::AddToolSuggestDisabledTool(disabled_tool) => {
|
ConfigEdit::AddToolSuggestDisabledTool(disabled_tool) => {
|
||||||
Ok(self.add_tool_suggest_disabled_tool(disabled_tool))
|
Ok(self.add_tool_suggest_disabled_tool(disabled_tool))
|
||||||
}
|
}
|
||||||
@@ -432,9 +432,12 @@ impl ConfigDocument {
|
|||||||
self.remove(segments)
|
self.remove(segments)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_mcp_servers(&mut self, servers: &BTreeMap<String, McpServerConfig>) -> bool {
|
fn replace_mcp_servers(
|
||||||
|
&mut self,
|
||||||
|
servers: &BTreeMap<String, McpServerConfig>,
|
||||||
|
) -> anyhow::Result<bool> {
|
||||||
if servers.is_empty() {
|
if servers.is_empty() {
|
||||||
return self.clear(&["mcp_servers"]);
|
return Ok(self.clear(&["mcp_servers"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
let root = self.doc.as_table_mut();
|
let root = self.doc.as_table_mut();
|
||||||
@@ -446,7 +449,7 @@ impl ConfigDocument {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let Some(item) = root.get_mut("mcp_servers") else {
|
let Some(item) = root.get_mut("mcp_servers") else {
|
||||||
return false;
|
return Ok(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
if document_helpers::ensure_table_for_write(item).is_none() {
|
if document_helpers::ensure_table_for_write(item).is_none() {
|
||||||
@@ -454,7 +457,7 @@ impl ConfigDocument {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let Some(table) = item.as_table_mut() else {
|
let Some(table) = item.as_table_mut() else {
|
||||||
return false;
|
return Ok(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
let keys_to_remove: Vec<String> = table
|
let keys_to_remove: Vec<String> = table
|
||||||
@@ -472,17 +475,17 @@ impl ConfigDocument {
|
|||||||
if let TomlItem::Value(value) = existing
|
if let TomlItem::Value(value) = existing
|
||||||
&& let Some(inline) = value.as_inline_table_mut()
|
&& let Some(inline) = value.as_inline_table_mut()
|
||||||
{
|
{
|
||||||
let replacement = document_helpers::serialize_mcp_server_inline(config);
|
let replacement = document_helpers::serialize_mcp_server_inline(config)?;
|
||||||
document_helpers::merge_inline_table(inline, replacement);
|
document_helpers::merge_inline_table(inline, replacement);
|
||||||
} else {
|
} else {
|
||||||
*existing = document_helpers::serialize_mcp_server(config);
|
*existing = document_helpers::serialize_mcp_server(config)?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
table.insert(name, document_helpers::serialize_mcp_server(config));
|
table.insert(name, document_helpers::serialize_mcp_server(config)?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_skill_config(&mut self, selector: SkillConfigSelector, enabled: bool) -> bool {
|
fn set_skill_config(&mut self, selector: SkillConfigSelector, enabled: bool) -> bool {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use anyhow::Context;
|
||||||
use codex_config::types::AppToolApproval;
|
use codex_config::types::AppToolApproval;
|
||||||
use codex_config::types::McpServerAuth;
|
use codex_config::types::McpServerAuth;
|
||||||
use codex_config::types::McpServerConfig;
|
use codex_config::types::McpServerConfig;
|
||||||
@@ -45,7 +46,7 @@ pub(super) fn ensure_table_for_read(item: &mut TomlItem) -> Option<&mut TomlTabl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialize_mcp_server_table(config: &McpServerConfig) -> TomlTable {
|
fn serialize_mcp_server_table(config: &McpServerConfig) -> anyhow::Result<TomlTable> {
|
||||||
let mut entry = TomlTable::new();
|
let mut entry = TomlTable::new();
|
||||||
entry.set_implicit(false);
|
entry.set_implicit(false);
|
||||||
|
|
||||||
@@ -175,15 +176,15 @@ fn serialize_mcp_server_table(config: &McpServerConfig) -> TomlTable {
|
|||||||
let mut tool_entries: Vec<_> = config.tools.iter().collect();
|
let mut tool_entries: Vec<_> = config.tools.iter().collect();
|
||||||
tool_entries.sort_by_key(|(name, _)| *name);
|
tool_entries.sort_by_key(|(name, _)| *name);
|
||||||
for (name, tool_config) in tool_entries {
|
for (name, tool_config) in tool_entries {
|
||||||
tools.insert(name, serialize_mcp_server_tool(tool_config));
|
tools.insert(name, serialize_mcp_server_tool(tool_config)?);
|
||||||
}
|
}
|
||||||
entry.insert("tools", TomlItem::Table(tools));
|
entry.insert("tools", TomlItem::Table(tools));
|
||||||
}
|
}
|
||||||
|
|
||||||
entry
|
Ok(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialize_mcp_server_tool(config: &McpServerToolConfig) -> TomlItem {
|
fn serialize_mcp_server_tool(config: &McpServerToolConfig) -> anyhow::Result<TomlItem> {
|
||||||
let mut entry = TomlTable::new();
|
let mut entry = TomlTable::new();
|
||||||
entry.set_implicit(false);
|
entry.set_implicit(false);
|
||||||
if let Some(approval_mode) = config.approval_mode {
|
if let Some(approval_mode) = config.approval_mode {
|
||||||
@@ -194,15 +195,21 @@ fn serialize_mcp_server_tool(config: &McpServerToolConfig) -> TomlItem {
|
|||||||
AppToolApproval::Approve => "approve",
|
AppToolApproval::Approve => "approve",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
TomlItem::Table(entry)
|
if let Some(output_token_limit) = config.output_token_limit {
|
||||||
|
entry["output_token_limit"] = value(
|
||||||
|
i64::try_from(output_token_limit.get())
|
||||||
|
.context("output_token_limit exceeds the TOML integer range")?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(TomlItem::Table(entry))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn serialize_mcp_server(config: &McpServerConfig) -> TomlItem {
|
pub(super) fn serialize_mcp_server(config: &McpServerConfig) -> anyhow::Result<TomlItem> {
|
||||||
TomlItem::Table(serialize_mcp_server_table(config))
|
Ok(TomlItem::Table(serialize_mcp_server_table(config)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn serialize_mcp_server_inline(config: &McpServerConfig) -> InlineTable {
|
pub(super) fn serialize_mcp_server_inline(config: &McpServerConfig) -> anyhow::Result<InlineTable> {
|
||||||
serialize_mcp_server_table(config).into_inline_table()
|
Ok(serialize_mcp_server_table(config)?.into_inline_table())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn merge_inline_table(existing: &mut InlineTable, replacement: InlineTable) {
|
pub(super) fn merge_inline_table(existing: &mut InlineTable, replacement: InlineTable) {
|
||||||
|
|||||||
@@ -1161,8 +1161,9 @@ B = \"2\"
|
|||||||
assert_eq!(raw, expected);
|
assert_eq!(raw, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_case::test_case(30_000; "positive limit")]
|
||||||
fn blocking_replace_mcp_servers_serializes_tool_approval_overrides() {
|
#[test_case::test_case(usize::MAX; "largest native limit")]
|
||||||
|
fn blocking_replace_mcp_servers_serializes_tool_approval_overrides(output_token_limit: usize) {
|
||||||
let tmp = tempdir().expect("tmpdir");
|
let tmp = tempdir().expect("tmpdir");
|
||||||
let codex_home = tmp.path();
|
let codex_home = tmp.path();
|
||||||
|
|
||||||
@@ -1196,22 +1197,36 @@ fn blocking_replace_mcp_servers_serializes_tool_approval_overrides() {
|
|||||||
"search".to_string(),
|
"search".to_string(),
|
||||||
McpServerToolConfig {
|
McpServerToolConfig {
|
||||||
approval_mode: Some(AppToolApproval::Approve),
|
approval_mode: Some(AppToolApproval::Approve),
|
||||||
|
output_token_limit: std::num::NonZeroUsize::new(output_token_limit),
|
||||||
},
|
},
|
||||||
)]),
|
)]),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");
|
let result = apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]);
|
||||||
|
if i64::try_from(output_token_limit).is_err() {
|
||||||
|
assert!(
|
||||||
|
result
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.contains("output_token_limit exceeds the TOML integer range")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.expect("persist");
|
||||||
|
|
||||||
let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||||
let expected = "\
|
let expected = format!(
|
||||||
|
"\
|
||||||
[mcp_servers.docs]
|
[mcp_servers.docs]
|
||||||
command = \"docs-server\"
|
command = \"docs-server\"
|
||||||
default_tools_approval_mode = \"prompt\"
|
default_tools_approval_mode = \"prompt\"
|
||||||
|
|
||||||
[mcp_servers.docs.tools.search]
|
[mcp_servers.docs.tools.search]
|
||||||
approval_mode = \"approve\"
|
approval_mode = \"approve\"
|
||||||
";
|
output_token_limit = {output_token_limit}
|
||||||
|
"
|
||||||
|
);
|
||||||
assert_eq!(raw, expected);
|
assert_eq!(raw, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ use codex_protocol::models::BaseInstructions;
|
|||||||
use codex_protocol::models::ContentItem;
|
use codex_protocol::models::ContentItem;
|
||||||
use codex_protocol::models::FunctionCallOutputBody;
|
use codex_protocol::models::FunctionCallOutputBody;
|
||||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||||
use codex_protocol::models::FunctionCallOutputPayload;
|
|
||||||
use codex_protocol::models::ImageDetail;
|
use codex_protocol::models::ImageDetail;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use codex_protocol::openai_models::InputModality;
|
use codex_protocol::openai_models::InputModality;
|
||||||
@@ -37,8 +36,7 @@ use codex_utils_output_truncation::TruncationPolicy;
|
|||||||
use codex_utils_output_truncation::approx_bytes_for_tokens;
|
use codex_utils_output_truncation::approx_bytes_for_tokens;
|
||||||
use codex_utils_output_truncation::approx_token_count;
|
use codex_utils_output_truncation::approx_token_count;
|
||||||
use codex_utils_output_truncation::approx_tokens_from_byte_count_i64;
|
use codex_utils_output_truncation::approx_tokens_from_byte_count_i64;
|
||||||
use codex_utils_output_truncation::truncate_function_output_items_with_policy;
|
use codex_utils_output_truncation::truncate_function_output_payload;
|
||||||
use codex_utils_output_truncation::truncate_text;
|
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -171,7 +169,7 @@ impl ContextManager {
|
|||||||
self.record_items_with_metadata(items.into_iter().map(|item| (item, None)), policy);
|
self.record_items_with_metadata(items.into_iter().map(|item| (item, None)), policy);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Records history envelopes while preserving their history-only metadata.
|
/// Records output while preserving its history-only metadata.
|
||||||
pub(crate) fn record_annotated_items(
|
pub(crate) fn record_annotated_items(
|
||||||
&mut self,
|
&mut self,
|
||||||
items: &[ResponseItemEnvelope],
|
items: &[ResponseItemEnvelope],
|
||||||
@@ -196,10 +194,20 @@ impl ContextManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let processed = ResponseItemEnvelope {
|
let mut processed = ResponseItemEnvelope {
|
||||||
item: Self::process_item(item, policy),
|
item: item.clone(),
|
||||||
metadata: metadata.cloned(),
|
metadata: metadata.cloned(),
|
||||||
};
|
};
|
||||||
|
if let ResponseItem::FunctionCallOutput { output, .. }
|
||||||
|
| ResponseItem::CustomToolCallOutput { output, .. } = &mut processed.item
|
||||||
|
{
|
||||||
|
// The override already includes the tool's serialization allowance.
|
||||||
|
let policy = metadata
|
||||||
|
.and_then(|metadata| metadata.fallback_token_limit_override)
|
||||||
|
.map(TruncationPolicy::Tokens)
|
||||||
|
.unwrap_or(policy * 1.2);
|
||||||
|
truncate_function_output_payload(output, policy, estimate_audio_token_count);
|
||||||
|
}
|
||||||
Arc::make_mut(&mut self.items).push(processed);
|
Arc::make_mut(&mut self.items).push(processed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -473,55 +481,6 @@ impl ContextManager {
|
|||||||
normalize::strip_audio_when_unsupported(input_modalities, items);
|
normalize::strip_audio_when_unsupported(input_modalities, items);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_item(item: &ResponseItem, policy: TruncationPolicy) -> ResponseItem {
|
|
||||||
let policy_with_serialization_budget = policy * 1.2;
|
|
||||||
match item {
|
|
||||||
ResponseItem::FunctionCallOutput {
|
|
||||||
id,
|
|
||||||
call_id,
|
|
||||||
name,
|
|
||||||
namespace,
|
|
||||||
output,
|
|
||||||
internal_chat_message_metadata_passthrough: metadata,
|
|
||||||
} => ResponseItem::FunctionCallOutput {
|
|
||||||
id: id.clone(),
|
|
||||||
call_id: call_id.clone(),
|
|
||||||
name: name.clone(),
|
|
||||||
namespace: namespace.clone(),
|
|
||||||
output: truncate_function_output_payload(output, policy_with_serialization_budget),
|
|
||||||
internal_chat_message_metadata_passthrough: metadata.clone(),
|
|
||||||
},
|
|
||||||
ResponseItem::CustomToolCallOutput {
|
|
||||||
id,
|
|
||||||
call_id,
|
|
||||||
name,
|
|
||||||
output,
|
|
||||||
internal_chat_message_metadata_passthrough: metadata,
|
|
||||||
} => ResponseItem::CustomToolCallOutput {
|
|
||||||
id: id.clone(),
|
|
||||||
call_id: call_id.clone(),
|
|
||||||
name: name.clone(),
|
|
||||||
output: truncate_function_output_payload(output, policy_with_serialization_budget),
|
|
||||||
internal_chat_message_metadata_passthrough: metadata.clone(),
|
|
||||||
},
|
|
||||||
ResponseItem::AdditionalTools { .. }
|
|
||||||
| ResponseItem::Message { .. }
|
|
||||||
| ResponseItem::AgentMessage { .. }
|
|
||||||
| ResponseItem::Reasoning { .. }
|
|
||||||
| ResponseItem::LocalShellCall { .. }
|
|
||||||
| ResponseItem::FunctionCall { .. }
|
|
||||||
| ResponseItem::ToolSearchCall { .. }
|
|
||||||
| ResponseItem::ToolSearchOutput { .. }
|
|
||||||
| ResponseItem::WebSearchCall { .. }
|
|
||||||
| ResponseItem::ImageGenerationCall { .. }
|
|
||||||
| ResponseItem::CustomToolCall { .. }
|
|
||||||
| ResponseItem::Compaction { .. }
|
|
||||||
| ResponseItem::CompactionTrigger { .. }
|
|
||||||
| ResponseItem::ContextCompaction { .. }
|
|
||||||
| ResponseItem::Other => item.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Walk backward from a rollback cut and trim contiguous pre-turn context-update items.
|
/// Walk backward from a rollback cut and trim contiguous pre-turn context-update items.
|
||||||
///
|
///
|
||||||
/// Returns the adjusted cut index after removing contextual developer/user items immediately
|
/// Returns the adjusted cut index after removing contextual developer/user items immediately
|
||||||
@@ -570,25 +529,6 @@ impl ContextManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn truncate_function_output_payload(
|
|
||||||
output: &FunctionCallOutputPayload,
|
|
||||||
policy: TruncationPolicy,
|
|
||||||
) -> FunctionCallOutputPayload {
|
|
||||||
let body = match &output.body {
|
|
||||||
FunctionCallOutputBody::Text(content) => {
|
|
||||||
FunctionCallOutputBody::Text(truncate_text(content, policy))
|
|
||||||
}
|
|
||||||
FunctionCallOutputBody::ContentItems(items) => FunctionCallOutputBody::ContentItems(
|
|
||||||
truncate_function_output_items_with_policy(items, policy, estimate_audio_token_count),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
FunctionCallOutputPayload {
|
|
||||||
body,
|
|
||||||
success: output.success,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// API messages include every non-system item (user/assistant messages, reasoning,
|
/// API messages include every non-system item (user/assistant messages, reasoning,
|
||||||
/// tool calls, tool outputs, shell calls, web-search calls, and image-generation
|
/// tool calls, tool outputs, shell calls, web-search calls, and image-generation
|
||||||
/// calls).
|
/// calls).
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ use image::Luma;
|
|||||||
use image::Rgba;
|
use image::Rgba;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use regex_lite::Regex;
|
use regex_lite::Regex;
|
||||||
|
use test_case::test_case;
|
||||||
|
|
||||||
const EXEC_FORMAT_MAX_BYTES: usize = 10_000;
|
const EXEC_FORMAT_MAX_BYTES: usize = 10_000;
|
||||||
const EXEC_FORMAT_MAX_TOKENS: usize = 2_500;
|
const EXEC_FORMAT_MAX_TOKENS: usize = 2_500;
|
||||||
@@ -526,8 +527,16 @@ fn annotated_history_apis_preserve_envelopes() {
|
|||||||
assert_eq!(history.into_raw_items(), vec![first_item]);
|
assert_eq!(history.into_raw_items(), vec![first_item]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_case(None, 100, 5, true; "model policy")]
|
||||||
fn record_annotated_items_preserves_metadata_while_processing_item() {
|
#[test_case(Some(200), 100, 200, false; "configured override")]
|
||||||
|
#[test_case(Some(100), 85, 100, true; "saved limit has no additional allowance")]
|
||||||
|
#[test_case(Some(30_000), 20_000, 30_000, false; "large explicit budget")]
|
||||||
|
fn record_annotated_items_preserves_metadata_while_processing_item(
|
||||||
|
fallback_token_limit_override: Option<usize>,
|
||||||
|
repeat_count: usize,
|
||||||
|
expected_token_limit: usize,
|
||||||
|
expected_truncation: bool,
|
||||||
|
) {
|
||||||
let envelope = ResponseItemEnvelope {
|
let envelope = ResponseItemEnvelope {
|
||||||
item: ResponseItem::FunctionCallOutput {
|
item: ResponseItem::FunctionCallOutput {
|
||||||
id: None,
|
id: None,
|
||||||
@@ -535,23 +544,36 @@ fn record_annotated_items_preserves_metadata_while_processing_item() {
|
|||||||
name: None,
|
name: None,
|
||||||
namespace: None,
|
namespace: None,
|
||||||
output: FunctionCallOutputPayload {
|
output: FunctionCallOutputPayload {
|
||||||
body: FunctionCallOutputBody::Text("word ".repeat(100)),
|
body: FunctionCallOutputBody::Text("word ".repeat(repeat_count)),
|
||||||
success: Some(true),
|
success: Some(true),
|
||||||
},
|
},
|
||||||
internal_chat_message_metadata_passthrough: None,
|
internal_chat_message_metadata_passthrough: None,
|
||||||
},
|
},
|
||||||
metadata: Some(CodexHarnessMetadata::default()),
|
metadata: Some(CodexHarnessMetadata {
|
||||||
|
fallback_token_limit_override,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
let mut history = ContextManager::new();
|
let mut history = ContextManager::new();
|
||||||
|
|
||||||
history.record_annotated_items(std::slice::from_ref(&envelope), TruncationPolicy::Tokens(4));
|
history.record_annotated_items(std::slice::from_ref(&envelope), TruncationPolicy::Tokens(4));
|
||||||
|
|
||||||
assert_eq!(history.annotated_items().len(), 1);
|
assert_eq!(history.annotated_items().len(), 1);
|
||||||
|
assert_eq!(history.annotated_items()[0].metadata, envelope.metadata);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
history.annotated_items()[0].metadata,
|
history.annotated_items()[0].item != envelope.item,
|
||||||
Some(CodexHarnessMetadata::default())
|
expected_truncation
|
||||||
|
);
|
||||||
|
let ResponseItem::FunctionCallOutput { output, .. } = &history.annotated_items()[0].item else {
|
||||||
|
panic!("expected function call output");
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
output.body,
|
||||||
|
FunctionCallOutputBody::Text(truncate_text(
|
||||||
|
&"word ".repeat(repeat_count),
|
||||||
|
TruncationPolicy::Tokens(expected_token_limit),
|
||||||
|
))
|
||||||
);
|
);
|
||||||
assert_ne!(history.annotated_items()[0].item, envelope.item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -6,4 +6,3 @@ pub(crate) use history::ContextManager;
|
|||||||
pub(crate) use history::estimate_image_bytes;
|
pub(crate) use history::estimate_image_bytes;
|
||||||
pub(crate) use history::estimate_item_token_count;
|
pub(crate) use history::estimate_item_token_count;
|
||||||
pub(crate) use history::is_user_turn_boundary;
|
pub(crate) use history::is_user_turn_boundary;
|
||||||
pub(crate) use history::truncate_function_output_payload;
|
|
||||||
@@ -2102,6 +2102,7 @@ async fn persist_custom_mcp_tool_approval_writes_tool_override() {
|
|||||||
tool,
|
tool,
|
||||||
&McpServerToolConfig {
|
&McpServerToolConfig {
|
||||||
approval_mode: Some(AppToolApproval::Approve),
|
approval_mode: Some(AppToolApproval::Approve),
|
||||||
|
..Default::default()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert!(contents.contains("[mcp_servers.docs.tools.search]"));
|
assert!(contents.contains("[mcp_servers.docs.tools.search]"));
|
||||||
@@ -2319,6 +2320,7 @@ async fn maybe_persist_mcp_tool_approval_reloads_session_config_for_custom_serve
|
|||||||
tool,
|
tool,
|
||||||
&McpServerToolConfig {
|
&McpServerToolConfig {
|
||||||
approval_mode: Some(AppToolApproval::Approve),
|
approval_mode: Some(AppToolApproval::Approve),
|
||||||
|
..Default::default()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(mcp_tool_approval_is_remembered(&session, &key).await, true);
|
assert_eq!(mcp_tool_approval_is_remembered(&session, &key).await, true);
|
||||||
@@ -2368,6 +2370,7 @@ enabled = true
|
|||||||
tool,
|
tool,
|
||||||
&McpServerToolConfig {
|
&McpServerToolConfig {
|
||||||
approval_mode: Some(AppToolApproval::Approve),
|
approval_mode: Some(AppToolApproval::Approve),
|
||||||
|
..Default::default()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert!(contents.contains(r#"[plugins."sample@test".mcp_servers.sample.tools.search]"#));
|
assert!(contents.contains(r#"[plugins."sample@test".mcp_servers.sample.tools.search]"#));
|
||||||
@@ -2423,6 +2426,7 @@ async fn maybe_persist_mcp_tool_approval_writes_project_config_for_project_serve
|
|||||||
tool,
|
tool,
|
||||||
&McpServerToolConfig {
|
&McpServerToolConfig {
|
||||||
approval_mode: Some(AppToolApproval::Approve),
|
approval_mode: Some(AppToolApproval::Approve),
|
||||||
|
..Default::default()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert!(contents.contains("[mcp_servers.docs.tools.search]"));
|
assert!(contents.contains("[mcp_servers.docs.tools.search]"));
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ impl Session {
|
|||||||
&& matches!(&item, ResponseItem::Message { role, .. } if role == "developer"))
|
&& matches!(&item, ResponseItem::Message { role, .. } if role == "developer"))
|
||||||
.then_some(CodexHarnessMetadata {
|
.then_some(CodexHarnessMetadata {
|
||||||
client_authored: true,
|
client_authored: true,
|
||||||
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
ResponseItemEnvelope { item, metadata }
|
ResponseItemEnvelope { item, metadata }
|
||||||
@@ -82,9 +83,7 @@ impl Session {
|
|||||||
turn_context: &TurnContext,
|
turn_context: &TurnContext,
|
||||||
items: Vec<ResponseItemEnvelope>,
|
items: Vec<ResponseItemEnvelope>,
|
||||||
) {
|
) {
|
||||||
if !self.enabled(Feature::RetainClientDeveloperMessages)
|
if items.iter().all(|item| item.metadata.is_none()) {
|
||||||
|| items.iter().all(|item| item.metadata.is_none())
|
|
||||||
{
|
|
||||||
let items = items
|
let items = items
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(ResponseItemEnvelope::into_item)
|
.map(ResponseItemEnvelope::into_item)
|
||||||
|
|||||||
@@ -401,6 +401,7 @@ mod tests {
|
|||||||
item: ResponseItem::Other,
|
item: ResponseItem::Other,
|
||||||
metadata: Some(CodexHarnessMetadata {
|
metadata: Some(CodexHarnessMetadata {
|
||||||
client_authored: true,
|
client_authored: true,
|
||||||
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
assert!(serde_json::to_value(annotated).is_err());
|
assert!(serde_json::to_value(annotated).is_err());
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ use crate::session::step_context::StepContext;
|
|||||||
use crate::session::turn_context::TurnContext;
|
use crate::session::turn_context::TurnContext;
|
||||||
use crate::skills::emit_explicit_skill_invocations;
|
use crate::skills::emit_explicit_skill_invocations;
|
||||||
use crate::stream_events_utils::HandleOutputCtx;
|
use crate::stream_events_utils::HandleOutputCtx;
|
||||||
|
use crate::stream_events_utils::InFlightFuture;
|
||||||
use crate::stream_events_utils::TurnItemContributorPolicy;
|
use crate::stream_events_utils::TurnItemContributorPolicy;
|
||||||
use crate::stream_events_utils::finalize_non_tool_response_item;
|
use crate::stream_events_utils::finalize_non_tool_response_item;
|
||||||
use crate::stream_events_utils::handle_non_tool_response_item;
|
use crate::stream_events_utils::handle_non_tool_response_item;
|
||||||
@@ -90,7 +91,6 @@ use codex_protocol::items::build_hook_prompt_message;
|
|||||||
use codex_protocol::models::BaseInstructions;
|
use codex_protocol::models::BaseInstructions;
|
||||||
use codex_protocol::models::ContentItem;
|
use codex_protocol::models::ContentItem;
|
||||||
use codex_protocol::models::MessagePhase;
|
use codex_protocol::models::MessagePhase;
|
||||||
use codex_protocol::models::ResponseInputItem;
|
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use codex_protocol::protocol::AgentMessageContentDeltaEvent;
|
use codex_protocol::protocol::AgentMessageContentDeltaEvent;
|
||||||
use codex_protocol::protocol::AgentReasoningSectionBreakEvent;
|
use codex_protocol::protocol::AgentReasoningSectionBreakEvent;
|
||||||
@@ -124,7 +124,6 @@ use codex_utils_stream_parser::AssistantTextStreamParser;
|
|||||||
use codex_utils_stream_parser::ProposedPlanSegment;
|
use codex_utils_stream_parser::ProposedPlanSegment;
|
||||||
use codex_utils_stream_parser::extract_proposed_plan_text;
|
use codex_utils_stream_parser::extract_proposed_plan_text;
|
||||||
use codex_utils_stream_parser::strip_citations;
|
use codex_utils_stream_parser::strip_citations;
|
||||||
use futures::future::BoxFuture;
|
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
use futures::stream::FuturesOrdered;
|
use futures::stream::FuturesOrdered;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
@@ -2154,22 +2153,21 @@ async fn handle_assistant_item_done_in_plan_mode(
|
|||||||
|
|
||||||
#[instrument(level = "trace", skip_all)]
|
#[instrument(level = "trace", skip_all)]
|
||||||
async fn drain_in_flight(
|
async fn drain_in_flight(
|
||||||
in_flight: &mut FuturesOrdered<BoxFuture<'static, CodexResult<ResponseInputItem>>>,
|
in_flight: &mut FuturesOrdered<InFlightFuture<'static>>,
|
||||||
sess: Arc<Session>,
|
sess: Arc<Session>,
|
||||||
turn_context: Arc<TurnContext>,
|
turn_context: Arc<TurnContext>,
|
||||||
) -> CodexResult<()> {
|
) -> CodexResult<()> {
|
||||||
while let Some(res) = in_flight.next().await {
|
while let Some(res) = in_flight.next().await {
|
||||||
match res {
|
match res {
|
||||||
Ok(response_input) => {
|
Ok(envelope) => {
|
||||||
let response_item = response_input.into();
|
|
||||||
sess.record_conversation_items(&turn_context, std::slice::from_ref(&response_item))
|
|
||||||
.await;
|
|
||||||
mark_thread_memory_mode_polluted_if_external_context(
|
mark_thread_memory_mode_polluted_if_external_context(
|
||||||
sess.as_ref(),
|
sess.as_ref(),
|
||||||
turn_context.as_ref(),
|
turn_context.as_ref(),
|
||||||
&response_item,
|
&envelope.item,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
sess.record_annotated_conversation_items(&turn_context, vec![envelope])
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error_or_panic(format!("in-flight tool future failed during drain: {err}"));
|
error_or_panic(format!("in-flight tool future failed during drain: {err}"));
|
||||||
@@ -2247,8 +2245,7 @@ async fn try_run_sampling_request(
|
|||||||
.instrument(trace_span!("stream_request"))
|
.instrument(trace_span!("stream_request"))
|
||||||
.or_cancel(&cancellation_token)
|
.or_cancel(&cancellation_token)
|
||||||
.await??;
|
.await??;
|
||||||
let mut in_flight: FuturesOrdered<BoxFuture<'static, CodexResult<ResponseInputItem>>> =
|
let mut in_flight: FuturesOrdered<InFlightFuture<'static>> = FuturesOrdered::new();
|
||||||
FuturesOrdered::new();
|
|
||||||
let mut needs_follow_up = false;
|
let mut needs_follow_up = false;
|
||||||
let mut last_agent_message: Option<String> = None;
|
let mut last_agent_message: Option<String> = None;
|
||||||
let mut active_item: Option<TurnItem> = None;
|
let mut active_item: Option<TurnItem> = None;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::pin::Pin;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use codex_extension_api::ExtensionData;
|
use codex_extension_api::ExtensionData;
|
||||||
|
use codex_history::ResponseItemEnvelope;
|
||||||
use codex_protocol::ResponseItemId;
|
use codex_protocol::ResponseItemId;
|
||||||
use codex_protocol::config_types::ModeKind;
|
use codex_protocol::config_types::ModeKind;
|
||||||
use codex_protocol::items::TurnItem;
|
use codex_protocol::items::TurnItem;
|
||||||
@@ -191,7 +192,7 @@ async fn record_stage1_output_usage_for_memory_citation(
|
|||||||
/// queuing any tool execution futures. This records items immediately so
|
/// queuing any tool execution futures. This records items immediately so
|
||||||
/// history and rollout stay in sync even if the turn is later cancelled.
|
/// history and rollout stay in sync even if the turn is later cancelled.
|
||||||
pub(crate) type InFlightFuture<'f> =
|
pub(crate) type InFlightFuture<'f> =
|
||||||
Pin<Box<dyn Future<Output = Result<ResponseInputItem>> + Send + 'f>>;
|
Pin<Box<dyn Future<Output = Result<ResponseItemEnvelope>> + Send + 'f>>;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct OutputItemResult {
|
pub(crate) struct OutputItemResult {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use crate::context_manager::truncate_function_output_payload;
|
|
||||||
use crate::original_image_detail::sanitize_original_image_detail;
|
use crate::original_image_detail::sanitize_original_image_detail;
|
||||||
use crate::session::session::Session;
|
use crate::session::session::Session;
|
||||||
use crate::session::step_context::StepContext;
|
use crate::session::step_context::StepContext;
|
||||||
@@ -16,9 +15,11 @@ use codex_protocol::models::ResponseItem;
|
|||||||
use codex_protocol::models::function_call_output_content_items_to_text;
|
use codex_protocol::models::function_call_output_content_items_to_text;
|
||||||
use codex_tools::LoadableToolSpec;
|
use codex_tools::LoadableToolSpec;
|
||||||
use codex_tools::ToolName;
|
use codex_tools::ToolName;
|
||||||
|
use codex_utils_audio::estimate_audio_token_count;
|
||||||
use codex_utils_output_truncation::TruncationPolicy;
|
use codex_utils_output_truncation::TruncationPolicy;
|
||||||
use codex_utils_output_truncation::approx_token_count;
|
use codex_utils_output_truncation::approx_token_count;
|
||||||
use codex_utils_output_truncation::formatted_truncate_text;
|
use codex_utils_output_truncation::formatted_truncate_text;
|
||||||
|
use codex_utils_output_truncation::truncate_function_output_payload;
|
||||||
use codex_utils_output_truncation::truncate_text;
|
use codex_utils_output_truncation::truncate_text;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
@@ -122,6 +123,10 @@ impl ToolOutput for McpToolOutput {
|
|||||||
self.result.success()
|
self.result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fallback_token_limit_override(&self) -> Option<usize> {
|
||||||
|
Some((self.truncation_policy * 1.2).token_budget())
|
||||||
|
}
|
||||||
|
|
||||||
fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem {
|
fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem {
|
||||||
ResponseInputItem::FunctionCallOutput {
|
ResponseInputItem::FunctionCallOutput {
|
||||||
call_id: call_id.to_string(),
|
call_id: call_id.to_string(),
|
||||||
@@ -165,13 +170,13 @@ impl McpToolOutput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is the context-injection form, so keep it aligned with the
|
// History receives this budget in tokens. Code Mode keeps the raw result.
|
||||||
// function-call output truncation that conversation history already
|
truncate_function_output_payload(
|
||||||
// applies. Code-mode consumers still get the raw `CallToolResult`.
|
&mut payload,
|
||||||
//
|
self.truncation_policy * 1.2,
|
||||||
// The text is serialized again inside the Responses payload, so allow
|
estimate_audio_token_count,
|
||||||
// a small buffer for JSON escaping and wrapper overhead.
|
);
|
||||||
truncate_function_output_payload(&payload, self.truncation_policy * 1.2)
|
payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,8 +230,11 @@ fn mcp_tool_output_response_item_preserves_content_items() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_case::test_case(TruncationPolicy::Bytes(64); "byte budget")]
|
||||||
fn mcp_tool_output_code_mode_result_preserves_content_without_private_metadata() {
|
#[test_case::test_case(TruncationPolicy::Tokens(1); "token budget")]
|
||||||
|
fn mcp_tool_output_code_mode_result_preserves_content_without_private_metadata(
|
||||||
|
truncation_policy: TruncationPolicy,
|
||||||
|
) {
|
||||||
let large_content = "large structured value ".repeat(1_000);
|
let large_content = "large structured value ".repeat(1_000);
|
||||||
let output = McpToolOutput {
|
let output = McpToolOutput {
|
||||||
result: CallToolResult {
|
result: CallToolResult {
|
||||||
@@ -250,12 +253,13 @@ fn mcp_tool_output_code_mode_result_preserves_content_without_private_metadata()
|
|||||||
tool_input: json!({}),
|
tool_input: json!({}),
|
||||||
wall_time: std::time::Duration::from_millis(1250),
|
wall_time: std::time::Duration::from_millis(1250),
|
||||||
original_image_detail_supported: false,
|
original_image_detail_supported: false,
|
||||||
truncation_policy: TruncationPolicy::Bytes(64),
|
truncation_policy,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = output.code_mode_result(&ToolPayload::Function {
|
let payload = ToolPayload::Function {
|
||||||
arguments: "{}".to_string(),
|
arguments: "{}".to_string(),
|
||||||
});
|
};
|
||||||
|
let result = output.code_mode_result(&payload);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result,
|
result,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ use codex_tools::agent_plugin_mcp_tool_to_responses_api_tool;
|
|||||||
use codex_tools::mcp_tool_to_responses_api_tool;
|
use codex_tools::mcp_tool_to_responses_api_tool;
|
||||||
use codex_utils_image::PromptImageMode;
|
use codex_utils_image::PromptImageMode;
|
||||||
use codex_utils_image::load_data_url_for_prompt_uncached;
|
use codex_utils_image::load_data_url_for_prompt_uncached;
|
||||||
|
use codex_utils_output_truncation::TruncationPolicy;
|
||||||
use codex_utils_string::take_bytes_at_char_boundary;
|
use codex_utils_string::take_bytes_at_char_boundary;
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use serde_json::Map;
|
use serde_json::Map;
|
||||||
@@ -217,6 +218,12 @@ impl McpHandler {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Capture presentation policy from the same config snapshot used for execution.
|
||||||
|
let truncation_policy = prepared_mcp_call
|
||||||
|
.as_ref()
|
||||||
|
.and_then(codex_mcp::PreparedMcpCall::output_token_limit)
|
||||||
|
.map(TruncationPolicy::Tokens)
|
||||||
|
.unwrap_or(turn.model_info().truncation_policy.into());
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
let result = handle_mcp_tool_call(
|
let result = handle_mcp_tool_call(
|
||||||
Arc::clone(&session),
|
Arc::clone(&session),
|
||||||
@@ -237,7 +244,7 @@ impl McpHandler {
|
|||||||
tool_input: result.tool_input,
|
tool_input: result.tool_input,
|
||||||
wall_time: started.elapsed(),
|
wall_time: started.elapsed(),
|
||||||
original_image_detail_supported: can_request_original_image_detail(turn.model_info()),
|
original_image_detail_supported: can_request_original_image_detail(turn.model_info()),
|
||||||
truncation_policy: turn.model_info().truncation_policy.into(),
|
truncation_policy,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ use crate::tools::registry::AnyToolResult;
|
|||||||
use crate::tools::registry::ToolArgumentDiffConsumer;
|
use crate::tools::registry::ToolArgumentDiffConsumer;
|
||||||
use crate::tools::router::ToolCall;
|
use crate::tools::router::ToolCall;
|
||||||
use crate::tools::router::ToolCallSource;
|
use crate::tools::router::ToolCallSource;
|
||||||
|
use codex_history::ResponseItemEnvelope;
|
||||||
use codex_protocol::error::CodexErr;
|
use codex_protocol::error::CodexErr;
|
||||||
use codex_protocol::models::ResponseInputItem;
|
use codex_protocol::models::ResponseInputItem;
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ impl ToolCallRuntime {
|
|||||||
self,
|
self,
|
||||||
call: ToolCall,
|
call: ToolCall,
|
||||||
cancellation_token: CancellationToken,
|
cancellation_token: CancellationToken,
|
||||||
) -> impl std::future::Future<Output = Result<ResponseInputItem, CodexErr>> {
|
) -> impl std::future::Future<Output = Result<ResponseItemEnvelope, CodexErr>> {
|
||||||
let error_call = call.clone();
|
let error_call = call.clone();
|
||||||
let source = call.direct_source();
|
let source = call.direct_source();
|
||||||
let future = self.handle_tool_call_with_source(call, source, cancellation_token);
|
let future = self.handle_tool_call_with_source(call, source, cancellation_token);
|
||||||
@@ -82,7 +83,9 @@ impl ToolCallRuntime {
|
|||||||
match future.await {
|
match future.await {
|
||||||
Ok(response) => Ok(response.into_response()),
|
Ok(response) => Ok(response.into_response()),
|
||||||
Err(FunctionCallError::Fatal(message)) => Err(CodexErr::Fatal(message)),
|
Err(FunctionCallError::Fatal(message)) => Err(CodexErr::Fatal(message)),
|
||||||
Err(other) => Ok(Self::failure_response(error_call, other)),
|
Err(other) => Ok(ResponseItemEnvelope::new(
|
||||||
|
Self::failure_response(error_call, other).into(),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.in_current_span()
|
.in_current_span()
|
||||||
@@ -655,7 +658,10 @@ mod tests {
|
|||||||
success: Some(true),
|
success: Some(true),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
assert_eq!(expected_response, response);
|
assert_eq!(
|
||||||
|
ResponseItemEnvelope::new(expected_response.into()),
|
||||||
|
response
|
||||||
|
);
|
||||||
|
|
||||||
let actual = records
|
let actual = records
|
||||||
.lock()
|
.lock()
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ use crate::tools::tool_dispatch_trace::ToolDispatchTrace;
|
|||||||
use crate::util::error_or_panic;
|
use crate::util::error_or_panic;
|
||||||
use codex_analytics::ControlToolCallStatus;
|
use codex_analytics::ControlToolCallStatus;
|
||||||
use codex_extension_api::ToolCallOutcome;
|
use codex_extension_api::ToolCallOutcome;
|
||||||
|
use codex_history::CodexHarnessMetadata;
|
||||||
|
use codex_history::ResponseItemEnvelope;
|
||||||
use codex_protocol::models::FunctionCallOutputPayload;
|
use codex_protocol::models::FunctionCallOutputPayload;
|
||||||
use codex_protocol::models::ResponseInputItem;
|
use codex_protocol::models::ResponseInputItem;
|
||||||
use codex_protocol::parse_command::ParsedCommand;
|
use codex_protocol::parse_command::ParsedCommand;
|
||||||
@@ -191,14 +193,22 @@ pub(crate) struct AnyToolResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AnyToolResult {
|
impl AnyToolResult {
|
||||||
pub(crate) fn into_response(self) -> ResponseInputItem {
|
pub(crate) fn into_response(self) -> ResponseItemEnvelope {
|
||||||
let Self {
|
let Self {
|
||||||
call_id,
|
call_id,
|
||||||
payload,
|
payload,
|
||||||
result,
|
result,
|
||||||
..
|
..
|
||||||
} = self;
|
} = self;
|
||||||
result.to_response_item(&call_id, &payload)
|
ResponseItemEnvelope {
|
||||||
|
item: result.to_response_item(&call_id, &payload).into(),
|
||||||
|
metadata: result
|
||||||
|
.fallback_token_limit_override()
|
||||||
|
.map(|limit| CodexHarnessMetadata {
|
||||||
|
fallback_token_limit_override: Some(limit),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn code_mode_result(self) -> serde_json::Value {
|
pub(crate) fn code_mode_result(self) -> serde_json::Value {
|
||||||
@@ -223,6 +233,10 @@ impl ToolOutput for PostToolUseFeedbackOutput {
|
|||||||
self.original.success_for_logging()
|
self.original.success_for_logging()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fallback_token_limit_override(&self) -> Option<usize> {
|
||||||
|
self.original.fallback_token_limit_override()
|
||||||
|
}
|
||||||
|
|
||||||
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem {
|
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem {
|
||||||
self.model_visible.to_response_item(call_id, payload)
|
self.model_visible.to_response_item(call_id, payload)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ use super::*;
|
|||||||
use crate::session::session::Session;
|
use crate::session::session::Session;
|
||||||
use crate::session::step_context::StepContext;
|
use crate::session::step_context::StepContext;
|
||||||
use codex_protocol::DEFAULT_FUNCTION_NAMESPACE;
|
use codex_protocol::DEFAULT_FUNCTION_NAMESPACE;
|
||||||
|
use codex_protocol::models::ResponseItem;
|
||||||
|
use codex_utils_output_truncation::TruncationPolicy;
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use std::sync::atomic::AtomicUsize;
|
use std::sync::atomic::AtomicUsize;
|
||||||
@@ -587,6 +589,53 @@ async fn write_stdin_does_not_expose_default_pre_tool_use_payload() {
|
|||||||
assert_eq!(write_stdin.pre_tool_use_payload(&invocation), None);
|
assert_eq!(write_stdin.pre_tool_use_payload(&invocation), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test_case::test_case(TruncationPolicy::Tokens(1), 2; "token budget")]
|
||||||
|
#[test_case::test_case(TruncationPolicy::Bytes(401), 121; "scale bytes before converting to tokens")]
|
||||||
|
fn post_tool_use_feedback_output_preserves_fallback_token_limit_override(
|
||||||
|
truncation_policy: TruncationPolicy,
|
||||||
|
expected_token_limit: usize,
|
||||||
|
) {
|
||||||
|
let result = AnyToolResult {
|
||||||
|
call_id: "call-1".to_string(),
|
||||||
|
payload: ToolPayload::Function {
|
||||||
|
arguments: "{}".to_string(),
|
||||||
|
},
|
||||||
|
result: Box::new(PostToolUseFeedbackOutput {
|
||||||
|
original: Box::new(crate::tools::context::McpToolOutput {
|
||||||
|
result: codex_protocol::mcp::CallToolResult {
|
||||||
|
content: Vec::new(),
|
||||||
|
structured_content: None,
|
||||||
|
is_error: None,
|
||||||
|
meta: None,
|
||||||
|
},
|
||||||
|
tool_input: serde_json::json!({}),
|
||||||
|
wall_time: Duration::ZERO,
|
||||||
|
original_image_detail_supported: false,
|
||||||
|
truncation_policy,
|
||||||
|
}),
|
||||||
|
model_visible: crate::tools::context::FunctionToolOutput::from_text(
|
||||||
|
"hook feedback".to_string(),
|
||||||
|
/*success*/ None,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
post_tool_use_payload: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result.into_response(),
|
||||||
|
ResponseItemEnvelope {
|
||||||
|
item: ResponseItem::from(ResponseInputItem::FunctionCallOutput {
|
||||||
|
call_id: "call-1".to_string(),
|
||||||
|
output: FunctionCallOutputPayload::from_text("hook feedback".to_string()),
|
||||||
|
}),
|
||||||
|
metadata: Some(CodexHarnessMetadata {
|
||||||
|
fallback_token_limit_override: Some(expected_token_limit),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn post_tool_use_feedback_output_keeps_code_mode_result_typed() {
|
fn post_tool_use_feedback_output_keeps_code_mode_result_typed() {
|
||||||
let result = AnyToolResult {
|
let result = AnyToolResult {
|
||||||
@@ -608,12 +657,15 @@ fn post_tool_use_feedback_output_keeps_code_mode_result_typed() {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.into_response(),
|
result.into_response(),
|
||||||
ResponseInputItem::FunctionCallOutput {
|
ResponseItemEnvelope::new(
|
||||||
call_id: "call-1".to_string(),
|
ResponseInputItem::FunctionCallOutput {
|
||||||
output: codex_protocol::models::FunctionCallOutputPayload::from_text(
|
call_id: "call-1".to_string(),
|
||||||
"hook feedback".to_string()
|
output: codex_protocol::models::FunctionCallOutputPayload::from_text(
|
||||||
),
|
"hook feedback".to_string()
|
||||||
}
|
),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = AnyToolResult {
|
let result = AnyToolResult {
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ use codex_protocol::dynamic_tools::DynamicToolNamespaceTool;
|
|||||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||||
use codex_protocol::models::ContentItem;
|
use codex_protocol::models::ContentItem;
|
||||||
use codex_protocol::models::FunctionCallOutputBody;
|
use codex_protocol::models::FunctionCallOutputBody;
|
||||||
use codex_protocol::models::ResponseInputItem;
|
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use codex_tools::ResponsesApiNamespace;
|
use codex_tools::ResponsesApiNamespace;
|
||||||
use codex_tools::ResponsesApiNamespaceTool;
|
use codex_tools::ResponsesApiNamespaceTool;
|
||||||
@@ -604,10 +603,12 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let response = result.into_response();
|
let response = result.into_response().item;
|
||||||
match response {
|
match response {
|
||||||
ResponseInputItem::FunctionCallOutput { call_id, output } => {
|
ResponseItem::FunctionCallOutput {
|
||||||
assert_eq!(call_id, "call-extension");
|
call_id, output, ..
|
||||||
|
} => {
|
||||||
|
assert_eq!(call_id.as_deref(), Some("call-extension"));
|
||||||
let FunctionCallOutputBody::Text(text) = output.body else {
|
let FunctionCallOutputBody::Text(text) = output.body else {
|
||||||
panic!("expected text function call output")
|
panic!("expected text function call output")
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ print({python_output_literal})
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_mcp_tool_hook(
|
pub(super) fn write_mcp_tool_hook(
|
||||||
home: &Path,
|
home: &Path,
|
||||||
event_name: &str,
|
event_name: &str,
|
||||||
matcher: Option<&str>,
|
matcher: Option<&str>,
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ use core_test_support::responses::mount_sse_once;
|
|||||||
use core_test_support::responses::sse;
|
use core_test_support::responses::sse;
|
||||||
use core_test_support::responses::start_mock_server;
|
use core_test_support::responses::start_mock_server;
|
||||||
use core_test_support::skip_if_no_network;
|
use core_test_support::skip_if_no_network;
|
||||||
|
use core_test_support::skip_if_wine_exec;
|
||||||
use core_test_support::stdio_server_bin;
|
use core_test_support::stdio_server_bin;
|
||||||
|
use core_test_support::test_codex::TestCodex;
|
||||||
|
use core_test_support::test_codex::TestCodexBuilder;
|
||||||
use core_test_support::test_codex::local_selections;
|
use core_test_support::test_codex::local_selections;
|
||||||
use core_test_support::test_codex::test_codex;
|
use core_test_support::test_codex::test_codex;
|
||||||
use core_test_support::wait_for_event;
|
use core_test_support::wait_for_event;
|
||||||
@@ -35,6 +38,11 @@ use serde_json::Value;
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use test_case::test_case;
|
||||||
|
use wiremock::MockServer;
|
||||||
|
|
||||||
|
use super::rmcp_client::remote_aware_environment_id;
|
||||||
|
use super::rmcp_client::remote_aware_stdio_server_bin;
|
||||||
|
|
||||||
fn assert_wall_time_header(output: &str) {
|
fn assert_wall_time_header(output: &str) {
|
||||||
let (wall_time, marker) = output
|
let (wall_time, marker) = output
|
||||||
@@ -745,21 +753,19 @@ async fn exec_command_output_not_truncated_with_custom_limit() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// MCP server output should also remain intact when the config increases the token limit.
|
async fn call_mcp_echo(
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
server: &MockServer,
|
||||||
async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> {
|
builder: TestCodexBuilder,
|
||||||
skip_if_no_network!(Ok(()));
|
output_token_limit: Option<usize>,
|
||||||
|
message_bytes: usize,
|
||||||
let server = start_mock_server().await;
|
) -> Result<(TestCodex, String)> {
|
||||||
|
let call_id = "rmcp-output";
|
||||||
let call_id = "rmcp-untruncated";
|
|
||||||
let server_name = "rmcp";
|
let server_name = "rmcp";
|
||||||
let namespace = format!("mcp__{server_name}");
|
let namespace = format!("mcp__{server_name}");
|
||||||
let large_msg = "a".repeat(80_000);
|
let args_json = json!({ "message": "a".repeat(message_bytes) });
|
||||||
let args_json = serde_json::json!({ "message": large_msg });
|
|
||||||
|
|
||||||
mount_sse_once(
|
mount_sse_once(
|
||||||
&server,
|
server,
|
||||||
sse(vec![
|
sse(vec![
|
||||||
responses::ev_response_created("resp-1"),
|
responses::ev_response_created("resp-1"),
|
||||||
responses::ev_function_call_with_namespace(
|
responses::ev_function_call_with_namespace(
|
||||||
@@ -772,8 +778,8 @@ async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> {
|
|||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let mock2 = mount_sse_once(
|
let response = mount_sse_once(
|
||||||
&server,
|
server,
|
||||||
sse(vec![
|
sse(vec![
|
||||||
responses::ev_assistant_message("msg-1", "rmcp echo tool completed."),
|
responses::ev_assistant_message("msg-1", "rmcp echo tool completed."),
|
||||||
responses::ev_completed("resp-2"),
|
responses::ev_completed("resp-2"),
|
||||||
@@ -781,67 +787,139 @@ async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let rmcp_test_server_bin = stdio_server_bin()?;
|
let mcp_server = serde_json::from_value(json!({
|
||||||
|
"command": remote_aware_stdio_server_bin()?,
|
||||||
let mut builder = test_codex().with_config(move |config| {
|
"environment_id": remote_aware_environment_id(),
|
||||||
config.tool_output_token_limit = Some(50_000);
|
"startup_timeout_sec": 10,
|
||||||
let mut servers = config.mcp_servers.get().clone();
|
"tools": { "echo": { "output_token_limit": output_token_limit } },
|
||||||
servers.insert(
|
}))?;
|
||||||
server_name.to_string(),
|
let mut builder = builder.with_config(move |config| {
|
||||||
codex_config::types::McpServerConfig {
|
|
||||||
auth: Default::default(),
|
|
||||||
transport: codex_config::types::McpServerTransportConfig::Stdio {
|
|
||||||
command: rmcp_test_server_bin,
|
|
||||||
args: Vec::new(),
|
|
||||||
env: None,
|
|
||||||
env_vars: Vec::new(),
|
|
||||||
cwd: None,
|
|
||||||
},
|
|
||||||
environment_id: "local".to_string(),
|
|
||||||
enabled: true,
|
|
||||||
required: false,
|
|
||||||
supports_parallel_tool_calls: false,
|
|
||||||
omit_tools_from: None,
|
|
||||||
disabled_reason: None,
|
|
||||||
startup_timeout_sec: Some(std::time::Duration::from_secs(10)),
|
|
||||||
tool_timeout_sec: None,
|
|
||||||
default_tools_approval_mode: None,
|
|
||||||
enabled_tools: None,
|
|
||||||
disabled_tools: None,
|
|
||||||
scopes: None,
|
|
||||||
oauth: None,
|
|
||||||
oauth_resource: None,
|
|
||||||
tools: HashMap::new(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
config
|
config
|
||||||
.mcp_servers
|
.mcp_servers
|
||||||
.set(servers)
|
.set(HashMap::from([(server_name.to_string(), mcp_server)]))
|
||||||
.expect("test mcp servers should accept any configuration");
|
.expect("test mcp servers should accept any configuration");
|
||||||
});
|
});
|
||||||
let fixture = builder.build(&server).await?;
|
let fixture = builder.build_with_auto_env(server).await?;
|
||||||
wait_for_mcp_server(&fixture.codex, server_name).await?;
|
wait_for_mcp_server(&fixture.codex, server_name).await?;
|
||||||
|
fixture.submit_text_turn("call the MCP echo tool").await?;
|
||||||
|
|
||||||
fixture
|
let output = response
|
||||||
.submit_turn_with_permission_profile(
|
|
||||||
"call the rmcp echo tool with a very large message",
|
|
||||||
PermissionProfile::read_only(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let output = mock2
|
|
||||||
.single_request()
|
.single_request()
|
||||||
.function_call_output_text(call_id)
|
.function_call_output_text(call_id)
|
||||||
.context("function_call_output present for rmcp call")?;
|
.context("model-facing MCP output text")?;
|
||||||
|
Ok((fixture, output))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_case(3_000, 13_000; "serialization allowance")]
|
||||||
|
#[test_case(30_000, 116_000; "large override")]
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn mcp_tool_output_limit_preserves_output_that_fits(
|
||||||
|
output_token_limit: usize,
|
||||||
|
message_bytes: usize,
|
||||||
|
) -> Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
skip_if_wine_exec!(Ok(()), "requires a Windows test_stdio_server binary");
|
||||||
|
|
||||||
|
let server = start_mock_server().await;
|
||||||
|
let builder = test_codex().with_config(|config| config.tool_output_token_limit = Some(50));
|
||||||
|
let (_fixture, output) =
|
||||||
|
call_mcp_echo(&server, builder, Some(output_token_limit), message_bytes).await?;
|
||||||
|
|
||||||
|
assert!(output.contains(&"a".repeat(message_bytes)));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn mcp_tool_output_limit_truncates_oversized_output() -> Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
skip_if_wine_exec!(Ok(()), "requires a Windows test_stdio_server binary");
|
||||||
|
|
||||||
|
let server = start_mock_server().await;
|
||||||
|
let builder = test_codex().with_config(|config| config.tool_output_token_limit = Some(50));
|
||||||
|
let (_fixture, output) = call_mcp_echo(
|
||||||
|
&server,
|
||||||
|
builder,
|
||||||
|
Some(30_000),
|
||||||
|
/*message_bytes*/ 150_000,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert!(output.contains("truncated"));
|
||||||
|
// 30k tokens plus the serialization allowance leaves about 144k bytes.
|
||||||
|
assert!((140_000..145_000).contains(&output.len()));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn mcp_tool_output_limit_applies_to_hook_feedback() -> Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
skip_if_wine_exec!(Ok(()), "requires a Windows test_stdio_server binary");
|
||||||
|
|
||||||
|
let server = start_mock_server().await;
|
||||||
|
let builder = test_codex()
|
||||||
|
.with_pre_build_hook(|home| {
|
||||||
|
super::hooks_mcp::write_mcp_tool_hook(
|
||||||
|
home,
|
||||||
|
"PostToolUse",
|
||||||
|
Some("^mcp__rmcp__echo$"),
|
||||||
|
"rmcp",
|
||||||
|
&json!({ "continue": false, "stopReason": "hook feedback ".repeat(100) })
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.expect("write MCP post-tool hook");
|
||||||
|
})
|
||||||
|
.with_config(|config| {
|
||||||
|
core_test_support::hooks::trust_discovered_hooks(config);
|
||||||
|
config.tool_output_token_limit = Some(50);
|
||||||
|
});
|
||||||
|
let (_fixture, output) =
|
||||||
|
call_mcp_echo(&server, builder, Some(100), /*message_bytes*/ 0).await?;
|
||||||
|
|
||||||
|
assert!(output.starts_with("hook feedback "));
|
||||||
|
assert!(output.contains("truncated"));
|
||||||
|
// The tool's 120-token budget applies, not the 60-token global budget.
|
||||||
|
assert!((400..600).contains(&output.len()));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_case(None; "model default")]
|
||||||
|
#[test_case(Some(30_000); "tool override")]
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn mcp_tool_output_limit_survives_resume(output_token_limit: Option<usize>) -> Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
skip_if_wine_exec!(Ok(()), "requires a Windows test_stdio_server binary");
|
||||||
|
|
||||||
|
let server = start_mock_server().await;
|
||||||
|
let builder = test_codex().with_config(|config| config.tool_output_token_limit = Some(50_000));
|
||||||
|
let (fixture, output) = call_mcp_echo(
|
||||||
|
&server,
|
||||||
|
builder,
|
||||||
|
output_token_limit,
|
||||||
|
/*message_bytes*/ 150_000,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
fixture.codex.ensure_rollout_materialized().await;
|
||||||
|
fixture.codex.flush_rollout().await?;
|
||||||
|
let resumed_response = mount_sse_once(
|
||||||
|
&server,
|
||||||
|
sse(vec![
|
||||||
|
responses::ev_assistant_message("msg-2", "resumed"),
|
||||||
|
responses::ev_completed("resp-3"),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let mut resume_builder = test_codex().with_config(|config| {
|
||||||
|
config.tool_output_token_limit = Some(50);
|
||||||
|
});
|
||||||
|
let resumed = resume_builder.restart(&server, &fixture).await?;
|
||||||
|
resumed.submit_turn("continue").await?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
output.len(),
|
resumed_response
|
||||||
80065,
|
.single_request()
|
||||||
"MCP output should retain its serialized length plus wall-time header"
|
.function_call_output_text("rmcp-output")
|
||||||
);
|
.context("resumed MCP output")?,
|
||||||
assert!(
|
output
|
||||||
!output.contains("truncated"),
|
|
||||||
"output should not include truncation markers when limit is raised: {output}"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -231,8 +231,8 @@ async fn high_level_discovery_matches_the_existing_plugin_provider() -> TestResu
|
|||||||
"enabled_tools": ["read", "deploy", "trusted", "package-only"],
|
"enabled_tools": ["read", "deploy", "trusted", "package-only"],
|
||||||
"disabled_tools": ["package-denied"],
|
"disabled_tools": ["package-denied"],
|
||||||
"tools": {
|
"tools": {
|
||||||
"read": {"approval_mode": "prompt"},
|
"read": {"approval_mode": "prompt", "output_token_limit": 12000},
|
||||||
"deploy": {"approval_mode": "approve"},
|
"deploy": {"approval_mode": "approve", "output_token_limit": 4000},
|
||||||
"trusted": {"approval_mode": "approve"}
|
"trusted": {"approval_mode": "approve"}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -255,6 +255,10 @@ disabled_tools = ["write"]
|
|||||||
|
|
||||||
[plugins."selected-root".mcp_servers.first.tools.read]
|
[plugins."selected-root".mcp_servers.first.tools.read]
|
||||||
approval_mode = "approve"
|
approval_mode = "approve"
|
||||||
|
output_token_limit = 8000
|
||||||
|
|
||||||
|
[plugins."selected-root".mcp_servers.first.tools.deploy]
|
||||||
|
output_token_limit = 9000
|
||||||
|
|
||||||
[plugins."selected-root".mcp_servers.first.tools.trusted]
|
[plugins."selected-root".mcp_servers.first.tools.trusted]
|
||||||
approval_mode = "approve"
|
approval_mode = "approve"
|
||||||
@@ -308,18 +312,21 @@ default_tools_approval_mode = "auto"
|
|||||||
"read".to_string(),
|
"read".to_string(),
|
||||||
McpServerToolConfig {
|
McpServerToolConfig {
|
||||||
approval_mode: Some(AppToolApproval::Prompt),
|
approval_mode: Some(AppToolApproval::Prompt),
|
||||||
|
output_token_limit: std::num::NonZeroUsize::new(8_000),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"deploy".to_string(),
|
"deploy".to_string(),
|
||||||
McpServerToolConfig {
|
McpServerToolConfig {
|
||||||
approval_mode: Some(AppToolApproval::Prompt),
|
approval_mode: Some(AppToolApproval::Prompt),
|
||||||
|
output_token_limit: std::num::NonZeroUsize::new(4_000),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"trusted".to_string(),
|
"trusted".to_string(),
|
||||||
McpServerToolConfig {
|
McpServerToolConfig {
|
||||||
approval_mode: Some(AppToolApproval::Approve),
|
approval_mode: Some(AppToolApproval::Approve),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ pub struct CodexHarnessMetadata {
|
|||||||
/// Whether a developer message was supplied by an app-server client.
|
/// Whether a developer message was supplied by an app-server client.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub client_authored: bool,
|
pub client_authored: bool,
|
||||||
|
|
||||||
|
/// Overrides history's fallback truncation budget, including on resume.
|
||||||
|
/// Measured in tokens, with any tool-specific allowance already included.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub fallback_token_limit_override: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseItemEnvelope {
|
impl ResponseItemEnvelope {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()>
|
|||||||
item: response_item.clone(),
|
item: response_item.clone(),
|
||||||
metadata: Some(CodexHarnessMetadata {
|
metadata: Some(CodexHarnessMetadata {
|
||||||
client_authored: true,
|
client_authored: true,
|
||||||
|
fallback_token_limit_override: Some(20_000),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -84,7 +85,7 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()>
|
|||||||
"ordinal": 7,
|
"ordinal": 7,
|
||||||
"type": "response_item",
|
"type": "response_item",
|
||||||
"payload": response_item,
|
"payload": response_item,
|
||||||
"metadata": { "client_authored": true },
|
"metadata": { "client_authored": true, "fallback_token_limit_override": 20_000 },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(serialized["payload"].get("metadata"), None);
|
assert_eq!(serialized["payload"].get("metadata"), None);
|
||||||
@@ -97,6 +98,7 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()>
|
|||||||
envelope.metadata,
|
envelope.metadata,
|
||||||
Some(CodexHarnessMetadata {
|
Some(CodexHarnessMetadata {
|
||||||
client_authored: true,
|
client_authored: true,
|
||||||
|
fallback_token_limit_override: Some(20_000),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -184,6 +186,7 @@ fn compacted_replacement_history_stores_metadata_in_an_aligned_sidecar() -> Resu
|
|||||||
item: developer_message.clone(),
|
item: developer_message.clone(),
|
||||||
metadata: Some(CodexHarnessMetadata {
|
metadata: Some(CodexHarnessMetadata {
|
||||||
client_authored: true,
|
client_authored: true,
|
||||||
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
ResponseItemEnvelope::new(compaction_item.clone()),
|
ResponseItemEnvelope::new(compaction_item.clone()),
|
||||||
@@ -216,6 +219,7 @@ fn compacted_replacement_history_stores_metadata_in_an_aligned_sidecar() -> Resu
|
|||||||
item: developer_message,
|
item: developer_message,
|
||||||
metadata: Some(CodexHarnessMetadata {
|
metadata: Some(CodexHarnessMetadata {
|
||||||
client_authored: true,
|
client_authored: true,
|
||||||
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
ResponseItemEnvelope {
|
ResponseItemEnvelope {
|
||||||
@@ -277,6 +281,7 @@ fn compacted_metadata_remains_compatible_with_legacy_response_item_readers() ->
|
|||||||
item: response_item.clone(),
|
item: response_item.clone(),
|
||||||
metadata: Some(CodexHarnessMetadata {
|
metadata: Some(CodexHarnessMetadata {
|
||||||
client_authored: true,
|
client_authored: true,
|
||||||
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
let response_line = serde_json::to_value(RolloutItem::ResponseItem(envelope.clone()))?;
|
let response_line = serde_json::to_value(RolloutItem::ResponseItem(envelope.clone()))?;
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ pub trait ToolOutput: Send {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Overrides history's fallback token limit after tool-specific truncation.
|
||||||
|
/// Include any serialization allowance; history uses this limit unchanged.
|
||||||
|
fn fallback_token_limit_override(&self) -> Option<usize> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem;
|
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem;
|
||||||
|
|
||||||
/// Returns the tool call id exposed to `PostToolUse` hooks for this output.
|
/// Returns the tool call id exposed to `PostToolUse` hooks for this output.
|
||||||
@@ -67,6 +73,10 @@ where
|
|||||||
(**self).contains_external_context()
|
(**self).contains_external_context()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fallback_token_limit_override(&self) -> Option<usize> {
|
||||||
|
(**self).fallback_token_limit_override()
|
||||||
|
}
|
||||||
|
|
||||||
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem {
|
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem {
|
||||||
(**self).to_response_item(call_id, payload)
|
(**self).to_response_item(call_id, payload)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//! Helpers for truncating tool and exec output using [`TruncationPolicy`](codex_protocol::protocol::TruncationPolicy).
|
//! Shared byte/token truncation for tool and exec output.
|
||||||
|
|
||||||
|
use codex_protocol::models::FunctionCallOutputBody;
|
||||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||||
|
use codex_protocol::models::FunctionCallOutputPayload;
|
||||||
pub use codex_utils_string::approx_bytes_for_tokens;
|
pub use codex_utils_string::approx_bytes_for_tokens;
|
||||||
pub use codex_utils_string::approx_token_count;
|
pub use codex_utils_string::approx_token_count;
|
||||||
pub use codex_utils_string::approx_tokens_from_byte_count;
|
pub use codex_utils_string::approx_tokens_from_byte_count;
|
||||||
@@ -29,6 +31,24 @@ pub fn truncate_text(content: &str, policy: TruncationPolicy) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Applies the existing byte/token budget without changing success metadata or media ordering.
|
||||||
|
pub fn truncate_function_output_payload(
|
||||||
|
output: &mut FunctionCallOutputPayload,
|
||||||
|
policy: TruncationPolicy,
|
||||||
|
estimate_audio_token_count: impl Fn(&str) -> usize,
|
||||||
|
) {
|
||||||
|
match &mut output.body {
|
||||||
|
FunctionCallOutputBody::Text(text) => *text = truncate_text(text, policy),
|
||||||
|
FunctionCallOutputBody::ContentItems(items) => {
|
||||||
|
*items = truncate_function_output_items_with_policy(
|
||||||
|
items,
|
||||||
|
policy,
|
||||||
|
estimate_audio_token_count,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn formatted_truncate_text_content_items_with_policy(
|
pub fn formatted_truncate_text_content_items_with_policy(
|
||||||
items: &[FunctionCallOutputContentItem],
|
items: &[FunctionCallOutputContentItem],
|
||||||
policy: TruncationPolicy,
|
policy: TruncationPolicy,
|
||||||
|
|||||||
Reference in new issue
Block a user