feat: add manifest output upload limits
This commit is contained in:
@@ -15,8 +15,8 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::defaults;
|
||||
use crate::model::{AuthRef, ModelManifest, ReasoningControl};
|
||||
use crate::{
|
||||
CompactionConfig, MemoryConfig, PodManifest, PodMeta, ScopeConfig, SkillsConfig,
|
||||
ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule, WorkerManifest,
|
||||
CompactionConfig, FileUploadLimits, MemoryConfig, PodManifest, PodMeta, ScopeConfig,
|
||||
SkillsConfig, ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule, WorkerManifest,
|
||||
};
|
||||
|
||||
/// Partial-form Pod manifest. Every field is optional; one or more
|
||||
@@ -81,6 +81,8 @@ pub struct WorkerManifestConfig {
|
||||
pub reasoning: Option<ReasoningControl>,
|
||||
#[serde(default)]
|
||||
pub tool_output: ToolOutputLimitsPartial,
|
||||
#[serde(default)]
|
||||
pub file_upload: FileUploadLimitsPartial,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
@@ -91,6 +93,12 @@ pub struct ToolOutputLimitsPartial {
|
||||
pub per_tool: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct FileUploadLimitsPartial {
|
||||
#[serde(default)]
|
||||
pub max_bytes: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct PermissionConfigPartial {
|
||||
#[serde(default)]
|
||||
@@ -158,6 +166,9 @@ impl PodManifestConfig {
|
||||
default_max_bytes: Some(defaults::TOOL_OUTPUT_MAX_BYTES),
|
||||
per_tool: HashMap::new(),
|
||||
},
|
||||
file_upload: FileUploadLimitsPartial {
|
||||
max_bytes: Some(defaults::FILE_UPLOAD_MAX_BYTES),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
@@ -287,6 +298,7 @@ impl WorkerManifestConfig {
|
||||
stop_sequences: upper.stop_sequences.or(self.stop_sequences),
|
||||
reasoning: upper.reasoning.or(self.reasoning),
|
||||
tool_output: self.tool_output.merge(upper.tool_output),
|
||||
file_upload: self.file_upload.merge(upper.file_upload),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,6 +314,14 @@ impl ToolOutputLimitsPartial {
|
||||
}
|
||||
}
|
||||
|
||||
impl FileUploadLimitsPartial {
|
||||
fn merge(self, upper: Self) -> Self {
|
||||
Self {
|
||||
max_bytes: upper.max_bytes.or(self.max_bytes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PermissionConfigPartial {
|
||||
fn merge(mut self, upper: Self) -> Self {
|
||||
self.rules.extend(upper.rules);
|
||||
@@ -423,6 +443,13 @@ impl TryFrom<PodManifestConfig> for PodManifest {
|
||||
.unwrap_or(defaults::TOOL_OUTPUT_MAX_BYTES),
|
||||
per_tool: cfg.worker.tool_output.per_tool,
|
||||
},
|
||||
file_upload: FileUploadLimits {
|
||||
max_bytes: cfg
|
||||
.worker
|
||||
.file_upload
|
||||
.max_bytes
|
||||
.unwrap_or(defaults::FILE_UPLOAD_MAX_BYTES),
|
||||
},
|
||||
};
|
||||
|
||||
if cfg.scope.allow.is_empty() {
|
||||
@@ -858,6 +885,29 @@ mod tests {
|
||||
assert_eq!(to.per_tool.get("Grep"), Some(&512));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_file_upload_max_bytes_upper_wins() {
|
||||
let lower = PodManifestConfig {
|
||||
worker: WorkerManifestConfig {
|
||||
file_upload: FileUploadLimitsPartial {
|
||||
max_bytes: Some(8192),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let upper = PodManifestConfig {
|
||||
worker: WorkerManifestConfig {
|
||||
file_upload: FileUploadLimitsPartial {
|
||||
max_bytes: Some(54_321),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let merged = lower.merge(upper);
|
||||
assert_eq!(merged.worker.file_upload.max_bytes, Some(54_321));
|
||||
}
|
||||
#[test]
|
||||
fn merge_option_struct_field_wise() {
|
||||
let lower = PodManifestConfig {
|
||||
@@ -1000,12 +1050,16 @@ permission = "write"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_defaults_populates_tool_output_max_bytes() {
|
||||
fn builtin_defaults_populates_worker_limit_defaults() {
|
||||
let cfg = PodManifestConfig::builtin_defaults();
|
||||
assert_eq!(
|
||||
cfg.worker.tool_output.default_max_bytes,
|
||||
Some(defaults::TOOL_OUTPUT_MAX_BYTES)
|
||||
);
|
||||
assert_eq!(
|
||||
cfg.worker.file_upload.max_bytes,
|
||||
Some(defaults::FILE_UPLOAD_MAX_BYTES)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1039,6 +1093,10 @@ permission = "write"
|
||||
manifest.worker.tool_output.default_max_bytes,
|
||||
defaults::TOOL_OUTPUT_MAX_BYTES
|
||||
);
|
||||
assert_eq!(
|
||||
manifest.worker.file_upload.max_bytes,
|
||||
defaults::FILE_UPLOAD_MAX_BYTES
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
|
||||
/// Byte-size cap applied to any tool's `content` output when no
|
||||
/// per-tool override is set. See [`crate::ToolOutputLimits`].
|
||||
pub const TOOL_OUTPUT_MAX_BYTES: usize = 16 * 1024;
|
||||
pub const TOOL_OUTPUT_MAX_BYTES: usize = 64 * 1024;
|
||||
|
||||
/// Byte-size cap applied to each submit-time FileRef upload / attachment.
|
||||
/// See [`crate::FileUploadLimits`].
|
||||
pub const FILE_UPLOAD_MAX_BYTES: usize = 256 * 1024;
|
||||
|
||||
/// Number of most-recent turns protected from pruning. See
|
||||
/// [`crate::CompactionConfig::prune_protected_turns`].
|
||||
|
||||
@@ -7,8 +7,8 @@ mod scope;
|
||||
|
||||
pub use cascade::{LayerLoadError, find_project_manifest_from, load_layer};
|
||||
pub use config::{
|
||||
CompactionConfigPartial, PermissionConfigPartial, PodManifestConfig, PodMetaConfig,
|
||||
ResolveError, ToolOutputLimitsPartial, WorkerManifestConfig,
|
||||
CompactionConfigPartial, FileUploadLimitsPartial, PermissionConfigPartial, PodManifestConfig,
|
||||
PodMetaConfig, ResolveError, ToolOutputLimitsPartial, WorkerManifestConfig,
|
||||
};
|
||||
pub use model::{
|
||||
AuthRef, ModelCapability, ModelManifest, ReasoningControl, ReasoningEffort, SchemeKind,
|
||||
@@ -183,10 +183,15 @@ pub struct WorkerManifest {
|
||||
pub reasoning: Option<ReasoningControl>,
|
||||
/// Byte-size caps applied to tool `content` before it reaches the
|
||||
/// conversation history. The section is optional in TOML — when
|
||||
/// omitted, `ToolOutputLimits::default()` (16KB default cap, no
|
||||
/// omitted, `ToolOutputLimits::default()` (64 KiB default cap, no
|
||||
/// per-tool overrides) is applied so truncation is on by default.
|
||||
#[serde(default)]
|
||||
pub tool_output: ToolOutputLimits,
|
||||
/// Byte-size cap applied to submit-time FileRef uploads / attachments.
|
||||
/// This is intentionally separate from tool-output truncation because
|
||||
/// user-requested file attachments can usually tolerate a larger budget.
|
||||
#[serde(default)]
|
||||
pub file_upload: FileUploadLimits,
|
||||
}
|
||||
|
||||
/// Byte-size caps applied to tool execution `content` before it enters
|
||||
@@ -206,10 +211,26 @@ pub struct ToolOutputLimits {
|
||||
pub per_tool: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
/// Byte-size cap for submit-time FileRef uploads / attachments.
|
||||
///
|
||||
/// This governs the `[File: <path>]` system-message attachment produced
|
||||
/// when a user explicitly submits a `@<path>` reference. It does not affect
|
||||
/// tool result truncation; see [`ToolOutputLimits`] for that path.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FileUploadLimits {
|
||||
/// Cap applied to each resolved FileRef body.
|
||||
#[serde(default = "default_file_upload_max_bytes")]
|
||||
pub max_bytes: usize,
|
||||
}
|
||||
|
||||
fn default_tool_output_max_bytes() -> usize {
|
||||
defaults::TOOL_OUTPUT_MAX_BYTES
|
||||
}
|
||||
|
||||
fn default_file_upload_max_bytes() -> usize {
|
||||
defaults::FILE_UPLOAD_MAX_BYTES
|
||||
}
|
||||
|
||||
fn default_instruction() -> String {
|
||||
defaults::DEFAULT_INSTRUCTION.to_string()
|
||||
}
|
||||
@@ -223,6 +244,14 @@ impl Default for ToolOutputLimits {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FileUploadLimits {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_bytes: default_file_upload_max_bytes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolOutputLimits {
|
||||
/// Resolve the cap for a given tool name.
|
||||
pub fn limit_for(&self, tool_name: &str) -> usize {
|
||||
@@ -635,15 +664,19 @@ model_id = "claude-sonnet-4-20250514"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omitted_tool_output_falls_back_to_default_16k() {
|
||||
fn omitted_limits_fall_back_to_defaults() {
|
||||
let manifest = PodManifest::from_toml(MINIMAL_REQUIRED).unwrap();
|
||||
let limits = &manifest.worker.tool_output;
|
||||
assert_eq!(limits.default_max_bytes, 16 * 1024);
|
||||
assert_eq!(limits.default_max_bytes, defaults::TOOL_OUTPUT_MAX_BYTES);
|
||||
assert!(limits.per_tool.is_empty());
|
||||
assert_eq!(
|
||||
manifest.worker.file_upload.max_bytes,
|
||||
defaults::FILE_UPLOAD_MAX_BYTES
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tool_output_limits() {
|
||||
fn parse_worker_output_limits() {
|
||||
let toml = MINIMAL_REQUIRED.replace(
|
||||
"[worker]\n",
|
||||
"[worker]\n\
|
||||
@@ -651,7 +684,9 @@ model_id = "claude-sonnet-4-20250514"
|
||||
default_max_bytes = 8192\n\n\
|
||||
[worker.tool_output.per_tool]\n\
|
||||
Read = 32768\n\
|
||||
Grep = 4096\n",
|
||||
Grep = 4096\n\n\
|
||||
[worker.file_upload]\n\
|
||||
max_bytes = 12345\n",
|
||||
);
|
||||
let manifest = PodManifest::from_toml(&toml).unwrap();
|
||||
let limits = &manifest.worker.tool_output;
|
||||
@@ -659,6 +694,7 @@ model_id = "claude-sonnet-4-20250514"
|
||||
assert_eq!(limits.limit_for("Read"), 32768);
|
||||
assert_eq!(limits.limit_for("Grep"), 4096);
|
||||
assert_eq!(limits.limit_for("Unknown"), 8192);
|
||||
assert_eq!(manifest.worker.file_upload.max_bytes, 12345);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -670,7 +706,7 @@ model_id = "claude-sonnet-4-20250514"
|
||||
);
|
||||
let manifest = PodManifest::from_toml(&toml).unwrap();
|
||||
let limits = &manifest.worker.tool_output;
|
||||
assert_eq!(limits.default_max_bytes, 16 * 1024);
|
||||
assert_eq!(limits.default_max_bytes, defaults::TOOL_OUTPUT_MAX_BYTES);
|
||||
assert!(limits.per_tool.is_empty());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user