mirror of
https://github.com/openai/codex.git
synced 2026-11-07 08:34:29 +00:00
Scale skill metadata budgets with model context windows (#34626)
## Why A fixed character limit does not account for the different context-window sizes supported by models. ## What changed - Budget extension-rendered skill metadata at 2% of the resolved model context window, capped at 4,000 tokens. - Keep the existing 8,000-character fallback when model context metadata is unavailable. - Include the omission marker in the budget and still emit it when no skill entry fits. - Apply the same resolved budget to executor and host skill catalogs assembled for a turn. ## Testing - Cover proportional and capped budgets, multibyte fallback accounting, and omission-marker behavior. - Verify through the production turn path that larger model context windows include more catalog entries without exceeding the computed budget. GitOrigin-RevId: 4667293f1594de4dd605b32b9fa4255d26772c0f
This commit is contained in:
1 parent
bbad09a83b
commit
37eef7bacc
10 files changed
+454
-31
No files matched your search
@@ -130,6 +130,7 @@ mod shell_serialization;
|
||||
mod shell_snapshot;
|
||||
mod skill_approval;
|
||||
mod skills;
|
||||
mod skills_extension;
|
||||
mod spawn_agent_description;
|
||||
mod sqlite_state;
|
||||
mod stream_error_allows_next_turn;
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_core::config::Config;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_skills_extension::SkillProvider;
|
||||
use codex_skills_extension::SkillProviderSource;
|
||||
use codex_skills_extension::SkillProviders;
|
||||
use codex_skills_extension::SkillsExtensionConfig;
|
||||
use codex_skills_extension::catalog::SkillAuthority;
|
||||
use codex_skills_extension::catalog::SkillCatalog;
|
||||
use codex_skills_extension::catalog::SkillCatalogEntry;
|
||||
use codex_skills_extension::catalog::SkillPackageId;
|
||||
use codex_skills_extension::catalog::SkillProviderError;
|
||||
use codex_skills_extension::catalog::SkillReadResult;
|
||||
use codex_skills_extension::catalog::SkillResourceId;
|
||||
use codex_skills_extension::catalog::SkillSearchResult;
|
||||
use codex_skills_extension::catalog::SkillSourceKind;
|
||||
use codex_skills_extension::install_with_providers;
|
||||
use codex_skills_extension::provider::SkillListQuery;
|
||||
use codex_skills_extension::provider::SkillProviderFuture;
|
||||
use codex_skills_extension::provider::SkillReadRequest;
|
||||
use codex_skills_extension::provider::SkillSearchRequest;
|
||||
use codex_utils_string::approx_token_count;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
|
||||
struct StaticSkillProvider {
|
||||
catalog: SkillCatalog,
|
||||
}
|
||||
|
||||
impl SkillProvider for StaticSkillProvider {
|
||||
fn list(&self, query: SkillListQuery) -> SkillProviderFuture<'_, SkillCatalog> {
|
||||
// Keep thread context empty so the catalog is exercised through the
|
||||
// production turn-input path, where the host snapshot is available.
|
||||
let catalog = if query.host_snapshot.is_some() {
|
||||
self.catalog.clone()
|
||||
} else {
|
||||
SkillCatalog::default()
|
||||
};
|
||||
Box::pin(async move { Ok(catalog) })
|
||||
}
|
||||
|
||||
fn read(&self, _request: SkillReadRequest) -> SkillProviderFuture<'_, SkillReadResult> {
|
||||
Box::pin(async {
|
||||
Err(SkillProviderError::new(
|
||||
"production-flow catalog test does not read skills",
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn search(&self, _request: SkillSearchRequest) -> SkillProviderFuture<'_, SkillSearchResult> {
|
||||
Box::pin(async { Ok(SkillSearchResult::default()) })
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn production_turn_scales_extension_catalog_from_resolved_model_window() -> Result<()> {
|
||||
let mut included_counts = Vec::new();
|
||||
for (context_window, max_context_window, expected_budget) in
|
||||
[(Some(10_000), None, 200), (None, Some(400_000), 4_000)]
|
||||
{
|
||||
let server = responses::start_mock_server().await;
|
||||
let response = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||
)
|
||||
.await;
|
||||
let source_kind = SkillSourceKind::Custom("test".to_string());
|
||||
let catalog = SkillCatalog {
|
||||
entries: (0..400)
|
||||
.map(|index| {
|
||||
let name = format!("skill-{index:03}");
|
||||
SkillCatalogEntry::new(
|
||||
SkillPackageId(format!("test/{name}")),
|
||||
SkillAuthority::new(source_kind.clone(), "test"),
|
||||
name.clone(),
|
||||
"A description long enough to keep the catalog under sustained budget pressure.",
|
||||
SkillResourceId::new(format!("{name}/SKILL.md")),
|
||||
)
|
||||
.with_display_path(format!("skill://test/{name}/SKILL.md"))
|
||||
})
|
||||
.collect(),
|
||||
warnings: Vec::new(),
|
||||
};
|
||||
let mut extensions = ExtensionRegistryBuilder::<Config>::new();
|
||||
install_with_providers(
|
||||
&mut extensions,
|
||||
SkillProviders::new().with_provider(SkillProviderSource::new(
|
||||
source_kind,
|
||||
"test",
|
||||
Arc::new(StaticSkillProvider { catalog }),
|
||||
)),
|
||||
|config: &Config| SkillsExtensionConfig {
|
||||
include_instructions: config.include_skill_instructions,
|
||||
bundled_skills_enabled: false,
|
||||
orchestrator_skills_enabled: false,
|
||||
shadow_selection_enabled: false,
|
||||
},
|
||||
);
|
||||
let mut builder = test_codex()
|
||||
.with_extensions(Arc::new(extensions.build()))
|
||||
.with_model_info_override("gpt-5.5", move |model_info| {
|
||||
model_info.context_window = context_window;
|
||||
model_info.max_context_window = max_context_window;
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.include_skill_instructions = true;
|
||||
});
|
||||
let test = builder.build_with_auto_env(&server).await?;
|
||||
|
||||
test.submit_turn("Inspect the available skills.").await?;
|
||||
let request = response.single_request();
|
||||
let developer_texts = request.message_input_texts("developer");
|
||||
let catalog_text = developer_texts
|
||||
.iter()
|
||||
.find(|text| text.contains("skill://test/"))
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"production request should include the extension skill catalog, got {developer_texts:?}"
|
||||
)
|
||||
});
|
||||
let metadata_lines = catalog_text
|
||||
.lines()
|
||||
.skip_while(|line| *line != "### Available skills")
|
||||
.skip(1)
|
||||
.take_while(|line| !line.starts_with("### "))
|
||||
.filter(|line| line.starts_with("- "))
|
||||
.collect::<Vec<_>>();
|
||||
let metadata_cost = metadata_lines.iter().fold(0usize, |cost, line| {
|
||||
cost.saturating_add(approx_token_count(&format!("{line}\n")))
|
||||
});
|
||||
let included_count = metadata_lines
|
||||
.iter()
|
||||
.filter(|line| line.starts_with("- skill-"))
|
||||
.count();
|
||||
|
||||
assert!(catalog_text.contains("additional skills omitted"));
|
||||
assert!(metadata_cost <= expected_budget);
|
||||
included_counts.push(included_count);
|
||||
}
|
||||
|
||||
assert!(included_counts[0] > 0);
|
||||
assert!(included_counts[0] < included_counts[1]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user