Supports the latest version of zcode (but I hope you won't use it)

This commit is contained in:
lzt404 committed 2026-09-28 16:42:22 +08:00
1 parent 4b9bb22c87
commit 3cfee2451d
7 files changed
+458 -377

No files matched your search

+57
View File
@@ -476,6 +476,37 @@ fn clear_integration_needs_no_backup_and_removes_state_for_each_client() {
}
}
#[test]
fn zcode_provider_config_restores_an_existing_provider_selection() {
let home = Home::new();
let paths = config_paths("zcode", &home.0).unwrap();
let original = serde_json::json!({
"schemaVersion": 1,
"config": {
"providerConfigRules": {"providerRules": [{
"providerId": "other",
"providerName": "Other",
"config": {
"group": "standard-personal",
"access": {"type": "api-key", "apiKey": "other-secret"},
"api": {"type": "anthropic-messages", "baseUrl": "https://example.com/api"},
"personalModelIds": ["other-model"]
}
}]},
"modelConfigRules": {"providerModelRules": [], "manualProviderModelRules": []},
"defaultModelSelection": {"providerId": "other", "modelId": "other-model"}
}
});
save(&paths[0], render(&paths[0], &original).unwrap());
apply(&home.0, AgentClient::ZCode, "gpt-one").unwrap();
let applied = parse(&paths[0], text(read_agent_bytes(&paths[0]).unwrap().as_deref()).unwrap()).unwrap();
assert_eq!(applied["config"]["defaultModelSelection"]["providerId"], "cpa-gui");
assert_eq!(applied["config"]["providerConfigRules"]["providerRules"].as_array().unwrap().len(), 2);
clear_agent_managed_configuration(AgentClient::ZCode, &home.0, 8317).unwrap();
let restored = parse(&paths[0], text(read_agent_bytes(&paths[0]).unwrap().as_deref()).unwrap()).unwrap();
assert_eq!(restored, original);
}
#[test]
fn clear_integration_removes_claude_model_overrides_but_keeps_custom_settings() {
let home = Home::new();
@@ -1200,6 +1231,7 @@ fn nested_model_options_and_third_party_providers_survive_all_updates() {
let mut root = parse(path, text(raw.as_deref()).unwrap()).unwrap();
let container = match client {
AgentClient::Codex if path == &paths[0] => Some("/model_providers"),
AgentClient::ZCode if path == &paths[0] => None,
AgentClient::OpenCode | AgentClient::ZCode => Some("/provider"),
AgentClient::OpenClaw => Some("/models/providers"),
AgentClient::DeepSeekHarness if path == &paths[0] => Some("/llm-pi-ai/providers"),
@@ -1221,10 +1253,28 @@ fn nested_model_options_and_third_party_providers_survive_all_updates() {
serde_json::json!({"key":"keep-secret","nested":{"enabled":true}}),
));
}
if client == AgentClient::ZCode && path == &paths[0] {
let pointer = "/config/providerConfigRules/providerRules";
let third_party = serde_json::json!({
"providerId": "third-party",
"providerName": "Third Party",
"config": {
"group": "standard-personal",
"access": {"type": "api-key", "apiKey": "keep-secret"},
"api": {"type": "anthropic-messages", "baseUrl": "https://example.com/api"},
"personalModelIds": ["third-model"]
}
});
let rules = root.pointer_mut(pointer).unwrap().as_array_mut().unwrap();
let index = rules.len();
rules.push(third_party.clone());
touched.push((path.clone(), format!("{pointer}/{index}"), third_party));
}
let inventory = match client {
AgentClient::ClaudeDesktop if path == &paths[2] => {
Some("/inferenceModels/0".to_string())
}
AgentClient::ZCode if path == &paths[0] => None,
AgentClient::OpenCode | AgentClient::ZCode => {
Some("/provider/cpa-gui/models/gpt-one".into())
}
@@ -1246,6 +1296,13 @@ fn nested_model_options_and_third_party_providers_survive_all_updates() {
.insert("extensions".into(), extensions.clone());
touched.push((path.clone(), format!("{pointer}/extensions"), extensions));
}
if client == AgentClient::ZCode && path == &paths[0] {
let pointer = "/config/modelConfigRules/providerModelRules/0/config/properties/supportsToolCall";
root.pointer_mut("/config/modelConfigRules/providerModelRules/0/config/properties")
.unwrap().as_object_mut().unwrap()
.insert("supportsToolCall".into(), serde_json::json!(true));
touched.push((path.clone(), pointer.into(), serde_json::json!(true)));
}
save(path, render(path, &root).unwrap());
}
let catalog = catalog("gpt-one");
+274 -210
View File
@@ -196,29 +196,11 @@ pub(crate) fn build_agent_updates_with_oauth(
}])
}
AgentClient::ZCode => {
let app_before = read_optional_text(&paths[0])?;
let cli_before = read_optional_text(&paths[1])?;
let app_after =
build_zcode_agent_config(app_before.as_deref(), &root_base, api_key, model, models)
?;
let cli_after = build_zcode_cli_agent_config(
cli_before.as_deref(),
&root_base,
api_key,
model,
models,
)
?;
Ok(vec![
AgentFileUpdate {
path: paths[0].clone(),
after: app_after,
},
AgentFileUpdate {
path: paths[1].clone(),
after: cli_after,
},
])
let before = read_optional_text(&paths[0])?;
let after = build_zcode_agent_config(
before.as_deref(), &root_base, api_key, model, models,
)?;
Ok(vec![AgentFileUpdate { path: paths[0].clone(), after }])
}
AgentClient::KimiCode => {
let before = read_optional_text(&paths[0])?;
@@ -1273,59 +1255,47 @@ pub(crate) fn prepare_opencode_managed_removal(paths: &[PathBuf]) -> Result<Imag
}
pub(crate) fn prepare_zcode_managed_removal(paths: &[PathBuf]) -> Result<Images, String> {
if paths.is_empty() {
return Err("ZCode configuration path is unavailable on the current platform".to_string());
}
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
let mut changed_paths = Vec::new();
for path in paths {
let updated = prepare_agent_json_removal(path, "ZCode configuration", |root| {
let mut changed = false;
let remove_model = if root
.get("model")
.and_then(serde_json::Value::as_str)
.is_some_and(|model| model.starts_with(&prefix))
{
true
} else if let Some(model) = root
.get_mut("model")
.and_then(serde_json::Value::as_object_mut)
{
if model
.get("main")
.and_then(serde_json::Value::as_str)
.is_some_and(|model| model.starts_with(&prefix))
{
model.remove("main");
changed = true;
}
model.is_empty()
} else {
false
};
if remove_model {
root.remove("model");
changed = true;
}
let providers_empty = if let Some(providers) = root
.get_mut("provider")
.and_then(serde_json::Value::as_object_mut)
{
changed |= providers.remove(MANAGED_AGENT_PROVIDER_ID).is_some();
providers.is_empty()
} else {
false
};
if providers_empty {
root.remove("provider");
}
changed
})?;
if let Some(update) = updated {
changed_paths.push(update);
let path = paths.first().ok_or("ZCode provider configuration path is unavailable")?;
let updated = prepare_agent_json_removal(path, "ZCode provider configuration", |root| {
let Some(config) = root.get_mut("config").and_then(serde_json::Value::as_object_mut) else {
return false;
};
let mut changed = false;
if config.get("defaultModelSelection")
.and_then(|selection| selection.get("providerId"))
.and_then(serde_json::Value::as_str) == Some(MANAGED_AGENT_PROVIDER_ID)
{
config.remove("defaultModelSelection");
changed = true;
}
}
Ok(changed_paths)
if let Some(order) = config.get_mut("providerOrder").and_then(serde_json::Value::as_array_mut) {
let old_len = order.len();
order.retain(|item| item.as_str() != Some(MANAGED_AGENT_PROVIDER_ID));
changed |= order.len() != old_len;
}
if let Some(rules) = config.get_mut("providerConfigRules")
.and_then(|value| value.get_mut("providerRules"))
.and_then(serde_json::Value::as_array_mut)
{
let old_len = rules.len();
rules.retain(|rule| rule.get("providerId").and_then(serde_json::Value::as_str)
!= Some(MANAGED_AGENT_PROVIDER_ID));
changed |= rules.len() != old_len;
}
for key in ["providerModelRules", "manualProviderModelRules"] {
if let Some(rules) = config.get_mut("modelConfigRules")
.and_then(|value| value.get_mut(key))
.and_then(serde_json::Value::as_array_mut)
{
let old_len = rules.len();
rules.retain(|rule| rule.get("providerId").and_then(serde_json::Value::as_str)
!= Some(MANAGED_AGENT_PROVIDER_ID));
changed |= rules.len() != old_len;
}
}
changed
})?;
Ok(updated.into_iter().collect())
}
pub(crate) fn prepare_kimi_code_managed_removal(paths: &[PathBuf]) -> Result<Images, String> {
@@ -1972,84 +1942,96 @@ pub(crate) fn build_restored_opencode_config(
}))
}
pub(crate) fn build_restored_zcode_config(
pub(crate) fn build_restored_zcode_provider_config(
current: &str,
original: Option<&str>,
) -> Result<Option<String>, String> {
let mut root = parse_agent_json_object(Some(current), "Current ZCode configuration")?;
let original_root = parse_restored_json_object(original, "Original ZCode configuration")?;
restore_json_key(&mut root, original_root.as_ref(), "model");
let original_provider = original_root
.as_ref()
.and_then(|root| root.get("provider"))
.and_then(serde_json::Value::as_object);
let original_managed = original_provider
.and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID))
.and_then(serde_json::Value::as_object);
if root
.get("provider")
.is_some_and(serde_json::Value::is_object)
{
let providers = root
.get_mut("provider")
.and_then(serde_json::Value::as_object_mut)
.expect("ZCode provider was checked as an object");
if providers
.get(MANAGED_AGENT_PROVIDER_ID)
.is_some_and(serde_json::Value::is_object)
{
let managed = providers
.get_mut(MANAGED_AGENT_PROVIDER_ID)
.and_then(serde_json::Value::as_object_mut)
.expect("ZCode managed provider was checked as an object");
for key in [
"enabled",
"name",
"source",
"kind",
"defaultKind",
"apiFormat",
"npm",
"models",
] {
restore_json_key(managed, original_managed, key);
}
let original_options = original_managed
.and_then(|managed| managed.get("options"))
.and_then(serde_json::Value::as_object);
if managed
.get("options")
.is_some_and(serde_json::Value::is_object)
{
let options = managed
.get_mut("options")
.and_then(serde_json::Value::as_object_mut)
.expect("ZCode options were checked as an object");
for key in ["baseURL", "apiKey"] {
restore_json_key(options, original_options, key);
}
if options.is_empty() && original_options.is_none() {
managed.remove("options");
}
} else {
restore_json_key(managed, original_managed, "options");
}
if managed.is_empty() && original_managed.is_none() {
providers.remove(MANAGED_AGENT_PROVIDER_ID);
}
} else if let Some(original_managed) = original_provider
.and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID))
.cloned()
{
providers.insert(MANAGED_AGENT_PROVIDER_ID.to_string(), original_managed);
}
if providers.is_empty() && original_provider.is_none() {
root.remove("provider");
}
} else {
restore_json_key(&mut root, original_root.as_ref(), "provider");
let mut root = serde_json::Value::Object(parse_agent_json_object(
Some(current), "Current ZCode provider configuration",
)?);
let original_root = parse_restored_json_object(original, "Original ZCode provider configuration")?
.map(serde_json::Value::Object);
if root.pointer("/config").is_none() {
return render_restored_json(
root.as_object().cloned().ok_or("Invalid ZCode provider configuration")?,
original.is_some(),
"Restored ZCode provider configuration",
);
}
render_restored_json(root, original.is_some(), "Restored ZCode configuration")
let config = root.pointer_mut("/config").and_then(serde_json::Value::as_object_mut)
.ok_or("ZCode provider configuration has invalid config")?;
let original_config = original_root.as_ref().and_then(|value| value.pointer("/config"));
if original.is_none() || config.get("defaultModelSelection")
.and_then(|selection| selection.get("providerId"))
.and_then(serde_json::Value::as_str) == Some(MANAGED_AGENT_PROVIDER_ID)
{
if let Some(selection) = original_config.and_then(|value| value.get("defaultModelSelection")) {
config.insert("defaultModelSelection".to_string(), selection.clone());
} else {
config.remove("defaultModelSelection");
}
}
if let Some(order) = config.get_mut("providerOrder").and_then(serde_json::Value::as_array_mut) {
order.retain(|item| item.as_str() != Some(MANAGED_AGENT_PROVIDER_ID));
if let Some(original_order) = original_config
.and_then(|value| value.get("providerOrder"))
.and_then(serde_json::Value::as_array)
{
if let Some(index) = original_order.iter().position(|item| item.as_str() == Some(MANAGED_AGENT_PROVIDER_ID)) {
order.insert(index.min(order.len()), serde_json::json!(MANAGED_AGENT_PROVIDER_ID));
}
}
}
for pointer in [
"/providerConfigRules/providerRules",
"/modelConfigRules/providerModelRules",
"/modelConfigRules/manualProviderModelRules",
] {
let Some(rules) = root.pointer(&format!("/config{pointer}"))
.and_then(serde_json::Value::as_array).cloned() else {
continue;
};
let mut retained = rules.into_iter().filter(|rule| {
rule.get("providerId").and_then(serde_json::Value::as_str)
!= Some(MANAGED_AGENT_PROVIDER_ID)
}).collect::<Vec<_>>();
if let Some(original_rules) = original_config.and_then(|value| value.pointer(pointer))
.and_then(serde_json::Value::as_array)
{
for (index, rule) in original_rules.iter().enumerate() {
if rule.get("providerId").and_then(serde_json::Value::as_str)
== Some(MANAGED_AGENT_PROVIDER_ID)
{
retained.insert(index.min(retained.len()), rule.clone());
}
}
}
if let Some(target) = root.pointer_mut(&format!("/config{pointer}")) {
*target = serde_json::Value::Array(retained);
}
}
if original.is_none() {
let config = root.get("config");
let only_empty_scaffold = config
.and_then(serde_json::Value::as_object)
.is_some_and(|config| {
config.get("defaultModelSelection").is_none()
&& config.get("providerOrder").and_then(serde_json::Value::as_array)
.is_none_or(Vec::is_empty)
&& root.pointer("/config/providerConfigRules/providerRules")
.and_then(serde_json::Value::as_array).is_some_and(Vec::is_empty)
&& root.pointer("/config/modelConfigRules/providerModelRules")
.and_then(serde_json::Value::as_array).is_some_and(Vec::is_empty)
&& root.pointer("/config/modelConfigRules/manualProviderModelRules")
.and_then(serde_json::Value::as_array).is_some_and(Vec::is_empty)
&& config.len() <= 3
});
if only_empty_scaffold && root.as_object().is_some_and(|root| root.len() <= 2) {
return Ok(None);
}
}
render_agent_json(root.as_object().cloned().ok_or("Invalid ZCode provider configuration")?,
"Restored ZCode provider configuration").map(Some)
}
pub(crate) fn build_restored_kimi_code_config(
@@ -2677,7 +2659,7 @@ pub(crate) fn build_agent_session_restored_bytes_with_preference(
_ => return Err("Invalid DeepSeek Harness restoration path index".to_string()),
}
}
AgentClient::ZCode => build_restored_zcode_config(current, original)?,
AgentClient::ZCode => build_restored_zcode_provider_config(current, original)?,
AgentClient::WorkBuddy => build_restored_workbuddy_config(current, original)?,
AgentClient::AntigravityCli => restore_antigravity_config(client, path, current, original)?,
AgentClient::KimiCode => build_restored_kimi_code_config(current, original)?,
@@ -3077,67 +3059,149 @@ pub(crate) fn build_zcode_agent_config(
model: &str,
available_models: &[AgentModelOption],
) -> Result<String, String> {
build_zcode_config(existing, base_url, api_key, model, available_models, false)
}
pub(crate) fn build_zcode_cli_agent_config(
existing: Option<&str>,
base_url: &str,
api_key: &str,
model: &str,
available_models: &[AgentModelOption],
) -> Result<String, String> {
build_zcode_config(existing, base_url, api_key, model, available_models, true)
}
fn build_zcode_config(
existing: Option<&str>,
base_url: &str,
api_key: &str,
model: &str,
available_models: &[AgentModelOption],
cli_config: bool,
) -> Result<String, String> {
let mut root = parse_agent_json_object(existing, "ZCode config.json")?;
let providers = ensure_json_object_entry(&mut root, "provider");
let models = ordered_agent_models(available_models, model)
.into_iter()
.map(|model| {
let display_name = model.alias.as_deref().unwrap_or(&model.name).to_string();
let mut entry = serde_json::Map::new();
entry.insert("name".to_string(), serde_json::json!(display_name));
if let Some(context_window) = model.context_window {
entry.insert(
"limit".to_string(),
serde_json::json!({ "context": context_window }),
);
}
(model.name, serde_json::Value::Object(entry))
})
.collect::<serde_json::Map<_, _>>();
let managed_provider = ensure_json_object_entry(providers, MANAGED_AGENT_PROVIDER_ID);
managed_provider.remove("npm");
managed_provider.insert("enabled".to_string(), serde_json::json!(true));
managed_provider.insert("name".to_string(), serde_json::json!("EasyCLIProxyAPI"));
managed_provider.insert("source".to_string(), serde_json::json!("custom"));
managed_provider.insert("kind".to_string(), serde_json::json!("anthropic"));
managed_provider.insert("defaultKind".to_string(), serde_json::json!("anthropic"));
managed_provider.insert(
"apiFormat".to_string(),
serde_json::json!("anthropic-messages"),
);
let options = ensure_json_object_entry(managed_provider, "options");
options.insert("baseURL".to_string(), serde_json::json!(base_url));
options.insert("apiKey".to_string(), serde_json::json!(api_key));
managed_provider.insert("models".to_string(), serde_json::Value::Object(models));
let model = format!("{MANAGED_AGENT_PROVIDER_ID}/{model}");
if cli_config {
ensure_json_object_entry(&mut root, "model")
.insert("main".to_string(), serde_json::json!(model));
} else {
root.insert("model".to_string(), serde_json::json!(model));
let mut root = parse_agent_json_object(existing, "ZCode provider_config.json")?;
if root.get("schemaVersion").is_some_and(|version| version != 1) {
return Err("Unsupported ZCode provider configuration schemaVersion".to_string());
}
render_agent_json(root, "ZCode configuration")
root.insert("schemaVersion".to_string(), serde_json::json!(1));
let config = ensure_json_object_entry(&mut root, "config");
let provider_rules = ensure_json_object_entry(config, "providerConfigRules");
let rules = provider_rules
.entry("providerRules")
.or_insert_with(|| serde_json::json!([]))
.as_array_mut()
.ok_or("ZCode providerRules must be an array")?;
let managed_index = rules.iter().position(|rule| {
rule.get("providerId").and_then(serde_json::Value::as_str)
== Some(MANAGED_AGENT_PROVIDER_ID)
});
let previous = managed_index.and_then(|index| rules.get(index));
let mut managed = previous.cloned().unwrap_or_else(|| serde_json::json!({}));
let managed_obj = managed.as_object_mut().ok_or("ZCode managed provider must be an object")?;
managed_obj.insert("providerId".to_string(), serde_json::json!(MANAGED_AGENT_PROVIDER_ID));
managed_obj.insert("providerName".to_string(), serde_json::json!("EasyCLIProxyAPI"));
managed_obj.insert("enabled".to_string(), serde_json::json!(true));
let provider_config = ensure_json_object_entry(managed_obj, "config");
provider_config.insert("group".to_string(), serde_json::json!("standard-personal"));
let access = ensure_json_object_entry(provider_config, "access");
access.insert("type".to_string(), serde_json::json!("api-key"));
access.insert("apiKey".to_string(), serde_json::json!(api_key));
let api = ensure_json_object_entry(provider_config, "api");
api.insert("type".to_string(), serde_json::json!("anthropic-messages"));
api.insert("baseUrl".to_string(), serde_json::json!(base_url));
let ordered = ordered_agent_models(available_models, model);
let model_names = ordered.iter().map(|option| option.name.as_str()).collect::<Vec<_>>();
provider_config.insert("personalModelIds".to_string(), serde_json::json!(model_names));
provider_config.insert("modelOrder".to_string(), serde_json::json!(model_names));
rules.retain(|rule| {
rule.get("providerId").and_then(serde_json::Value::as_str)
!= Some(MANAGED_AGENT_PROVIDER_ID)
});
rules.insert(managed_index.unwrap_or(rules.len()).min(rules.len()), managed);
let model_rules = ensure_json_object_entry(config, "modelConfigRules");
let manual_rules = model_rules
.entry("manualProviderModelRules")
.or_insert_with(|| serde_json::json!([]))
.as_array()
.ok_or("ZCode manualProviderModelRules must be an array")?;
let existing_manual_rules = manual_rules.iter().filter_map(|rule| {
if rule.get("providerId").and_then(serde_json::Value::as_str)
!= Some(MANAGED_AGENT_PROVIDER_ID) {
return None;
}
Some((rule.get("modelId")?.as_str()?.to_string(), rule.clone()))
}).collect::<std::collections::HashMap<_, _>>();
let first_manual_index = manual_rules.iter().position(|rule| {
rule.get("providerId").and_then(serde_json::Value::as_str)
== Some(MANAGED_AGENT_PROVIDER_ID)
}).unwrap_or(manual_rules.len());
let rules = model_rules
.entry("providerModelRules")
.or_insert_with(|| serde_json::json!([]))
.as_array_mut()
.ok_or("ZCode providerModelRules must be an array")?;
let existing_rules = rules.iter().filter_map(|rule| {
if rule.get("providerId").and_then(serde_json::Value::as_str)
!= Some(MANAGED_AGENT_PROVIDER_ID) {
return None;
}
Some((rule.get("modelId")?.as_str()?.to_string(), rule.clone()))
}).collect::<std::collections::HashMap<_, _>>();
let first_managed_index = rules.iter().position(|rule| {
rule.get("providerId").and_then(serde_json::Value::as_str)
== Some(MANAGED_AGENT_PROVIDER_ID)
}).unwrap_or(rules.len());
rules.retain(|rule| {
rule.get("providerId").and_then(serde_json::Value::as_str)
!= Some(MANAGED_AGENT_PROVIDER_ID)
});
let mut manual_updates = Vec::new();
for option in ordered {
let manual = existing_manual_rules.get(&option.name);
if option.context_window.is_none()
&& manual.is_none()
&& !existing_rules.contains_key(&option.name)
{
continue;
}
let mut rule = manual.or_else(|| existing_rules.get(&option.name))
.cloned().unwrap_or_else(|| serde_json::json!({}));
let entry = rule.as_object_mut().ok_or("ZCode model rule must be an object")?;
entry.insert("providerId".to_string(), serde_json::json!(MANAGED_AGENT_PROVIDER_ID));
entry.insert("modelId".to_string(), serde_json::json!(option.name));
let properties = ensure_json_object_entry(ensure_json_object_entry(entry, "config"), "properties");
if let Some(context_window) = option.context_window {
properties.insert("contextWindow".to_string(), serde_json::json!(context_window));
} else {
properties.remove("contextWindow");
}
if properties.is_empty() {
entry.get_mut("config").and_then(serde_json::Value::as_object_mut)
.expect("ZCode model config was created").remove("properties");
}
if manual.is_some() {
manual_updates.push(rule);
} else {
rules.insert((first_managed_index + rules.iter().filter(|entry| {
entry.get("providerId").and_then(serde_json::Value::as_str)
== Some(MANAGED_AGENT_PROVIDER_ID)
}).count()).min(rules.len()), rule);
}
}
let manual_rules = model_rules.get_mut("manualProviderModelRules")
.and_then(serde_json::Value::as_array_mut)
.expect("ZCode manual model rules were checked as an array");
manual_rules.retain(|rule| {
rule.get("providerId").and_then(serde_json::Value::as_str)
!= Some(MANAGED_AGENT_PROVIDER_ID)
});
for (offset, rule) in manual_updates.into_iter().enumerate() {
manual_rules.insert((first_manual_index + offset).min(manual_rules.len()), rule);
}
if let Some(provider_order) = config.get_mut("providerOrder") {
let provider_order = provider_order.as_array_mut()
.ok_or("ZCode providerOrder must be an array")?;
if !provider_order.iter().any(|value| value.as_str() == Some(MANAGED_AGENT_PROVIDER_ID)) {
provider_order.push(serde_json::json!(MANAGED_AGENT_PROVIDER_ID));
}
}
let mut selection = serde_json::json!({
"providerId": MANAGED_AGENT_PROVIDER_ID,
"modelId": model,
});
if let Some(previous) = config.get("defaultModelSelection") {
if previous.get("providerId").and_then(serde_json::Value::as_str)
== Some(MANAGED_AGENT_PROVIDER_ID)
&& previous.get("modelId").and_then(serde_json::Value::as_str) == Some(model)
{
if let Some(options) = previous.get("options") {
selection["options"] = options.clone();
}
}
}
config.insert("defaultModelSelection".to_string(), selection);
render_agent_json(root, "ZCode provider configuration")
}
pub(crate) fn ensure_toml_child_table<'a>(
+61 -82
View File
@@ -49,10 +49,7 @@ pub(crate) fn agent_config_paths(client: AgentClient, home: &Path) -> Vec<PathBu
}
AgentClient::WorkBuddy => vec![workbuddy_home(home).join("models.json")],
AgentClient::AntigravityCli => antigravity_config_paths(client, home),
AgentClient::ZCode => vec![
home.join(".zcode/v2").join(ZCODE_CONFIG_FILE),
home.join(".zcode/cli").join(ZCODE_CONFIG_FILE),
],
AgentClient::ZCode => vec![home.join(".zcode/v2").join(ZCODE_PROVIDER_CONFIG_FILE)],
AgentClient::KimiCode => vec![kimi_code_home(home).join(KIMI_CODE_CONFIG_FILE)],
AgentClient::GrokBuild => vec![grok_build_home(home).join(GROK_BUILD_CONFIG_FILE)],
}
@@ -1301,17 +1298,11 @@ pub(crate) fn inspect_agent_managed_config(
AgentClient::DeepSeekHarness => inspect_deepseek_harness_config(paths, port, api_key)
.map(|(configured, model)| (configured, model, false)),
AgentClient::ZCode => {
if paths.len() != 2 {
if paths.len() != 1 {
return Err("Invalid number of ZCode configuration paths".to_string());
}
let (app_configured, app_model) = inspect_zcode_agent_config(&paths[0], port, api_key)?;
let (cli_configured, cli_model) = inspect_zcode_agent_config(&paths[1], port, api_key)?;
let models_match = app_model.is_some() && app_model == cli_model;
Ok((
app_configured && cli_configured && models_match,
cli_model.or(app_model),
false,
))
inspect_zcode_agent_config(&paths[0], port, api_key)
.map(|(configured, model)| (configured, model, false))
}
AgentClient::AntigravityCli => inspect_antigravity_config(client, paths, port, api_key).map(|(configured, model)| (configured, model, false)),
AgentClient::WorkBuddy => inspect_workbuddy_agent_config(&paths[0], port, api_key)
@@ -1525,29 +1516,19 @@ pub(crate) fn agent_has_managed_marker(
AgentClient::WorkBuddy => workbuddy_has_managed_marker(&paths[0]),
AgentClient::AntigravityCli => antigravity_has_marker(client, paths),
AgentClient::ZCode => {
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
for path in paths {
if !path.is_file() {
continue;
}
let root = read_agent_json_or_empty(path, "ZCode configuration")?;
let provider_exists = root
.get("provider")
.and_then(|value| value.get(MANAGED_AGENT_PROVIDER_ID))
.is_some();
let model_selected = root
.get("model")
.and_then(|model| {
model
.as_str()
.or_else(|| model.get("main").and_then(serde_json::Value::as_str))
})
.is_some_and(|model| model.starts_with(&prefix));
if provider_exists && model_selected {
return Ok(true);
}
}
Ok(false)
let Some(path) = paths.first() else { return Ok(false); };
if !path.is_file() { return Ok(false); }
let root = read_agent_json_or_empty(path, "ZCode provider configuration")?;
let provider_exists = root.pointer("/config/providerConfigRules/providerRules")
.and_then(serde_json::Value::as_array)
.is_some_and(|rules| rules.iter().any(|rule| {
rule.get("providerId").and_then(serde_json::Value::as_str)
== Some(MANAGED_AGENT_PROVIDER_ID)
}));
let model_selected = root.pointer("/config/defaultModelSelection/providerId")
.and_then(serde_json::Value::as_str)
== Some(MANAGED_AGENT_PROVIDER_ID);
Ok(provider_exists && model_selected)
}
AgentClient::KimiCode => inspect_managed_toml_model_marker(
&paths[0],
@@ -3300,59 +3281,57 @@ pub(crate) fn inspect_zcode_agent_config(
return Ok((false, None));
}
let root: serde_json::Value = serde_json::from_str(
&fs::read_to_string(path).map_err(|error| format!("Failed to read ZCode configuration: {error}"))?,
&fs::read_to_string(path).map_err(|error| format!("Failed to read ZCode provider configuration: {error}"))?,
)
.map_err(|error| format!("Failed to parse ZCode configuration: {error}"))?;
let provider = root
.get("provider")
.and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID));
let expected_base = managed_core_loopback_origin(port);
let api_format_matches = provider
.and_then(|provider| provider.get("apiFormat"))
.and_then(serde_json::Value::as_str)
.is_none_or(|api_format| api_format == "anthropic-messages");
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
let model = root
.get("model")
.and_then(|model| {
model
.as_str()
.or_else(|| model.get("main").and_then(serde_json::Value::as_str))
.map_err(|error| format!("Failed to parse ZCode provider configuration: {error}"))?;
if root.get("schemaVersion").and_then(serde_json::Value::as_u64) != Some(1) {
return Ok((false, None));
}
let config = root.get("config");
let provider = config
.and_then(|config| config.get("providerConfigRules"))
.and_then(|rules| rules.get("providerRules"))
.and_then(serde_json::Value::as_array)
.and_then(|rules| rules.iter().find(|rule| {
rule.get("providerId").and_then(serde_json::Value::as_str)
== Some(MANAGED_AGENT_PROVIDER_ID)
}));
let selected = config
.and_then(|config| config.get("defaultModelSelection"));
let model = selected
.filter(|selection| {
selection.get("providerId").and_then(serde_json::Value::as_str)
== Some(MANAGED_AGENT_PROVIDER_ID)
})
.map(str::trim)
.filter(|value| !value.is_empty())
.and_then(|value| value.strip_prefix(&prefix))
.and_then(|selection| selection.get("modelId"))
.and_then(serde_json::Value::as_str)
.map(str::to_string);
let selected_model_exists = model.as_deref().is_some_and(|selected_model| {
let configured = model.as_deref().is_some_and(|model| {
provider
.and_then(|provider| provider.get("models"))
.and_then(serde_json::Value::as_object)
.is_some_and(|models| {
models
.keys()
.any(|model| model.eq_ignore_ascii_case(selected_model))
})
});
let configured = selected_model_exists
&& api_format_matches
.and_then(|provider| provider.get("config"))
.and_then(|config| config.get("personalModelIds"))
.and_then(serde_json::Value::as_array)
.is_some_and(|models| models.iter().any(|value| value.as_str() == Some(model)))
}) && provider
.and_then(|provider| provider.get("enabled"))
.and_then(serde_json::Value::as_bool) == Some(true)
&& provider
.and_then(|provider| provider.get("enabled"))
.and_then(serde_json::Value::as_bool)
== Some(true)
.and_then(|provider| provider.get("config"))
.and_then(|config| config.get("group"))
.and_then(serde_json::Value::as_str) == Some("standard-personal")
&& provider
.and_then(|provider| provider.get("kind"))
.and_then(|provider| provider.pointer("/config/access/type"))
.and_then(serde_json::Value::as_str) == Some("api-key")
&& provider
.and_then(|provider| provider.pointer("/config/access/apiKey"))
.and_then(serde_json::Value::as_str) == Some(api_key)
&& provider
.and_then(|provider| provider.pointer("/config/api/type"))
.and_then(serde_json::Value::as_str) == Some("anthropic-messages")
&& provider
.and_then(|provider| provider.pointer("/config/api/baseUrl"))
.and_then(serde_json::Value::as_str)
== Some("anthropic")
&& provider
.and_then(|provider| provider.get("options"))
.and_then(|options| options.get("baseURL"))
.and_then(serde_json::Value::as_str)
== Some(expected_base.as_str())
&& provider
.and_then(|provider| provider.get("options"))
.and_then(|options| options.get("apiKey"))
.and_then(serde_json::Value::as_str)
== Some(api_key);
== Some(managed_core_loopback_origin(port).as_str());
Ok((configured, model))
}
+3 -4
View File
@@ -731,10 +731,9 @@ pub(crate) fn fresh_agent_contents_with_oauth(
AgentClient::WorkBuddy => Ok(vec![build_workbuddy_agent_config(
None, &openai_base, api_key, model, models,
)?]),
AgentClient::ZCode => Ok(vec![
build_zcode_agent_config(None, &root_base, api_key, model, models)?,
build_zcode_cli_agent_config(None, &root_base, api_key, model, models)?,
]),
AgentClient::ZCode => Ok(vec![build_zcode_agent_config(
None, &root_base, api_key, model, models,
)?]),
AgentClient::KimiCode => Ok(vec![build_kimi_code_agent_config(
None,
&openai_base,
+2 -5
View File
@@ -675,7 +675,7 @@ fn preserve_model_extensions(client: &str, path: &Path, before: &Value, after: &
let mut extensions = before.clone();
let owned: &[&str] = match client {
"claude-desktop" => &["name", "labelOverride", "anthropicFamilyTier", "isFamilyDefault", "contextWindow", "supports1m", "prefer1m"],
"opencode" | "zcode" => &["name"],
"opencode" => &["name"],
"openclaw" => &["id", "name", "alias"],
"deepseek-harness" => &["id", "name", "contextWindow", "input", "maxTokens", "reasoningEfforts", "compat"],
"kimi-code" => &[
@@ -703,9 +703,6 @@ fn preserve_model_extensions(client: &str, path: &Path, before: &Value, after: &
object.remove(*key);
}
}
if client == "zcode" {
set(&mut extensions, &["limit".into(), "context".into()], None);
}
fill_missing(&extensions, after);
}
fn inventory(client: &str, before: &Value, after: &mut Value) {
@@ -735,7 +732,7 @@ fn preserve_model_extensions(client: &str, path: &Path, before: &Value, after: &
&["/models"]
}
"claude-desktop" => &["/inferenceModels"],
"opencode" | "zcode" => &["/provider/cpa-gui/models"],
"opencode" => &["/provider/cpa-gui/models"],
"openclaw" => &[
"/models/providers/cpa-gui/models",
"/agents/defaults/models",
+1 -1
View File
@@ -162,7 +162,7 @@ const DEFAULT_ERROR_LOGS_MAX_FILES: u32 = 10;
const DEFAULT_REDIS_USAGE_QUEUE_RETENTION_SECONDS: u32 = 60;
const LEGACY_DEFAULT_MANAGEMENT_SECRET_KEY: &str = "123456";
const MANAGED_AGENT_PROVIDER_ID: &str = "cpa-gui";
const ZCODE_CONFIG_FILE: &str = "config.json";
const ZCODE_PROVIDER_CONFIG_FILE: &str = "provider_config.json";
const KIMI_CODE_CONFIG_FILE: &str = "config.toml";
const GROK_BUILD_CONFIG_FILE: &str = "config.toml";
const DEEPSEEK_HARNESS_PROVIDER_ID: &str = "easy-cliproxyapi";
+60 -75
View File
@@ -967,14 +967,14 @@ fn opencode_agent_config_accepts_jsonc_and_preserves_comments() {
#[test]
fn zcode_agent_config_preserves_other_providers_and_uses_anthropic_messages() {
let home = agent_test_home("zcode-agent-config");
let path = home.join(".zcode/v2/config.json");
let path = home.join(".zcode/v2/provider_config.json");
fs::create_dir_all(path.parent().unwrap()).unwrap();
let mut models = test_agent_models(&["gpt-test", "deepseek-test"]);
models[0].context_window = Some(372_000);
models[1].context_window = Some(272_000);
let rendered = build_zcode_agent_config(
Some(
r#"{"locale":"zh-CN","provider":{"other":{"kind":"openai"},"cpa-gui":{"custom":"keep","npm":"@ai-sdk/anthropic","options":{"timeout":30}}}}"#,
r#"{"schemaVersion":1,"config":{"providerConfigRules":{"providerRules":[{"providerId":"other","providerName":"Other","config":{"group":"standard-personal","personalModelIds":["other-model"]}},{"providerId":"cpa-gui","config":{"access":{"type":"api-key","apiKeyManagementUrl":"https://example.com/keys"}}}]},"modelConfigRules":{"providerModelRules":[{"providerId":"other","modelId":"other-model","config":{"enabled":true}},{"providerId":"cpa-gui","modelId":"gpt-test","config":{"properties":{"supportsToolCall":true}}}],"manualProviderModelRules":[]},"providerOrder":["other","cpa-gui"]}}"#,
),
"http://127.0.0.1:8317",
DEFAULT_API_KEY,
@@ -985,68 +985,47 @@ fn zcode_agent_config_preserves_other_providers_and_uses_anthropic_messages() {
fs::write(&path, &rendered).unwrap();
let value: serde_json::Value = serde_json::from_str(&rendered).unwrap();
assert_eq!(value["locale"], "zh-CN");
assert_eq!(value["provider"]["other"]["kind"], "openai");
assert_eq!(
value["provider"][MANAGED_AGENT_PROVIDER_ID]["custom"],
"keep"
);
assert_eq!(
value["provider"][MANAGED_AGENT_PROVIDER_ID]["options"]["timeout"],
30
);
assert_eq!(
value["provider"][MANAGED_AGENT_PROVIDER_ID]["kind"],
"anthropic"
);
assert_eq!(
value["provider"][MANAGED_AGENT_PROVIDER_ID]["apiFormat"],
"anthropic-messages"
);
assert!(value["provider"][MANAGED_AGENT_PROVIDER_ID]
.get("npm")
.is_none());
assert_eq!(
value["provider"][MANAGED_AGENT_PROVIDER_ID]["options"]["baseURL"],
"http://127.0.0.1:8317"
);
assert_eq!(value["model"], "cpa-gui/gpt-test");
assert_eq!(
value["provider"][MANAGED_AGENT_PROVIDER_ID]["models"]["gpt-test"]["limit"]["context"],
372_000
);
assert_eq!(
value["provider"][MANAGED_AGENT_PROVIDER_ID]["models"]["deepseek-test"]["limit"]["context"],
272_000
);
assert_eq!(value["schemaVersion"], 1);
let rules = value["config"]["providerConfigRules"]["providerRules"].as_array().unwrap();
assert_eq!(rules[0]["providerId"], "other");
let managed = rules.iter().find(|rule| rule["providerId"] == MANAGED_AGENT_PROVIDER_ID).unwrap();
assert_eq!(managed["config"]["access"]["apiKeyManagementUrl"], "https://example.com/keys");
assert_eq!(managed["config"]["access"]["apiKey"], DEFAULT_API_KEY);
assert_eq!(managed["config"]["api"]["type"], "anthropic-messages");
assert_eq!(managed["config"]["api"]["baseUrl"], "http://127.0.0.1:8317");
assert_eq!(managed["config"]["personalModelIds"], serde_json::json!(["gpt-test", "deepseek-test"]));
assert_eq!(value["config"]["defaultModelSelection"], serde_json::json!({
"providerId": MANAGED_AGENT_PROVIDER_ID, "modelId": "gpt-test"
}));
let model_rules = value["config"]["modelConfigRules"]["providerModelRules"].as_array().unwrap();
assert_eq!(model_rules[0]["providerId"], "other");
assert_eq!(model_rules[1]["config"]["properties"]["supportsToolCall"], true);
assert_eq!(model_rules[1]["config"]["properties"]["contextWindow"], 372_000);
assert_eq!(model_rules[2]["config"]["properties"]["contextWindow"], 272_000);
assert_eq!(
inspect_zcode_agent_config(&path, 8317, DEFAULT_API_KEY).unwrap(),
(true, Some("gpt-test".to_string()))
);
let mut normalized = value;
let provider = normalized["provider"][MANAGED_AGENT_PROVIDER_ID]
.as_object_mut()
.unwrap();
provider.remove("apiFormat");
provider.remove("defaultKind");
provider.remove("npm");
normalized["config"]["defaultModelSelection"]["providerId"] = serde_json::json!("other");
fs::write(&path, serde_json::to_string_pretty(&normalized).unwrap()).unwrap();
assert_eq!(
inspect_zcode_agent_config(&path, 8317, DEFAULT_API_KEY).unwrap(),
(true, Some("gpt-test".to_string()))
(false, None)
);
fs::remove_dir_all(home).unwrap();
}
#[test]
fn zcode_cli_config_sets_main_model_and_both_configs_must_match() {
let home = agent_test_home("zcode-cli-default-model");
fn zcode_desktop_and_cli_share_the_provider_configuration() {
let home = agent_test_home("zcode-shared-provider-config");
let paths = agent_config_paths(AgentClient::ZCode, &home);
assert_eq!(paths.len(), 1);
assert_eq!(paths[0], home.join(".zcode/v2/provider_config.json"));
fs::create_dir_all(paths[0].parent().unwrap()).unwrap();
fs::create_dir_all(paths[1].parent().unwrap()).unwrap();
let models = test_agent_models(&["gpt-test", "deepseek-test"]);
let app_config = build_zcode_agent_config(
let config = build_zcode_agent_config(
None,
"http://127.0.0.1:8317",
DEFAULT_API_KEY,
@@ -1054,47 +1033,53 @@ fn zcode_cli_config_sets_main_model_and_both_configs_must_match() {
&models,
)
.unwrap();
let cli_config = build_zcode_cli_agent_config(
Some(r#"{"model":{"lite":"other/lite"},"plugins":{"keep":true}}"#),
"http://127.0.0.1:8317",
DEFAULT_API_KEY,
"gpt-test",
&models,
)
.unwrap();
let cli_value: serde_json::Value = serde_json::from_str(&cli_config).unwrap();
assert_eq!(cli_value["model"]["main"], "cpa-gui/gpt-test");
assert_eq!(cli_value["model"]["lite"], "other/lite");
assert_eq!(cli_value["plugins"]["keep"], true);
assert!(cli_value["provider"][MANAGED_AGENT_PROVIDER_ID]
.get("npm")
.is_none());
fs::write(&paths[0], app_config).unwrap();
fs::write(&paths[1], &cli_config).unwrap();
fs::write(&paths[0], &config).unwrap();
assert_eq!(
inspect_agent_managed_config(AgentClient::ZCode, &paths, 8317, DEFAULT_API_KEY).unwrap(),
(true, Some("gpt-test".to_string()), false)
);
let mut mismatched = cli_value;
mismatched["model"]["main"] = serde_json::json!("cpa-gui/deepseek-test");
fs::write(
&paths[1],
serde_json::to_string_pretty(&mismatched).unwrap(),
)
.unwrap();
let mut changed: serde_json::Value = serde_json::from_str(&config).unwrap();
changed["config"]["defaultModelSelection"]["modelId"] = serde_json::json!("deepseek-test");
fs::write(&paths[0], serde_json::to_string_pretty(&changed).unwrap()).unwrap();
assert_eq!(
inspect_agent_managed_config(AgentClient::ZCode, &paths, 8317, DEFAULT_API_KEY).unwrap(),
(false, Some("deepseek-test".to_string()), false)
(true, Some("deepseek-test".to_string()), false)
);
fs::remove_dir_all(home).unwrap();
}
#[test]
fn zcode_preserves_manual_model_rules_and_rejects_unknown_schema_versions() {
let existing = r#"{"schemaVersion":1,"config":{"providerConfigRules":{"providerRules":[]},"modelConfigRules":{"providerModelRules":[],"manualProviderModelRules":[{"providerId":"cpa-gui","modelId":"gpt-test","config":{"enabled":true,"properties":{"supportsToolCall":true}}}]}}}"#;
let mut models = test_agent_models(&["gpt-test", "without-context"]);
models[1].context_window = None;
let rendered = build_zcode_agent_config(
Some(existing),
"http://127.0.0.1:8317",
DEFAULT_API_KEY,
"gpt-test",
&models,
).unwrap();
let value: serde_json::Value = serde_json::from_str(&rendered).unwrap();
assert!(value["config"]["modelConfigRules"]["providerModelRules"]
.as_array().unwrap().is_empty());
let manual = &value["config"]["modelConfigRules"]["manualProviderModelRules"][0];
assert_eq!(manual["config"]["properties"]["supportsToolCall"], true);
assert_eq!(manual["config"]["properties"]["contextWindow"], 200_000);
assert!(build_zcode_agent_config(
Some(r#"{"schemaVersion":2,"config":{}}"#),
"http://127.0.0.1:8317",
DEFAULT_API_KEY,
"gpt-test",
&test_agent_models(&["gpt-test"]),
).is_err());
}
#[test]
fn zcode_inspection_requires_the_default_model_to_belong_to_the_managed_provider() {
let home = agent_test_home("zcode-default-model-inspection");
let path = home.join(".zcode/v2/config.json");
let path = home.join(".zcode/v2/provider_config.json");
fs::create_dir_all(path.parent().unwrap()).unwrap();
let rendered = build_zcode_agent_config(
None,
@@ -1105,7 +1090,7 @@ fn zcode_inspection_requires_the_default_model_to_belong_to_the_managed_provider
)
.unwrap();
let mut value: serde_json::Value = serde_json::from_str(&rendered).unwrap();
value["model"] = serde_json::json!("other/gpt-test");
value["config"]["defaultModelSelection"]["providerId"] = serde_json::json!("other");
fs::write(&path, serde_json::to_string_pretty(&value).unwrap()).unwrap();
assert_eq!(