Add Oh My Pi support through the OpenAI Responses API
Discover CPA models dynamically and route the managed provider to /v1/responses.
This commit is contained in:
1 parent
aba62fa6bf
commit
d7e39e8fdc
23 files changed
+414
-20
No files matched your search
@@ -111,6 +111,7 @@ OpenAI、Claude、Gemini、およびその他の互換形式の間で変換で
|
||||
- Antigravity CLI
|
||||
- Kimi Code
|
||||
- Grok Build
|
||||
- Oh My Pi
|
||||
|
||||
対応クライアントでは、利用可能なモデルカタログの同期、デフォルトモデルの選択、管理設定を適用する前の
|
||||
元設定のバックアップ、以前の設定への復元、利用可能なデスクトップまたは CLI エントリーポイントの起動ができます。
|
||||
|
||||
@@ -128,6 +128,7 @@ proxy. Supported clients include:
|
||||
- Antigravity CLI
|
||||
- Kimi Code
|
||||
- Grok Build
|
||||
- Oh My Pi
|
||||
|
||||
For supported clients, the application can synchronize the available model catalog, select a
|
||||
default model, back up the original configuration before applying managed settings, and restore the
|
||||
|
||||
@@ -119,6 +119,7 @@ API 接入页面按照协议或 Provider 管理上游 API 凭证和服务地址
|
||||
- Antigravity CLI
|
||||
- Kimi Code
|
||||
- Grok Build
|
||||
- Oh My Pi
|
||||
|
||||
对于受支持的客户端,软件可以同步可用模型目录、选择默认模型、在应用托管配置前备份原始配置,
|
||||
以及恢复之前的配置。
|
||||
|
||||
@@ -87,6 +87,7 @@ fn clients() -> Vec<AgentClient> {
|
||||
"workbuddy",
|
||||
"kimi-code",
|
||||
"grok-build",
|
||||
"omp",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|id| AgentClient::parse(id).unwrap())
|
||||
@@ -417,6 +418,7 @@ fn clear_integration_needs_no_backup_and_removes_state_for_each_client() {
|
||||
AgentClient::WorkBuddy,
|
||||
AgentClient::KimiCode,
|
||||
AgentClient::GrokBuild,
|
||||
AgentClient::Omp,
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|client| client.supported_platform())
|
||||
|
||||
@@ -33,6 +33,7 @@ pub(crate) fn inspect_agent_config_statuses(
|
||||
AgentStatusDetectionTarget::Client(AgentClient::AntigravityCli),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::KimiCode),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::GrokBuild),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::Omp),
|
||||
AgentStatusDetectionTarget::PiProvider,
|
||||
];
|
||||
let queue = Mutex::new(targets.into_iter().enumerate());
|
||||
|
||||
@@ -232,6 +232,23 @@ pub(crate) fn build_agent_updates_with_oauth(
|
||||
after,
|
||||
}])
|
||||
}
|
||||
AgentClient::Omp => {
|
||||
if paths.len() != 2 {
|
||||
return Err("Invalid number of Oh My Pi configuration paths".to_string());
|
||||
}
|
||||
let models_before = read_optional_text(&paths[0])?;
|
||||
let settings_before = read_optional_text(&paths[1])?;
|
||||
Ok(vec![
|
||||
AgentFileUpdate {
|
||||
path: paths[0].clone(),
|
||||
after: build_omp_models_config(models_before.as_deref(), &openai_base, api_key)?,
|
||||
},
|
||||
AgentFileUpdate {
|
||||
path: paths[1].clone(),
|
||||
after: build_omp_settings_config(settings_before.as_deref(), model)?,
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1649,6 +1666,7 @@ pub(crate) fn prepare_agent_managed_removal(
|
||||
AgentClient::AntigravityCli => prepare_antigravity_removal(client, paths),
|
||||
AgentClient::KimiCode => prepare_kimi_code_managed_removal(paths),
|
||||
AgentClient::GrokBuild => prepare_grok_build_managed_removal(paths),
|
||||
AgentClient::Omp => prepare_omp_managed_removal(paths),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2624,7 +2642,7 @@ pub(crate) fn agent_config_semantically_equal(
|
||||
json5::from_str::<serde_json::Value>(actual).ok()
|
||||
== json5::from_str::<serde_json::Value>(expected).ok()
|
||||
}
|
||||
AgentClient::Hermes => {
|
||||
AgentClient::Hermes | AgentClient::Omp => {
|
||||
serde_norway::from_str::<serde_norway::Value>(actual).ok()
|
||||
== serde_norway::from_str::<serde_norway::Value>(expected).ok()
|
||||
}
|
||||
@@ -2703,6 +2721,17 @@ pub(crate) fn build_agent_session_restored_bytes_with_preference(
|
||||
AgentClient::AntigravityCli => restore_antigravity_config(client, path, current, original)?,
|
||||
AgentClient::KimiCode => build_restored_kimi_code_config(current, original)?,
|
||||
AgentClient::GrokBuild => build_restored_grok_build_config(current, original)?,
|
||||
AgentClient::Omp => {
|
||||
let index = paths
|
||||
.iter()
|
||||
.position(|candidate| candidate == path)
|
||||
.ok_or_else(|| "Oh My Pi restoration path mismatch".to_string())?;
|
||||
match index {
|
||||
0 => build_restored_omp_models(current, original)?,
|
||||
1 => build_restored_omp_settings(current, original)?,
|
||||
_ => return Err("Invalid Oh My Pi restoration path index".to_string()),
|
||||
}
|
||||
}
|
||||
};
|
||||
if prefer_exact_original {
|
||||
if let (Some(restored), Some(original), Some(original_bytes)) =
|
||||
@@ -3263,6 +3292,170 @@ pub(crate) fn managed_model_alias(model: &str) -> String {
|
||||
format!("{MANAGED_AGENT_PROVIDER_ID}/{model}")
|
||||
}
|
||||
|
||||
|
||||
pub(crate) fn build_omp_models_config(
|
||||
existing: Option<&str>,
|
||||
base_url: &str,
|
||||
api_key: &str,
|
||||
) -> Result<String, String> {
|
||||
let _ = api_key;
|
||||
render_agent_yaml_mapping_update(existing, "Oh My Pi models.yml", |root| {
|
||||
let providers = root
|
||||
.entry(yaml_key("providers"))
|
||||
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()));
|
||||
let providers = providers
|
||||
.as_mapping_mut()
|
||||
.ok_or_else(|| "Oh My Pi providers must be a YAML mapping".to_string())?;
|
||||
let mut provider = serde_norway::Mapping::new();
|
||||
provider.insert(yaml_key("baseUrl"), serde_norway::Value::String(base_url.to_string()));
|
||||
provider.insert(
|
||||
yaml_key("apiKey"),
|
||||
serde_norway::Value::String(OMP_AGENT_API_KEY_ENV.to_string()),
|
||||
);
|
||||
provider.insert(
|
||||
yaml_key("api"),
|
||||
serde_norway::Value::String(OMP_AGENT_API.to_string()),
|
||||
);
|
||||
let mut discovery = serde_norway::Mapping::new();
|
||||
discovery.insert(
|
||||
yaml_key("type"),
|
||||
serde_norway::Value::String("openai-models-list".to_string()),
|
||||
);
|
||||
provider.insert(yaml_key("discovery"), serde_norway::Value::Mapping(discovery));
|
||||
providers.insert(
|
||||
yaml_key(OMP_AGENT_PROVIDER_ID),
|
||||
serde_norway::Value::Mapping(provider),
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn build_omp_settings_config(
|
||||
existing: Option<&str>,
|
||||
model: &str,
|
||||
) -> Result<String, String> {
|
||||
render_agent_yaml_mapping_update(existing, "Oh My Pi config.yml", |root| {
|
||||
let roles = root
|
||||
.entry(yaml_key("modelRoles"))
|
||||
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()));
|
||||
let roles = roles
|
||||
.as_mapping_mut()
|
||||
.ok_or_else(|| "Oh My Pi modelRoles must be a YAML mapping".to_string())?;
|
||||
roles.insert(
|
||||
yaml_key("default"),
|
||||
serde_norway::Value::String(format!("{OMP_AGENT_PROVIDER_ID}/{model}")),
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_omp_managed_removal(paths: &[PathBuf]) -> Result<Images, String> {
|
||||
if paths.len() != 2 {
|
||||
return Err("Invalid number of Oh My Pi configuration paths".to_string());
|
||||
}
|
||||
let mut updates = Vec::new();
|
||||
if paths[0].is_file() {
|
||||
let content = fs::read_to_string(&paths[0]).map_err(|error| {
|
||||
format!("Failed to read Oh My Pi models.yml {}: {error}", path_to_string(&paths[0]))
|
||||
})?;
|
||||
let rendered = render_agent_yaml_mapping_update(Some(&content), "Oh My Pi models.yml", |root| {
|
||||
let mut remove_providers = false;
|
||||
if let Some(providers) = root
|
||||
.get_mut(yaml_key("providers"))
|
||||
.and_then(serde_norway::Value::as_mapping_mut)
|
||||
{
|
||||
providers.remove(yaml_key(OMP_AGENT_PROVIDER_ID));
|
||||
remove_providers = providers.is_empty();
|
||||
}
|
||||
if remove_providers {
|
||||
root.remove(yaml_key("providers"));
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
let root = parse_agent_yaml_mapping(Some(&rendered), "Oh My Pi models.yml")?;
|
||||
updates.push((
|
||||
paths[0].clone(),
|
||||
(!root.is_empty()).then(|| rendered.into_bytes()),
|
||||
));
|
||||
}
|
||||
if paths[1].is_file() {
|
||||
let content = fs::read_to_string(&paths[1]).map_err(|error| {
|
||||
format!("Failed to read Oh My Pi config.yml {}: {error}", path_to_string(&paths[1]))
|
||||
})?;
|
||||
let rendered = render_agent_yaml_mapping_update(Some(&content), "Oh My Pi config.yml", |root| {
|
||||
let mut remove_roles = false;
|
||||
if let Some(roles) = root
|
||||
.get_mut(yaml_key("modelRoles"))
|
||||
.and_then(serde_norway::Value::as_mapping_mut)
|
||||
{
|
||||
if roles
|
||||
.get(yaml_key("default"))
|
||||
.and_then(serde_norway::Value::as_str)
|
||||
.is_some_and(|value| value.starts_with(&format!("{OMP_AGENT_PROVIDER_ID}/")))
|
||||
{
|
||||
roles.remove(yaml_key("default"));
|
||||
}
|
||||
remove_roles = roles.is_empty();
|
||||
}
|
||||
if remove_roles {
|
||||
root.remove(yaml_key("modelRoles"));
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
let root = parse_agent_yaml_mapping(Some(&rendered), "Oh My Pi config.yml")?;
|
||||
updates.push((
|
||||
paths[1].clone(),
|
||||
(!root.is_empty()).then(|| rendered.into_bytes()),
|
||||
));
|
||||
}
|
||||
Ok(updates.into_iter().collect())
|
||||
}
|
||||
|
||||
pub(crate) fn build_restored_omp_models(
|
||||
current: &str,
|
||||
original: Option<&str>,
|
||||
) -> Result<Option<String>, String> {
|
||||
let original_root = parse_agent_yaml_mapping(original, "Original Oh My Pi models.yml")?;
|
||||
let rendered = render_agent_yaml_mapping_update(Some(current), "Current Oh My Pi models.yml", |root| {
|
||||
let original_provider = yaml_mapping_value(&original_root, "providers")
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
.and_then(|providers| yaml_mapping_value(providers, OMP_AGENT_PROVIDER_ID))
|
||||
.cloned();
|
||||
let providers = root
|
||||
.entry(yaml_key("providers"))
|
||||
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()));
|
||||
let providers = providers
|
||||
.as_mapping_mut()
|
||||
.ok_or_else(|| "Oh My Pi providers must be a YAML mapping".to_string())?;
|
||||
if let Some(provider) = original_provider {
|
||||
providers.insert(yaml_key(OMP_AGENT_PROVIDER_ID), provider);
|
||||
} else {
|
||||
providers.remove(yaml_key(OMP_AGENT_PROVIDER_ID));
|
||||
}
|
||||
if providers.is_empty()
|
||||
&& yaml_mapping_value(&original_root, "providers").is_none()
|
||||
{
|
||||
root.remove(yaml_key("providers"));
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
let root = parse_agent_yaml_mapping(Some(&rendered), "Restored Oh My Pi models.yml")?;
|
||||
Ok((!root.is_empty() || original.is_some()).then_some(rendered))
|
||||
}
|
||||
|
||||
pub(crate) fn build_restored_omp_settings(
|
||||
current: &str,
|
||||
original: Option<&str>,
|
||||
) -> Result<Option<String>, String> {
|
||||
let original_root = parse_agent_yaml_mapping(original, "Original Oh My Pi config.yml")?;
|
||||
let rendered = render_agent_yaml_mapping_update(Some(current), "Current Oh My Pi config.yml", |root| {
|
||||
restore_yaml_key(root, Some(&original_root), "modelRoles");
|
||||
Ok(())
|
||||
})?;
|
||||
let root = parse_agent_yaml_mapping(Some(&rendered), "Restored Oh My Pi config.yml")?;
|
||||
Ok((!root.is_empty() || original.is_some()).then_some(rendered))
|
||||
}
|
||||
|
||||
pub(crate) fn build_kimi_code_agent_config(
|
||||
existing: Option<&str>,
|
||||
base_url: &str,
|
||||
|
||||
@@ -52,6 +52,10 @@ pub(crate) fn agent_config_paths(client: AgentClient, home: &Path) -> Vec<PathBu
|
||||
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)],
|
||||
AgentClient::Omp => {
|
||||
let directory = omp_agent_directory(home);
|
||||
vec![directory.join(OMP_MODELS_FILE), directory.join(OMP_SETTINGS_FILE)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +111,10 @@ pub(crate) fn grok_build_home(home: &Path) -> PathBuf {
|
||||
agent_configuration_environment("GROK_HOME").unwrap_or_else(|| home.join(".grok"))
|
||||
}
|
||||
|
||||
pub(crate) fn omp_agent_directory(home: &Path) -> PathBuf {
|
||||
agent_configuration_environment("PI_CODING_AGENT_DIR").unwrap_or_else(|| home.join(".omp/agent"))
|
||||
}
|
||||
|
||||
pub(crate) fn deepseek_harness_home(home: &Path) -> PathBuf {
|
||||
agent_configuration_environment("DSH_HOME").unwrap_or_else(|| home.join(".dsh"))
|
||||
}
|
||||
@@ -1326,6 +1334,8 @@ pub(crate) fn inspect_agent_managed_config(
|
||||
.map(|(configured, model)| (configured, model, false)),
|
||||
AgentClient::GrokBuild => inspect_grok_build_agent_config(&paths[0], port, api_key)
|
||||
.map(|(configured, model)| (configured, model, false)),
|
||||
AgentClient::Omp => inspect_omp_agent_config(paths, port, api_key)
|
||||
.map(|(configured, model)| (configured, model, false)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1561,6 +1571,7 @@ pub(crate) fn agent_has_managed_marker(
|
||||
Some("models"),
|
||||
"default",
|
||||
),
|
||||
AgentClient::Omp => omp_has_managed_marker(paths),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2452,6 +2463,9 @@ pub(crate) fn find_agent_executable(client: AgentClient, home: &Path) -> Option<
|
||||
if client == AgentClient::GrokBuild {
|
||||
return find_grok_build_executable(home);
|
||||
}
|
||||
if client == AgentClient::Omp {
|
||||
return find_omp_executable(home);
|
||||
}
|
||||
if client == AgentClient::OpenCode {
|
||||
return find_opencode_executable(home);
|
||||
}
|
||||
@@ -3453,6 +3467,81 @@ pub(crate) fn inspect_zcode_agent_config(
|
||||
Ok((configured, model))
|
||||
}
|
||||
|
||||
|
||||
pub(crate) fn omp_provider_mapping(root: &serde_norway::Mapping) -> Option<&serde_norway::Mapping> {
|
||||
yaml_mapping_value(root, "providers")
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
.and_then(|providers| yaml_mapping_value(providers, OMP_AGENT_PROVIDER_ID))
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
}
|
||||
|
||||
pub(crate) fn omp_selected_model(root: &serde_norway::Mapping) -> Option<String> {
|
||||
let selector = yaml_mapping_value(root, "modelRoles")
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
.and_then(|roles| yaml_mapping_value(roles, "default"))
|
||||
.and_then(serde_norway::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())?;
|
||||
let prefix = format!("{OMP_AGENT_PROVIDER_ID}/");
|
||||
selector.strip_prefix(&prefix).map(str::to_string)
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_omp_agent_config(
|
||||
paths: &[PathBuf],
|
||||
port: u16,
|
||||
api_key: &str,
|
||||
) -> Result<(bool, Option<String>), String> {
|
||||
if paths.len() != 2 || !paths[0].is_file() || !paths[1].is_file() {
|
||||
return Ok((false, None));
|
||||
}
|
||||
let models = read_agent_yaml_mapping_or_empty(&paths[0], "Oh My Pi models.yml")?;
|
||||
let settings = read_agent_yaml_mapping_or_empty(&paths[1], "Oh My Pi config.yml")?;
|
||||
let provider = omp_provider_mapping(&models);
|
||||
let model = omp_selected_model(&settings);
|
||||
let expected = format!("{}/v1", managed_core_loopback_origin(port));
|
||||
let configured = provider
|
||||
.and_then(|provider| yaml_mapping_value(provider, "baseUrl"))
|
||||
.and_then(serde_norway::Value::as_str)
|
||||
== Some(expected.as_str())
|
||||
&& provider
|
||||
.and_then(|provider| yaml_mapping_value(provider, "apiKey"))
|
||||
.and_then(serde_norway::Value::as_str)
|
||||
== Some(OMP_AGENT_API_KEY_ENV)
|
||||
&& provider
|
||||
.and_then(|provider| yaml_mapping_value(provider, "api"))
|
||||
.and_then(serde_norway::Value::as_str)
|
||||
.is_some_and(|api| api == OMP_AGENT_API || api == "openai-completions")
|
||||
&& provider
|
||||
.and_then(|provider| yaml_mapping_value(provider, "discovery"))
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
.and_then(|discovery| yaml_mapping_value(discovery, "type"))
|
||||
.and_then(serde_norway::Value::as_str)
|
||||
== Some("openai-models-list")
|
||||
&& model.is_some();
|
||||
let _ = api_key;
|
||||
Ok((configured, model))
|
||||
}
|
||||
|
||||
pub(crate) fn omp_has_managed_marker(paths: &[PathBuf]) -> Result<bool, String> {
|
||||
if paths.first().is_none_or(|path| !path.is_file()) {
|
||||
return Ok(false);
|
||||
}
|
||||
let models = read_agent_yaml_mapping_or_empty(&paths[0], "Oh My Pi models.yml")?;
|
||||
Ok(omp_provider_mapping(&models).is_some())
|
||||
}
|
||||
|
||||
pub(crate) fn find_omp_executable(home: &Path) -> Option<PathBuf> {
|
||||
let bun = home.join(".bun/bin");
|
||||
#[cfg(target_os = "windows")]
|
||||
let managed = [bun.join("omp.exe"), bun.join("omp.cmd")];
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let managed = [bun.join("omp")];
|
||||
managed
|
||||
.into_iter()
|
||||
.find(|path| agent_cli_executable(path))
|
||||
.or_else(|| find_named_agent_executable(home, &["omp"]))
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_kimi_code_agent_config(
|
||||
path: &Path,
|
||||
port: u16,
|
||||
|
||||
@@ -161,6 +161,7 @@ pub(crate) fn launch_agent(
|
||||
&launch_directory,
|
||||
&[],
|
||||
&[],
|
||||
&[],
|
||||
&terminal,
|
||||
);
|
||||
}
|
||||
@@ -188,7 +189,7 @@ pub(crate) fn launch_agent(
|
||||
}
|
||||
let executable = env::current_exe().map_err(|_| "Unable to locate CPA launch adapter")?;
|
||||
let directory = resolve_launch_directory(working_directory.as_deref(), &home)?;
|
||||
launch_cli_agent(&executable, client.name(), &directory, &antigravity_cli_helper_arguments(&home), &[], &terminal)
|
||||
launch_cli_agent(&executable, client.name(), &directory, &antigravity_cli_helper_arguments(&home), &[], &[], &terminal)
|
||||
}
|
||||
(AgentClient::WorkBuddy, "app") => {
|
||||
let executable = find_workbuddy_desktop_executable(&home)
|
||||
@@ -233,12 +234,14 @@ pub(crate) fn launch_agent(
|
||||
&[]
|
||||
};
|
||||
let arguments = agent_cli_launch_arguments(client, deepseek_harness_options.as_ref())?;
|
||||
let environment_to_set = omp_launch_environment(client, effective_agent_api_key(&config));
|
||||
launch_cli_agent(
|
||||
&executable,
|
||||
client.name(),
|
||||
&launch_directory,
|
||||
&arguments,
|
||||
environment_to_remove,
|
||||
&environment_to_set,
|
||||
&terminal,
|
||||
)
|
||||
}
|
||||
@@ -247,6 +250,15 @@ pub(crate) fn launch_agent(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn omp_launch_environment(client: AgentClient, api_key: &str) -> Vec<(&str, &str)> {
|
||||
if client == AgentClient::Omp {
|
||||
vec![(OMP_AGENT_API_KEY_ENV, api_key)]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn deepseek_harness_launch_mode(
|
||||
options: Option<&DeepSeekHarnessLaunchOptions>,
|
||||
) -> Result<String, String> {
|
||||
@@ -1186,6 +1198,7 @@ fn launch_cli_agent(
|
||||
working_directory: &Path,
|
||||
arguments: &[String],
|
||||
environment_to_remove: &[&str],
|
||||
environment_to_set: &[(&str, &str)],
|
||||
terminal: &str,
|
||||
) -> Result<(), String> {
|
||||
let removals = environment_to_remove
|
||||
@@ -1193,6 +1206,11 @@ fn launch_cli_agent(
|
||||
.map(|key| format!("-u {}", shell_single_quote(key)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let assignments = environment_to_set
|
||||
.iter()
|
||||
.map(|(key, value)| format!("{}={}", shell_single_quote(key), shell_single_quote(value)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let invocation = std::iter::once(shell_single_quote(&path_to_string(executable)))
|
||||
.chain(
|
||||
arguments
|
||||
@@ -1202,9 +1220,10 @@ fn launch_cli_agent(
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let command_line = format!(
|
||||
"cd {} && exec env {} {}",
|
||||
"cd {} && exec env {} {} {}",
|
||||
shell_single_quote(&path_to_string(working_directory)),
|
||||
removals,
|
||||
assignments,
|
||||
invocation,
|
||||
);
|
||||
let script = if terminal == "iterm2" {
|
||||
@@ -1245,6 +1264,7 @@ fn launch_cli_agent(
|
||||
working_directory: &Path,
|
||||
command_arguments: &[String],
|
||||
environment_to_remove: &[&str],
|
||||
environment_to_set: &[(&str, &str)],
|
||||
terminal: &str,
|
||||
) -> Result<(), String> {
|
||||
let definitions = linux_terminal_definitions();
|
||||
@@ -1269,6 +1289,9 @@ fn launch_cli_agent(
|
||||
for key in environment_to_remove {
|
||||
command.env_remove(key);
|
||||
}
|
||||
for (key, value) in environment_to_set {
|
||||
command.env(key, value);
|
||||
}
|
||||
match command.spawn() {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(error) => last_error = Some(error.to_string()),
|
||||
@@ -1347,6 +1370,7 @@ fn launch_cli_agent(
|
||||
working_directory: &Path,
|
||||
arguments: &[String],
|
||||
environment_to_remove: &[&str],
|
||||
environment_to_set: &[(&str, &str)],
|
||||
terminal: &str,
|
||||
) -> Result<(), String> {
|
||||
use std::os::windows::process::CommandExt;
|
||||
@@ -1413,6 +1437,9 @@ fn launch_cli_agent(
|
||||
for key in environment_to_remove {
|
||||
command.env_remove(key);
|
||||
}
|
||||
for (key, value) in environment_to_set {
|
||||
command.env(key, value);
|
||||
}
|
||||
command
|
||||
.spawn()
|
||||
.map(|_| ())
|
||||
@@ -1426,6 +1453,7 @@ fn launch_cli_agent(
|
||||
_working_directory: &Path,
|
||||
_arguments: &[String],
|
||||
_environment_to_remove: &[&str],
|
||||
_environment_to_set: &[(&str, &str)],
|
||||
_terminal: &str,
|
||||
) -> Result<(), String> {
|
||||
Err(format!("The current platform does not support launching {label}"))
|
||||
|
||||
@@ -748,6 +748,10 @@ pub(crate) fn fresh_agent_contents_with_oauth(
|
||||
model,
|
||||
models,
|
||||
)?]),
|
||||
AgentClient::Omp => Ok(vec![
|
||||
build_omp_models_config(None, &openai_base, api_key)?,
|
||||
build_omp_settings_config(None, model)?,
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,7 +774,7 @@ pub(crate) fn agent_contents_equal(client: AgentClient, actual: &str, expected:
|
||||
serde_yaml::from_str::<serde_yaml::Value>(actual).ok()
|
||||
== serde_yaml::from_str::<serde_yaml::Value>(expected).ok()
|
||||
}
|
||||
AgentClient::DeepSeekHarness => {
|
||||
AgentClient::Omp | AgentClient::DeepSeekHarness => {
|
||||
serde_norway::from_str::<serde_norway::Value>(actual).ok()
|
||||
== serde_norway::from_str::<serde_norway::Value>(expected).ok()
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ fn tracked_configuration_paths(app: &tauri::AppHandle) -> Result<Vec<PathBuf>, S
|
||||
AgentClient::AntigravityCli,
|
||||
AgentClient::KimiCode,
|
||||
AgentClient::GrokBuild,
|
||||
AgentClient::Omp,
|
||||
] {
|
||||
paths.extend(agent_managed_paths(client, &home));
|
||||
}
|
||||
|
||||
@@ -174,6 +174,11 @@ const DEEPSEEK_HARNESS_SETTINGS_FILE: &str = "settings.yaml";
|
||||
const DEEPSEEK_HARNESS_CREDENTIALS_FILE: &str = ".credentials.yaml";
|
||||
const PI_AGENT_ID: &str = "pi";
|
||||
const PI_AGENT_NAME: &str = "Pi";
|
||||
const OMP_AGENT_PROVIDER_ID: &str = "easy-cliproxyapi";
|
||||
const OMP_AGENT_API_KEY_ENV: &str = "EASYCLIPROXYAPI_API_KEY";
|
||||
const OMP_AGENT_API: &str = "openai-responses";
|
||||
const OMP_MODELS_FILE: &str = "models.yml";
|
||||
const OMP_SETTINGS_FILE: &str = "config.yml";
|
||||
const PI_CLIPROXYAPI_PACKAGE: &str = "npm:@router-for-me/pi-cliproxyapi-provider";
|
||||
const PI_CLIPROXYAPI_NPM_LATEST_URL: &str =
|
||||
"https://registry.npmjs.org/@router-for-me%2Fpi-cliproxyapi-provider/latest";
|
||||
@@ -1358,6 +1363,7 @@ enum AgentClient {
|
||||
AntigravityCli,
|
||||
KimiCode,
|
||||
GrokBuild,
|
||||
Omp,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -1382,6 +1388,7 @@ impl AgentClient {
|
||||
"antigravity-cli" => Ok(Self::AntigravityCli),
|
||||
"kimi-code" => Ok(Self::KimiCode),
|
||||
"grok-build" => Ok(Self::GrokBuild),
|
||||
"omp" => Ok(Self::Omp),
|
||||
_ => Err(format!("Unsupported agent client: {value}")),
|
||||
}
|
||||
}
|
||||
@@ -1400,6 +1407,7 @@ impl AgentClient {
|
||||
Self::AntigravityCli => "antigravity-cli",
|
||||
Self::KimiCode => "kimi-code",
|
||||
Self::GrokBuild => "grok-build",
|
||||
Self::Omp => "omp",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1417,6 +1425,7 @@ impl AgentClient {
|
||||
Self::AntigravityCli => "Antigravity CLI",
|
||||
Self::KimiCode => "Kimi Code",
|
||||
Self::GrokBuild => "Grok Build",
|
||||
Self::Omp => "Oh My Pi",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1443,6 +1452,7 @@ impl AgentClient {
|
||||
Self::AntigravityCli => &["agy"],
|
||||
Self::KimiCode => &["kimi"],
|
||||
Self::GrokBuild => &["grok"],
|
||||
Self::Omp => &["omp"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1549,6 +1549,53 @@ async fn codex_configuration_uses_public_v1_model_ids_for_selection_and_files()
|
||||
assert_eq!(ids, written);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omp_configuration_discovers_models_without_storing_the_api_key() {
|
||||
let home = agent_test_home("omp-provider");
|
||||
let existing_models = "providers:\n other:\n baseUrl: https://example.test/v1\n apiKey: KEEP_OTHER\n";
|
||||
let existing_settings = "shellPath: E:/software/toolchain/w64devkit/bin/bash.exe\nmodelRoles:\n tiny: local/lfm2.5-230m\n";
|
||||
let models = build_omp_models_config(Some(existing_models), "http://127.0.0.1:8317/v1", "secret-value").unwrap();
|
||||
let settings = build_omp_settings_config(Some(existing_settings), "gpt-test").unwrap();
|
||||
let models_value: serde_norway::Value = serde_norway::from_str(&models).unwrap();
|
||||
let settings_value: serde_norway::Value = serde_norway::from_str(&settings).unwrap();
|
||||
let provider = &models_value["providers"]["easy-cliproxyapi"];
|
||||
assert_eq!(provider["baseUrl"].as_str(), Some("http://127.0.0.1:8317/v1"));
|
||||
assert_eq!(provider["apiKey"].as_str(), Some("EASYCLIPROXYAPI_API_KEY"));
|
||||
assert_eq!(provider["api"].as_str(), Some("openai-responses"));
|
||||
assert_eq!(provider["discovery"]["type"].as_str(), Some("openai-models-list"));
|
||||
assert!(provider.get("models").is_none());
|
||||
assert!(!models.contains("secret-value"));
|
||||
assert_eq!(models_value["providers"]["other"]["apiKey"].as_str(), Some("KEEP_OTHER"));
|
||||
assert_eq!(settings_value["shellPath"].as_str(), Some("E:/software/toolchain/w64devkit/bin/bash.exe"));
|
||||
assert_eq!(settings_value["modelRoles"]["tiny"].as_str(), Some("local/lfm2.5-230m"));
|
||||
assert_eq!(settings_value["modelRoles"]["default"].as_str(), Some("easy-cliproxyapi/gpt-test"));
|
||||
|
||||
let paths = agent_config_paths(AgentClient::Omp, &home);
|
||||
fs::create_dir_all(paths[0].parent().unwrap()).unwrap();
|
||||
fs::write(&paths[0], models).unwrap();
|
||||
fs::write(&paths[1], settings).unwrap();
|
||||
let (configured, model) = inspect_omp_agent_config(&paths, 8317, "secret-value").unwrap();
|
||||
assert!(configured);
|
||||
assert_eq!(model.as_deref(), Some("gpt-test"));
|
||||
assert!(omp_has_managed_marker(&paths).unwrap());
|
||||
|
||||
let removed = prepare_omp_managed_removal(&paths).unwrap();
|
||||
for (path, bytes) in removed {
|
||||
match bytes {
|
||||
Some(bytes) => fs::write(&path, bytes).unwrap(),
|
||||
None => fs::write(&path, "").unwrap(),
|
||||
}
|
||||
}
|
||||
let restored_models = fs::read_to_string(&paths[0]).unwrap();
|
||||
let restored_settings = fs::read_to_string(&paths[1]).unwrap();
|
||||
assert!(!restored_models.contains("easy-cliproxyapi"));
|
||||
assert!(restored_models.contains("KEEP_OTHER"));
|
||||
assert!(!restored_settings.contains("easy-cliproxyapi"));
|
||||
assert!(restored_settings.contains("shellPath"));
|
||||
assert!(restored_settings.contains("local/lfm2.5-230m"));
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kimi_grok_and_zcode_use_cpa_runtime_context_windows() {
|
||||
for client in [
|
||||
@@ -1566,6 +1613,7 @@ fn kimi_grok_and_zcode_use_cpa_runtime_context_windows() {
|
||||
AgentClient::OpenClaw,
|
||||
AgentClient::Hermes,
|
||||
AgentClient::DeepSeekHarness,
|
||||
AgentClient::Omp,
|
||||
] {
|
||||
assert!(!agent_uses_cpa_runtime_context_windows(client));
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ fn installation_detection_separates_executables_from_version_metadata() {
|
||||
AgentClient::DeepSeekHarness,
|
||||
AgentClient::KimiCode,
|
||||
AgentClient::GrokBuild,
|
||||
AgentClient::Omp,
|
||||
] {
|
||||
assert!(agent_installation_detected(client, None, true, false));
|
||||
assert!(!agent_installation_detected(client, None, false, false));
|
||||
|
||||
@@ -1159,6 +1159,7 @@ export const jaOverrides = {
|
||||
'agents.description.zcode': 'CPA の Anthropic Messages API と動的モデルカタログを使用',
|
||||
'agents.description.kimiCode': 'CPA の OpenAI Chat Completions API と動的モデルカタログを使用',
|
||||
'agents.description.grokBuild': 'CPA の OpenAI Chat Completions API とカスタムモデルカタログを使用',
|
||||
'agents.description.omp': 'OpenAI Responses エンドポイントから CPA モデルを動的に検出',
|
||||
'agents.description.pi': 'CLIProxyAPI プロバイダーを使用して Pi CLI のモデルを動的に読み込みます',
|
||||
'agents.clients.pages': 'クライアントのページ',
|
||||
'agents.clients.previous': '前のクライアントページ',
|
||||
|
||||
@@ -1159,6 +1159,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'agents.description.zcode': 'Uses the CPA Anthropic Messages API and dynamic model catalog',
|
||||
'agents.description.kimiCode': 'Uses the CPA OpenAI Chat Completions API and dynamic model catalog',
|
||||
'agents.description.grokBuild': 'Uses the CPA OpenAI Chat Completions API and custom model catalog',
|
||||
'agents.description.omp': 'Discovers CPA models through the OpenAI Responses endpoint',
|
||||
'agents.description.pi': 'Loads models dynamically in Pi through the CLIProxyAPI provider',
|
||||
'agents.clients.pages': 'Client pages',
|
||||
'agents.clients.previous': 'Previous client page',
|
||||
|
||||
@@ -1158,6 +1158,7 @@ export const zhCN = {
|
||||
'agents.description.zcode': '使用 CPA 的 Anthropic Messages 接口和动态模型目录',
|
||||
'agents.description.kimiCode': '使用 CPA 的 OpenAI Chat Completions 接口和动态模型目录',
|
||||
'agents.description.grokBuild': '使用 CPA 的 OpenAI Chat Completions 接口和自定义模型目录',
|
||||
'agents.description.omp': '通过 OpenAI Responses 接口动态发现 CPA 模型',
|
||||
'agents.description.pi': '使用 Pi CLI 和 CLIProxyAPI 提供方动态加载模型',
|
||||
'agents.clients.pages': '客户端分页',
|
||||
'agents.clients.previous': '上一页客户端',
|
||||
|
||||
@@ -207,6 +207,7 @@ function createAgentStatuses() {
|
||||
['antigravity-cli', 'Antigravity CLI'],
|
||||
['kimi-code', 'Kimi Code'],
|
||||
['grok-build', 'Grok Build'],
|
||||
['omp', 'Oh My Pi'],
|
||||
['pi', 'Pi'],
|
||||
] as const;
|
||||
const claudeMappings = {
|
||||
|
||||
@@ -82,6 +82,7 @@ type AgentClientId =
|
||||
| 'antigravity-cli'
|
||||
| 'kimi-code'
|
||||
| 'grok-build'
|
||||
| 'omp'
|
||||
| 'pi';
|
||||
|
||||
type ClaudeModelMappingClientId = 'claude-code' | 'claude-desktop';
|
||||
@@ -234,7 +235,7 @@ type AgentDefinition = {
|
||||
name: string;
|
||||
icon?: string;
|
||||
Icon?: ComponentType<{ size?: number; 'aria-hidden'?: boolean }>;
|
||||
descriptionKey: 'agents.description.claudeCode' | 'agents.description.claudeDesktop' | 'agents.description.codex' | 'agents.description.opencode' | 'agents.description.openclaw' | 'agents.description.hermes' | 'agents.description.deepseekHarness' | 'agents.description.zcode' | 'agents.description.workbuddy' | 'agents.description.antigravityCli' | 'agents.description.kimiCode' | 'agents.description.grokBuild' | 'agents.description.pi';
|
||||
descriptionKey: 'agents.description.claudeCode' | 'agents.description.claudeDesktop' | 'agents.description.codex' | 'agents.description.opencode' | 'agents.description.openclaw' | 'agents.description.hermes' | 'agents.description.deepseekHarness' | 'agents.description.zcode' | 'agents.description.workbuddy' | 'agents.description.antigravityCli' | 'agents.description.kimiCode' | 'agents.description.grokBuild' | 'agents.description.omp' | 'agents.description.pi';
|
||||
};
|
||||
|
||||
type AgentSubpageId = 'core' | 'management' | 'sessions';
|
||||
@@ -276,6 +277,12 @@ const agentDefinitions: AgentDefinition[] = [
|
||||
icon: opencodeIcon,
|
||||
descriptionKey: 'agents.description.opencode',
|
||||
},
|
||||
{
|
||||
id: 'omp',
|
||||
name: 'Oh My Pi',
|
||||
icon: piIcon,
|
||||
descriptionKey: 'agents.description.omp',
|
||||
},
|
||||
{
|
||||
id: 'pi',
|
||||
name: 'Pi',
|
||||
@@ -341,6 +348,7 @@ const agentSubpages: AgentSubpageDefinition[] = [
|
||||
'workbuddy',
|
||||
'antigravity-cli',
|
||||
'kimi-code',
|
||||
'omp',
|
||||
'grok-build',
|
||||
'pi',
|
||||
],
|
||||
|
||||
@@ -9,6 +9,7 @@ export type AgentConfigurationClientId =
|
||||
| 'zcode'
|
||||
| 'kimi-code'
|
||||
| 'grok-build'
|
||||
| 'omp'
|
||||
| 'pi';
|
||||
|
||||
export type AgentConfigurationModificationState = 'unconfigured' | 'applied' | 'invalid';
|
||||
|
||||
@@ -104,7 +104,7 @@ const screenshots = path.join(os.tmpdir(), 'easycliproxy-agent-clients');
|
||||
|
||||
// Search reaches uninstalled clients; a cancelled draft never affects navigation or storage.
|
||||
await manage().click();
|
||||
assert.equal(await dialog().getByRole('checkbox').count(), 13);
|
||||
assert.equal(await dialog().getByRole('checkbox').count(), 14);
|
||||
await search().fill('herMES');
|
||||
assert.equal(await dialog().getByRole('checkbox').count(), 1);
|
||||
await checkbox('Hermes Agent').check();
|
||||
@@ -173,7 +173,7 @@ const screenshots = path.join(os.tmpdir(), 'easycliproxy-agent-clients');
|
||||
await open('not-installed&no-plugin&fresh');
|
||||
await waitClients(['Codex']);
|
||||
await manage().click();
|
||||
assert.equal(await dialog().getByRole('checkbox').count(), 13);
|
||||
assert.equal(await dialog().getByRole('checkbox').count(), 14);
|
||||
await closeAndFocus(() => cancel().click());
|
||||
await open('mixed-clients&fail-detection');
|
||||
await page.getByText(/模拟客户端检测失败/).waitFor();
|
||||
@@ -191,11 +191,11 @@ const screenshots = path.join(os.tmpdir(), 'easycliproxy-agent-clients');
|
||||
|
||||
// Every saved client is reachable exactly once, in the same order, without a second switcher.
|
||||
await page.setViewportSize({ width: 1368, height: 912 });
|
||||
const twelve = ['claude-code', 'claude-desktop', 'codex', 'deepseek-harness', 'opencode', 'pi',
|
||||
const savedClients = ['claude-code', 'claude-desktop', 'codex', 'deepseek-harness', 'opencode', 'omp', 'pi',
|
||||
'grok-build', 'antigravity-cli', 'workbuddy', 'zcode', 'kimi-code', 'openclaw'];
|
||||
const twelveNames = ['Claude Code', 'Claude Desktop', 'Codex', 'DeepSeek Harness', 'OpenCode', 'Pi',
|
||||
const savedNames = ['Claude Code', 'Claude Desktop', 'Codex', 'DeepSeek Harness', 'OpenCode', 'Oh My Pi', 'Pi',
|
||||
'Grok Build', 'Antigravity CLI', 'WorkBuddy', 'ZCode', 'Kimi Code', 'OpenClaw'];
|
||||
await page.evaluate(({ key, ids }) => localStorage.setItem(key, JSON.stringify(ids)), { key: storageKey, ids: twelve });
|
||||
await page.evaluate(({ key, ids }) => localStorage.setItem(key, JSON.stringify(ids)), { key: storageKey, ids: savedClients });
|
||||
await page.goto(`${base}/tests/fixtures/agent-backups.html?shell&client=claude-code`, { waitUntil: 'domcontentloaded' });
|
||||
await ready();
|
||||
const previous = () => page.getByRole('button', { name: '上一页客户端', exact: true });
|
||||
@@ -216,7 +216,7 @@ const screenshots = path.join(os.tmpdir(), 'easycliproxy-agent-clients');
|
||||
const collectPages = async () => {
|
||||
while (await previous().isEnabled()) await previous().click();
|
||||
const names = [];
|
||||
for (let index = 0; index < 13; index++) {
|
||||
for (let index = 0; index < savedClients.length; index++) {
|
||||
await assertFits();
|
||||
names.push(...await namesOnPage());
|
||||
if (await next().isDisabled()) break;
|
||||
@@ -225,33 +225,33 @@ const screenshots = path.join(os.tmpdir(), 'easycliproxy-agent-clients');
|
||||
return names;
|
||||
};
|
||||
assert.equal(await page.getByRole('button', { name: '切换客户端', exact: true }).count(), 0);
|
||||
assert.deepEqual(await collectPages(), twelveNames, 'no saved client is omitted, duplicated or reordered');
|
||||
assert.deepEqual(JSON.parse(await stored()), twelve, 'browsing leaves the management list unchanged');
|
||||
assert.deepEqual(await collectPages(), savedNames, 'no saved client is omitted, duplicated or reordered');
|
||||
assert.deepEqual(JSON.parse(await stored()), savedClients, 'browsing leaves the management list unchanged');
|
||||
assert.equal(await page.evaluate(() => localStorage.getItem('cpa-gui.agent-selected-client.v1')), 'claude-code',
|
||||
'browsing pages does not select a different client');
|
||||
const lastPage = await namesOnPage();
|
||||
await page.locator('.agent-list-items button').filter({ has: page.getByText('OpenClaw', { exact: true }) }).click();
|
||||
assert.deepEqual(await namesOnPage(), lastPage, 'selecting a client does not replace or shuffle any row');
|
||||
assert.equal(await activeClient(), 'OpenClaw');
|
||||
assert.deepEqual(JSON.parse(await stored()), twelve, 'selecting leaves the management list unchanged');
|
||||
assert.deepEqual(JSON.parse(await stored()), savedClients, 'selecting leaves the management list unchanged');
|
||||
await page.setViewportSize({ width: 360, height: 600 });
|
||||
await page.waitForFunction(() => document.querySelector('.agent-list-items button.active strong')?.textContent === 'OpenClaw'
|
||||
&& document.querySelectorAll('.agent-list-items button').length <= 2);
|
||||
await assertFits();
|
||||
assert.deepEqual(await collectPages(), twelveNames, 'all managed clients remain reachable after resizing');
|
||||
assert.deepEqual(JSON.parse(await stored()), twelve);
|
||||
assert.deepEqual(await collectPages(), savedNames, 'all managed clients remain reachable after resizing');
|
||||
assert.deepEqual(JSON.parse(await stored()), savedClients);
|
||||
// Add/remove only in management. Cancel is inert; Save controls the entire list.
|
||||
await manage().click();
|
||||
await checkbox('Hermes Agent').check();
|
||||
await checkbox('OpenClaw').uncheck();
|
||||
await closeAndFocus(() => cancel().click());
|
||||
assert.deepEqual(JSON.parse(await stored()), twelve);
|
||||
assert.deepEqual(JSON.parse(await stored()), savedClients);
|
||||
assert.equal(await activeClient(), 'OpenClaw');
|
||||
await manage().click();
|
||||
await checkbox('Hermes Agent').check();
|
||||
await checkbox('OpenClaw').uncheck();
|
||||
await closeAndFocus(() => save().click());
|
||||
const updatedNames = [...twelveNames.filter(name => name !== 'OpenClaw'), 'Hermes Agent'];
|
||||
const updatedNames = [...savedNames.filter(name => name !== 'OpenClaw'), 'Hermes Agent'];
|
||||
assert.equal(await activeClient(), 'Claude Code', 'explicitly removing the current client selects a remaining client');
|
||||
assert.deepEqual(await collectPages(), updatedNames);
|
||||
await page.locator('.agent-list-items button').filter({ has: page.getByText('Hermes Agent', { exact: true }) }).click();
|
||||
|
||||
@@ -153,7 +153,7 @@ const path = require('node:path');
|
||||
await open(mode + 'client=kimi-code');
|
||||
assert.equal(await button('重启 App').count(), 0);
|
||||
|
||||
for (const client of ['claude-code', 'claude-desktop', 'opencode', 'openclaw', 'hermes', 'deepseek-harness', 'zcode', 'workbuddy', 'antigravity-cli', 'kimi-code', 'grok-build']) {
|
||||
for (const client of ['claude-code', 'claude-desktop', 'opencode', 'openclaw', 'hermes', 'deepseek-harness', 'zcode', 'workbuddy', 'antigravity-cli', 'kimi-code', 'grok-build', 'omp']) {
|
||||
await open(mode + 'client=' + client);
|
||||
assert.ok(await button('关闭配置修改').isEnabled());
|
||||
await tab('配置管理').click();
|
||||
|
||||
@@ -154,6 +154,7 @@ describe('agent configuration update action', () => {
|
||||
'zcode',
|
||||
'kimi-code',
|
||||
'grok-build',
|
||||
'omp',
|
||||
])('%s updates only while its model draft differs', (client) => {
|
||||
expect(appliedConfiguration(client, 'model-b')).toBe('update');
|
||||
expect(appliedConfiguration(client, ' MODEL-A ')).toBe('close');
|
||||
|
||||
Vendored
+1
-1
@@ -9,7 +9,7 @@ if(params.has('reset-selections'))localStorage.removeItem('cpa-gui.agent-model-s
|
||||
localStorage.setItem('easy-cli-proxy-api.locale',params.get('locale') || 'zh-CN');
|
||||
document.documentElement.dataset.theme = params.get('theme') || 'light';
|
||||
localStorage.setItem('cpa-gui.agent-selected-client.v1', params.get('client') || 'codex');
|
||||
const ids = ['claude-code','claude-desktop','codex','opencode','openclaw','hermes','deepseek-harness','antigravity-cli','workbuddy','zcode','kimi-code','grok-build','pi'];
|
||||
const ids = ['claude-code','claude-desktop','codex','opencode','openclaw','hermes','deepseek-harness','antigravity-cli','workbuddy','zcode','kimi-code','grok-build','omp','pi'];
|
||||
let count=0; let backupCount=0; const backups:any[]=[]; let currentModel=params.has('fresh')?null:'gpt-one';
|
||||
let nativeOauth=params.has('native-oauth'); let nativeSwitchCount=0; let codexClosed=false; let closeCount=0;
|
||||
let currentOauth=false; const currentMappings:Record<string,any>={};
|
||||
|
||||
Reference in new issue
Block a user