Add a configurable skill catalog token budget (#38978)

## What changed

- Add `[skills].max_context_tokens` to override the token budget used to render the available-skills catalog.
- Require a positive value and cap configured budgets at 10,000 tokens.
- Preserve the existing default of 2% of the model context window when the option is unset.

## Testing

- Cover configuration parsing, override behavior, the 10,000-token cap, and catalog rendering within the configured budget.

GitOrigin-RevId: f29ddcb57fc2d800c0006d03e1c33cbe3ab6272d
This commit is contained in:
jif authored and copyberry committed 2026-08-17 10:01:59 +00:00
1 parent c6058ccaa9
commit 632e35ce8d
19 files changed
+126 -7

No files matched your search

+4
View File
@@ -1,8 +1,12 @@
use std::num::NonZeroUsize;
/// Host-supplied configuration used by the skills extension.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SkillsExtensionConfig {
/// Whether the available-skills catalog is included in model context.
pub include_instructions: bool,
/// Optional token budget override for the available-skills catalog.
pub max_context_tokens: Option<NonZeroUsize>,
/// Whether bundled skills are eligible for discovery.
pub bundled_skills_enabled: bool,
/// Whether orchestrator-owned skills are eligible for discovery.
+3 -2
View File
@@ -228,7 +228,7 @@ where
&catalog,
include_usage,
SkillCatalogRenderPolicy::ExtensionCompatible,
skill_metadata_budget(/*context_window*/ None),
skill_metadata_budget(/*context_window*/ None, config.max_context_tokens),
);
if let Some(message) = rendered.warning_message {
self.emit_warning(thread_store.level_id(), /*turn_id*/ None, message);
@@ -420,7 +420,8 @@ where
let context_window = model_info
.as_deref()
.and_then(ModelInfo::resolved_context_window);
let metadata_budget = skill_metadata_budget(context_window);
let metadata_budget =
skill_metadata_budget(context_window, config.max_context_tokens);
let rendered = render_catalog(
extension_metrics.as_deref(),
CatalogSurface::TurnInput,
+14 -1
View File
@@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::num::NonZeroUsize;
use codex_protocol::protocol::SkillScope;
use codex_utils_string::approx_token_count;
@@ -14,6 +15,7 @@ use crate::fragments::AvailableSkillsInstructions;
use crate::host_aliases::shared_host_alias_roots;
const DEFAULT_SKILL_METADATA_CHAR_BUDGET: usize = 8_000;
const MAX_CONFIGURED_SKILL_METADATA_TOKEN_BUDGET: usize = 10_000;
const MAX_SKILL_PROMPT_BYTES: usize = 8_000;
const SKILL_METADATA_CONTEXT_WINDOW_PERCENT: usize = 2;
const MAX_CATALOG_SKILL_DESCRIPTION_CHARS: usize = 1_024;
@@ -122,7 +124,18 @@ impl SkillRenderReport {
}
}
pub(crate) fn skill_metadata_budget(context_window: Option<i64>) -> SkillMetadataBudget {
pub(crate) fn skill_metadata_budget(
context_window: Option<i64>,
max_context_tokens: Option<NonZeroUsize>,
) -> SkillMetadataBudget {
if let Some(max_context_tokens) = max_context_tokens {
return SkillMetadataBudget::Tokens(
max_context_tokens
.get()
.min(MAX_CONFIGURED_SKILL_METADATA_TOKEN_BUDGET),
);
}
context_window
.and_then(|window| usize::try_from(window).ok())
.filter(|window| *window > 0)
+13 -3
View File
@@ -194,17 +194,27 @@ fn description_selection_follows_render_policy() {
#[test]
fn catalog_budget_uses_context_percentage_or_character_fallback() {
assert_eq!(
skill_metadata_budget(Some(100_000)),
skill_metadata_budget(Some(100_000), /*max_context_tokens*/ None),
SkillMetadataBudget::Tokens(2_000)
);
assert_eq!(
skill_metadata_budget(Some(400_000)),
skill_metadata_budget(Some(400_000), /*max_context_tokens*/ None),
SkillMetadataBudget::Tokens(8_000)
);
assert_eq!(
skill_metadata_budget(/*context_window*/ None),
skill_metadata_budget(
/*context_window*/ None, /*max_context_tokens*/ None
),
SkillMetadataBudget::Characters(8_000)
);
assert_eq!(
skill_metadata_budget(Some(100_000), NonZeroUsize::new(5_000)),
SkillMetadataBudget::Tokens(5_000)
);
assert_eq!(
skill_metadata_budget(/*context_window*/ None, NonZeroUsize::new(50_000)),
SkillMetadataBudget::Tokens(10_000)
);
}
#[test]
@@ -107,6 +107,7 @@ impl<'a> CatalogContext<'a> {
let context_window = model_info
.as_deref()
.and_then(ModelInfo::resolved_context_window);
let metadata_budget = skill_metadata_budget(context_window, config.max_context_tokens);
let emitted_warnings = input
.turn_store
.get_or_init(EmittedCatalogBudgetWarnings::default);
@@ -127,7 +128,7 @@ impl<'a> CatalogContext<'a> {
input,
thread_state,
config,
metadata_budget: skill_metadata_budget(context_window),
metadata_budget,
include_usage,
warning_emitter,
})
@@ -2393,6 +2393,7 @@ fn default_config() -> TestConfig {
fn skills_extension_config(config: &TestConfig) -> SkillsExtensionConfig {
SkillsExtensionConfig {
include_instructions: config.include_instructions,
max_context_tokens: None,
bundled_skills_enabled: config.bundled_skills_enabled,
orchestrator_skills_enabled: config.orchestrator_skills_enabled,
shadow_selection_enabled: config.shadow_selection_enabled,