fix(codex): load context defaults from CPA model definitions
- Resolve context limits from core model definitions, aliases, and configuration - Preserve user overrides and display context sources and fallback hints - Refresh defaults from CPA when restoring all model settings - Keep restore-all available without customizations and remove its icon - Allow saving restored defaults even when values remain unchanged - Add regression coverage for context resolution and default restoration
This commit is contained in:
1 parent
ce9944c316
commit
40ba85d6f0
10 files changed
+452
-66
No files matched your search
@@ -414,8 +414,7 @@ pub(crate) async fn get_codex_model_catalog_editor(
|
||||
) -> Result<codex_catalog::CatalogEditorSnapshot, String> {
|
||||
let _sync_guard = CODEX_CATALOG_SYNC_LOCK.lock().await;
|
||||
let config = gui_config_state.snapshot()?;
|
||||
let runtime_models =
|
||||
fetch_codex_runtime_models(config.port, effective_agent_api_key(&config)).await?;
|
||||
let runtime_models = fetch_codex_catalog_runtime_models(&config).await?;
|
||||
codex_catalog::editor_snapshot(&runtime_models)
|
||||
}
|
||||
|
||||
@@ -434,8 +433,7 @@ pub(crate) async fn save_codex_model_catalog_editor(
|
||||
) -> Result<CodexCatalogEditorSaveResult, String> {
|
||||
let _sync_guard = CODEX_CATALOG_SYNC_LOCK.lock().await;
|
||||
let config = gui_config_state.snapshot()?;
|
||||
let runtime_models =
|
||||
fetch_codex_runtime_models(config.port, effective_agent_api_key(&config)).await?;
|
||||
let runtime_models = fetch_codex_catalog_runtime_models(&config).await?;
|
||||
let snapshot = codex_catalog::save_customizations(
|
||||
&codex_model_customizations_path(&app)?,
|
||||
&runtime_models,
|
||||
@@ -796,13 +794,60 @@ pub(crate) async fn fetch_codex_runtime_models(
|
||||
Err("本地内核不支持 Codex 模型列表接口".to_string())
|
||||
}
|
||||
|
||||
// The client_version response contains synthesized Codex templates; its context
|
||||
// fields are not the raw core model definitions. Resolve defaults through the
|
||||
// management APIs before generating or editing the Codex catalog.
|
||||
pub(crate) async fn fetch_codex_catalog_runtime_models(
|
||||
config: &GuiConfigFile,
|
||||
) -> Result<Vec<codex_catalog::CodexRuntimeModel>, String> {
|
||||
let (runtime, definitions, content) = tokio::join!(
|
||||
fetch_codex_runtime_models(config.port, effective_agent_api_key(config)),
|
||||
fetch_codex_context_definitions(config),
|
||||
fetch_management_config_yaml(config),
|
||||
);
|
||||
let mut runtime = runtime?;
|
||||
let definitions = definitions?;
|
||||
let content = content?;
|
||||
let mut aliases = runtime.iter().map(|model| AgentModelOption {
|
||||
name: model.slug.clone(), alias: None, is_alias: false, context_window: None,
|
||||
}).collect::<Vec<_>>();
|
||||
mark_configured_agent_model_aliases(&mut aliases, &content)?;
|
||||
codex_catalog::merge_context_definitions(&mut runtime, &definitions, &aliases);
|
||||
codex_catalog::apply_configured_context_limits(&mut runtime, &content)?;
|
||||
Ok(runtime)
|
||||
}
|
||||
|
||||
async fn fetch_codex_context_definitions(
|
||||
config: &GuiConfigFile,
|
||||
) -> Result<Vec<CodexModelDefinition>, String> {
|
||||
// API-key sources may expose models from other channels, so do not filter by
|
||||
// active OAuth credentials. Model IDs still come only from the available list.
|
||||
let channels = ["gemini", "vertex", "aistudio", "antigravity", "claude", "codex", "kimi", "xai"];
|
||||
let results = futures_util::future::join_all(channels.iter().map(|channel|
|
||||
fetch_oauth_channel_model_definitions(config, channel)
|
||||
)).await;
|
||||
let mut definitions = Vec::new();
|
||||
let mut successes = 0;
|
||||
let mut first_error = None;
|
||||
for result in results {
|
||||
match result {
|
||||
Ok(models) => { successes += 1; definitions.extend(models); }
|
||||
Err(error) => { first_error.get_or_insert(error); }
|
||||
}
|
||||
}
|
||||
if successes == 0 {
|
||||
return Err(format!("读取 CPA 模型上下文定义失败: {}", first_error.unwrap_or_default()));
|
||||
}
|
||||
Ok(definitions)
|
||||
}
|
||||
|
||||
pub(crate) async fn fetch_prepared_agent_models(
|
||||
client: AgentClient,
|
||||
config: &GuiConfigFile,
|
||||
) -> Result<PreparedAgentModels, String> {
|
||||
let api_key = effective_agent_api_key(config);
|
||||
if client == AgentClient::Codex {
|
||||
let runtime_models = fetch_codex_runtime_models(config.port, api_key).await?;
|
||||
let runtime_models = fetch_codex_catalog_runtime_models(config).await?;
|
||||
prepare_codex_agent_models(&runtime_models)
|
||||
} else {
|
||||
let mut models = fetch_agent_models(config.port, api_key).await?;
|
||||
@@ -872,8 +917,7 @@ pub(crate) async fn refresh_applied_codex_model_catalog(
|
||||
config: &GuiConfigFile,
|
||||
) -> Result<bool, String> {
|
||||
let _sync_guard = CODEX_CATALOG_SYNC_LOCK.lock().await;
|
||||
let runtime_models =
|
||||
fetch_codex_runtime_models(config.port, effective_agent_api_key(config)).await?;
|
||||
let runtime_models = fetch_codex_catalog_runtime_models(config).await?;
|
||||
let prepared = prepare_codex_agent_models(&runtime_models)?;
|
||||
sync_prepared_codex_model_catalog(app, config, &prepared)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{OnceLock, RwLock};
|
||||
|
||||
mod customizations;
|
||||
mod runtime_context;
|
||||
pub(crate) use customizations::{
|
||||
editor_snapshot, load_customizations, save_customizations, CatalogEditorRequest,
|
||||
CatalogEditorSnapshot,
|
||||
};
|
||||
pub(crate) use runtime_context::{apply_configured_context_limits, merge_context_definitions};
|
||||
|
||||
const MODEL_CATALOG_JSON: &str = include_str!("../resources/codex_models/model-catalog.json");
|
||||
const FALLBACK_MODEL_JSON: &str = include_str!("../resources/codex_models/fallback-model.json");
|
||||
@@ -21,6 +23,7 @@ pub(crate) struct CodexRuntimeModel {
|
||||
description: Option<String>,
|
||||
context_window: Option<u64>,
|
||||
max_context_window: Option<u64>,
|
||||
context_source: &'static str,
|
||||
input_modalities: Option<Vec<String>>,
|
||||
default_reasoning_level: Option<String>,
|
||||
hidden: bool,
|
||||
@@ -138,8 +141,15 @@ pub(crate) fn parse_runtime_models(payload: &Value) -> Result<Vec<CodexRuntimeMo
|
||||
"contextLength",
|
||||
],
|
||||
);
|
||||
let max_context_window =
|
||||
positive_u64_field(value, &["max_context_window", "maxContextWindow"]);
|
||||
let max_context_window = positive_u64_field(
|
||||
value,
|
||||
&[
|
||||
"max_context_window",
|
||||
"maxContextWindow",
|
||||
"max_context_length",
|
||||
"maxContextLength",
|
||||
],
|
||||
);
|
||||
let input_modalities = parse_modalities(value);
|
||||
let default_reasoning_level =
|
||||
optional_string(value, &["default_reasoning_level", "defaultReasoningLevel"])
|
||||
@@ -154,6 +164,11 @@ pub(crate) fn parse_runtime_models(payload: &Value) -> Result<Vec<CodexRuntimeMo
|
||||
description: description.clone(),
|
||||
context_window,
|
||||
max_context_window,
|
||||
context_source: if context_window.or(max_context_window).is_some() {
|
||||
"compatibility"
|
||||
} else {
|
||||
"template"
|
||||
},
|
||||
input_modalities: input_modalities.clone(),
|
||||
default_reasoning_level: default_reasoning_level.clone(),
|
||||
hidden,
|
||||
@@ -414,6 +429,7 @@ fn prepare_catalog_with_customizations(
|
||||
"display_name".to_string(),
|
||||
Value::String(runtime.slug.clone()),
|
||||
);
|
||||
apply_runtime_context_windows(&mut value, runtime);
|
||||
enable_fast_mode(&mut value);
|
||||
entries.push(CatalogEntry {
|
||||
value,
|
||||
@@ -565,6 +581,25 @@ fn normalize_fallback_model(model: &mut Map<String, Value>) {
|
||||
}
|
||||
}
|
||||
|
||||
// Both official and fallback templates take context defaults from the CPA API.
|
||||
// Template values are used only when the API provides no valid context metadata.
|
||||
fn apply_runtime_context_windows(model: &mut Map<String, Value>, runtime: &CodexRuntimeModel) {
|
||||
if let Some(context_window) = runtime.context_window.or(runtime.max_context_window) {
|
||||
let max_context_window = runtime
|
||||
.max_context_window
|
||||
.unwrap_or(context_window)
|
||||
.max(context_window);
|
||||
model.insert(
|
||||
"context_window".to_string(),
|
||||
Value::Number(context_window.into()),
|
||||
);
|
||||
model.insert(
|
||||
"max_context_window".to_string(),
|
||||
Value::Number(max_context_window.into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_runtime_metadata(model: &mut Map<String, Value>, runtime: &CodexRuntimeModel) {
|
||||
model.insert("slug".to_string(), Value::String(runtime.slug.clone()));
|
||||
model.insert(
|
||||
@@ -579,26 +614,7 @@ fn apply_runtime_metadata(model: &mut Map<String, Value>, runtime: &CodexRuntime
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(context_window) = runtime.context_window {
|
||||
let max_context_window = runtime
|
||||
.max_context_window
|
||||
.unwrap_or(context_window)
|
||||
.max(context_window);
|
||||
model.insert(
|
||||
"context_window".to_string(),
|
||||
Value::Number(context_window.into()),
|
||||
);
|
||||
model.insert(
|
||||
"max_context_window".to_string(),
|
||||
Value::Number(max_context_window.into()),
|
||||
);
|
||||
} else if let Some(max_context_window) = runtime.max_context_window {
|
||||
let context_window = positive_u64_value(model.get("context_window")).unwrap_or(128_000);
|
||||
model.insert(
|
||||
"max_context_window".to_string(),
|
||||
Value::Number(max_context_window.max(context_window).into()),
|
||||
);
|
||||
}
|
||||
apply_runtime_context_windows(model, runtime);
|
||||
|
||||
if let Some(modalities) = runtime.input_modalities.as_ref() {
|
||||
model.insert(
|
||||
@@ -1096,7 +1112,8 @@ mod tests {
|
||||
["b", "C"]
|
||||
);
|
||||
assert_eq!(models[0]["base_instructions"], "Known B");
|
||||
assert_eq!(models[0]["context_window"], 300_000);
|
||||
assert_eq!(models[0]["context_window"], 1);
|
||||
assert_eq!(models[0]["max_context_window"], 1);
|
||||
assert_eq!(
|
||||
models[1]["base_instructions"],
|
||||
"You are Codex, a model-neutral coding agent."
|
||||
@@ -1106,6 +1123,54 @@ mod tests {
|
||||
assert!(models[1]["priority"].as_i64().unwrap() > 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_context_defaults_apply_to_official_and_fallback_models() {
|
||||
let sources = parse_sources(MODEL_CATALOG_JSON).unwrap();
|
||||
let runtime = runtime(serde_json::json!({"models":[
|
||||
{"slug":"gpt-6-astra","context_window":372000,"max_context_window":872000},
|
||||
{"slug":"gpt-5.6-sol","context_window":921000,"max_context_window":1000000},
|
||||
{"slug":"deepseek-v4-flash","context_length":1000000,"max_context_length":1048576},
|
||||
{"slug":"other-model","context_window":128000}
|
||||
]}));
|
||||
let catalog = prepare_catalog_with_sources(&runtime, &sources).unwrap();
|
||||
let output = output_models(&catalog);
|
||||
for (slug, context, maximum) in [
|
||||
("gpt-6-astra", 372_000, 872_000),
|
||||
("gpt-5.6-sol", 921_000, 1_000_000),
|
||||
("deepseek-v4-flash", 1_000_000, 1_048_576),
|
||||
("other-model", 128_000, 128_000),
|
||||
] {
|
||||
let model = output.iter().find(|model| model["slug"] == slug).unwrap();
|
||||
assert_eq!(model["context_window"], context, "{slug}");
|
||||
assert_eq!(model["max_context_window"], maximum, "{slug}");
|
||||
let option = catalog
|
||||
.models
|
||||
.iter()
|
||||
.find(|model| model.name == slug)
|
||||
.unwrap();
|
||||
assert_eq!(option.context_window, Some(context));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_only_runtime_context_does_not_inherit_an_unrelated_default() {
|
||||
let sources = parse_sources(MODEL_CATALOG_JSON).unwrap();
|
||||
for (slug, maximum) in [
|
||||
("gpt-6-astra", 64_000),
|
||||
("gpt-6-astra", 1_000_000),
|
||||
("max-only", 64_000),
|
||||
("max-only", 1_000_000),
|
||||
] {
|
||||
let runtime = runtime(serde_json::json!({"models":[{
|
||||
"slug": slug, "max_context_window": maximum
|
||||
}]}));
|
||||
let catalog = prepare_catalog_with_sources(&runtime, &sources).unwrap();
|
||||
let model = &output_models(&catalog)[0];
|
||||
assert_eq!(model["context_window"], maximum);
|
||||
assert_eq!(model["max_context_window"], maximum);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_template_preserves_unrelated_capabilities() {
|
||||
let sources = test_sources();
|
||||
@@ -1121,7 +1186,12 @@ mod tests {
|
||||
for (key, expected) in template {
|
||||
if !matches!(
|
||||
key.as_str(),
|
||||
"slug" | "display_name" | "service_tiers" | "additional_speed_tiers"
|
||||
"slug"
|
||||
| "display_name"
|
||||
| "context_window"
|
||||
| "max_context_window"
|
||||
| "service_tiers"
|
||||
| "additional_speed_tiers"
|
||||
) {
|
||||
assert_eq!(model.get(key), Some(expected), "changed field {key}");
|
||||
}
|
||||
@@ -1129,6 +1199,8 @@ mod tests {
|
||||
assert_eq!(model["slug"], "a");
|
||||
assert_eq!(model["display_name"], "a");
|
||||
assert_eq!(model["nested"]["unknown"], true);
|
||||
assert_eq!(model["context_window"], 1);
|
||||
assert_eq!(model["max_context_window"], 1);
|
||||
assert_eq!(
|
||||
model["service_tiers"],
|
||||
serde_json::json!([{
|
||||
@@ -1182,7 +1254,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gpt_6_astra_generation_uses_official_template_and_optional_fast_mode() {
|
||||
fn gpt_6_astra_uses_api_context_with_official_capabilities_and_fast_mode() {
|
||||
let sources = parse_sources(MODEL_CATALOG_JSON).unwrap();
|
||||
let runtime = runtime(serde_json::json!({"models":[{
|
||||
"id":"gpt-6-astra",
|
||||
@@ -1193,8 +1265,8 @@ mod tests {
|
||||
let catalog = prepare_catalog_with_sources(&runtime, &sources).unwrap();
|
||||
let model = &output_models(&catalog)[0];
|
||||
|
||||
assert_eq!(model["context_window"], 272_000);
|
||||
assert_eq!(model["max_context_window"], 872_000);
|
||||
assert_eq!(model["context_window"], 1_048_576);
|
||||
assert_eq!(model["max_context_window"], 1_048_576);
|
||||
assert_eq!(model["default_reasoning_level"], "low");
|
||||
assert_eq!(
|
||||
reasoning_efforts(model),
|
||||
|
||||
@@ -39,6 +39,7 @@ pub(crate) struct CatalogEditorSnapshot {
|
||||
struct CatalogEditorModel {
|
||||
slug: String,
|
||||
has_official_template: bool,
|
||||
context_source: &'static str,
|
||||
customized: bool,
|
||||
configuration: Map<String, Value>,
|
||||
defaults: Map<String, Value>,
|
||||
@@ -64,7 +65,15 @@ fn editable_configuration(model: &Map<String, Value>) -> Map<String, Value> {
|
||||
.map(|field| {
|
||||
(
|
||||
field.to_string(),
|
||||
model.get(*field).cloned().unwrap_or(Value::Null),
|
||||
model.get(*field).cloned().unwrap_or_else(|| {
|
||||
// Codex defaults this optional field to 95 when it is omitted.
|
||||
// The editor needs the same value to validate and save context changes.
|
||||
if *field == "effective_context_window_percent" {
|
||||
Value::from(95)
|
||||
} else {
|
||||
Value::Null
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
@@ -198,7 +207,7 @@ pub(super) fn apply_customizations(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn snapshot_for_state(
|
||||
pub(super) fn snapshot_for_state(
|
||||
runtime_models: &[CodexRuntimeModel],
|
||||
state: &CatalogState,
|
||||
) -> Result<CatalogEditorSnapshot, String> {
|
||||
@@ -222,6 +231,10 @@ fn snapshot_for_state(
|
||||
models.push(CatalogEditorModel {
|
||||
slug,
|
||||
has_official_template: state.sources.templates.contains_key(&key),
|
||||
context_source: runtime_models
|
||||
.iter()
|
||||
.find(|runtime| normalize_id(&runtime.slug) == key)
|
||||
.map_or("template", |runtime| runtime.context_source),
|
||||
customized: state.customizations.contains_key(&key),
|
||||
configuration: editable_configuration(&model),
|
||||
defaults,
|
||||
@@ -379,6 +392,7 @@ mod tests {
|
||||
description: None,
|
||||
context_window: None,
|
||||
max_context_window: None,
|
||||
context_source: "template",
|
||||
input_modalities: None,
|
||||
default_reasoning_level: None,
|
||||
hidden: false,
|
||||
@@ -460,6 +474,70 @@ mod tests {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_context_defaults_follow_api_updates_and_preserve_explicit_overrides() {
|
||||
let mut state = CatalogState {
|
||||
sources: parse_sources(MODEL_CATALOG_JSON).unwrap(),
|
||||
json: MODEL_CATALOG_JSON.to_string(),
|
||||
customizations: Default::default(),
|
||||
};
|
||||
let mut runtime = runtime_model("gpt-6-astra");
|
||||
runtime.context_window = Some(128_000);
|
||||
runtime.max_context_window = Some(256_000);
|
||||
let snapshot = snapshot_for_state(&[runtime.clone()], &state).unwrap();
|
||||
assert_eq!(snapshot.models[0].configuration["context_window"], 128_000);
|
||||
assert_eq!(snapshot.models[0].defaults["max_context_window"], 256_000);
|
||||
assert_eq!(
|
||||
snapshot.models[0].defaults["effective_context_window_percent"],
|
||||
95
|
||||
);
|
||||
let mut configuration = snapshot.models[0].configuration.clone();
|
||||
configuration.insert("context_window".to_string(), serde_json::json!(192_000));
|
||||
state.customizations = customizations_from_request(
|
||||
&snapshot,
|
||||
CatalogEditorRequest {
|
||||
revision: snapshot.revision.clone(),
|
||||
models: vec![CatalogEditorModelRequest {
|
||||
slug: runtime.slug.clone(),
|
||||
configuration,
|
||||
}],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
runtime.context_window = Some(512_000);
|
||||
runtime.max_context_window = Some(1_000_000);
|
||||
let updated = snapshot_for_state(&[runtime.clone()], &state).unwrap();
|
||||
assert_ne!(updated.revision, snapshot.revision);
|
||||
assert_eq!(updated.models[0].defaults["context_window"], 512_000);
|
||||
assert_eq!(updated.models[0].defaults["max_context_window"], 1_000_000);
|
||||
assert_eq!(updated.models[0].configuration["context_window"], 192_000);
|
||||
assert_eq!(
|
||||
updated.models[0].configuration["max_context_window"],
|
||||
256_000
|
||||
);
|
||||
|
||||
// Restoring defaults drops the saved override and follows the current API response.
|
||||
state.customizations = customizations_from_request(
|
||||
&updated,
|
||||
CatalogEditorRequest {
|
||||
revision: updated.revision.clone(),
|
||||
models: vec![CatalogEditorModelRequest {
|
||||
slug: runtime.slug.clone(),
|
||||
configuration: updated.models[0].defaults.clone(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(state.customizations.is_empty());
|
||||
let generated =
|
||||
prepare_catalog_with_customizations(&[runtime], &state.sources, &state.customizations)
|
||||
.unwrap();
|
||||
let model: Value = serde_json::from_str(&generated.json).unwrap();
|
||||
assert_eq!(model["models"][0]["context_window"], 512_000);
|
||||
assert_eq!(model["models"][0]["max_context_window"], 1_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_editor_revision_is_rejected_without_changing_state() {
|
||||
let sources = parse_sources(MODEL_CATALOG_JSON).unwrap();
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn merge_context_definitions(
|
||||
runtime_models: &mut [CodexRuntimeModel],
|
||||
definitions: &[crate::CodexModelDefinition],
|
||||
aliases: &[AgentModelOption],
|
||||
) {
|
||||
for runtime in runtime_models {
|
||||
let source = aliases
|
||||
.iter()
|
||||
.find(|model| model.is_alias && model.name.eq_ignore_ascii_case(&runtime.slug))
|
||||
.and_then(|model| model.alias.as_deref())
|
||||
.unwrap_or(&runtime.slug);
|
||||
// These are core model definitions, not the synthesized Codex client catalog.
|
||||
let context = definitions
|
||||
.iter()
|
||||
.filter(|definition| definition.id.eq_ignore_ascii_case(source))
|
||||
.filter_map(|definition| definition.context_window)
|
||||
.filter(|value| *value > 0)
|
||||
.min();
|
||||
if let Some(context) = context {
|
||||
runtime.context_window = Some(context);
|
||||
runtime.max_context_window =
|
||||
Some(runtime.max_context_window.unwrap_or(context).max(context));
|
||||
runtime.context_source = "definition";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply_configured_context_limits(
|
||||
runtime_models: &mut [CodexRuntimeModel],
|
||||
content: &str,
|
||||
) -> Result<(), String> {
|
||||
let document: serde_norway::Value = serde_norway::from_str(content)
|
||||
.map_err(|error| format!("解析内核模型上下文配置失败: {error}"))?;
|
||||
let root = document
|
||||
.as_mapping()
|
||||
.ok_or("内核配置顶层必须是 YAML 映射")?;
|
||||
let mut limits: HashMap<String, u64> = HashMap::new();
|
||||
for section in crate::MODEL_ALIAS_CONFIG_SECTIONS {
|
||||
let Some(providers) =
|
||||
crate::yaml_mapping_value(root, section).and_then(serde_norway::Value::as_sequence)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
for provider in providers {
|
||||
let Some(models) = provider
|
||||
.as_mapping()
|
||||
.and_then(|provider| crate::yaml_mapping_value(provider, "models"))
|
||||
.and_then(serde_norway::Value::as_sequence)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
for model in models {
|
||||
let Some((_, public_name, _)) = crate::configured_model_identity(model) else {
|
||||
continue;
|
||||
};
|
||||
let Some(limit) = model
|
||||
.as_mapping()
|
||||
.and_then(|model| crate::yaml_mapping_value(model, "max-context-length"))
|
||||
.and_then(serde_norway::Value::as_u64)
|
||||
.filter(|value| *value > 0)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
limits
|
||||
.entry(normalize_id(&public_name))
|
||||
.and_modify(|existing| *existing = (*existing).min(limit))
|
||||
.or_insert(limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
for runtime in runtime_models {
|
||||
if let Some(&limit) = limits.get(&normalize_id(&runtime.slug)) {
|
||||
runtime.context_window = Some(limit);
|
||||
runtime.max_context_window = Some(limit);
|
||||
runtime.context_source = "configuration";
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn raw_core_definitions_replace_synthesized_272k_for_models_and_aliases() {
|
||||
let mut runtime = parse_runtime_models(&serde_json::json!({"models":[
|
||||
{"slug":"gpt-5.6-sol","context_window":272000,"max_context_window":872000},
|
||||
{"slug":"gemini-3.8-flash-high","context_window":272000,"max_context_window":272000},
|
||||
{"slug":"fast-alias","context_window":272000,"max_context_window":272000},
|
||||
{"slug":"no-definition","context_window":272000,"max_context_window":272000}
|
||||
]}))
|
||||
.unwrap();
|
||||
let definitions = crate::parse_codex_model_definitions(&serde_json::json!({"models":[
|
||||
{"id":"gpt-5.6-sol","context_length":921000},
|
||||
{"id":"gemini-3.8-flash-high","context_length":1048576}
|
||||
]}))
|
||||
.unwrap();
|
||||
let aliases = vec![AgentModelOption {
|
||||
name: "fast-alias".to_string(),
|
||||
alias: Some("GPT-5.6-SOL".to_string()),
|
||||
is_alias: true,
|
||||
context_window: None,
|
||||
}];
|
||||
merge_context_definitions(&mut runtime, &definitions, &aliases);
|
||||
assert_eq!(runtime[0].context_window, Some(921_000));
|
||||
assert_eq!(runtime[0].max_context_window, Some(921_000));
|
||||
assert_eq!(runtime[1].context_window, Some(1_048_576));
|
||||
assert_eq!(runtime[2].context_window, Some(921_000));
|
||||
assert_eq!(runtime[2].context_source, "definition");
|
||||
assert_eq!(runtime[3].context_source, "compatibility");
|
||||
let sources = parse_sources(MODEL_CATALOG_JSON).unwrap();
|
||||
let state = CatalogState {
|
||||
sources,
|
||||
json: MODEL_CATALOG_JSON.to_string(),
|
||||
customizations: Default::default(),
|
||||
};
|
||||
let snapshot = customizations::snapshot_for_state(&runtime, &state).unwrap();
|
||||
let snapshot = serde_json::to_value(snapshot).unwrap();
|
||||
let model = snapshot["models"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|model| model["slug"] == "gpt-5.6-sol")
|
||||
.unwrap();
|
||||
assert_eq!(model["configuration"]["context_window"], 921_000);
|
||||
assert_eq!(model["defaults"]["context_window"], 921_000);
|
||||
assert_eq!(model["contextSource"], "definition");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn core_configured_context_limit_takes_precedence_over_static_definitions() {
|
||||
let mut runtime = parse_runtime_models(&serde_json::json!({"models":[
|
||||
{"slug":"custom-alias","context_window":921000,"max_context_window":921000},
|
||||
{"slug":"unmodified","context_window":128000,"max_context_window":128000}
|
||||
]}))
|
||||
.unwrap();
|
||||
apply_configured_context_limits(&mut runtime, "codex-api-key:\n - models:\n - name: upstream-model\n alias: custom-alias\n max-context-length: 64000\n").unwrap();
|
||||
assert_eq!(runtime[0].context_window, Some(64_000));
|
||||
assert_eq!(runtime[0].max_context_window, Some(64_000));
|
||||
assert_eq!(runtime[0].context_source, "configuration");
|
||||
assert_eq!(runtime[1].context_window, Some(128_000));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user