Supports the latest version of zcode (but I hope you won't use it)
This commit is contained in:
1 parent
4b9bb22c87
commit
3cfee2451d
7 files changed
+458
-377
No files matched your search
@@ -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");
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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!(
|
||||
|
||||
Reference in new issue
Block a user