[codex] add rollout token budget configuration (varlength 1/N) (#28746)

## What

This PR defines the structured configuration contract for shared rollout
token budgets (across ALL agent threads under 1 rollout).

```toml
[features.rollout_budget]
enabled = true
limit_tokens = 100000
reminder_interval_tokens = 10000
sampling_token_weight = 1.0
prefill_token_weight = 0.1
```

The reminder interval defaults to 10% of the rollout limit. Sampling and
prefill weights default to `1.0`.

## Scope

This PR only defines and validates configuration. It does not track
usage, inject reminders, or stop a rollout. Accounting and reminders are
implemented in the stacked follow-up #28494.

The existing `token_budget` feature remains unchanged. `rollout_budget`
has its own feature key and configuration type.

## Tests

The config test verifies that the structured fields resolve into
`RolloutBudgetConfig` and do not enable the existing `token_budget`
feature.

Local checks:

- `just write-config-schema`
- `just test -p codex-core load_config_resolves_rollout_budget`
- `cargo check -p codex-thread-manager-sample`
- `git diff --check`

The full workspace test suite was not run locally.
This commit is contained in:
rka-oai
2026-06-18 04:29:47 -07:00
committed by GitHub
parent c73296a0f0
commit ecc4c30e28
8 changed files with 264 additions and 0 deletions
+29
View File
@@ -73,6 +73,35 @@ impl FeatureConfig for MultiAgentV2ConfigToml {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RolloutBudgetConfigToml {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 1))]
pub limit_tokens: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 1))]
pub reminder_interval_tokens: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0.0))]
pub sampling_token_weight: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0.0))]
pub prefill_token_weight: Option<f64>,
}
impl FeatureConfig for RolloutBudgetConfigToml {
fn enabled(&self) -> Option<bool> {
self.enabled
}
fn set_enabled(&mut self, enabled: bool) {
self.enabled = Some(enabled);
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub(crate) struct RemovedAppsMcpPathOverrideConfigToml {
+17
View File
@@ -23,6 +23,7 @@ pub use feature_configs::NetworkProxyDomainPermissionToml;
pub use feature_configs::NetworkProxyModeToml;
pub use feature_configs::NetworkProxyUnixSocketPermissionToml;
use feature_configs::RemovedAppsMcpPathOverrideConfigToml;
pub use feature_configs::RolloutBudgetConfigToml;
use legacy::LegacyFeatureToggles;
pub use legacy::legacy_feature_keys;
@@ -203,6 +204,8 @@ pub enum Feature {
Goals,
/// Add current context-window metadata to model-visible context.
TokenBudget,
/// Track and report a shared token budget across a session's agent threads.
RolloutBudget,
/// Expose an input-interruptible sleep tool.
SleepTool,
/// Route MCP tool approval prompts through the MCP elicitation request path.
@@ -618,6 +621,8 @@ pub struct FeaturesToml {
pub code_mode: Option<FeatureToml<CodeModeConfigToml>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub multi_agent_v2: Option<FeatureToml<MultiAgentV2ConfigToml>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rollout_budget: Option<FeatureToml<RolloutBudgetConfigToml>>,
#[serde(default, rename = "apps_mcp_path_override", skip_serializing)]
#[schemars(skip)]
removed_apps_mcp_path_override: Option<FeatureToml<RemovedAppsMcpPathOverrideConfigToml>>,
@@ -650,6 +655,9 @@ impl FeaturesToml {
if let Some(enabled) = self.multi_agent_v2.as_ref().and_then(FeatureToml::enabled) {
entries.insert(Feature::MultiAgentV2.key().to_string(), enabled);
}
if let Some(enabled) = self.rollout_budget.as_ref().and_then(FeatureToml::enabled) {
entries.insert(Feature::RolloutBudget.key().to_string(), enabled);
}
if let Some(enabled) = self.network_proxy.as_ref().and_then(FeatureToml::enabled) {
entries.insert(Feature::NetworkProxy.key().to_string(), enabled);
}
@@ -661,6 +669,7 @@ impl FeaturesToml {
let Self {
code_mode,
multi_agent_v2,
rollout_budget,
removed_apps_mcp_path_override: _,
network_proxy,
entries,
@@ -674,6 +683,8 @@ impl FeaturesToml {
materialize_resolved_feature_enabled(code_mode, enabled);
} else if spec.id == Feature::MultiAgentV2 {
materialize_resolved_feature_enabled(multi_agent_v2, enabled);
} else if spec.id == Feature::RolloutBudget {
materialize_resolved_feature_enabled(rollout_budget, enabled);
} else if spec.id == Feature::NetworkProxy {
materialize_resolved_feature_enabled(network_proxy, enabled);
} else {
@@ -1167,6 +1178,12 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::RolloutBudget,
key: "rollout_budget",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::SleepTool,
key: "sleep_tool",