fix(agents): sync Kimi and Grok context windows with CPA core

This commit is contained in:
lzt404 committed 2026-08-15 15:51:56 +08:00
1 parent b312b904a2
commit 51ea7fd69f
11 files changed
+218 -37

No files matched your search

+8 -1
View File
@@ -688,7 +688,7 @@ pub(crate) async fn fetch_prepared_agent_models(
prepare_codex_agent_models(&runtime_models)
} else {
let mut models = fetch_agent_models(config.port, api_key).await?;
if client == AgentClient::ZCode {
if agent_uses_cpa_runtime_context_windows(client) {
let runtime_models = fetch_codex_runtime_models(config.port, api_key).await?;
codex_catalog::merge_runtime_context_windows(&mut models, &runtime_models);
}
@@ -714,6 +714,13 @@ pub(crate) async fn fetch_prepared_agent_models(
}
}
pub(crate) fn agent_uses_cpa_runtime_context_windows(client: AgentClient) -> bool {
matches!(
client,
AgentClient::ZCode | AgentClient::KimiCode | AgentClient::GrokBuild
)
}
pub(crate) fn resolve_claude_desktop_model_mappings(
client: AgentClient,
models: &[AgentModelOption],
+6
View File
@@ -525,6 +525,10 @@ pub(crate) fn build_claude_agent_config(
CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV.to_string(),
serde_json::Value::String(max_context_tokens.to_string()),
);
env.insert(
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV.to_string(),
serde_json::Value::String("1".to_string()),
);
env.insert(
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV.to_string(),
serde_json::Value::String(mappings.auto_compact_pct.to_string()),
@@ -922,6 +926,7 @@ pub(crate) fn remove_claude_code_managed_configuration(
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_FABLE_MODEL",
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV,
CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV,
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV,
DISABLE_AUTO_COMPACT_ENV,
@@ -1582,6 +1587,7 @@ pub(crate) fn build_restored_claude_code_config(
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_FABLE_MODEL",
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV,
CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV,
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV,
DISABLE_AUTO_COMPACT_ENV,
+25 -1
View File
@@ -368,6 +368,7 @@ pub(crate) fn inspect_pi_provider_status(
config_exists,
config_valid,
configured,
configuration_synchronized: configured,
current_model: current_model.clone(),
oauth_configuration: false,
modification_enabled: configured,
@@ -849,6 +850,7 @@ pub(crate) fn inspect_agent_config(
warnings.push(message.clone());
}
let modification = inspect_agent_application(client, home);
let configuration_synchronized = agent_configuration_is_synchronized(client, home, configured);
warnings.extend(modification.warnings.iter().cloned());
AgentConfigStatus {
@@ -865,6 +867,7 @@ pub(crate) fn inspect_agent_config(
config_exists,
config_valid,
configured,
configuration_synchronized,
current_model,
oauth_configuration,
modification_enabled: modification.enabled,
@@ -880,6 +883,23 @@ pub(crate) fn inspect_agent_config(
}
}
pub(crate) fn agent_configuration_is_synchronized(
client: AgentClient,
home: &Path,
configured: bool,
) -> bool {
if !configured {
return false;
}
if !matches!(client, AgentClient::KimiCode | AgentClient::GrokBuild) {
return true;
}
load_agent_applied_state(client, home)
.ok()
.flatten()
.is_some_and(|state| state.configuration_revision >= AGENT_CONFIGURATION_REVISION)
}
pub(crate) fn inspect_agent_application(
client: AgentClient,
home: &Path,
@@ -2063,7 +2083,11 @@ pub(crate) fn inspect_claude_agent_config(
&& env
.and_then(|env| env.get("ANTHROPIC_AUTH_TOKEN"))
.and_then(serde_json::Value::as_str)
== Some(api_key);
== Some(api_key)
&& env
.and_then(|env| env.get(CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV))
.and_then(serde_json::Value::as_str)
== Some("1");
let model = env
.and_then(|env| env.get("ANTHROPIC_MODEL"))
.and_then(serde_json::Value::as_str)
+3
View File
@@ -161,6 +161,7 @@ pub(crate) fn recover_codex_applied_state_from_backups(
version: AGENT_APPLIED_STATE_VERSION,
client: AgentClient::Codex.id().to_string(),
model,
configuration_revision: AGENT_CONFIGURATION_REVISION,
claude_desktop_model_mappings: None,
backup_files,
updated_at_unix: SystemTime::now()
@@ -284,6 +285,7 @@ pub(crate) fn load_agent_applied_state(
version: AGENT_APPLIED_STATE_VERSION,
client: client.id().to_string(),
model: record.model,
configuration_revision: 0,
claude_desktop_model_mappings: None,
backup_files,
updated_at_unix: SystemTime::now()
@@ -1485,6 +1487,7 @@ pub(crate) fn commit_agent_configuration(
version: AGENT_APPLIED_STATE_VERSION,
client: client.id().to_string(),
model: model.to_string(),
configuration_revision: AGENT_CONFIGURATION_REVISION,
claude_desktop_model_mappings: (client == AgentClient::ClaudeDesktop)
.then(|| claude_desktop_model_mappings.cloned())
.flatten(),
+6
View File
@@ -159,6 +159,8 @@ const MANAGED_CLAUDE_HAIKU_ALIAS_DISPLAY_NAME: &str =
const DEFAULT_CLAUDE_CONTEXT_WINDOW: u64 = 200_000;
const CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW: u64 = 1_000_000;
const CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV: &str = "CLAUDE_CODE_MAX_CONTEXT_TOKENS";
const CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV: &str =
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY";
const CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV: &str = "CLAUDE_AUTOCOMPACT_PCT_OVERRIDE";
const DISABLE_AUTO_COMPACT_ENV: &str = "DISABLE_AUTO_COMPACT";
const DEFAULT_CLAUDE_AUTO_COMPACT_PCT: u8 = 90;
@@ -171,6 +173,7 @@ const MODEL_ALIAS_CONFIG_SECTIONS: &[&str] = &[
const LEGACY_AGENT_MODIFICATION_STATE_VERSION: u8 = 1;
const AGENT_MODIFICATION_STATE_VERSION: u8 = 2;
const AGENT_APPLIED_STATE_VERSION: u8 = 4;
const AGENT_CONFIGURATION_REVISION: u8 = 1;
const AGENT_PHASE_APPLYING: &str = "applying";
const AGENT_PHASE_ACTIVE: &str = "active";
const AGENT_PHASE_RESTORING: &str = "restoring";
@@ -768,6 +771,7 @@ struct AgentConfigStatus {
config_exists: bool,
config_valid: bool,
configured: bool,
configuration_synchronized: bool,
current_model: Option<String>,
oauth_configuration: bool,
modification_enabled: bool,
@@ -951,6 +955,8 @@ struct AgentAppliedState {
version: u8,
client: String,
model: String,
#[serde(default)]
configuration_revision: u8,
#[serde(default, skip_serializing_if = "Option::is_none")]
claude_desktop_model_mappings: Option<ClaudeDesktopModelMappings>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
+73 -2
View File
@@ -22,6 +22,10 @@ fn claude_agent_config_preserves_existing_fields() {
assert_eq!(value["env"]["ANTHROPIC_BASE_URL"], "http://127.0.0.1:8317");
assert_eq!(value["env"]["ANTHROPIC_AUTH_TOKEN"], DEFAULT_API_KEY);
assert_eq!(value["env"]["ANTHROPIC_MODEL"], "claude-test");
assert_eq!(
value["env"][CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV],
"1"
);
assert_eq!(value["env"][CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV], "200000");
assert_eq!(value["env"][CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV], "90");
assert!(value["env"].get(DISABLE_AUTO_COMPACT_ENV).is_none());
@@ -44,6 +48,7 @@ fn claude_code_inspection_normalizes_legacy_1m_suffix() {
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8317",
"ANTHROPIC_AUTH_TOKEN": "test-key",
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "1",
"ANTHROPIC_MODEL": "deepseek-v4-pro[1m]",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-pro[1m]",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "deepseek-v4-pro[1m]",
@@ -71,6 +76,30 @@ fn claude_code_inspection_normalizes_legacy_1m_suffix() {
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn claude_code_requires_gateway_model_discovery_to_be_enabled() {
let directory = agent_test_home("claude-code-gateway-model-discovery");
let path = directory.join("settings.json");
fs::write(
&path,
r#"{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8317",
"ANTHROPIC_AUTH_TOKEN": "test-key",
"ANTHROPIC_MODEL": "gpt-test",
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "0"
}
}"#,
)
.unwrap();
let (configured, model) = inspect_claude_agent_config(&path, 8317, "test-key").unwrap();
assert!(!configured);
assert_eq!(model.as_deref(), Some("gpt-test"));
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn claude_mapping_legacy_json_defaults_1m_preferences_off() {
let mappings: ClaudeDesktopModelMappings =
@@ -762,7 +791,9 @@ fn kimi_code_config_preserves_existing_tables_and_uses_openai_provider() {
let home = agent_test_home("kimi-code-agent-config");
let path = home.join(".kimi-code/config.toml");
fs::create_dir_all(path.parent().unwrap()).unwrap();
let models = test_agent_models(&["gpt-test", "deepseek-test"]);
let mut models = test_agent_models(&["gpt-test", "deepseek-test"]);
models[0].context_window = Some(1_048_576);
models[1].context_window = Some(131_072);
let rendered = build_kimi_code_agent_config(
Some(
r#"theme = "dark"
@@ -806,6 +837,14 @@ model = "model"
.as_str(),
Some("tool_use")
);
assert_eq!(
value["models"]["cpa-gui/gpt-test"]["max_context_size"].as_integer(),
Some(1_048_576)
);
assert_eq!(
value["models"]["cpa-gui/deepseek-test"]["max_context_size"].as_integer(),
Some(131_072)
);
assert_eq!(
inspect_kimi_code_agent_config(&path, 8317, DEFAULT_API_KEY).unwrap(),
(true, Some("gpt-test".to_string()))
@@ -818,7 +857,9 @@ fn grok_build_config_preserves_existing_models_and_uses_chat_completions() {
let home = agent_test_home("grok-build-agent-config");
let path = home.join(".grok/config.toml");
fs::create_dir_all(path.parent().unwrap()).unwrap();
let models = test_agent_models(&["gpt-test", "deepseek-test"]);
let mut models = test_agent_models(&["gpt-test", "deepseek-test"]);
models[0].context_window = Some(1_048_576);
models[1].context_window = Some(131_072);
let rendered = build_grok_build_agent_config(
Some(
r#"theme = "dark"
@@ -858,6 +899,14 @@ api_key = "other-key"
value["model"]["cpa-gui/gpt-test"]["api_backend"].as_str(),
Some("chat_completions")
);
assert_eq!(
value["model"]["cpa-gui/gpt-test"]["context_window"].as_integer(),
Some(1_048_576)
);
assert_eq!(
value["model"]["cpa-gui/deepseek-test"]["context_window"].as_integer(),
Some(131_072)
);
assert_eq!(
inspect_grok_build_agent_config(&path, 8317, DEFAULT_API_KEY).unwrap(),
(true, Some("gpt-test".to_string()))
@@ -1077,6 +1126,28 @@ fn agent_model_list_parser_exposes_aliases_as_selectable_model_ids() {
);
}
#[test]
fn kimi_grok_and_zcode_use_cpa_runtime_context_windows() {
for client in [
AgentClient::KimiCode,
AgentClient::GrokBuild,
AgentClient::ZCode,
] {
assert!(agent_uses_cpa_runtime_context_windows(client));
}
for client in [
AgentClient::ClaudeCode,
AgentClient::ClaudeDesktop,
AgentClient::Codex,
AgentClient::OpenCode,
AgentClient::OpenClaw,
AgentClient::Hermes,
AgentClient::DeepSeekHarness,
] {
assert!(!agent_uses_cpa_runtime_context_windows(client));
}
}
#[test]
fn claude_model_lists_mark_yaml_aliases_when_api_returns_only_model_ids() {
let mut models = test_agent_models(&[
+43 -1
View File
@@ -453,6 +453,7 @@ fn zcode_legacy_single_file_state_upgrades_and_restores_cli_config() {
version: AGENT_APPLIED_STATE_VERSION,
client: AgentClient::ZCode.id().to_string(),
model: "gpt-one".to_string(),
configuration_revision: 0,
claude_desktop_model_mappings: None,
backup_files: vec![AgentAppliedBackupFile {
path: paths[0].clone(),
@@ -489,9 +490,37 @@ fn zcode_legacy_single_file_state_upgrades_and_restores_cli_config() {
fs::remove_dir_all(home).unwrap();
}
#[test]
fn kimi_and_grok_legacy_states_require_context_catalog_resynchronization() {
for client in [AgentClient::KimiCode, AgentClient::GrokBuild] {
let home = agent_test_home(&format!("{}-context-revision", client.id()));
let paths = agent_config_paths(client, &home);
let state_path = agent_state_path(&paths).unwrap();
fs::create_dir_all(state_path.parent().unwrap()).unwrap();
let mut state = AgentAppliedState {
version: AGENT_APPLIED_STATE_VERSION,
client: client.id().to_string(),
model: "gpt-test".to_string(),
configuration_revision: 0,
claude_desktop_model_mappings: None,
backup_files: Vec::new(),
updated_at_unix: 1,
};
write_agent_applied_state(&state_path, &state).unwrap();
assert!(!agent_configuration_is_synchronized(client, &home, true));
state.configuration_revision = AGENT_CONFIGURATION_REVISION;
write_agent_applied_state(&state_path, &state).unwrap();
assert!(agent_configuration_is_synchronized(client, &home, true));
assert!(!agent_configuration_is_synchronized(client, &home, false));
fs::remove_dir_all(home).unwrap();
}
}
#[test]
fn session_merge_preserves_runtime_fields_for_other_agent_formats() {
let claude_original = r#"{"env":{"KEEP_ENV":"original"},"keep":"claude"}"#;
let claude_original = r#"{"env":{"KEEP_ENV":"original","CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY":"0"},"keep":"claude"}"#;
let claude_managed = build_claude_agent_config(
Some(claude_original),
"http://127.0.0.1:8317",
@@ -516,6 +545,10 @@ fn session_merge_preserves_runtime_fields_for_other_agent_formats() {
assert_eq!(claude["runtimeAdded"], true);
assert_eq!(claude["env"]["RUNTIME_ENV"], "keep");
assert_eq!(claude["env"]["KEEP_ENV"], "original");
assert_eq!(
claude["env"][CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV],
"0"
);
assert!(claude["env"].get("ANTHROPIC_BASE_URL").is_none());
assert!(claude.get("model").is_none());
@@ -735,6 +768,7 @@ fn legacy_codex_single_file_state_remains_restorable() {
version: AGENT_APPLIED_STATE_VERSION,
client: AgentClient::Codex.id().to_string(),
model: "gpt-test".to_string(),
configuration_revision: AGENT_CONFIGURATION_REVISION,
claude_desktop_model_mappings: None,
backup_files: vec![AgentAppliedBackupFile {
path: path.clone(),
@@ -799,6 +833,7 @@ fn claude_desktop_version_three_state_can_be_closed_safely() {
version: 3,
client: AgentClient::ClaudeDesktop.id().to_string(),
model: "gpt-test".to_string(),
configuration_revision: 0,
claude_desktop_model_mappings: None,
backup_files: Vec::new(),
updated_at_unix: 1,
@@ -930,6 +965,7 @@ fn applied_state_rejects_backup_paths_outside_the_managed_config_set() {
version: AGENT_APPLIED_STATE_VERSION,
client: AgentClient::OpenCode.id().to_string(),
model: "gpt-test".to_string(),
configuration_revision: AGENT_CONFIGURATION_REVISION,
claude_desktop_model_mappings: None,
backup_files: vec![AgentAppliedBackupFile {
path: path.clone(),
@@ -966,6 +1002,7 @@ fn missing_session_backup_is_detected_before_any_file_is_restored() {
version: AGENT_APPLIED_STATE_VERSION,
client: AgentClient::Codex.id().to_string(),
model: "gpt-test".to_string(),
configuration_revision: AGENT_CONFIGURATION_REVISION,
claude_desktop_model_mappings: None,
backup_files: vec![
AgentAppliedBackupFile {
@@ -1003,6 +1040,7 @@ fn version_three_cleanup_covers_every_non_desktop_agent_format() {
version: 3,
client: client.id().to_string(),
model: "gpt-test".to_string(),
configuration_revision: 0,
claude_desktop_model_mappings: None,
backup_files: Vec::new(),
updated_at_unix: 1,
@@ -1017,6 +1055,7 @@ fn version_three_cleanup_covers_every_non_desktop_agent_format() {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8317",
"ANTHROPIC_API_KEY": "secret",
"ANTHROPIC_AUTH_TOKEN": "secret",
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "1",
"ANTHROPIC_MODEL": "gpt-test",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "gpt-test",
"KEEP_ENV": "yes"
@@ -1039,6 +1078,9 @@ fn version_three_cleanup_covers_every_non_desktop_agent_format() {
serde_json::from_str(&fs::read_to_string(&claude_path).unwrap()).unwrap();
assert!(claude.get("model").is_none());
assert!(claude["env"].get("ANTHROPIC_BASE_URL").is_none());
assert!(claude["env"]
.get(CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV)
.is_none());
assert_eq!(claude["env"]["KEEP_ENV"], "yes");
assert_eq!(claude["keep"], "claude");
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+33 -31
View File
@@ -32,6 +32,7 @@ import kimiIcon from '../assets/icons/kimi-light.svg';
import openclawIcon from '../assets/icons/openclaw.svg';
import opencodeIcon from '../assets/icons/opencode.svg';
import piIcon from '../assets/icons/pi-logo-on-light.svg';
import zcodeIcon from '../assets/icons/zcode.png';
import {
agentModelAlias,
filterAgentModels,
@@ -79,6 +80,7 @@ type AgentConfigStatus = {
pluginVersion: string | null;
configValid: boolean;
configured: boolean;
configurationSynchronized: boolean;
currentModel: string | null;
oauthConfiguration: boolean;
modificationEnabled: boolean;
@@ -205,12 +207,42 @@ const agentDefinitions: AgentDefinition[] = [
icon: codexIcon,
descriptionKey: 'agents.description.codex',
},
{
id: 'deepseek-harness',
name: 'DeepSeek Harness',
icon: deepseekIcon,
descriptionKey: 'agents.description.deepseekHarness',
},
{
id: 'opencode',
name: 'OpenCode',
icon: opencodeIcon,
descriptionKey: 'agents.description.opencode',
},
{
id: 'pi',
name: 'Pi',
icon: piIcon,
descriptionKey: 'agents.description.pi',
},
{
id: 'grok-build',
name: 'Grok Build',
icon: grokIcon,
descriptionKey: 'agents.description.grokBuild',
},
{
id: 'zcode',
name: 'ZCode',
icon: zcodeIcon,
descriptionKey: 'agents.description.zcode',
},
{
id: 'kimi-code',
name: 'Kimi Code',
icon: kimiIcon,
descriptionKey: 'agents.description.kimiCode',
},
{
id: 'openclaw',
name: 'OpenClaw',
@@ -223,36 +255,6 @@ const agentDefinitions: AgentDefinition[] = [
icon: hermesIcon,
descriptionKey: 'agents.description.hermes',
},
{
id: 'deepseek-harness',
name: 'DeepSeek Harness',
icon: deepseekIcon,
descriptionKey: 'agents.description.deepseekHarness',
},
{
id: 'zcode',
name: 'ZCode',
Icon: Bot,
descriptionKey: 'agents.description.zcode',
},
{
id: 'kimi-code',
name: 'Kimi Code',
icon: kimiIcon,
descriptionKey: 'agents.description.kimiCode',
},
{
id: 'grok-build',
name: 'Grok Build',
icon: grokIcon,
descriptionKey: 'agents.description.grokBuild',
},
{
id: 'pi',
name: 'Pi',
icon: piIcon,
descriptionKey: 'agents.description.pi',
},
];
const agentSubpages: AgentSubpageDefinition[] = [
@@ -885,7 +887,7 @@ export function AgentsPage() {
const configurationAction = resolveAgentConfigurationAction({
client: selected,
modificationState: activeStatus?.modificationState ?? 'unconfigured',
configurationSynchronized: Boolean(activeStatus?.configured),
configurationSynchronized: Boolean(activeStatus?.configurationSynchronized),
selectedModel,
appliedModel,
oauthConfiguration,
+4 -1
View File
@@ -93,7 +93,10 @@ export function resolveAgentConfigurationAction({
}: ResolveAgentConfigurationActionOptions): AgentConfigurationAction {
if (modificationState !== 'applied') return 'apply';
if (client === 'pi') return 'close';
if (client === 'zcode' && !configurationSynchronized) return 'update';
if (
(client === 'zcode' || client === 'kimi-code' || client === 'grok-build')
&& !configurationSynchronized
) return 'update';
const modelChanged = client === 'claude-code' || client === 'claude-desktop'
? !sameAgentModelMappings(modelMappings, appliedModelMappings)
+17
View File
@@ -175,6 +175,23 @@ describe('agent configuration update action', () => {
})).toBe('update');
});
test.each<AgentConfigurationClientId>(['kimi-code', 'grok-build'])(
'%s updates an applied configuration whose CPA context catalog is stale',
(client) => {
expect(resolveAgentConfigurationAction({
client,
modificationState: 'applied',
configurationSynchronized: false,
selectedModel: 'model-a',
appliedModel: 'model-a',
oauthConfiguration: false,
appliedOauthConfiguration: false,
modelMappings: { opus: '', sonnet: '', haiku: '' },
appliedModelMappings: { opus: '', sonnet: '', haiku: '' },
})).toBe('update');
},
);
test('Codex also updates when only OAuth configuration changes', () => {
expect(resolveAgentConfigurationAction({
client: 'codex',