feat(agents): add configurable DeepSeek Harness model catalog
- Separate default model selection from catalog configuration - Use API metadata with per-model and provider overrides - Expose documented capability, reasoning, protocol, and request settings - Preserve customizations with conflict detection and transactional rollback
This commit is contained in:
1 parent
145b585c3f
commit
4d8d549e04
28 files changed
+2047
-20
No files matched your search
@@ -5,6 +5,7 @@ use super::*;
|
||||
mod commands;
|
||||
mod configuration;
|
||||
mod discovery;
|
||||
mod deepseek_harness;
|
||||
mod transactions;
|
||||
mod backups;
|
||||
mod templates;
|
||||
@@ -13,6 +14,7 @@ mod state;
|
||||
pub(crate) use commands::*;
|
||||
pub(crate) use configuration::*;
|
||||
pub(crate) use discovery::*;
|
||||
pub(crate) use deepseek_harness::*;
|
||||
pub(crate) use transactions::*;
|
||||
pub(crate) use backups::*;
|
||||
pub(crate) use templates::*;
|
||||
|
||||
@@ -410,6 +410,13 @@ pub(crate) fn desktop_mapping_path(paths: &[PathBuf]) -> Result<PathBuf, String>
|
||||
.join("current-mapping.json"))
|
||||
}
|
||||
|
||||
pub(crate) fn deepseek_harness_catalog_state_path(paths: &[PathBuf]) -> Result<PathBuf, String> {
|
||||
Ok(agent_data_directory(paths)?
|
||||
.join("agents/deepseek-harness")
|
||||
.join(path_identity(paths))
|
||||
.join("catalog-state.json"))
|
||||
}
|
||||
|
||||
fn profile_models_hash(images: &Images) -> Option<String> {
|
||||
let (path, bytes) = images.get(2)?;
|
||||
let profile = parse(path, text(bytes.as_deref()).ok()?).ok()?;
|
||||
@@ -452,6 +459,9 @@ pub(crate) fn current_desktop_mappings(home: &Path) -> Option<ClaudeDesktopModel
|
||||
}
|
||||
|
||||
pub(crate) fn mapping_revision(client: &str, paths: &[PathBuf]) -> Result<String, String> {
|
||||
if client == "deepseek-harness" {
|
||||
return Ok(image_hash(read_agent_bytes(&deepseek_harness_catalog_state_path(paths)?)?.as_deref()));
|
||||
}
|
||||
if client != "claude-desktop" {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
@@ -63,6 +63,8 @@ fn desktop_restore_models(plan: &RestorePlan) -> Result<Option<Vec<AgentModelOpt
|
||||
.into_iter()
|
||||
.zip([&mappings.opus, &mappings.sonnet, &mappings.haiku])
|
||||
.map(|(route, source)| AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: source.clone(),
|
||||
alias: None,
|
||||
is_alias: source != route
|
||||
|
||||
@@ -25,6 +25,8 @@ fn models() -> Vec<AgentModelOption> {
|
||||
["gpt-one", "gpt-two"]
|
||||
.into_iter()
|
||||
.map(|name| AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: name.into(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
|
||||
@@ -1173,8 +1173,15 @@ pub(crate) async fn update_agent_config(
|
||||
claude_code_model_mappings: Option<ClaudeDesktopModelMappings>,
|
||||
claude_desktop_model_mappings: Option<ClaudeDesktopModelMappings>,
|
||||
) -> Result<AgentConfigActionResult, String> {
|
||||
apply_agent_config(app, gui_config_state, client, model, oauth_configuration,
|
||||
claude_code_model_mappings, claude_desktop_model_mappings).await
|
||||
apply_agent_config(
|
||||
app,
|
||||
gui_config_state,
|
||||
client,
|
||||
model,
|
||||
oauth_configuration,
|
||||
claude_code_model_mappings,
|
||||
claude_desktop_model_mappings,
|
||||
).await
|
||||
}
|
||||
|
||||
pub(crate) fn validate_agent_can_enable(
|
||||
@@ -1224,11 +1231,18 @@ pub(crate) fn resolve_available_agent_model(
|
||||
pub(crate) fn parse_agent_model_options(
|
||||
payload: &serde_json::Value,
|
||||
) -> Result<Vec<AgentModelOption>, String> {
|
||||
let source = payload
|
||||
.as_array()
|
||||
let object_models = payload.get("models").and_then(serde_json::Value::as_object).map(|models| {
|
||||
models.iter().map(|(id, value)| {
|
||||
let mut entry = value.as_object().cloned().unwrap_or_default();
|
||||
entry.entry("id").or_insert(serde_json::json!(id));
|
||||
serde_json::Value::Object(entry)
|
||||
}).collect::<Vec<_>>()
|
||||
});
|
||||
let source = payload.as_array()
|
||||
.or_else(|| payload.get("data").and_then(serde_json::Value::as_array))
|
||||
.or_else(|| payload.get("models").and_then(serde_json::Value::as_array))
|
||||
.ok_or_else(|| "本机模型列表响应缺少 data 或 models 数组".to_string())?;
|
||||
.or(object_models.as_ref())
|
||||
.ok_or_else(|| "本机模型列表响应缺少 data 数组或 models 目录".to_string())?;
|
||||
let mut models = Vec::new();
|
||||
for item in source {
|
||||
let name = if let Some(name) = item.as_str() {
|
||||
@@ -1241,7 +1255,7 @@ pub(crate) fn parse_agent_model_options(
|
||||
.trim()
|
||||
.to_string()
|
||||
};
|
||||
let display_name = ["display_name", "displayName"]
|
||||
let display_name = ["display_name", "displayName", "name"]
|
||||
.into_iter()
|
||||
.find_map(|key| item.get(key).and_then(serde_json::Value::as_str))
|
||||
.map(str::trim)
|
||||
@@ -1270,14 +1284,18 @@ pub(crate) fn parse_agent_model_options(
|
||||
]
|
||||
.into_iter()
|
||||
.find_map(|key| item.get(key).and_then(json_positive_u64));
|
||||
let input_modalities = codex_catalog::parse_modalities(item).or_else(|| {
|
||||
codex_catalog::parse_modalities(&serde_json::json!({"input_modalities": item.get("input")}))
|
||||
});
|
||||
let harness_metadata = harness_api_metadata(item);
|
||||
|
||||
if let Some(model_alias) = model_alias {
|
||||
if keep_original {
|
||||
append_agent_model_option(&mut models, &name, display_name, false, context_window);
|
||||
append_agent_model_option(&mut models, &name, display_name, false, context_window, input_modalities.clone(), harness_metadata.clone());
|
||||
}
|
||||
append_agent_model_option(&mut models, &model_alias, Some(name), true, context_window);
|
||||
append_agent_model_option(&mut models, &model_alias, Some(name), true, context_window, input_modalities, harness_metadata);
|
||||
} else {
|
||||
append_agent_model_option(&mut models, &name, display_name, false, context_window);
|
||||
append_agent_model_option(&mut models, &name, display_name, false, context_window, input_modalities, harness_metadata);
|
||||
}
|
||||
}
|
||||
Ok(models)
|
||||
@@ -1289,6 +1307,8 @@ pub(crate) fn append_agent_model_option(
|
||||
alias: Option<String>,
|
||||
is_alias: bool,
|
||||
context_window: Option<u64>,
|
||||
input_modalities: Option<Vec<String>>,
|
||||
harness_metadata: Option<serde_json::Value>,
|
||||
) {
|
||||
let name = name.trim();
|
||||
if name.is_empty()
|
||||
@@ -1304,6 +1324,8 @@ pub(crate) fn append_agent_model_option(
|
||||
.filter(|value| !value.is_empty() && !value.eq_ignore_ascii_case(name))
|
||||
.map(str::to_string);
|
||||
models.push(AgentModelOption {
|
||||
input_modalities,
|
||||
harness_metadata,
|
||||
name: name.to_string(),
|
||||
alias,
|
||||
is_alias,
|
||||
|
||||
@@ -322,6 +322,13 @@ pub(crate) fn build_deepseek_harness_models(
|
||||
.map_err(|error| format!("序列化 Harness 模型上下文失败: {error}"))?,
|
||||
);
|
||||
}
|
||||
if let Some(input) = model.input_modalities {
|
||||
entry.insert(
|
||||
yaml_key("input"),
|
||||
serde_norway::to_value(input)
|
||||
.map_err(|_| "序列化 Harness 模型输入类型失败")?,
|
||||
);
|
||||
}
|
||||
Ok(serde_norway::Value::Mapping(entry))
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()?;
|
||||
@@ -334,7 +341,21 @@ pub(crate) fn build_deepseek_harness_settings(
|
||||
model: &str,
|
||||
models: &[AgentModelOption],
|
||||
) -> Result<String, String> {
|
||||
let published_models = build_deepseek_harness_models(models, model)?;
|
||||
build_deepseek_harness_catalog_settings(
|
||||
existing,
|
||||
base_url,
|
||||
model,
|
||||
models,
|
||||
&mut DeepSeekHarnessCatalogState::default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn render_deepseek_harness_settings(
|
||||
existing: Option<&str>,
|
||||
base_url: &str,
|
||||
model: &str,
|
||||
published_models: serde_norway::Value,
|
||||
) -> Result<String, String> {
|
||||
render_agent_yaml_mapping_update(existing, "DeepSeek Harness settings", |root| {
|
||||
let llm = root
|
||||
.entry(yaml_key("llm-pi-ai"))
|
||||
@@ -369,6 +390,8 @@ pub(crate) fn build_deepseek_harness_settings(
|
||||
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
|
||||
.as_mapping_mut()
|
||||
.ok_or_else(|| "DeepSeek Harness agent-default-model 必须是映射".to_string())?;
|
||||
let same_model = selection.get(yaml_key("provider")).and_then(serde_norway::Value::as_str) == Some(DEEPSEEK_HARNESS_PROVIDER_ID)
|
||||
&& selection.get(yaml_key("model")).and_then(serde_norway::Value::as_str) == Some(model);
|
||||
selection.insert(
|
||||
yaml_key("provider"),
|
||||
serde_norway::Value::String(DEEPSEEK_HARNESS_PROVIDER_ID.to_string()),
|
||||
@@ -377,7 +400,7 @@ pub(crate) fn build_deepseek_harness_settings(
|
||||
yaml_key("model"),
|
||||
serde_norway::Value::String(model.to_string()),
|
||||
);
|
||||
selection.remove(yaml_key("reasoningEffort"));
|
||||
if !same_model { selection.remove(yaml_key("reasoningEffort")); }
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -693,6 +716,8 @@ pub(crate) fn ordered_agent_models(
|
||||
ordered.push(selected.clone());
|
||||
} else {
|
||||
ordered.push(AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: selected_model.to_string(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
|
||||
@@ -0,0 +1,681 @@
|
||||
use super::*;
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
type Profile = Map<String, Value>;
|
||||
|
||||
static HARNESS_SCHEMA: std::sync::LazyLock<Value> = std::sync::LazyLock::new(|| {
|
||||
serde_json::from_str(include_str!(
|
||||
"../../../src/services/deepSeekHarnessSchema.json"
|
||||
))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
pub(crate) fn harness_fields(group: &str) -> impl Iterator<Item = &str> {
|
||||
HARNESS_SCHEMA[group]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|field| field["name"].as_str().unwrap())
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub(crate) struct DeepSeekHarnessCatalogState {
|
||||
models: BTreeMap<String, Profile>,
|
||||
provider: Profile,
|
||||
generated_models: BTreeMap<String, Profile>,
|
||||
generated_provider: Option<Profile>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct HarnessEditorModel {
|
||||
pub id: String,
|
||||
pub defaults: Profile,
|
||||
pub configuration: Profile,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct HarnessEditorSnapshot {
|
||||
pub revision: String,
|
||||
pub models: Vec<HarnessEditorModel>,
|
||||
pub provider: Profile,
|
||||
base_url: String,
|
||||
default_model: Option<String>,
|
||||
configured: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct HarnessEditorRequest {
|
||||
pub revision: String,
|
||||
pub models: Vec<HarnessEditorModel>,
|
||||
pub provider: Profile,
|
||||
}
|
||||
|
||||
fn read_harness_state(bytes: Option<&[u8]>) -> Result<DeepSeekHarnessCatalogState, String> {
|
||||
bytes
|
||||
.map(|bytes| {
|
||||
serde_json::from_slice(bytes)
|
||||
.map_err(|_| "读取 DSH 模型配置状态失败,请检查 catalog-state.json".into())
|
||||
})
|
||||
.unwrap_or_else(|| Ok(Default::default()))
|
||||
}
|
||||
|
||||
fn harness_provider(root: &Value) -> Option<&Value> {
|
||||
root.get("llm-pi-ai")?
|
||||
.get("providers")?
|
||||
.get(DEEPSEEK_HARNESS_PROVIDER_ID)
|
||||
}
|
||||
|
||||
fn harness_root(existing: Option<&str>) -> Result<Value, String> {
|
||||
serde_json::to_value(parse_agent_yaml_mapping(
|
||||
existing,
|
||||
"DeepSeek Harness settings",
|
||||
)?)
|
||||
.map_err(|_| "DSH settings 必须是有效的配置映射".into())
|
||||
}
|
||||
|
||||
fn selected_fields(value: &Value, group: &str) -> Profile {
|
||||
harness_fields(group)
|
||||
.filter_map(|key| value.get(key).map(|value| (key.into(), value.clone())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn import_changes(
|
||||
current: &Profile,
|
||||
generated: Option<&Profile>,
|
||||
overrides: &mut Profile,
|
||||
group: &str,
|
||||
) {
|
||||
for key in harness_fields(group) {
|
||||
let current = current.get(key);
|
||||
let previous = generated.and_then(|fields| fields.get(key));
|
||||
if current != previous {
|
||||
if let Some(value) = current {
|
||||
overrides.insert(key.into(), value.clone());
|
||||
} else {
|
||||
overrides.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn import_harness_configuration(root: &Value, state: &mut DeepSeekHarnessCatalogState) {
|
||||
let Some(provider) = harness_provider(root) else {
|
||||
return;
|
||||
};
|
||||
import_changes(
|
||||
&selected_fields(provider, "provider"),
|
||||
state.generated_provider.as_ref(),
|
||||
&mut state.provider,
|
||||
"provider",
|
||||
);
|
||||
if let Some(models) = provider.get("models").and_then(Value::as_array) {
|
||||
for model in models {
|
||||
if let Some(id) = model.get("id").and_then(Value::as_str) {
|
||||
import_changes(
|
||||
&selected_fields(model, "model"),
|
||||
state.generated_models.get(id),
|
||||
state.models.entry(id.into()).or_default(),
|
||||
"model",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
state.models.retain(|_, profile| !profile.is_empty());
|
||||
}
|
||||
|
||||
pub(crate) fn harness_api_metadata(item: &Value) -> Option<Value> {
|
||||
let mut fields = Profile::new();
|
||||
for (field, keys) in [
|
||||
(
|
||||
"maxTokens",
|
||||
&[
|
||||
"maxTokens",
|
||||
"max_tokens",
|
||||
"max_output_tokens",
|
||||
"maxOutputTokens",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
"reasoningEfforts",
|
||||
&["reasoningEfforts", "reasoning_efforts"][..],
|
||||
),
|
||||
("compat", &["compat"][..]),
|
||||
] {
|
||||
if let Some(value) = keys.iter().find_map(|key| item.get(key)) {
|
||||
let definition = HARNESS_SCHEMA["model"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|item| item["name"] == field)
|
||||
.unwrap();
|
||||
if validate_harness_field(value, definition, "", field).is_ok() {
|
||||
fields.insert(field.into(), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
(!fields.is_empty()).then_some(Value::Object(fields))
|
||||
}
|
||||
|
||||
fn harness_model_defaults(model: &AgentModelOption, api: &str) -> Profile {
|
||||
let mut fields = model
|
||||
.harness_metadata
|
||||
.as_ref()
|
||||
.and_then(Value::as_object)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if let Some(compat) = fields.get_mut("compat").and_then(Value::as_object_mut) {
|
||||
compat.retain(|key, _| {
|
||||
HARNESS_SCHEMA["compat"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|field| field["name"] == key.as_str())
|
||||
.is_some_and(|field| field["apis"].as_array().unwrap().contains(&json!(api)))
|
||||
});
|
||||
if compat.is_empty() {
|
||||
fields.remove("compat");
|
||||
}
|
||||
}
|
||||
if let Some(name) = &model.alias {
|
||||
fields.insert("name".into(), json!(name));
|
||||
}
|
||||
if let Some(context) = model.context_window {
|
||||
fields.insert("contextWindow".into(), json!(context));
|
||||
}
|
||||
if let Some(input) = &model.input_modalities {
|
||||
fields.insert("input".into(), json!(input));
|
||||
}
|
||||
fields
|
||||
}
|
||||
|
||||
fn merge_profile(mut defaults: Profile, configuration: &Profile) -> Profile {
|
||||
for (key, value) in configuration {
|
||||
if key == "compat" {
|
||||
if let (Some(old), Some(new)) = (
|
||||
defaults.get_mut(key).and_then(Value::as_object_mut),
|
||||
value.as_object(),
|
||||
) {
|
||||
old.extend(new.clone());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
defaults.insert(key.clone(), value.clone());
|
||||
}
|
||||
defaults
|
||||
}
|
||||
|
||||
pub(crate) fn validate_harness_profile(
|
||||
profile: &Profile,
|
||||
group: &str,
|
||||
api: &str,
|
||||
path: &str,
|
||||
) -> Result<(), String> {
|
||||
let definitions = HARNESS_SCHEMA[group].as_array().unwrap();
|
||||
for (key, value) in profile {
|
||||
let field = definitions
|
||||
.iter()
|
||||
.find(|field| field["name"] == key.as_str())
|
||||
.ok_or_else(|| format!("{path}.{key}: 不支持的配置字段"))?;
|
||||
validate_harness_field(value, field, api, &format!("{path}.{key}"))?;
|
||||
}
|
||||
for field in definitions {
|
||||
if field["required"] == true && !profile.contains_key(field["name"].as_str().unwrap()) {
|
||||
return Err(format!(
|
||||
"{path}.{}: 必须填写",
|
||||
field["name"].as_str().unwrap()
|
||||
));
|
||||
}
|
||||
}
|
||||
if group == "backoff"
|
||||
&& profile
|
||||
.get("initialDelayMs")
|
||||
.and_then(Value::as_f64)
|
||||
.unwrap_or(500.0)
|
||||
> profile
|
||||
.get("maxDelayMs")
|
||||
.and_then(Value::as_f64)
|
||||
.unwrap_or(10000.0)
|
||||
{
|
||||
return Err(format!("{path}: initialDelayMs 不能大于 maxDelayMs"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_harness_field(
|
||||
value: &Value,
|
||||
field: &Value,
|
||||
api: &str,
|
||||
path: &str,
|
||||
) -> Result<(), String> {
|
||||
let invalid = || format!("{path}: 配置值不符合 DSH 文档要求");
|
||||
if !api.is_empty()
|
||||
&& field
|
||||
.get("apis")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|apis| !apis.contains(&json!(api)))
|
||||
{
|
||||
return Err(format!("{path}: 不适用于 {api},请恢复自动或选择对应协议"));
|
||||
}
|
||||
let valid = match field["kind"].as_str().unwrap() {
|
||||
"group" => {
|
||||
return validate_harness_profile(
|
||||
value.as_object().ok_or_else(invalid)?,
|
||||
field["group"].as_str().unwrap(),
|
||||
api,
|
||||
path,
|
||||
)
|
||||
}
|
||||
"string" => value.as_str().is_some_and(|value| !value.trim().is_empty()),
|
||||
"boolean" => value.is_boolean(),
|
||||
"integer" | "number" => value.as_f64().is_some_and(|n| {
|
||||
n.is_finite()
|
||||
&& n.abs() <= 9007199254740991.0
|
||||
&& (field["kind"] != "integer" || n.fract() == 0.0)
|
||||
&& field["min"].as_f64().is_none_or(|min| n >= min)
|
||||
&& field["exclusiveMin"].as_f64().is_none_or(|min| n > min)
|
||||
&& field["max"].as_f64().is_none_or(|max| n <= max)
|
||||
}),
|
||||
"enum" => field["values"].as_array().unwrap().contains(value),
|
||||
"modalities" | "strings" => value.as_array().is_some_and(|values| {
|
||||
(!values.is_empty() || field["name"] == "input")
|
||||
&& values.iter().enumerate().all(|(index, value)| {
|
||||
value.as_str().is_some_and(|s| {
|
||||
!s.trim().is_empty()
|
||||
&& (field["kind"] != "modalities" || matches!(s, "text" | "image"))
|
||||
}) && !values[..index].contains(value)
|
||||
})
|
||||
}),
|
||||
"reasoning" => {
|
||||
value == &json!(false)
|
||||
|| value.as_object().is_some_and(|values| {
|
||||
!values.is_empty()
|
||||
&& values.iter().all(|(key, value)| {
|
||||
["off", "minimal", "low", "medium", "high", "xhigh", "max"]
|
||||
.contains(&key.as_str())
|
||||
&& ((key == "off" && value.is_null())
|
||||
|| value.as_str().is_some_and(|s| !s.trim().is_empty()))
|
||||
})
|
||||
})
|
||||
}
|
||||
"headers" => value.as_object().is_some_and(|values| {
|
||||
values.iter().all(|(key, value)| {
|
||||
reqwest::header::HeaderName::from_bytes(key.as_bytes()).is_ok()
|
||||
&& value.as_str().is_some_and(|s| {
|
||||
s.chars().all(|c| (c as u32) <= 255)
|
||||
&& reqwest::header::HeaderValue::from_str(s).is_ok()
|
||||
})
|
||||
})
|
||||
}),
|
||||
"kwargs" => value.as_object().is_some_and(|values| {
|
||||
values.values().all(|value| {
|
||||
!value.is_array()
|
||||
&& (!value.is_object()
|
||||
|| value.as_object().is_some_and(|object| {
|
||||
object
|
||||
.keys()
|
||||
.all(|key| matches!(key.as_str(), "$var" | "omitWhenOff"))
|
||||
&& value["$var"].as_str().is_some_and(|s| {
|
||||
["thinking.enabled", "thinking.effort", "thinking.budget"]
|
||||
.contains(&s)
|
||||
})
|
||||
&& value.get("omitWhenOff").is_none_or(Value::is_boolean)
|
||||
}))
|
||||
})
|
||||
}),
|
||||
_ => false,
|
||||
};
|
||||
if valid {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(invalid())
|
||||
}
|
||||
}
|
||||
|
||||
fn render_harness_profiles(
|
||||
existing: Option<&str>,
|
||||
base_url: &str,
|
||||
models: &[AgentModelOption],
|
||||
state: &mut DeepSeekHarnessCatalogState,
|
||||
) -> Result<String, String> {
|
||||
let root = harness_root(existing)?;
|
||||
let old = harness_provider(&root);
|
||||
let api = state
|
||||
.provider
|
||||
.get("api")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("openai-completions");
|
||||
validate_harness_profile(&state.provider, "provider", api, "provider")?;
|
||||
let mut published = Vec::new();
|
||||
let mut generated = BTreeMap::new();
|
||||
for model in models {
|
||||
let profile = merge_profile(
|
||||
harness_model_defaults(model, api),
|
||||
state.models.get(&model.name).unwrap_or(&Profile::new()),
|
||||
);
|
||||
validate_harness_profile(&profile, "model", api, &model.name)?;
|
||||
let mut entry = old
|
||||
.and_then(|p| p.get("models"))
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|entries| entries.iter().find(|entry| entry["id"] == model.name))
|
||||
.and_then(Value::as_object)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
for key in harness_fields("model") {
|
||||
entry.remove(key);
|
||||
}
|
||||
entry.extend(profile.clone());
|
||||
entry.insert("id".into(), json!(model.name));
|
||||
generated.insert(model.name.clone(), profile);
|
||||
published.push(entry);
|
||||
}
|
||||
let mut provider = state.provider.clone();
|
||||
provider
|
||||
.entry("displayName")
|
||||
.or_insert(json!("EasyCLIProxyAPI"));
|
||||
provider.entry("api").or_insert(json!("openai-completions"));
|
||||
let rendered =
|
||||
render_agent_yaml_mapping_update(existing, "DeepSeek Harness settings", |root| {
|
||||
let managed = root
|
||||
.get_mut(yaml_key("llm-pi-ai"))
|
||||
.and_then(|v| v.get_mut("providers"))
|
||||
.and_then(|v| v.get_mut(DEEPSEEK_HARNESS_PROVIDER_ID))
|
||||
.and_then(serde_norway::Value::as_mapping_mut)
|
||||
.ok_or("DSH provider 配置缺失")?;
|
||||
managed.insert(
|
||||
yaml_key("baseURL"),
|
||||
serde_norway::Value::String(
|
||||
if api == "anthropic-messages" {
|
||||
base_url.trim_end_matches("/v1")
|
||||
} else {
|
||||
base_url
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
for key in harness_fields("provider") {
|
||||
managed.remove(yaml_key(key));
|
||||
}
|
||||
for (key, value) in &provider {
|
||||
managed.insert(
|
||||
yaml_key(key),
|
||||
serde_norway::to_value(value).map_err(|_| "序列化 DSH provider 失败")?,
|
||||
);
|
||||
}
|
||||
managed.insert(
|
||||
yaml_key("models"),
|
||||
serde_norway::to_value(&published).map_err(|_| "序列化 DSH 模型失败")?,
|
||||
);
|
||||
Ok(())
|
||||
})?;
|
||||
state.generated_models = generated;
|
||||
state.generated_provider = Some(provider);
|
||||
Ok(rendered)
|
||||
}
|
||||
|
||||
pub(crate) fn deepseek_harness_template_catalog_state(images: &Images) -> Result<Vec<u8>, String> {
|
||||
let root = harness_root(text(
|
||||
images.first().ok_or("缺少 DSH settings")?.1.as_deref(),
|
||||
)?)?;
|
||||
let mut state = DeepSeekHarnessCatalogState::default();
|
||||
if let Some(provider) = harness_provider(&root) {
|
||||
state.generated_provider = Some(selected_fields(provider, "provider"));
|
||||
if let Some(models) = provider.get("models").and_then(Value::as_array) {
|
||||
for model in models {
|
||||
if let Some(id) = model.get("id").and_then(Value::as_str) {
|
||||
state
|
||||
.generated_models
|
||||
.insert(id.into(), selected_fields(model, "model"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
serde_json::to_vec(&state).map_err(|_| "生成 DSH 模型配置状态失败".into())
|
||||
}
|
||||
|
||||
pub(crate) fn build_deepseek_harness_catalog_settings(
|
||||
existing: Option<&str>,
|
||||
base_url: &str,
|
||||
selected: &str,
|
||||
models: &[AgentModelOption],
|
||||
state: &mut DeepSeekHarnessCatalogState,
|
||||
) -> Result<String, String> {
|
||||
import_harness_configuration(&harness_root(existing)?, state);
|
||||
let ordered = ordered_agent_models(models, selected);
|
||||
let settings = render_deepseek_harness_settings(
|
||||
existing,
|
||||
base_url,
|
||||
selected,
|
||||
build_deepseek_harness_models(models, selected)?,
|
||||
)?;
|
||||
render_harness_profiles(Some(&settings), base_url, &ordered, state)
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_deepseek_harness_configuration(
|
||||
home: &Path,
|
||||
port: u16,
|
||||
api_key: &str,
|
||||
selected: &str,
|
||||
models: &[AgentModelOption],
|
||||
) -> Result<(Vec<PathBuf>, Images, Images), String> {
|
||||
let mut paths = config_paths("deepseek-harness", home)?;
|
||||
paths.push(deepseek_harness_catalog_state_path(&paths)?);
|
||||
let before = config_images(&paths)?;
|
||||
let mut state = read_harness_state(before[2].1.as_deref())?;
|
||||
let settings = build_deepseek_harness_catalog_settings(
|
||||
text(before[0].1.as_deref())?,
|
||||
&format!("{}/v1", managed_core_loopback_origin(port)),
|
||||
selected,
|
||||
models,
|
||||
&mut state,
|
||||
)?;
|
||||
let updates = vec![
|
||||
AgentFileUpdate {
|
||||
path: paths[0].clone(),
|
||||
after: settings,
|
||||
},
|
||||
AgentFileUpdate {
|
||||
path: paths[1].clone(),
|
||||
after: build_deepseek_harness_credentials(text(before[1].1.as_deref())?, api_key)?,
|
||||
},
|
||||
];
|
||||
let mut after = prepare_config_updates(
|
||||
"deepseek-harness",
|
||||
&paths[..2],
|
||||
&before[..2].to_vec(),
|
||||
&updates,
|
||||
false,
|
||||
)?;
|
||||
after.push((
|
||||
paths[2].clone(),
|
||||
Some(serde_json::to_vec(&state).map_err(|_| "生成 DSH 模型配置状态失败")?),
|
||||
));
|
||||
Ok((paths, before, after))
|
||||
}
|
||||
|
||||
pub(crate) fn apply_deepseek_harness_configuration(
|
||||
home: &Path,
|
||||
port: u16,
|
||||
api_key: &str,
|
||||
selected: &str,
|
||||
models: &[AgentModelOption],
|
||||
) -> Result<AgentConfigActionResult, String> {
|
||||
let (paths, before, after) =
|
||||
prepare_deepseek_harness_configuration(home, port, api_key, selected, models)?;
|
||||
commit_config(
|
||||
"deepseek-harness",
|
||||
&paths,
|
||||
&before,
|
||||
&after,
|
||||
"update",
|
||||
Some(selected.into()),
|
||||
)
|
||||
}
|
||||
|
||||
fn harness_editor_from_images(
|
||||
before: &Images,
|
||||
models: &[AgentModelOption],
|
||||
port: u16,
|
||||
) -> Result<(HarnessEditorSnapshot, DeepSeekHarnessCatalogState), String> {
|
||||
let root = harness_root(text(before[0].1.as_deref())?)?;
|
||||
let mut state = read_harness_state(before[2].1.as_deref())?;
|
||||
import_harness_configuration(&root, &mut state);
|
||||
let api = state
|
||||
.provider
|
||||
.get("api")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("openai-completions");
|
||||
let models = models
|
||||
.iter()
|
||||
.map(|model| HarnessEditorModel {
|
||||
id: model.name.clone(),
|
||||
defaults: harness_model_defaults(model, api),
|
||||
configuration: state.models.get(&model.name).cloned().unwrap_or_default(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let base_url = format!("{}/v1", managed_core_loopback_origin(port));
|
||||
let revision = sha256_bytes(
|
||||
&serde_json::to_vec(&(image_revision(before), &models, &base_url))
|
||||
.map_err(|_| "生成 DSH 配置版本失败")?,
|
||||
);
|
||||
Ok((
|
||||
HarnessEditorSnapshot {
|
||||
revision,
|
||||
models,
|
||||
provider: state.provider.clone(),
|
||||
base_url,
|
||||
default_model: root
|
||||
.get("agent-default-model")
|
||||
.filter(|s| s["provider"] == DEEPSEEK_HARNESS_PROVIDER_ID)
|
||||
.and_then(|s| s.get("model"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string),
|
||||
configured: harness_provider(&root).is_some(),
|
||||
},
|
||||
state,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn harness_editor_snapshot(
|
||||
home: &Path,
|
||||
models: &[AgentModelOption],
|
||||
port: u16,
|
||||
) -> Result<HarnessEditorSnapshot, String> {
|
||||
let mut paths = config_paths("deepseek-harness", home)?;
|
||||
paths.push(deepseek_harness_catalog_state_path(&paths)?);
|
||||
harness_editor_from_images(&config_images(&paths)?, models, port).map(|(snapshot, _)| snapshot)
|
||||
}
|
||||
|
||||
pub(crate) fn save_harness_editor(
|
||||
home: &Path,
|
||||
models: &[AgentModelOption],
|
||||
port: u16,
|
||||
request: HarnessEditorRequest,
|
||||
) -> Result<HarnessEditorSnapshot, String> {
|
||||
let mut paths = config_paths("deepseek-harness", home)?;
|
||||
paths.push(deepseek_harness_catalog_state_path(&paths)?);
|
||||
let before = config_images(&paths)?;
|
||||
let (snapshot, mut state) = harness_editor_from_images(&before, models, port)?;
|
||||
if request.revision != snapshot.revision {
|
||||
return Err("DSH_MODEL_CATALOG_CHANGED: 模型列表或配置已变化,请重新加载".into());
|
||||
}
|
||||
if request.models.len() != models.len() {
|
||||
return Err("DSH 模型列表不完整,请重新加载".into());
|
||||
}
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for model in request.models {
|
||||
if !seen.insert(model.id.clone()) || !models.iter().any(|m| m.name == model.id) {
|
||||
return Err("DSH 模型列表包含重复或不可用的 ID".into());
|
||||
}
|
||||
state.models.insert(model.id, model.configuration);
|
||||
}
|
||||
state.provider = request.provider;
|
||||
let api = state
|
||||
.provider
|
||||
.get("api")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("openai-completions");
|
||||
validate_harness_profile(&state.provider, "provider", api, "provider")?;
|
||||
for model in models {
|
||||
validate_harness_profile(
|
||||
&merge_profile(
|
||||
harness_model_defaults(model, api),
|
||||
&state.models[&model.name],
|
||||
),
|
||||
"model",
|
||||
api,
|
||||
&model.name,
|
||||
)?;
|
||||
}
|
||||
let mut after = before.clone();
|
||||
if snapshot.configured {
|
||||
if snapshot
|
||||
.default_model
|
||||
.as_ref()
|
||||
.is_some_and(|id| !models.iter().any(|m| &m.name == id))
|
||||
{
|
||||
return Err("当前默认模型已不可用,请先更新默认模型".into());
|
||||
}
|
||||
let settings = render_harness_profiles(
|
||||
text(before[0].1.as_deref())?,
|
||||
&format!("{}/v1", managed_core_loopback_origin(port)),
|
||||
models,
|
||||
&mut state,
|
||||
)?;
|
||||
let updates = vec![AgentFileUpdate {
|
||||
path: paths[0].clone(),
|
||||
after: settings,
|
||||
}];
|
||||
let prepared = prepare_config_updates(
|
||||
"deepseek-harness",
|
||||
&paths[..2],
|
||||
&before[..2].to_vec(),
|
||||
&updates,
|
||||
false,
|
||||
)?;
|
||||
after[0] = prepared[0].clone();
|
||||
}
|
||||
state.models.retain(|_, profile| !profile.is_empty());
|
||||
after[2].1 = Some(serde_json::to_vec(&state).map_err(|_| "生成 DSH 模型配置状态失败")?);
|
||||
commit_config(
|
||||
"deepseek-harness",
|
||||
&paths,
|
||||
&before,
|
||||
&after,
|
||||
"catalog",
|
||||
snapshot.default_model.clone(),
|
||||
)?;
|
||||
harness_editor_snapshot(home, models, port)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn get_deepseek_harness_model_catalog_editor(
|
||||
app: tauri::AppHandle,
|
||||
gui_config_state: tauri::State<'_, GuiConfigState>,
|
||||
) -> Result<HarnessEditorSnapshot, String> {
|
||||
let config = gui_config_state.snapshot()?;
|
||||
let models = fetch_agent_models(config.port, effective_agent_api_key(&config)).await?;
|
||||
let home = app.path().home_dir().map_err(|_| "无法获取用户目录")?;
|
||||
let _guard = AGENT_CONFIG_FILE_LOCK
|
||||
.lock()
|
||||
.map_err(|_| "智能体配置文件锁已损坏")?;
|
||||
harness_editor_snapshot(&home, &models, config.port)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn save_deepseek_harness_model_catalog_editor(
|
||||
app: tauri::AppHandle,
|
||||
gui_config_state: tauri::State<'_, GuiConfigState>,
|
||||
request: HarnessEditorRequest,
|
||||
) -> Result<HarnessEditorSnapshot, String> {
|
||||
let config = gui_config_state.snapshot()?;
|
||||
let models = fetch_agent_models(config.port, effective_agent_api_key(&config)).await?;
|
||||
let home = app.path().home_dir().map_err(|_| "无法获取用户目录")?;
|
||||
let _guard = AGENT_CONFIG_FILE_LOCK
|
||||
.lock()
|
||||
.map_err(|_| "智能体配置文件锁已损坏")?;
|
||||
save_harness_editor(&home, &models, config.port, request)
|
||||
}
|
||||
@@ -3855,7 +3855,11 @@ pub(crate) fn inspect_deepseek_harness_config(
|
||||
.map(str::to_string)
|
||||
})
|
||||
.flatten();
|
||||
let expected_base = format!("{}/v1", managed_core_loopback_origin(port));
|
||||
let expected_base = if provider.and_then(|p| yaml_mapping_value(p, "api")).and_then(serde_norway::Value::as_str) == Some("anthropic-messages") {
|
||||
managed_core_loopback_origin(port)
|
||||
} else {
|
||||
format!("{}/v1", managed_core_loopback_origin(port))
|
||||
};
|
||||
let credentials_layout_supported = deepseek_harness_credentials_layout_supported(&credentials);
|
||||
let credential = yaml_mapping_value(&credentials, "refs")
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
@@ -3882,7 +3886,7 @@ pub(crate) fn inspect_deepseek_harness_config(
|
||||
&& provider
|
||||
.and_then(|provider| yaml_mapping_value(provider, "api"))
|
||||
.and_then(serde_norway::Value::as_str)
|
||||
== Some("openai-completions")
|
||||
.is_some_and(|api| matches!(api, "openai-completions" | "openai-responses" | "anthropic-messages"))
|
||||
&& provider
|
||||
.and_then(|provider| yaml_mapping_value(provider, "baseURL"))
|
||||
.and_then(serde_norway::Value::as_str)
|
||||
|
||||
@@ -635,6 +635,8 @@ pub(crate) fn fresh_agent_contents(
|
||||
model: &str,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let models = [AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: model.to_string(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
@@ -1445,6 +1447,9 @@ pub(crate) fn apply_agent_configuration_with_oauth(
|
||||
model: &str,
|
||||
options: AgentConfigurationOptions<'_>,
|
||||
) -> Result<AgentConfigActionResult, String> {
|
||||
if client == AgentClient::DeepSeekHarness {
|
||||
return apply_deepseek_harness_configuration(home, port, api_key, model, options.models);
|
||||
}
|
||||
let before = config_images(&config_paths(client.id(), home)?)?;
|
||||
validate_config_images(&before)?;
|
||||
let mappings = options.claude_desktop_model_mappings;
|
||||
@@ -1469,6 +1474,8 @@ pub(crate) fn reset_agent_configuration_to_default(
|
||||
api_key,
|
||||
model,
|
||||
models: &[AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: model.to_string(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
|
||||
@@ -181,6 +181,17 @@ fn commit_config_transaction(
|
||||
}
|
||||
let mut previous = before.clone();
|
||||
let mut target = after.clone();
|
||||
if client == "deepseek-harness" && matches!(source, "template" | "restore") {
|
||||
let state_path = deepseek_harness_catalog_state_path(paths)?;
|
||||
let state_before = read_agent_bytes(&state_path)?;
|
||||
let state_after = if source == "template" {
|
||||
Some(deepseek_harness_template_catalog_state(after)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
previous.push((state_path.clone(), state_before));
|
||||
target.push((state_path, state_after));
|
||||
}
|
||||
if client == "claude-desktop" {
|
||||
let state_path = desktop_mapping_path(paths)?;
|
||||
let state_before = read_agent_bytes(&state_path)?;
|
||||
@@ -382,7 +393,7 @@ fn preserve_model_extensions(client: &str, path: &Path, before: &Value, after: &
|
||||
"claude-desktop" => &["name", "contextWindow", "supports1m", "prefer1m"],
|
||||
"opencode" | "zcode" => &["name"],
|
||||
"openclaw" => &["id", "name", "alias"],
|
||||
"deepseek-harness" => &["id", "name", "contextWindow"],
|
||||
"deepseek-harness" => &["id", "name", "contextWindow", "input", "maxTokens", "reasoningEfforts", "compat"],
|
||||
"kimi-code" => &[
|
||||
"provider",
|
||||
"model",
|
||||
@@ -553,7 +564,7 @@ fn validate_unmanaged_preserved(
|
||||
}
|
||||
if client == "deepseek-harness" {
|
||||
if path == &paths[0] {
|
||||
for key in ["displayName", "apiKeyEnv", "api", "baseURL", "models"] {
|
||||
for key in ["apiKeyEnv", "baseURL", "models"].into_iter().chain(harness_fields("provider")) {
|
||||
set(
|
||||
&mut value,
|
||||
&[
|
||||
|
||||
@@ -519,6 +519,8 @@ fn prepare_catalog_with_customizations(
|
||||
let models = entries
|
||||
.iter()
|
||||
.map(|entry| AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: string_value(&entry.value, "slug"),
|
||||
alias: optional_map_string(&entry.value, "display_name").filter(|display| {
|
||||
!display.eq_ignore_ascii_case(&string_value(&entry.value, "slug"))
|
||||
@@ -681,7 +683,7 @@ fn enable_fast_mode(model: &mut Map<String, Value>) {
|
||||
);
|
||||
}
|
||||
|
||||
fn parse_modalities(value: &Value) -> Option<Vec<String>> {
|
||||
pub(crate) fn parse_modalities(value: &Value) -> Option<Vec<String>> {
|
||||
let raw = value
|
||||
.get("input_modalities")
|
||||
.or_else(|| value.get("inputModalities"))
|
||||
@@ -1070,18 +1072,24 @@ mod tests {
|
||||
fn runtime_context_windows_override_generic_model_metadata() {
|
||||
let mut models = vec![
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "GPT-Test".to_string(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
context_window: Some(200_000),
|
||||
},
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "unmatched".to_string(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
context_window: Some(128_000),
|
||||
},
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "max-only".to_string(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
|
||||
@@ -1121,8 +1121,13 @@ struct AgentModelOption {
|
||||
is_alias: bool,
|
||||
#[serde(default)]
|
||||
context_window: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
input_modalities: Option<Vec<String>>,
|
||||
#[serde(skip)]
|
||||
harness_metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ClaudeDesktopModelMappings {
|
||||
@@ -2521,6 +2526,8 @@ fn main() {
|
||||
uninstall_pi_provider,
|
||||
check_codex_oauth_login,
|
||||
update_codex_model_catalog,
|
||||
get_deepseek_harness_model_catalog_editor,
|
||||
save_deepseek_harness_model_catalog_editor,
|
||||
get_codex_model_catalog_editor,
|
||||
save_codex_model_catalog_editor,
|
||||
get_thinking_aliases,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::*;
|
||||
|
||||
mod agent_configuration;
|
||||
mod deepseek_harness_catalog;
|
||||
mod agent_paths;
|
||||
mod agent_state;
|
||||
mod agent_transactions;
|
||||
|
||||
@@ -128,24 +128,32 @@ fn claude_code_role_mappings_drive_settings() {
|
||||
};
|
||||
let models = vec![
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "gpt-opus-base".to_string(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
context_window: Some(1_000_000),
|
||||
},
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: mappings.opus.clone(),
|
||||
alias: Some("gpt-opus-base".to_string()),
|
||||
is_alias: true,
|
||||
context_window: Some(128_000),
|
||||
},
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: mappings.sonnet.clone(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
context_window: Some(272_000),
|
||||
},
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: mappings.haiku.clone(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
@@ -188,12 +196,16 @@ fn claude_code_runtime_settings_keep_per_role_1m_suffixes() {
|
||||
};
|
||||
let models = vec![
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "custom-pro".to_string(),
|
||||
alias: Some("Custom Pro".to_string()),
|
||||
is_alias: false,
|
||||
context_window: Some(200_000),
|
||||
},
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "custom-flash".to_string(),
|
||||
alias: Some("Custom Flash".to_string()),
|
||||
is_alias: false,
|
||||
@@ -251,6 +263,8 @@ fn claude_code_runtime_settings_keep_per_role_1m_suffixes() {
|
||||
#[test]
|
||||
fn claude_desktop_keeps_original_context_when_1m_is_off() {
|
||||
let models = vec![AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "runtime-model".to_string(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
@@ -574,18 +588,24 @@ fn claude_desktop_profile_keeps_non_claude_models_internal() {
|
||||
};
|
||||
let models = vec![
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: mappings.opus.clone(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
context_window: Some(1_000_000),
|
||||
},
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: mappings.sonnet.clone(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
context_window: Some(272_000),
|
||||
},
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: mappings.haiku.clone(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
@@ -1358,30 +1378,40 @@ fn agent_model_list_parser_exposes_aliases_as_selectable_model_ids() {
|
||||
models,
|
||||
vec![
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "gpt-5".to_string(),
|
||||
alias: Some("GPT 5".to_string()),
|
||||
is_alias: false,
|
||||
context_window: Some(272_000),
|
||||
},
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "claude-sonnet".to_string(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
context_window: Some(1_000_000),
|
||||
},
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "claude-sonnet-xhigh".to_string(),
|
||||
alias: Some("claude-sonnet".to_string()),
|
||||
is_alias: true,
|
||||
context_window: Some(1_000_000),
|
||||
},
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "visible-alias".to_string(),
|
||||
alias: Some("hidden-original".to_string()),
|
||||
is_alias: true,
|
||||
context_window: Some(128_000),
|
||||
},
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "deepseek-chat".to_string(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
@@ -1483,12 +1513,16 @@ agent-default-model:
|
||||
"#;
|
||||
let models = vec![
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "gpt-selected".to_string(),
|
||||
alias: Some("Selected Model".to_string()),
|
||||
is_alias: false,
|
||||
context_window: Some(272_000),
|
||||
},
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "gpt-other".to_string(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
@@ -2067,12 +2101,16 @@ fn claude_desktop_uses_selected_alias_directly_with_original_context() {
|
||||
};
|
||||
let models = vec![
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "gpt-original".to_string(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
context_window: Some(1_000_000),
|
||||
},
|
||||
AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "gpt-high".to_string(),
|
||||
alias: Some("gpt-original".to_string()),
|
||||
is_alias: true,
|
||||
@@ -2306,6 +2344,8 @@ fn codex_model_list_is_empty_when_cpa_has_no_writable_models() {
|
||||
#[test]
|
||||
fn agent_model_validation_only_accepts_models_in_current_list() {
|
||||
let models = vec![AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: "gpt-5.4".to_string(),
|
||||
alias: Some("GPT 5.4".to_string()),
|
||||
is_alias: false,
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
use super::support::*;
|
||||
use super::*;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
fn models() -> Vec<AgentModelOption> {
|
||||
parse_agent_model_options(&json!({"data":[
|
||||
{"id":"vision","input_modalities":["text","image"],"context_window":128000},
|
||||
{"id":"text-only","inputModalities":["text"]},
|
||||
{"id":"unknown"}
|
||||
]}))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn root(home: &Path) -> Value {
|
||||
let paths = agent_config_paths(AgentClient::DeepSeekHarness, home);
|
||||
serde_json::to_value(
|
||||
serde_norway::from_str::<serde_norway::Value>(&fs::read_to_string(&paths[0]).unwrap())
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn provider(home: &Path) -> Value {
|
||||
root(home)["llm-pi-ai"]["providers"][DEEPSEEK_HARNESS_PROVIDER_ID].clone()
|
||||
}
|
||||
|
||||
fn apply(home: &Path, models: &[AgentModelOption]) -> AgentConfigActionResult {
|
||||
apply_agent_configuration(
|
||||
AgentClient::DeepSeekHarness,
|
||||
home,
|
||||
8317,
|
||||
"test-key",
|
||||
"vision",
|
||||
models,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn request(snapshot: HarnessEditorSnapshot) -> HarnessEditorRequest {
|
||||
HarnessEditorRequest {
|
||||
revision: snapshot.revision,
|
||||
models: snapshot.models,
|
||||
provider: snapshot.provider,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn harness_api_defaults_support_aliases_and_model_maps_without_guessing() {
|
||||
let models = parse_agent_model_options(&json!({"data":[
|
||||
{"id":"original","alias":"alias","fork":true,"supported_input_modalities":[" TEXT ","IMAGE","image"],"max_output_tokens":8192,"reasoning_efforts":{"off":null,"high":"high"}},
|
||||
{"id":"empty","input_modalities":[]}, {"id":"gpt-known-name"}
|
||||
]})).unwrap();
|
||||
assert_eq!(
|
||||
models[0].input_modalities,
|
||||
Some(vec!["text".into(), "image".into()])
|
||||
);
|
||||
assert_eq!(models[0].harness_metadata, models[1].harness_metadata);
|
||||
assert_eq!(models[1].name, "alias");
|
||||
assert_eq!(
|
||||
models[1].harness_metadata.as_ref().unwrap()["maxTokens"],
|
||||
8192
|
||||
);
|
||||
assert!(models[2..]
|
||||
.iter()
|
||||
.all(|model| model.input_modalities.is_none()));
|
||||
let mapped =
|
||||
parse_agent_model_options(&json!({"models":{"vision":{"input":["image"]},"unknown":{}}}))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
mapped
|
||||
.iter()
|
||||
.find(|m| m.name == "vision")
|
||||
.unwrap()
|
||||
.input_modalities,
|
||||
Some(vec!["image".into()])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn harness_discovery_uses_authenticated_models_endpoint() {
|
||||
use std::io::{Read, Write};
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
let server = std::thread::spawn(move || {
|
||||
let (mut socket, _) = listener.accept().unwrap();
|
||||
socket
|
||||
.set_read_timeout(Some(Duration::from_secs(5)))
|
||||
.unwrap();
|
||||
let mut request = Vec::new();
|
||||
let mut buffer = [0; 2048];
|
||||
while !request.windows(4).any(|w| w == b"\r\n\r\n") {
|
||||
let read = socket.read(&mut buffer).unwrap();
|
||||
assert!(read > 0);
|
||||
request.extend_from_slice(&buffer[..read]);
|
||||
}
|
||||
let request = String::from_utf8(request).unwrap().to_ascii_lowercase();
|
||||
assert!(request.starts_with("get /v1/models http/1.1\r\n"));
|
||||
assert!(request.contains("authorization: bearer test-harness-key\r\n"));
|
||||
assert!(!request.contains("client_version"));
|
||||
let body =
|
||||
r#"{"data":[{"id":"vision","input_modalities":["text","image"]},{"id":"unknown"}]}"#;
|
||||
write!(socket, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body).unwrap();
|
||||
});
|
||||
let models = fetch_agent_models(port, "test-harness-key").await.unwrap();
|
||||
server.join().unwrap();
|
||||
assert_eq!(models.len(), 2);
|
||||
assert!(models[1].input_modalities.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn harness_refreshes_api_defaults_and_preserves_catalog_overrides() {
|
||||
let home = agent_test_home("harness-catalog-refresh");
|
||||
let mut models = models();
|
||||
apply(&home, &models);
|
||||
assert_eq!(
|
||||
provider(&home)["models"][0]["input"],
|
||||
json!(["text", "image"])
|
||||
);
|
||||
assert!(provider(&home)["models"][2].get("input").is_none());
|
||||
assert_eq!(apply(&home, &models).outcome, "unchanged");
|
||||
let initial = harness_editor_snapshot(&home, &models, 8317).unwrap();
|
||||
assert!(initial.models.iter().all(|m| m.configuration.is_empty()));
|
||||
let mut edit = request(initial);
|
||||
edit.models[1].configuration = json!({"name":"Text model","maxTokens":4096,"reasoningEfforts":false,"compat":{"supportsDeveloperRole":false}}).as_object().unwrap().clone();
|
||||
edit.models[2].configuration = json!({"input":["text","image"]})
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.clone();
|
||||
edit.provider = json!({"defaultInput":["text","image"],"timeoutMs":60000,"retryPolicy":{"mode":"normal","maxRetries":3}}).as_object().unwrap().clone();
|
||||
save_harness_editor(&home, &models, 8317, edit).unwrap();
|
||||
models[0].input_modalities = Some(vec!["text".into()]);
|
||||
models[0].context_window = Some(256000);
|
||||
apply(&home, &models);
|
||||
let provider = provider(&home);
|
||||
assert_eq!(provider["models"][0]["input"], json!(["text"]));
|
||||
assert_eq!(provider["models"][0]["contextWindow"], 256000);
|
||||
assert_eq!(provider["models"][1]["name"], "Text model");
|
||||
assert_eq!(provider["models"][1]["maxTokens"], 4096);
|
||||
assert_eq!(provider["models"][2]["input"], json!(["text", "image"]));
|
||||
assert_eq!(provider["defaultInput"], json!(["text", "image"]));
|
||||
assert_eq!(root(&home)["agent-default-model"]["model"], "vision");
|
||||
let mut reset = request(harness_editor_snapshot(&home, &models, 8317).unwrap());
|
||||
reset
|
||||
.models
|
||||
.iter_mut()
|
||||
.for_each(|m| m.configuration.clear());
|
||||
reset.provider.clear();
|
||||
save_harness_editor(&home, &models, 8317, reset).unwrap();
|
||||
apply(&home, &models);
|
||||
let updated = self::provider(&home);
|
||||
assert!(updated["models"][1].get("maxTokens").is_none());
|
||||
assert!(updated["models"][1].get("compat").is_none());
|
||||
assert!(updated["models"][2].get("input").is_none());
|
||||
assert!(updated.get("defaultInput").is_none());
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn harness_saves_before_connection_and_applies_documented_protocol() {
|
||||
let home = agent_test_home("harness-catalog-draft");
|
||||
let mut models = models();
|
||||
models[2].harness_metadata = harness_api_metadata(&json!({"compat":{
|
||||
"supportsStore":false,"supportsMaxOutputTokens":false,"supportsTemperature":false
|
||||
}}));
|
||||
let mut edit = request(harness_editor_snapshot(&home, &models, 8317).unwrap());
|
||||
edit.provider = json!({"api":"openai-responses","compat":{"supportsMaxOutputTokens":false},"transport":"sse"}).as_object().unwrap().clone();
|
||||
edit.models[2]
|
||||
.configuration
|
||||
.insert("input".into(), json!(["text", "image"]));
|
||||
save_harness_editor(&home, &models, 8317, edit).unwrap();
|
||||
let paths = agent_config_paths(AgentClient::DeepSeekHarness, &home);
|
||||
assert!(!paths[0].exists());
|
||||
assert!(!paths[1].exists());
|
||||
apply(&home, &models);
|
||||
assert_eq!(provider(&home)["api"], "openai-responses");
|
||||
assert_eq!(
|
||||
provider(&home)["models"][2]["compat"],
|
||||
json!({"supportsMaxOutputTokens":false})
|
||||
);
|
||||
assert_eq!(
|
||||
provider(&home)["models"][2]["input"],
|
||||
json!(["text", "image"])
|
||||
);
|
||||
assert!(
|
||||
inspect_deepseek_harness_config(&paths, 8317, "test-key")
|
||||
.unwrap()
|
||||
.0
|
||||
);
|
||||
let mut edit = request(harness_editor_snapshot(&home, &models, 8317).unwrap());
|
||||
edit.provider = json!({"api":"anthropic-messages","compat":{"supportsTemperature":false}})
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.clone();
|
||||
save_harness_editor(&home, &models, 8317, edit).unwrap();
|
||||
assert_eq!(provider(&home)["baseURL"], "http://127.0.0.1:8317");
|
||||
assert_eq!(
|
||||
provider(&home)["models"][2]["compat"],
|
||||
json!({"supportsTemperature":false})
|
||||
);
|
||||
assert!(
|
||||
inspect_deepseek_harness_config(&paths, 8317, "test-key")
|
||||
.unwrap()
|
||||
.0
|
||||
);
|
||||
apply(&home, &models);
|
||||
assert_eq!(provider(&home)["baseURL"], "http://127.0.0.1:8317");
|
||||
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn harness_rejects_stale_catalog_and_invalid_fields_without_writing() {
|
||||
let home = agent_test_home("harness-catalog-validation");
|
||||
let models = models();
|
||||
apply(&home, &models);
|
||||
let mut changed_models = models.clone();
|
||||
changed_models[0].context_window = Some(64);
|
||||
let stale = request(harness_editor_snapshot(&home, &models, 8317).unwrap());
|
||||
assert!(save_harness_editor(&home, &changed_models, 8317, stale)
|
||||
.err()
|
||||
.unwrap()
|
||||
.contains("DSH_MODEL_CATALOG_CHANGED"));
|
||||
let paths = agent_config_paths(AgentClient::DeepSeekHarness, &home);
|
||||
let before = config_images(&paths).unwrap();
|
||||
for configuration in [
|
||||
json!({"contextWindow":0}),
|
||||
json!({"maxTokens":1.5}),
|
||||
json!({"input":["audio"]}),
|
||||
json!({"reasoningEfforts":{"high":null}}),
|
||||
json!({"compat":{"supportsTemperature":true}}),
|
||||
json!({"compat":{"supportsStore":null}}),
|
||||
json!({"compat":{"vllmPriority":0.5}}),
|
||||
json!({"unknownField":true}),
|
||||
] {
|
||||
let mut edit = request(harness_editor_snapshot(&home, &models, 8317).unwrap());
|
||||
edit.models[0].configuration = configuration.as_object().unwrap().clone();
|
||||
assert!(save_harness_editor(&home, &models, 8317, edit).is_err());
|
||||
assert_eq!(config_images(&paths).unwrap(), before);
|
||||
}
|
||||
for configuration in [
|
||||
json!({"defaultInput":[]}),
|
||||
json!({"streamIdleTimeoutMs":2147483648_u64}),
|
||||
json!({"headers":{"x-test":"bad\r\nheader"}}),
|
||||
json!({"retryPolicy":{"mode":"normal","backoff":{"initialDelayMs":20000}}}),
|
||||
json!({"apiKeyEnv":"UNMANAGED"}),
|
||||
] {
|
||||
let mut edit = request(harness_editor_snapshot(&home, &models, 8317).unwrap());
|
||||
edit.provider = configuration.as_object().unwrap().clone();
|
||||
assert!(save_harness_editor(&home, &models, 8317, edit).is_err());
|
||||
assert_eq!(config_images(&paths).unwrap(), before);
|
||||
}
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn harness_imports_external_edits_and_keeps_default_selection_and_other_settings() {
|
||||
let home = agent_test_home("harness-catalog-external");
|
||||
let models = models();
|
||||
apply(&home, &models);
|
||||
let paths = agent_config_paths(AgentClient::DeepSeekHarness, &home);
|
||||
let stale = request(harness_editor_snapshot(&home, &models, 8317).unwrap());
|
||||
let mut document = root(&home);
|
||||
document["agent-default-model"]["reasoningEffort"] = json!("high");
|
||||
document["unrelated"] = json!({"enabled":true});
|
||||
document["llm-pi-ai"]["providers"]["another"] = json!({"apiKeyEnv":"OTHER"});
|
||||
document["llm-pi-ai"]["providers"][DEEPSEEK_HARNESS_PROVIDER_ID]["models"][0]["name"] =
|
||||
json!("Manual name");
|
||||
document["llm-pi-ai"]["providers"][DEEPSEEK_HARNESS_PROVIDER_ID]["models"][0]["input"] =
|
||||
json!(["text"]);
|
||||
document["llm-pi-ai"]["providers"][DEEPSEEK_HARNESS_PROVIDER_ID]["models"][0]
|
||||
["futureExtension"] = json!({"keep":true});
|
||||
fs::write(&paths[0], serde_norway::to_string(&document).unwrap()).unwrap();
|
||||
assert!(save_harness_editor(&home, &models, 8317, stale).is_err());
|
||||
let edit = request(harness_editor_snapshot(&home, &models, 8317).unwrap());
|
||||
assert_eq!(edit.models[0].configuration["name"], "Manual name");
|
||||
save_harness_editor(&home, &models, 8317, edit).unwrap();
|
||||
apply(&home, &models);
|
||||
assert_eq!(
|
||||
root(&home)["agent-default-model"]["reasoningEffort"],
|
||||
"high"
|
||||
);
|
||||
assert_eq!(root(&home)["unrelated"], json!({"enabled":true}));
|
||||
assert_eq!(
|
||||
provider(&home)["models"][0]["futureExtension"],
|
||||
json!({"keep":true})
|
||||
);
|
||||
assert_eq!(provider(&home)["models"][0]["input"], json!(["text"]));
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn harness_state_failure_rolls_back_settings_and_credentials() {
|
||||
let home = agent_test_home("harness-catalog-rollback");
|
||||
apply(&home, &models());
|
||||
let mut changed = models();
|
||||
changed[0].input_modalities = Some(vec!["text".into()]);
|
||||
let (paths, before, after) =
|
||||
prepare_deepseek_harness_configuration(&home, 8317, "new-key", "vision", &changed).unwrap();
|
||||
let mut failed = false;
|
||||
let result = commit_config_with_writer(
|
||||
"deepseek-harness",
|
||||
&paths,
|
||||
&before,
|
||||
&after,
|
||||
"update",
|
||||
Some("vision".into()),
|
||||
None,
|
||||
&mut |client, images| {
|
||||
for image in images {
|
||||
if image.0 == paths[2] && !failed {
|
||||
failed = true;
|
||||
return Err("simulated failure".into());
|
||||
}
|
||||
write_config_images(client, &vec![image.clone()])?;
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(config_images(&paths).unwrap(), before);
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn harness_template_and_backup_restore_reconcile_catalog_state() {
|
||||
let home = agent_test_home("harness-catalog-template");
|
||||
let paths = agent_config_paths(AgentClient::DeepSeekHarness, &home);
|
||||
let before = config_images(&paths).unwrap();
|
||||
let settings =
|
||||
build_deepseek_harness_settings(None, "http://127.0.0.1:8317/v1", "vision", &models())
|
||||
.unwrap();
|
||||
let after = vec![
|
||||
(paths[0].clone(), Some(settings.into_bytes())),
|
||||
(
|
||||
paths[1].clone(),
|
||||
Some(
|
||||
build_deepseek_harness_credentials(None, "test-key")
|
||||
.unwrap()
|
||||
.into_bytes(),
|
||||
),
|
||||
),
|
||||
];
|
||||
commit_config(
|
||||
"deepseek-harness",
|
||||
&paths,
|
||||
&before,
|
||||
&after,
|
||||
"template",
|
||||
Some("vision".into()),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(harness_editor_snapshot(&home, &models(), 8317)
|
||||
.unwrap()
|
||||
.models
|
||||
.iter()
|
||||
.all(|m| m.configuration.is_empty()));
|
||||
let state_path = deepseek_harness_catalog_state_path(&paths).unwrap();
|
||||
assert!(state_path.is_file());
|
||||
commit_config(
|
||||
"deepseek-harness",
|
||||
&paths,
|
||||
&after,
|
||||
&after,
|
||||
"restore",
|
||||
Some("vision".into()),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!state_path.exists());
|
||||
assert_eq!(
|
||||
harness_editor_snapshot(&home, &models(), 8317)
|
||||
.unwrap()
|
||||
.models[0]
|
||||
.configuration["input"],
|
||||
json!(["text", "image"])
|
||||
);
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
@@ -17,6 +17,8 @@ pub(super) fn test_agent_models(names: &[&str]) -> Vec<AgentModelOption> {
|
||||
names
|
||||
.iter()
|
||||
.map(|name| AgentModelOption {
|
||||
input_modalities: None,
|
||||
harness_metadata: None,
|
||||
name: (*name).to_string(),
|
||||
alias: None,
|
||||
is_alias: false,
|
||||
|
||||
@@ -63,6 +63,104 @@ export const jaOverrides = {
|
||||
'config.software.defaultTerminal': '既定のターミナル',
|
||||
'config.software.defaultTerminalDescription': 'CLI エージェントの起動に使用するターミナルアプリ',
|
||||
'config.software.terminal.auto': '自動選択',
|
||||
"agents.harness.button": "モデル設定",
|
||||
"agents.harness.providerInput": "プロバイダーの既定入力を使用",
|
||||
"agents.harness.defaultModel": "デフォルトモデル",
|
||||
"agents.harness.defaultHint": "新しいセッションの初期モデルです。DSH 内で自由に切り替えられます。",
|
||||
"agents.harness.title": "DeepSeek Harness モデル設定",
|
||||
"agents.harness.subtitle": "API のモデル一覧を取得し、モデル能力とプロバイダー設定を調整します。",
|
||||
"agents.harness.provider": "プロバイダー設定",
|
||||
"agents.harness.auto": "自動",
|
||||
"agents.harness.apiSource": "API に従う",
|
||||
"agents.harness.inheritHint": "空欄は API または DSH の既定値に従います。",
|
||||
"agents.harness.apiInput": "API の入力形式:{value}",
|
||||
"agents.harness.unknown": "未提供(非対応とは限りません)",
|
||||
"agents.harness.managedConnection": "接続先:{url}。URL と認証は EasyCLIProxyAPI が管理します。",
|
||||
"agents.harness.reset": "自動に戻す",
|
||||
"agents.harness.resetGroup": "このグループをリセット",
|
||||
"agents.harness.yes": "有効",
|
||||
"agents.harness.no": "無効",
|
||||
"agents.harness.textOnly": "テキストのみ",
|
||||
"agents.harness.textImage": "テキストと画像",
|
||||
"agents.harness.reasoningDisabled": "推論なし",
|
||||
"agents.harness.reasoningCustom": "推論レベルを指定",
|
||||
"agents.harness.noWireValue": "パラメーターを省略",
|
||||
"agents.harness.invalid": "設定値を確認してください:{field}",
|
||||
"agents.harness.changed": "モデル一覧または設定が変更されました。再読み込みしてください。",
|
||||
"agents.harness.saved": "保存しました。次のリクエストから適用されます。デフォルトモデルは変更されません。",
|
||||
"agents.harness.savedDraft": "保存しました。DSH 接続時に適用されます。",
|
||||
"agents.harness.saveHint": "デフォルトモデルを変更せずに一覧の設定を保存します。",
|
||||
"agents.harness.hint.input": "API の情報を使用し、未提供の場合はプロバイダーの既定値に従います。",
|
||||
"agents.harness.hint.defaultInput": "API に入力情報がないモデルの既定値です。画像を有効にして、例外をモデルごとに変更できます。",
|
||||
"agents.harness.hint.maxTokens": "設定すると、リクエストごとの既定の出力上限にもなります。",
|
||||
"agents.harness.hint.defaultMaxTokens": "モデル能力の既定値です。リクエストの出力制限にはなりません。",
|
||||
"agents.harness.hint.api": "全モデルに同じプロトコルを使用します。変更後は互換性設定を確認してください。",
|
||||
"agents.harness.hint.headers": "ヘッダー名と値の JSON オブジェクト。空欄は既定値を使用します。",
|
||||
"agents.harness.hint.reasoningEfforts": "利用可能なレベルと送信する reasoning_effort 値を指定します。off のみ空欄にできます。",
|
||||
"agents.harness.hint.compat": "現在のプロトコルの互換性設定です。モデル設定が優先され、自動値は DSH が決定します。",
|
||||
"agents.harness.hint.thinkingBudgets": "推論予算に対応するプロバイダーの各レベルのトークン数。",
|
||||
"agents.harness.hint.retryPolicy": "normal は既定で最大 5 回再試行します。always は成功またはキャンセルまで続けます。",
|
||||
"agents.harness.hint.backoff": "再試行間隔とジッターを設定します。初期間隔は最大間隔以下にしてください。",
|
||||
"agents.harness.field.name": "表示名",
|
||||
"agents.harness.field.contextWindow": "コンテキスト(tokens)",
|
||||
"agents.harness.field.maxTokens": "出力上限(tokens)",
|
||||
"agents.harness.field.input": "入力形式",
|
||||
"agents.harness.field.reasoningEfforts": "推論レベル",
|
||||
"agents.harness.field.compat": "プロトコル互換性",
|
||||
"agents.harness.field.displayName": "プロバイダー名",
|
||||
"agents.harness.field.api": "API プロトコル",
|
||||
"agents.harness.field.defaultContextWindow": "既定コンテキスト(tokens)",
|
||||
"agents.harness.field.defaultMaxTokens": "既定出力能力(tokens)",
|
||||
"agents.harness.field.defaultInput": "既定入力形式",
|
||||
"agents.harness.field.reasoning": "既定推論レベル",
|
||||
"agents.harness.field.thinkingBudgets": "推論予算(tokens)",
|
||||
"agents.harness.field.cacheRetention": "キャッシュ保持",
|
||||
"agents.harness.field.transport": "ストリーミング方式",
|
||||
"agents.harness.field.timeoutMs": "リクエストタイムアウト(ms)",
|
||||
"agents.harness.field.websocketConnectTimeoutMs": "WebSocket 接続タイムアウト(ms)",
|
||||
"agents.harness.field.streamIdleTimeoutMs": "ストリーム待機タイムアウト(ms)",
|
||||
"agents.harness.field.maxRequestImageBytes": "リクエスト画像合計(bytes)",
|
||||
"agents.harness.field.requestImagePixelBudget": "画像ピクセル予算",
|
||||
"agents.harness.field.requestImageMaxBytes": "画像サイズ(bytes)",
|
||||
"agents.harness.field.headers": "追加ヘッダー(JSON)",
|
||||
"agents.harness.field.retryPolicy": "再試行ポリシー",
|
||||
"agents.harness.field.minimal": "minimal",
|
||||
"agents.harness.field.low": "low",
|
||||
"agents.harness.field.medium": "medium",
|
||||
"agents.harness.field.high": "high",
|
||||
"agents.harness.field.mode": "再試行モード",
|
||||
"agents.harness.field.maxRetries": "最大再試行回数",
|
||||
"agents.harness.field.retryableCodes": "再試行するエラー(JSON)",
|
||||
"agents.harness.field.backoff": "再試行間隔",
|
||||
"agents.harness.field.initialDelayMs": "初期間隔(ms)",
|
||||
"agents.harness.field.maxDelayMs": "最大間隔(ms)",
|
||||
"agents.harness.field.jitterRatio": "ジッター比率(0–1)",
|
||||
"agents.harness.field.supportsStore": "supportsStore",
|
||||
"agents.harness.field.supportsDeveloperRole": "supportsDeveloperRole",
|
||||
"agents.harness.field.supportsReasoningEffort": "supportsReasoningEffort",
|
||||
"agents.harness.field.supportsUsageInStreaming": "supportsUsageInStreaming",
|
||||
"agents.harness.field.supportsFinishReason": "supportsFinishReason",
|
||||
"agents.harness.field.maxTokensField": "maxTokensField",
|
||||
"agents.harness.field.requiresToolResultName": "requiresToolResultName",
|
||||
"agents.harness.field.requiresAssistantAfterToolResult": "requiresAssistantAfterToolResult",
|
||||
"agents.harness.field.requiresThinkingAsText": "requiresThinkingAsText",
|
||||
"agents.harness.field.requiresReasoningContentOnAssistantMessages": "requiresReasoningContentOnAssistantMessages",
|
||||
"agents.harness.field.thinkingFormat": "thinkingFormat",
|
||||
"agents.harness.field.chatTemplateKwargs": "chatTemplateKwargs",
|
||||
"agents.harness.field.chatTemplateArgs": "chatTemplateArgs",
|
||||
"agents.harness.field.supportsThinkingTokenBudget": "supportsThinkingTokenBudget",
|
||||
"agents.harness.field.thinkingTokenBudgetField": "thinkingTokenBudgetField",
|
||||
"agents.harness.field.vllmPriority": "vllmPriority",
|
||||
"agents.harness.field.supportsMaxOutputTokens": "supportsMaxOutputTokens",
|
||||
"agents.harness.field.supportsStrictMode": "supportsStrictMode",
|
||||
"agents.harness.field.cacheControlFormat": "cacheControlFormat",
|
||||
"agents.harness.field.supportsLongCacheRetention": "supportsLongCacheRetention",
|
||||
"agents.harness.field.supportsEagerToolInputStreaming": "supportsEagerToolInputStreaming",
|
||||
"agents.harness.field.supportsCacheControlOnTools": "supportsCacheControlOnTools",
|
||||
"agents.harness.field.supportsTemperature": "supportsTemperature",
|
||||
"agents.harness.field.forceAdaptiveThinking": "forceAdaptiveThinking",
|
||||
"agents.harness.field.allowEmptySignature": "allowEmptySignature",
|
||||
"agents.harness.field.supportsStrictTools": "supportsStrictTools",
|
||||
'agents.catalog.button': 'モデル一覧の設定',
|
||||
'agents.catalog.title': 'Codex モデル一覧',
|
||||
'agents.catalog.subtitle': 'コンテキストの既定値は CPA コア API から取得します。モデルのパラメーターを編集し、Codex モデル一覧に保存できます。',
|
||||
|
||||
@@ -13,6 +13,104 @@ export const en: Record<MessageKey, string> = {
|
||||
'apiAccess.cloak.never': 'Never enabled',
|
||||
'usage.unit.requests': 'requests',
|
||||
'usage.unit.tokens': 'Token',
|
||||
"agents.harness.button": "Configure models",
|
||||
"agents.harness.providerInput": "Use provider default input",
|
||||
"agents.harness.defaultModel": "Default model",
|
||||
"agents.harness.defaultHint": "Used for new sessions. You can switch to any catalog model inside DSH.",
|
||||
"agents.harness.title": "DeepSeek Harness model configuration",
|
||||
"agents.harness.subtitle": "Load the model catalog from the API and customize models and provider settings.",
|
||||
"agents.harness.provider": "Provider settings",
|
||||
"agents.harness.auto": "Automatic",
|
||||
"agents.harness.apiSource": "Follow API",
|
||||
"agents.harness.inheritHint": "Leave fields empty to follow the API or DSH defaults.",
|
||||
"agents.harness.apiInput": "API input modalities: {value}",
|
||||
"agents.harness.unknown": "Not provided (capability unknown)",
|
||||
"agents.harness.managedConnection": "Endpoint: {url}. EasyCLIProxyAPI manages the endpoint and credentials.",
|
||||
"agents.harness.reset": "Restore automatic",
|
||||
"agents.harness.resetGroup": "Reset this group",
|
||||
"agents.harness.yes": "Enabled",
|
||||
"agents.harness.no": "Disabled",
|
||||
"agents.harness.textOnly": "Text only",
|
||||
"agents.harness.textImage": "Text and image",
|
||||
"agents.harness.reasoningDisabled": "No reasoning",
|
||||
"agents.harness.reasoningCustom": "Custom reasoning levels",
|
||||
"agents.harness.noWireValue": "Omit parameter",
|
||||
"agents.harness.invalid": "Check the configuration value: {field}",
|
||||
"agents.harness.changed": "The model list or configuration changed. Reload before saving.",
|
||||
"agents.harness.saved": "Saved. Applies on the next model request; the default model is unchanged.",
|
||||
"agents.harness.savedDraft": "Saved. These settings will be used when connecting DSH.",
|
||||
"agents.harness.saveHint": "Save catalog settings without changing the selected default model.",
|
||||
"agents.harness.hint.input": "Use API metadata automatically; otherwise inherit the provider default input.",
|
||||
"agents.harness.hint.defaultInput": "Fallback for models without API input metadata. Set text and image here, then override exceptions per model.",
|
||||
"agents.harness.hint.maxTokens": "An explicit value also becomes the model’s default output limit per request.",
|
||||
"agents.harness.hint.defaultMaxTokens": "Fallback output capacity; does not set a per-request output limit.",
|
||||
"agents.harness.hint.api": "One protocol applies to the whole provider. Review compatibility settings after changing it.",
|
||||
"agents.harness.hint.headers": "JSON object of header names and values; empty uses default headers.",
|
||||
"agents.harness.hint.reasoningEfforts": "Choose offered levels and their wire reasoning_effort values. Only off may have an empty wire value.",
|
||||
"agents.harness.hint.compat": "Compatibility switches for the current protocol. Model settings override provider settings; DSH resolves automatic values.",
|
||||
"agents.harness.hint.thinkingBudgets": "Token budgets for providers that support reasoning budgets.",
|
||||
"agents.harness.hint.retryPolicy": "Normal retries up to five times by default. Always retries until success or cancellation.",
|
||||
"agents.harness.hint.backoff": "Configure retry delays and jitter. Initial delay must not exceed maximum delay.",
|
||||
"agents.harness.field.name": "Display name",
|
||||
"agents.harness.field.contextWindow": "Context window (tokens)",
|
||||
"agents.harness.field.maxTokens": "Maximum output (tokens)",
|
||||
"agents.harness.field.input": "Input modalities",
|
||||
"agents.harness.field.reasoningEfforts": "Reasoning levels",
|
||||
"agents.harness.field.compat": "Protocol compatibility",
|
||||
"agents.harness.field.displayName": "Provider name",
|
||||
"agents.harness.field.api": "API protocol",
|
||||
"agents.harness.field.defaultContextWindow": "Default context (tokens)",
|
||||
"agents.harness.field.defaultMaxTokens": "Default output capacity (tokens)",
|
||||
"agents.harness.field.defaultInput": "Default input modalities",
|
||||
"agents.harness.field.reasoning": "Default reasoning level",
|
||||
"agents.harness.field.thinkingBudgets": "Thinking budgets (tokens)",
|
||||
"agents.harness.field.cacheRetention": "Cache retention",
|
||||
"agents.harness.field.transport": "Streaming transport",
|
||||
"agents.harness.field.timeoutMs": "Request timeout (ms)",
|
||||
"agents.harness.field.websocketConnectTimeoutMs": "WebSocket connection timeout (ms)",
|
||||
"agents.harness.field.streamIdleTimeoutMs": "Stream idle timeout (ms)",
|
||||
"agents.harness.field.maxRequestImageBytes": "Total request image budget (bytes)",
|
||||
"agents.harness.field.requestImagePixelBudget": "Pixels per image",
|
||||
"agents.harness.field.requestImageMaxBytes": "Encoded bytes per image",
|
||||
"agents.harness.field.headers": "Additional headers (JSON)",
|
||||
"agents.harness.field.retryPolicy": "Retry policy",
|
||||
"agents.harness.field.minimal": "minimal",
|
||||
"agents.harness.field.low": "low",
|
||||
"agents.harness.field.medium": "medium",
|
||||
"agents.harness.field.high": "high",
|
||||
"agents.harness.field.mode": "Retry mode",
|
||||
"agents.harness.field.maxRetries": "Maximum retries",
|
||||
"agents.harness.field.retryableCodes": "Retryable error codes (JSON)",
|
||||
"agents.harness.field.backoff": "Retry backoff",
|
||||
"agents.harness.field.initialDelayMs": "Initial delay (ms)",
|
||||
"agents.harness.field.maxDelayMs": "Maximum delay (ms)",
|
||||
"agents.harness.field.jitterRatio": "Jitter ratio (0–1)",
|
||||
"agents.harness.field.supportsStore": "supportsStore",
|
||||
"agents.harness.field.supportsDeveloperRole": "supportsDeveloperRole",
|
||||
"agents.harness.field.supportsReasoningEffort": "supportsReasoningEffort",
|
||||
"agents.harness.field.supportsUsageInStreaming": "supportsUsageInStreaming",
|
||||
"agents.harness.field.supportsFinishReason": "supportsFinishReason",
|
||||
"agents.harness.field.maxTokensField": "maxTokensField",
|
||||
"agents.harness.field.requiresToolResultName": "requiresToolResultName",
|
||||
"agents.harness.field.requiresAssistantAfterToolResult": "requiresAssistantAfterToolResult",
|
||||
"agents.harness.field.requiresThinkingAsText": "requiresThinkingAsText",
|
||||
"agents.harness.field.requiresReasoningContentOnAssistantMessages": "requiresReasoningContentOnAssistantMessages",
|
||||
"agents.harness.field.thinkingFormat": "thinkingFormat",
|
||||
"agents.harness.field.chatTemplateKwargs": "chatTemplateKwargs",
|
||||
"agents.harness.field.chatTemplateArgs": "chatTemplateArgs",
|
||||
"agents.harness.field.supportsThinkingTokenBudget": "supportsThinkingTokenBudget",
|
||||
"agents.harness.field.thinkingTokenBudgetField": "thinkingTokenBudgetField",
|
||||
"agents.harness.field.vllmPriority": "vllmPriority",
|
||||
"agents.harness.field.supportsMaxOutputTokens": "supportsMaxOutputTokens",
|
||||
"agents.harness.field.supportsStrictMode": "supportsStrictMode",
|
||||
"agents.harness.field.cacheControlFormat": "cacheControlFormat",
|
||||
"agents.harness.field.supportsLongCacheRetention": "supportsLongCacheRetention",
|
||||
"agents.harness.field.supportsEagerToolInputStreaming": "supportsEagerToolInputStreaming",
|
||||
"agents.harness.field.supportsCacheControlOnTools": "supportsCacheControlOnTools",
|
||||
"agents.harness.field.supportsTemperature": "supportsTemperature",
|
||||
"agents.harness.field.forceAdaptiveThinking": "forceAdaptiveThinking",
|
||||
"agents.harness.field.allowEmptySignature": "allowEmptySignature",
|
||||
"agents.harness.field.supportsStrictTools": "supportsStrictTools",
|
||||
'agents.catalog.button': 'Model list settings',
|
||||
'agents.catalog.title': 'Codex model catalog',
|
||||
'agents.catalog.subtitle': 'Default context settings come from the CPA core API. Customize model parameters and save them to the Codex catalog.',
|
||||
|
||||
@@ -12,6 +12,104 @@ export const zhCN = {
|
||||
'apiAccess.cloak.never': '从不启用',
|
||||
'usage.unit.requests': '次请求',
|
||||
'usage.unit.tokens': 'Token',
|
||||
"agents.harness.button": "配置模型",
|
||||
"agents.harness.providerInput": "使用供应商默认输入",
|
||||
"agents.harness.defaultModel": "默认模型",
|
||||
"agents.harness.defaultHint": "仅作为新会话的默认模型;启动 DSH 后仍可自由切换整个目录中的模型。",
|
||||
"agents.harness.title": "DeepSeek Harness 模型配置",
|
||||
"agents.harness.subtitle": "从 API 获取整个模型目录,按需覆盖模型能力和供应商参数。",
|
||||
"agents.harness.provider": "供应商设置",
|
||||
"agents.harness.auto": "自动",
|
||||
"agents.harness.apiSource": "跟随 API",
|
||||
"agents.harness.inheritHint": "留空即跟随 API 或 DSH 默认值。",
|
||||
"agents.harness.apiInput": "API 声明的输入类型:{value}",
|
||||
"agents.harness.unknown": "未提供(不代表不支持)",
|
||||
"agents.harness.managedConnection": "连接地址:{url}。地址和鉴权由 EasyCLIProxyAPI 管理。",
|
||||
"agents.harness.reset": "恢复自动",
|
||||
"agents.harness.resetGroup": "此组恢复自动",
|
||||
"agents.harness.yes": "开启",
|
||||
"agents.harness.no": "关闭",
|
||||
"agents.harness.textOnly": "仅文本",
|
||||
"agents.harness.textImage": "文本和图像",
|
||||
"agents.harness.reasoningDisabled": "不支持推理",
|
||||
"agents.harness.reasoningCustom": "自定义推理等级",
|
||||
"agents.harness.noWireValue": "不发送参数",
|
||||
"agents.harness.invalid": "请检查配置值:{field}",
|
||||
"agents.harness.changed": "模型列表或配置已变化,请重新加载后再保存。",
|
||||
"agents.harness.saved": "已保存,下次模型请求时生效;默认模型保持不变。",
|
||||
"agents.harness.savedDraft": "已保存,接入 DSH 时会使用这些配置。",
|
||||
"agents.harness.saveHint": "保存整个目录的配置,不改变页面选择的默认模型。",
|
||||
"agents.harness.hint.input": "自动使用 API 声明;未声明时继承供应商默认输入类型。",
|
||||
"agents.harness.hint.defaultInput": "仅用于 API 未声明输入类型的模型。可统一设为文本和图像,再单独修正例外模型。",
|
||||
"agents.harness.hint.maxTokens": "手动设置后也会成为该模型每次请求的默认输出上限。",
|
||||
"agents.harness.hint.defaultMaxTokens": "模型的输出能力兜底值,不会自动成为每次请求的输出限制。",
|
||||
"agents.harness.hint.api": "整个供应商使用同一种协议;切换协议后请检查兼容选项。",
|
||||
"agents.harness.hint.headers": "JSON 键值对象;留空使用默认请求头。",
|
||||
"agents.harness.hint.reasoningEfforts": "勾选 DSH 中可选的等级,并填写实际发送的 reasoning_effort 值。只有 off 可留空。",
|
||||
"agents.harness.hint.compat": "仅显示当前协议的兼容开关。单个模型的设置优先于供应商设置,自动值由 DSH 决定。",
|
||||
"agents.harness.hint.thinkingBudgets": "为支持推理预算的供应商指定各等级的 token 预算。",
|
||||
"agents.harness.hint.retryPolicy": "normal 默认最多重试 5 次;always 会持续重试直到成功或取消。",
|
||||
"agents.harness.hint.backoff": "配置重试间隔与抖动;初始间隔不能大于最大间隔。",
|
||||
"agents.harness.field.name": "显示名称",
|
||||
"agents.harness.field.contextWindow": "上下文窗口(tokens)",
|
||||
"agents.harness.field.maxTokens": "输出上限(tokens)",
|
||||
"agents.harness.field.input": "输入类型",
|
||||
"agents.harness.field.reasoningEfforts": "推理等级",
|
||||
"agents.harness.field.compat": "协议兼容选项",
|
||||
"agents.harness.field.displayName": "供应商名称",
|
||||
"agents.harness.field.api": "API 协议",
|
||||
"agents.harness.field.defaultContextWindow": "默认上下文(tokens)",
|
||||
"agents.harness.field.defaultMaxTokens": "默认输出能力(tokens)",
|
||||
"agents.harness.field.defaultInput": "默认输入类型",
|
||||
"agents.harness.field.reasoning": "默认推理等级",
|
||||
"agents.harness.field.thinkingBudgets": "推理预算(tokens)",
|
||||
"agents.harness.field.cacheRetention": "缓存保留",
|
||||
"agents.harness.field.transport": "流式传输方式",
|
||||
"agents.harness.field.timeoutMs": "请求超时(ms)",
|
||||
"agents.harness.field.websocketConnectTimeoutMs": "WebSocket 连接超时(ms)",
|
||||
"agents.harness.field.streamIdleTimeoutMs": "流式空闲超时(ms)",
|
||||
"agents.harness.field.maxRequestImageBytes": "请求图像总预算(bytes)",
|
||||
"agents.harness.field.requestImagePixelBudget": "单图像素预算",
|
||||
"agents.harness.field.requestImageMaxBytes": "单图编码预算(bytes)",
|
||||
"agents.harness.field.headers": "额外请求头(JSON)",
|
||||
"agents.harness.field.retryPolicy": "重试策略",
|
||||
"agents.harness.field.minimal": "minimal",
|
||||
"agents.harness.field.low": "low",
|
||||
"agents.harness.field.medium": "medium",
|
||||
"agents.harness.field.high": "high",
|
||||
"agents.harness.field.mode": "重试模式",
|
||||
"agents.harness.field.maxRetries": "最大重试次数",
|
||||
"agents.harness.field.retryableCodes": "可重试错误码(JSON)",
|
||||
"agents.harness.field.backoff": "重试间隔",
|
||||
"agents.harness.field.initialDelayMs": "初始间隔(ms)",
|
||||
"agents.harness.field.maxDelayMs": "最大间隔(ms)",
|
||||
"agents.harness.field.jitterRatio": "抖动比例(0–1)",
|
||||
"agents.harness.field.supportsStore": "supportsStore",
|
||||
"agents.harness.field.supportsDeveloperRole": "supportsDeveloperRole",
|
||||
"agents.harness.field.supportsReasoningEffort": "supportsReasoningEffort",
|
||||
"agents.harness.field.supportsUsageInStreaming": "supportsUsageInStreaming",
|
||||
"agents.harness.field.supportsFinishReason": "supportsFinishReason",
|
||||
"agents.harness.field.maxTokensField": "maxTokensField",
|
||||
"agents.harness.field.requiresToolResultName": "requiresToolResultName",
|
||||
"agents.harness.field.requiresAssistantAfterToolResult": "requiresAssistantAfterToolResult",
|
||||
"agents.harness.field.requiresThinkingAsText": "requiresThinkingAsText",
|
||||
"agents.harness.field.requiresReasoningContentOnAssistantMessages": "requiresReasoningContentOnAssistantMessages",
|
||||
"agents.harness.field.thinkingFormat": "thinkingFormat",
|
||||
"agents.harness.field.chatTemplateKwargs": "chatTemplateKwargs",
|
||||
"agents.harness.field.chatTemplateArgs": "chatTemplateArgs",
|
||||
"agents.harness.field.supportsThinkingTokenBudget": "supportsThinkingTokenBudget",
|
||||
"agents.harness.field.thinkingTokenBudgetField": "thinkingTokenBudgetField",
|
||||
"agents.harness.field.vllmPriority": "vllmPriority",
|
||||
"agents.harness.field.supportsMaxOutputTokens": "supportsMaxOutputTokens",
|
||||
"agents.harness.field.supportsStrictMode": "supportsStrictMode",
|
||||
"agents.harness.field.cacheControlFormat": "cacheControlFormat",
|
||||
"agents.harness.field.supportsLongCacheRetention": "supportsLongCacheRetention",
|
||||
"agents.harness.field.supportsEagerToolInputStreaming": "supportsEagerToolInputStreaming",
|
||||
"agents.harness.field.supportsCacheControlOnTools": "supportsCacheControlOnTools",
|
||||
"agents.harness.field.supportsTemperature": "supportsTemperature",
|
||||
"agents.harness.field.forceAdaptiveThinking": "forceAdaptiveThinking",
|
||||
"agents.harness.field.allowEmptySignature": "allowEmptySignature",
|
||||
"agents.harness.field.supportsStrictTools": "supportsStrictTools",
|
||||
'agents.catalog.button': '模型列表设置',
|
||||
'agents.catalog.title': 'Codex 模型列表',
|
||||
'agents.catalog.subtitle': '默认上下文参数读取自 CPA 内核 API;可自定义当前模型参数并保存到 Codex 模型列表。',
|
||||
|
||||
@@ -63,6 +63,7 @@ import type { ModelOption } from '../services/modelService';
|
||||
import { getCurrentLocale, translate, useI18n } from '../i18n';
|
||||
import { CodexSessionsPanel } from './CodexSessionsPanel';
|
||||
import { CodexModelCatalogDialog } from './CodexModelCatalogDialog';
|
||||
import { DeepSeekHarnessCatalogDialog } from './DeepSeekHarnessCatalogDialog';
|
||||
import { AgentConfigBackupDialog } from './AgentConfigBackupDialog';
|
||||
import { AgentConfigManagementPanel, AgentConfigurationFeedback, AgentRunControls } from './AgentControls';
|
||||
|
||||
@@ -753,6 +754,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
const [clearConfirmOpen, setClearConfirmOpen] = useState(false);
|
||||
const [launchDirectoryDialogOpen, setLaunchDirectoryDialogOpen] = useState(false);
|
||||
const [codexCatalogDialogOpen, setCodexCatalogDialogOpen] = useState(false);
|
||||
const [harnessCatalogDialogOpen, setHarnessCatalogDialogOpen] = useState(false);
|
||||
const [launchDirectory, setLaunchDirectory] = useState('');
|
||||
const [launchDirectoryTarget, setLaunchDirectoryTarget] = useState<AgentLaunchTarget | null>(null);
|
||||
const [launchDirectoryError, setLaunchDirectoryError] = useState('');
|
||||
@@ -1326,6 +1328,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
claudeModelMappingsDirtyRef.current[selected] = false;
|
||||
}
|
||||
await reloadStatusesAfterAction();
|
||||
if (isDeepSeekHarnessClient) await loadModels('deepseek-harness');
|
||||
setOauthConfigurationDraft(null);
|
||||
setConfigurationNotice(t(result.outcome === 'unchanged' ? 'agents.backup.unchanged' : 'agents.backup.updated'));
|
||||
onConfigurationApplied?.();
|
||||
@@ -1686,6 +1689,13 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
const harnessCatalogButton = isDeepSeekHarnessClient ? (
|
||||
<button type="button" className="secondary-button agent-codex-catalog-button"
|
||||
onClick={() => setHarnessCatalogDialogOpen(true)} disabled={busy || configurationWriteBlocked}>
|
||||
<SlidersHorizontal size={16} />{t('agents.harness.button')}
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<section className={`page management-page agents-page${embedded ? ' agents-page-embedded' : ''}`}>
|
||||
<header className="management-header">
|
||||
@@ -1796,7 +1806,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
|
||||
{selected === 'claude-desktop' && !activeStatus?.claudeDesktopModelMappings ? <p className="agent-inline-message warning">{t('agents.backup.mappingRequired')}</p> : null}
|
||||
<div className="agent-minimal-field">
|
||||
<label htmlFor="embedded-agent-model">{t('agents.useModel')}</label>
|
||||
<label htmlFor="embedded-agent-model">{t(isDeepSeekHarnessClient ? 'agents.harness.defaultModel' : 'agents.useModel')}</label>
|
||||
<AgentModelPicker
|
||||
models={isClaudeModelMappingClient ? claudeMappingModels : models}
|
||||
value={isClaudeModelMappingClient ? claudeModelMappingsDraft.sonnet : selectedModel}
|
||||
@@ -1806,9 +1816,11 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
onChange={selectEmbeddedModel}
|
||||
onRefresh={refreshModels}
|
||||
/>
|
||||
{codexCatalogButton}
|
||||
{codexCatalogButton}{harnessCatalogButton}
|
||||
</div>
|
||||
|
||||
{isDeepSeekHarnessClient ? <p className="agent-model-hint">{t('agents.harness.defaultHint')}</p> : null}
|
||||
|
||||
<div className="agent-minimal-actions">
|
||||
<button
|
||||
type="button"
|
||||
@@ -1894,7 +1906,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
{!isClaudeModelMappingClient ? (
|
||||
<section className="agent-core-setting-section agent-model-section">
|
||||
<div className="agent-section-heading">
|
||||
<div><strong>{t('agents.useModel')}</strong></div>
|
||||
<div><strong>{t(isDeepSeekHarnessClient ? 'agents.harness.defaultModel' : 'agents.useModel')}</strong></div>
|
||||
</div>
|
||||
<AgentModelPicker
|
||||
models={models}
|
||||
@@ -1914,6 +1926,8 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
{modelHint}
|
||||
</span>
|
||||
) : null}
|
||||
{isDeepSeekHarnessClient ? <p className="agent-model-hint">{t('agents.harness.defaultHint')}</p> : null}
|
||||
{harnessCatalogButton}
|
||||
{selected === 'codex' ? (
|
||||
<div className="agent-codex-options">
|
||||
<div className="agent-auth-method">
|
||||
@@ -2140,6 +2154,10 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
setConfigurationNotice(t('agents.backup.restored'));
|
||||
}} /> : null}
|
||||
|
||||
{harnessCatalogDialogOpen ? (
|
||||
<DeepSeekHarnessCatalogDialog onClose={() => setHarnessCatalogDialogOpen(false)}
|
||||
onSaved={async () => { await loadModels('deepseek-harness', selectedModel); await loadStatuses(true); }} />
|
||||
) : null}
|
||||
{codexCatalogDialogOpen ? (
|
||||
<CodexModelCatalogDialog
|
||||
onClose={() => setCodexCatalogDialogOpen(false)}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { LoaderCircle, RefreshCw, RotateCcw, Search, Settings2, X } from 'lucide-react';
|
||||
import { useI18n } from '../i18n';
|
||||
import type { MessageKey } from '../i18n/resources';
|
||||
import {
|
||||
harnessDraft, harnessReasoningLevels, harnessSchema, isHarnessRecord, parseHarnessDraft, sameHarnessDraft, updateHarnessDraft,
|
||||
type HarnessDraft, type HarnessEditorSnapshot, type HarnessProfile,
|
||||
} from '../services/deepSeekHarnessCatalog';
|
||||
|
||||
function HarnessFields({ group, draft, defaults = {}, prefix = '', api, onChange }: {
|
||||
group: string; draft: HarnessDraft; defaults?: HarnessProfile; prefix?: string; api: string;
|
||||
onChange: (next: HarnessDraft) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const label = (name: string) => t(`agents.harness.field.${name}` as MessageKey);
|
||||
return <div className="codex-catalog-form harness-catalog-form">{harnessSchema[group].map(field => {
|
||||
const key = `${prefix}${field.name}`;
|
||||
const value = draft[key] ?? '';
|
||||
const supplied = defaults[field.name] ?? field.default;
|
||||
const automatic = `${t('agents.harness.auto')}${supplied === undefined ? '' : ` (${typeof supplied === 'string' ? supplied : JSON.stringify(supplied)})`}`;
|
||||
const set = (next: string) => onChange(updateHarnessDraft(draft, key, next));
|
||||
if (field.apis && !field.apis.includes(api) && !value) return null;
|
||||
if (field.group) {
|
||||
const customized = Object.keys(draft).some(k => k.startsWith(`${key}.`));
|
||||
return <details className="harness-catalog-group" key={key}>
|
||||
<summary>{label(field.name)}{customized ? ` · ${t('agents.catalog.customized')}` : ''}</summary>
|
||||
<p className="codex-catalog-hint">{t(`agents.harness.hint.${field.name}` as MessageKey)}</p>
|
||||
<HarnessFields group={field.group} draft={draft} defaults={isHarnessRecord(supplied) ? supplied : {}} prefix={`${key}.`} api={api} onChange={onChange} />
|
||||
{customized ? <button type="button" className="secondary-button" onClick={() => onChange(Object.fromEntries(Object.entries(draft).filter(([k]) => !k.startsWith(`${key}.`))))}>{t('agents.harness.resetGroup')}</button> : null}
|
||||
</details>;
|
||||
}
|
||||
if (field.kind === 'reasoning') {
|
||||
let mapping: HarnessProfile = {};
|
||||
try { const parsed: unknown = JSON.parse(value || '{}'); if (isHarnessRecord(parsed)) mapping = parsed; } catch {}
|
||||
return <div className="harness-reasoning wide" key={key}>
|
||||
<label><span>{label(field.name)}</span><select aria-label={label(field.name)} value={!value ? '' : value === 'false' ? 'false' : 'custom'}
|
||||
onChange={e => set(e.currentTarget.value === 'custom' ? JSON.stringify({ off: null, low: 'low', medium: 'medium', high: 'high' }) : e.currentTarget.value)}>
|
||||
<option value="">{automatic}</option><option value="false">{t('agents.harness.reasoningDisabled')}</option><option value="custom">{t('agents.harness.reasoningCustom')}</option>
|
||||
</select></label>
|
||||
{value && value !== 'false' ? <div className="harness-reasoning-levels">{harnessReasoningLevels.map(level => <div key={level}>
|
||||
<label><input type="checkbox" checked={Object.prototype.hasOwnProperty.call(mapping, level)} onChange={e => {
|
||||
const next = { ...mapping }; if (e.currentTarget.checked) next[level] = level === 'off' ? null : level; else delete next[level]; set(JSON.stringify(next));
|
||||
}} />{level}</label>
|
||||
{Object.prototype.hasOwnProperty.call(mapping, level) ? <input aria-label={`${level} reasoning_effort`} value={String(mapping[level] ?? '')} placeholder={level === 'off' ? t('agents.harness.noWireValue') : level}
|
||||
onChange={e => set(JSON.stringify({ ...mapping, [level]: level === 'off' && !e.currentTarget.value ? null : e.currentTarget.value }))} /> : null}
|
||||
</div>)}</div> : null}
|
||||
<small>{t('agents.harness.hint.reasoningEfforts')}</small>
|
||||
</div>;
|
||||
}
|
||||
const jsonField = ['headers', 'kwargs', 'strings'].includes(field.kind);
|
||||
return <label key={key} className={jsonField ? 'wide' : undefined}>
|
||||
<span title={field.name}>{label(field.name)}{Object.prototype.hasOwnProperty.call(draft, key) ? <small className="harness-override">{t('agents.catalog.customized')}</small> : null}</span>
|
||||
{['enum', 'boolean', 'modalities'].includes(field.kind) ? <select aria-label={label(field.name)} value={value} onChange={e => set(e.currentTarget.value)}>
|
||||
<option value="">{automatic}</option>
|
||||
{field.kind === 'boolean' ? <><option value="true">{t('agents.harness.yes')}</option><option value="false">{t('agents.harness.no')}</option></> : null}
|
||||
{field.kind === 'enum' ? field.values!.map(v => <option key={v} value={v}>{v}</option>) : null}
|
||||
{field.kind === 'modalities' ? <>{field.name === 'input' ? <option value="[]">{t('agents.harness.providerInput')}</option> : null}<option value='["text"]'>{t('agents.harness.textOnly')}</option><option value='["text","image"]'>{t('agents.harness.textImage')}</option><option value='["image"]'>{t('agents.catalog.image')}</option></> : null}
|
||||
</select> : jsonField ? <textarea aria-label={label(field.name)} rows={4} spellCheck={false} value={value} placeholder={JSON.stringify(supplied ?? field.example, null, 2) ?? automatic} onChange={e => set(e.currentTarget.value)} />
|
||||
: <input aria-label={label(field.name)} type={['integer', 'number'].includes(field.kind) ? 'number' : 'text'} min={field.min ?? field.exclusiveMin} max={field.max} step={field.kind === 'integer' ? 1 : 'any'} value={value} placeholder={automatic} onChange={e => set(e.currentTarget.value)} />}
|
||||
{['input', 'defaultInput', 'maxTokens', 'defaultMaxTokens', 'api', 'headers'].includes(field.name) ? <small>{t(`agents.harness.hint.${field.name}` as MessageKey)}</small> : null}
|
||||
</label>;
|
||||
})}</div>;
|
||||
}
|
||||
|
||||
export function DeepSeekHarnessCatalogDialog({ onClose, onSaved }: { onClose: () => void; onSaved: () => void | Promise<void> }) {
|
||||
const { t } = useI18n();
|
||||
const [snapshot, setSnapshot] = useState<HarnessEditorSnapshot | null>(null);
|
||||
const [drafts, setDrafts] = useState<Record<string, HarnessDraft>>({});
|
||||
const [provider, setProvider] = useState<HarnessDraft>({});
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [search, setSearch] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [notice, setNotice] = useState('');
|
||||
const [confirmation, setConfirmation] = useState<'close' | 'reload' | null>(null);
|
||||
const dialogRef = useRef<HTMLElement>(null);
|
||||
const install = (next: HarnessEditorSnapshot) => {
|
||||
setSnapshot(next);
|
||||
setDrafts(Object.fromEntries(next.models.map(m => [m.id, harnessDraft(m.configuration, 'model')])));
|
||||
setProvider(harnessDraft(next.provider, 'provider'));
|
||||
};
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true); setError(''); setNotice('');
|
||||
try {
|
||||
const next = await invoke<HarnessEditorSnapshot>('get_deepseek_harness_model_catalog_editor');
|
||||
install(next);
|
||||
setSelected(current => current && next.models.some(m => m.id === current) ? current : next.defaultModel ?? next.models[0]?.id ?? null);
|
||||
} catch (e) { setError(String(e)); }
|
||||
finally { setLoading(false); }
|
||||
}, []);
|
||||
useEffect(() => { void load(); }, [load]);
|
||||
const dirty = !!snapshot && (!sameHarnessDraft(provider, harnessDraft(snapshot.provider, 'provider'))
|
||||
|| snapshot.models.some(m => !sameHarnessDraft(drafts[m.id] ?? {}, harnessDraft(m.configuration, 'model'))));
|
||||
const close = () => { if (!saving) { if (dirty) setConfirmation('close'); else onClose(); } };
|
||||
useEffect(() => {
|
||||
const previous = document.activeElement as HTMLElement | null;
|
||||
dialogRef.current?.querySelector<HTMLElement>('button')?.focus();
|
||||
return () => previous?.focus();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const handleKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') { event.stopPropagation(); if (confirmation) setConfirmation(null); else close(); }
|
||||
if (event.key === 'Tab') {
|
||||
const scope = confirmation ? dialogRef.current?.querySelector('[role="alertdialog"]') : dialogRef.current;
|
||||
const targets = [...(scope?.querySelectorAll<HTMLElement>('button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), summary') ?? [])].filter(el => el.getClientRects().length);
|
||||
const first = targets[0], last = targets[targets.length - 1];
|
||||
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
|
||||
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKey);
|
||||
return () => window.removeEventListener('keydown', handleKey);
|
||||
});
|
||||
const active = snapshot?.models.find(m => m.id === selected);
|
||||
const api = provider.api || 'openai-completions';
|
||||
const changed = () => { setNotice(''); setError(''); };
|
||||
const save = async () => {
|
||||
if (!snapshot || saving || loading) return;
|
||||
let request;
|
||||
try {
|
||||
let configuration;
|
||||
try { configuration = parseHarnessDraft(provider, 'provider', api); }
|
||||
catch (e) { setSelected(null); throw e; }
|
||||
request = { revision: snapshot.revision, provider: configuration, models: snapshot.models.map(m => {
|
||||
try { return { ...m, configuration: parseHarnessDraft(drafts[m.id] ?? {}, 'model', api) }; }
|
||||
catch (e) { setSelected(m.id); throw e; }
|
||||
}) };
|
||||
} catch (e) { setError(t('agents.harness.invalid', { field: String(e).replace(/^Error: /, '') })); return; }
|
||||
setSaving(true); setError(''); setNotice('');
|
||||
try {
|
||||
const next = await invoke<HarnessEditorSnapshot>('save_deepseek_harness_model_catalog_editor', { request });
|
||||
install(next); setNotice(t(next.configured ? 'agents.harness.saved' : 'agents.harness.savedDraft'));
|
||||
await onSaved();
|
||||
} catch (e) { setError(String(e).includes('DSH_MODEL_CATALOG_CHANGED') ? t('agents.harness.changed') : String(e)); }
|
||||
finally { setSaving(false); }
|
||||
};
|
||||
return <div className="config-dialog-backdrop codex-catalog-backdrop" onMouseDown={e => { if (e.target === e.currentTarget) close(); }}>
|
||||
<section ref={dialogRef} className="config-dialog codex-catalog-dialog harness-catalog-dialog" role="dialog" aria-modal="true" aria-labelledby="harness-catalog-title">
|
||||
<header className="config-dialog-heading codex-catalog-heading"><div><h2 id="harness-catalog-title">{t('agents.harness.title')}</h2><p>{t('agents.harness.subtitle')}</p></div><button className="icon-button quiet" onClick={close} disabled={saving} aria-label={t('common.close')}><X size={18} /></button></header>
|
||||
<div className="codex-catalog-body">
|
||||
<aside className="codex-catalog-sidebar">
|
||||
<button className={`secondary-button harness-provider-button${selected === null ? ' active' : ''}`} onClick={() => setSelected(null)}><Settings2 size={16} />{t('agents.harness.provider')}</button>
|
||||
<div className="codex-catalog-search"><Search size={15} /><input aria-label={t('agents.catalog.search')} value={search} placeholder={t('agents.catalog.search')} onChange={e => setSearch(e.currentTarget.value)} /></div>
|
||||
<div className="codex-catalog-models" role="listbox" aria-label={t('agents.harness.title')}>
|
||||
{loading ? <div className="codex-catalog-state"><LoaderCircle className="spin" size={18} />{t('agents.catalog.loading')}</div> : snapshot?.models.filter(m => `${m.id} ${drafts[m.id]?.name ?? m.defaults.name ?? ''}`.toLowerCase().includes(search.toLowerCase())).map(m => <button role="option" aria-selected={selected === m.id} className={selected === m.id ? 'active' : ''} key={m.id} onClick={() => setSelected(m.id)}>
|
||||
<strong>{drafts[m.id]?.name || String(m.defaults.name ?? m.id)}</strong><span>{m.id}</span><small>{t(Object.keys(drafts[m.id] ?? {}).length ? 'agents.catalog.customized' : 'agents.harness.apiSource')}{m.id === snapshot.defaultModel ? ` · ${t('agents.harness.defaultModel')}` : ''}</small>
|
||||
</button>)}
|
||||
{!loading && snapshot?.models.length === 0 ? <div className="codex-catalog-state">{t('agents.catalog.empty')}</div> : null}
|
||||
</div>
|
||||
<button className="secondary-button codex-catalog-reload" disabled={loading || saving} onClick={() => dirty ? setConfirmation('reload') : void load()}><RefreshCw size={15} />{t('agents.catalog.reload')}</button>
|
||||
</aside>
|
||||
<main className="codex-catalog-editor"><fieldset disabled={loading || saving} className="harness-editor-fieldset">
|
||||
{snapshot && (active || selected === null) ? <>
|
||||
<div className="codex-catalog-model-heading"><div><h3>{active ? active.id : t('agents.harness.provider')}</h3><span>{t('agents.harness.inheritHint')}</span></div><button className="secondary-button" onClick={() => { if (active) setDrafts(current => ({ ...current, [active.id]: {} })); else setProvider({}); changed(); }}><RotateCcw size={14} />{t('agents.harness.reset')}</button></div>
|
||||
{active ? <p className="codex-catalog-hint" role="note">{t('agents.harness.apiInput', { value: Array.isArray(active.defaults.input) ? active.defaults.input.join(', ') : t('agents.harness.unknown') })}</p> : <p className="codex-catalog-hint">{t('agents.harness.managedConnection', { url: api === 'anthropic-messages' ? snapshot.baseUrl.replace(/\/v1$/, '') : snapshot.baseUrl })}</p>}
|
||||
<HarnessFields key={active?.id ?? 'provider'} group={active ? 'model' : 'provider'} draft={active ? drafts[active.id] ?? {} : provider} defaults={active?.defaults} api={api} onChange={next => { if (active) setDrafts(current => ({ ...current, [active.id]: next })); else setProvider(next); changed(); }} />
|
||||
</> : null}
|
||||
</fieldset></main>
|
||||
</div>
|
||||
<footer className="codex-catalog-footer"><div aria-live="polite">{error ? <span className="agent-inline-message error" role="alert">{error}</span> : notice ? <span role="status">{notice}</span> : <span>{t(dirty ? 'agents.catalog.unsaved' : 'agents.harness.saveHint')}</span>}</div><div>
|
||||
<button className="secondary-button" disabled={loading || saving || !snapshot} onClick={() => { setDrafts(Object.fromEntries(snapshot!.models.map(m => [m.id, {}]))); setProvider({}); changed(); }}>{t('agents.catalog.resetAll')}</button>
|
||||
<button className="secondary-button" disabled={saving} onClick={close}>{t('common.cancel')}</button><button className="primary-button" disabled={!dirty || loading || saving} onClick={() => void save()}>{saving ? <LoaderCircle size={16} className="spin" /> : null}{t(saving ? 'common.saving' : 'common.save')}</button>
|
||||
</div></footer>
|
||||
{confirmation ? <div className="codex-catalog-confirm"><div role="alertdialog" aria-modal="true" aria-label={t('agents.catalog.unsaved')}><strong>{t('agents.catalog.unsaved')}</strong><span>{t('agents.catalog.discardHint')}</span><div><button autoFocus className="secondary-button" onClick={() => setConfirmation(null)}>{t('agents.catalog.keepEditing')}</button><button className="danger-button" onClick={() => { if (confirmation === 'close') onClose(); else { setConfirmation(null); void load(); } }}>{t('agents.catalog.discard')}</button></div></div></div> : null}
|
||||
</section>
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import schemaSource from './deepSeekHarnessSchema.json';
|
||||
|
||||
export type HarnessProfile = Record<string, unknown>;
|
||||
export type HarnessDraft = Record<string, string>;
|
||||
export type HarnessField = {
|
||||
name: string;
|
||||
kind: string;
|
||||
group?: string;
|
||||
values?: string[];
|
||||
apis?: string[];
|
||||
default?: unknown;
|
||||
example?: unknown;
|
||||
min?: number;
|
||||
max?: number;
|
||||
exclusiveMin?: number;
|
||||
};
|
||||
export const harnessSchema = schemaSource as unknown as Record<string, HarnessField[]>;
|
||||
export const harnessReasoningLevels = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
||||
export type HarnessEditorModel = { id: string; defaults: HarnessProfile; configuration: HarnessProfile };
|
||||
export type HarnessEditorSnapshot = {
|
||||
revision: string;
|
||||
models: HarnessEditorModel[];
|
||||
provider: HarnessProfile;
|
||||
baseUrl: string;
|
||||
defaultModel: string | null;
|
||||
configured: boolean;
|
||||
};
|
||||
export const isHarnessRecord = (value: unknown): value is HarnessProfile =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
export function harnessDraft(profile: HarnessProfile, group: string, prefix = ''): HarnessDraft {
|
||||
return Object.fromEntries(harnessSchema[group].flatMap(field => {
|
||||
const value = profile[field.name];
|
||||
const key = `${prefix}${field.name}`;
|
||||
if (value === undefined) return [];
|
||||
if (field.group && isHarnessRecord(value)) return Object.entries(harnessDraft(value, field.group, `${key}.`));
|
||||
return [[key, ['string', 'enum'].includes(field.kind) ? String(value) : JSON.stringify(value)]];
|
||||
}));
|
||||
}
|
||||
|
||||
export function updateHarnessDraft(draft: HarnessDraft, key: string, value: string): HarnessDraft {
|
||||
const next = { ...draft };
|
||||
if (!value.length) delete next[key];
|
||||
else return { ...next, [key]: value };
|
||||
return next;
|
||||
}
|
||||
|
||||
export function parseHarnessDraft(draft: HarnessDraft, group: string, api: string, prefix = ''): HarnessProfile {
|
||||
const profile: HarnessProfile = {};
|
||||
for (const field of harnessSchema[group]) {
|
||||
const key = `${prefix}${field.name}`;
|
||||
if (field.group) {
|
||||
const nested = parseHarnessDraft(draft, field.group, api, `${key}.`);
|
||||
if (Object.keys(nested).length) profile[field.name] = nested;
|
||||
continue;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(draft, key) || !draft[key].trim()) continue;
|
||||
let value: unknown;
|
||||
try {
|
||||
value = ['string', 'enum'].includes(field.kind) ? draft[key].trim() : JSON.parse(draft[key]);
|
||||
} catch { throw new Error(`${key}: JSON`); }
|
||||
if (field.apis && !field.apis.includes(api)) throw new Error(`${key}: ${api}`);
|
||||
let valid = true;
|
||||
if (['integer', 'number'].includes(field.kind)) valid = typeof value === 'number' && Number.isFinite(value)
|
||||
&& Math.abs(value) <= Number.MAX_SAFE_INTEGER
|
||||
&& (field.kind !== 'integer' || Number.isInteger(value))
|
||||
&& (field.min === undefined || value >= field.min)
|
||||
&& (field.max === undefined || value <= field.max)
|
||||
&& (field.exclusiveMin === undefined || value > field.exclusiveMin);
|
||||
if (field.kind === 'enum') valid = field.values!.includes(value as string);
|
||||
if (field.kind === 'boolean') valid = typeof value === 'boolean';
|
||||
if (['modalities', 'strings'].includes(field.kind)) valid = Array.isArray(value) && (value.length > 0 || field.name === 'input')
|
||||
&& value.every(v => typeof v === 'string' && v.trim() && (field.kind !== 'modalities' || ['text', 'image'].includes(v)))
|
||||
&& new Set(value).size === value.length;
|
||||
if (field.kind === 'reasoning') valid = value === false || (isHarnessRecord(value) && Object.keys(value).length > 0
|
||||
&& Object.entries(value).every(([level, wire]) => harnessReasoningLevels.includes(level)
|
||||
&& ((level === 'off' && wire === null) || (typeof wire === 'string' && wire.trim()))));
|
||||
if (field.kind === 'headers') valid = isHarnessRecord(value)
|
||||
&& Object.entries(value).every(([name, v]) => /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)
|
||||
&& typeof v === 'string' && [...v].every(c => c.charCodeAt(0) <= 255) && !/[\r\n\0]/.test(v));
|
||||
if (field.kind === 'kwargs') valid = isHarnessRecord(value) && Object.values(value).every(v => !Array.isArray(v)
|
||||
&& (!isHarnessRecord(v) || (Object.keys(v).every(key => ['$var', 'omitWhenOff'].includes(key))
|
||||
&& ['thinking.enabled', 'thinking.effort', 'thinking.budget'].includes(v.$var as string)
|
||||
&& (v.omitWhenOff === undefined || typeof v.omitWhenOff === 'boolean'))));
|
||||
if (!valid) throw new Error(key);
|
||||
profile[field.name] = value;
|
||||
}
|
||||
if (group === 'retryPolicy' && Object.keys(profile).length && profile.mode === undefined) profile.mode = 'normal';
|
||||
if (group === 'backoff' && Number(profile.initialDelayMs ?? 500) > Number(profile.maxDelayMs ?? 10000)) throw new Error(`${prefix}initialDelayMs > maxDelayMs`);
|
||||
return profile;
|
||||
}
|
||||
|
||||
export function sameHarnessDraft(left: HarnessDraft, right: HarnessDraft): boolean {
|
||||
return Object.keys(left).length === Object.keys(right).length && Object.keys(left).every(key => left[key] === right[key]);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"sources": [
|
||||
"https://deepseek-harness.github.io/deepseek-harness/guide/providers",
|
||||
"https://deepseek-harness.github.io/deepseek-harness/reference/config-catalog#deepseek-ai-dsh-llm-pi-ai",
|
||||
"https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/llm/llm/src/retry-policy.ts"
|
||||
],
|
||||
"model": [
|
||||
{"name":"name","kind":"string"},
|
||||
{"name":"contextWindow","kind":"integer","min":1},
|
||||
{"name":"maxTokens","kind":"integer","min":1},
|
||||
{"name":"input","kind":"modalities"},
|
||||
{"name":"reasoningEfforts","kind":"reasoning","example":{"off":null,"low":"low","high":"high"}},
|
||||
{"name":"compat","kind":"group","group":"compat"}
|
||||
],
|
||||
"provider": [
|
||||
{"name":"displayName","kind":"string","default":"EasyCLIProxyAPI"},
|
||||
{"name":"api","kind":"enum","values":["openai-completions","openai-responses","anthropic-messages"],"default":"openai-completions"},
|
||||
{"name":"defaultContextWindow","kind":"integer","min":1,"default":262144},
|
||||
{"name":"defaultMaxTokens","kind":"integer","min":1,"default":32768},
|
||||
{"name":"defaultInput","kind":"modalities","default":["text"]},
|
||||
{"name":"reasoning","kind":"enum","values":["off","minimal","low","medium","high","xhigh","max"]},
|
||||
{"name":"thinkingBudgets","kind":"group","group":"thinkingBudgets"},
|
||||
{"name":"cacheRetention","kind":"enum","values":["none","short","long"]},
|
||||
{"name":"transport","kind":"enum","values":["sse","websocket","websocket-cached","auto"]},
|
||||
{"name":"timeoutMs","kind":"integer","min":0},
|
||||
{"name":"websocketConnectTimeoutMs","kind":"integer","min":0},
|
||||
{"name":"streamIdleTimeoutMs","kind":"number","exclusiveMin":0,"max":2147483647,"default":300000},
|
||||
{"name":"maxRequestImageBytes","kind":"integer","min":1,"default":20971520},
|
||||
{"name":"requestImagePixelBudget","kind":"integer","min":1,"default":4194304},
|
||||
{"name":"requestImageMaxBytes","kind":"integer","min":1,"default":1048576},
|
||||
{"name":"headers","kind":"headers","example":{"X-Custom-Header":"value"}},
|
||||
{"name":"retryPolicy","kind":"group","group":"retryPolicy"},
|
||||
{"name":"compat","kind":"group","group":"compat"}
|
||||
],
|
||||
"thinkingBudgets": [
|
||||
{"name":"minimal","kind":"number"},
|
||||
{"name":"low","kind":"number"},
|
||||
{"name":"medium","kind":"number"},
|
||||
{"name":"high","kind":"number"}
|
||||
],
|
||||
"retryPolicy": [
|
||||
{"name":"mode","kind":"enum","values":["normal","always"],"default":"normal","required":true},
|
||||
{"name":"maxRetries","kind":"integer","min":0,"default":5},
|
||||
{"name":"retryableCodes","kind":"strings","default":["EMPTY_RESPONSE","RATE_LIMIT","SERVER","TIMEOUT","TRANSPORT"]},
|
||||
{"name":"backoff","kind":"group","group":"backoff"}
|
||||
],
|
||||
"backoff": [
|
||||
{"name":"initialDelayMs","kind":"number","exclusiveMin":0,"max":2147483647,"default":500},
|
||||
{"name":"maxDelayMs","kind":"number","exclusiveMin":0,"max":2147483647,"default":10000},
|
||||
{"name":"jitterRatio","kind":"number","min":0,"max":1,"default":0.1}
|
||||
],
|
||||
"compat": [
|
||||
{"name":"supportsStore","kind":"boolean","apis":["openai-completions"]},
|
||||
{"name":"supportsDeveloperRole","kind":"boolean","apis":["openai-completions","openai-responses"]},
|
||||
{"name":"supportsReasoningEffort","kind":"boolean","apis":["openai-completions"]},
|
||||
{"name":"supportsUsageInStreaming","kind":"boolean","apis":["openai-completions"]},
|
||||
{"name":"supportsFinishReason","kind":"boolean","apis":["openai-completions"]},
|
||||
{"name":"maxTokensField","kind":"enum","values":["max_completion_tokens","max_tokens"],"apis":["openai-completions"]},
|
||||
{"name":"requiresToolResultName","kind":"boolean","apis":["openai-completions"]},
|
||||
{"name":"requiresAssistantAfterToolResult","kind":"boolean","apis":["openai-completions"]},
|
||||
{"name":"requiresThinkingAsText","kind":"boolean","apis":["openai-completions"]},
|
||||
{"name":"requiresReasoningContentOnAssistantMessages","kind":"boolean","apis":["openai-completions"]},
|
||||
{"name":"thinkingFormat","kind":"enum","values":["openai","deepseek","openrouter","together","baseten","zai","qwen","chat-template","qwen-chat-template","string-thinking","ant-ling"],"apis":["openai-completions"]},
|
||||
{"name":"chatTemplateKwargs","kind":"kwargs","example":{"enable_thinking":{"$var":"thinking.enabled"}},"apis":["openai-completions"]},
|
||||
{"name":"chatTemplateArgs","kind":"kwargs","example":{"reasoning_effort":{"$var":"thinking.effort","omitWhenOff":true}},"apis":["openai-completions"]},
|
||||
{"name":"supportsThinkingTokenBudget","kind":"boolean","apis":["openai-completions"]},
|
||||
{"name":"thinkingTokenBudgetField","kind":"enum","values":["thinking_token_budget","thinking_budget","thinking_budget_tokens"],"apis":["openai-completions"]},
|
||||
{"name":"vllmPriority","kind":"integer","apis":["openai-completions"]},
|
||||
{"name":"supportsMaxOutputTokens","kind":"boolean","apis":["openai-responses"]},
|
||||
{"name":"supportsStrictMode","kind":"boolean","apis":["openai-completions","openai-responses"]},
|
||||
{"name":"cacheControlFormat","kind":"enum","values":["anthropic"],"apis":["openai-completions"]},
|
||||
{"name":"supportsLongCacheRetention","kind":"boolean","apis":["openai-completions","openai-responses","anthropic-messages"]},
|
||||
{"name":"supportsEagerToolInputStreaming","kind":"boolean","apis":["anthropic-messages"]},
|
||||
{"name":"supportsCacheControlOnTools","kind":"boolean","apis":["anthropic-messages"]},
|
||||
{"name":"supportsTemperature","kind":"boolean","apis":["anthropic-messages"]},
|
||||
{"name":"forceAdaptiveThinking","kind":"boolean","apis":["anthropic-messages"]},
|
||||
{"name":"allowEmptySignature","kind":"boolean","apis":["anthropic-messages"]},
|
||||
{"name":"supportsStrictTools","kind":"boolean","apis":["anthropic-messages"]}
|
||||
]
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export type ModelOption = {
|
||||
alias?: string;
|
||||
isAlias?: boolean;
|
||||
contextWindow?: number;
|
||||
inputModalities?: Array<'text' | 'image'>;
|
||||
thinking?: Record<string, unknown>;
|
||||
};
|
||||
export type ModelProvider = 'gemini' | 'codex' | 'claude' | 'openai';
|
||||
|
||||
@@ -13828,3 +13828,35 @@ body.table-col-resizing * {
|
||||
flex: 0 0 42px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.harness-catalog-dialog .codex-catalog-sidebar { grid-template-rows: auto auto minmax(0,1fr) auto; }
|
||||
.harness-editor-fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
|
||||
.harness-provider-button { flex-shrink: 0; justify-content: flex-start; }
|
||||
.harness-provider-button.active { border-color: var(--theme-bd5e31); }
|
||||
.harness-catalog-form > label > span { display: flex; flex-wrap: wrap; gap: 6px; overflow-wrap: anywhere; }
|
||||
.harness-catalog-form small, .harness-reasoning small { color: var(--theme-716b61); font-size: 12px; line-height: 1.6; }
|
||||
.harness-override { font-weight: 400; }
|
||||
.harness-catalog-group { grid-column: 1 / -1; border-top: 1px solid var(--theme-d5d2cb); padding-top: 14px; min-width: 0; }
|
||||
.harness-catalog-group > summary { cursor: pointer; font-size: 13px; font-weight: 600; padding: 4px 0; }
|
||||
.harness-catalog-group > button { margin-top: 12px; }
|
||||
.harness-reasoning { grid-column: 1 / -1; display: grid; gap: 10px; }
|
||||
.harness-reasoning > label { display: grid; gap: 8px; font-size: 12px; font-weight: 600; }
|
||||
.harness-reasoning-levels { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 8px 16px; }
|
||||
.harness-reasoning-levels > div { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
.harness-reasoning-levels label { display: flex; align-items: center; gap: 6px; flex: 0 0 75px; font-size: 12px; }
|
||||
.harness-reasoning-levels input:not([type=checkbox]) { min-width: 0; width: 100%; }
|
||||
.harness-catalog-form textarea { font-family: var(--font-mono, monospace); }
|
||||
@media (max-width: 640px) { .harness-reasoning-levels { grid-template-columns: 1fr; } }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.harness-catalog-dialog .codex-catalog-body { grid-template-rows: 210px minmax(0,1fr); }
|
||||
.harness-catalog-dialog .codex-catalog-sidebar { grid-template-rows: auto auto minmax(0,1fr); }
|
||||
.harness-catalog-dialog .harness-provider-button { grid-column: 1 / -1; }
|
||||
.harness-catalog-dialog .codex-catalog-search { grid-column: 1; grid-row: 2; }
|
||||
.harness-catalog-dialog .codex-catalog-reload { grid-column: 2; grid-row: 2; }
|
||||
.harness-catalog-dialog .codex-catalog-models { grid-row: 3; }
|
||||
}
|
||||
|
||||
.harness-catalog-dialog .secondary-button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; }
|
||||
.harness-catalog-form small { font-weight: 400; }
|
||||
.harness-catalog-form { margin-top: 12px; }
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { harnessDraft, parseHarnessDraft, sameHarnessDraft, updateHarnessDraft } from '../src/services/deepSeekHarnessCatalog';
|
||||
|
||||
describe('DSH catalog overrides', () => {
|
||||
test('empty values inherit while explicit false and zero remain overrides', () => {
|
||||
const draft = harnessDraft({ timeoutMs: 0, retryPolicy: { mode: 'normal', maxRetries: 0 }, compat: { supportsStore: false } }, 'provider');
|
||||
expect(parseHarnessDraft(draft, 'provider', 'openai-completions')).toEqual({ timeoutMs: 0, retryPolicy: { mode: 'normal', maxRetries: 0 }, compat: { supportsStore: false } });
|
||||
expect(parseHarnessDraft(updateHarnessDraft(draft, 'timeoutMs', ''), 'provider', 'openai-completions').timeoutMs).toBeUndefined();
|
||||
expect(parseHarnessDraft({}, 'model', 'openai-completions')).toEqual({});
|
||||
expect(parseHarnessDraft({ input: '[]' }, 'model', 'openai-completions')).toEqual({ input: [] });
|
||||
});
|
||||
test('reasoning levels keep wire values and only off allows null', () => {
|
||||
expect(parseHarnessDraft({ reasoningEfforts: '{"off":null,"high":"custom-high"}' }, 'model', 'openai-completions')).toEqual({ reasoningEfforts: { off: null, high: 'custom-high' } });
|
||||
expect(parseHarnessDraft({ reasoningEfforts: 'false' }, 'model', 'openai-completions')).toEqual({ reasoningEfforts: false });
|
||||
expect(() => parseHarnessDraft({ reasoningEfforts: '{"high":null}' }, 'model', 'openai-completions')).toThrow();
|
||||
expect(() => parseHarnessDraft({ reasoningEfforts: '{}' }, 'model', 'openai-completions')).toThrow();
|
||||
});
|
||||
test('invalid numbers, JSON, headers and incompatible protocol switches are rejected', () => {
|
||||
for (const draft of [{ maxTokens: '-1' }, { input: '["audio"]' }, { contextWindow: '2.5' }, { 'compat.supportsStore': 'null' }, { 'compat.supportsTemperature': 'true' }]) {
|
||||
expect(() => parseHarnessDraft(draft, 'model', 'openai-completions')).toThrow();
|
||||
}
|
||||
expect(() => parseHarnessDraft({ headers: '{bad json}' }, 'provider', 'openai-completions')).toThrow();
|
||||
expect(() => parseHarnessDraft({ headers: '{"x-test":"a\\r\\nb"}' }, 'provider', 'openai-completions')).toThrow();
|
||||
expect(() => parseHarnessDraft({ 'retryPolicy.backoff.initialDelayMs': '20000' }, 'provider', 'openai-completions')).toThrow();
|
||||
});
|
||||
test('nested groups round trip and follow current protocol', () => {
|
||||
const profile = { api: 'anthropic-messages', compat: { supportsTemperature: false }, retryPolicy: { mode: 'always', backoff: { maxDelayMs: 60000 } }, thinkingBudgets: { high: 8192 } };
|
||||
expect(parseHarnessDraft(harnessDraft(profile, 'provider'), 'provider', 'anthropic-messages')).toEqual(profile);
|
||||
expect(sameHarnessDraft({ input: '["text"]', name: 'A' }, { name: 'A', input: '["text"]' })).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ channel: 'msedge', headless: true });
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
const open = async (query = '') => {
|
||||
await page.goto(`http://127.0.0.1:1421/tests/fixtures/agent-backups.html?client=deepseek-harness&reset-selections&${query}`);
|
||||
await page.getByRole('button', { name: '配置模型', exact: true }).click();
|
||||
await page.getByRole('option', { name: /gpt-one/ }).waitFor();
|
||||
};
|
||||
const dialog = () => page.getByRole('dialog', { name: 'DeepSeek Harness 模型配置' });
|
||||
const save = async () => {
|
||||
await dialog().getByRole('button', { name: '保存', exact: true }).click();
|
||||
await dialog().getByRole('status').filter({ hasText: /已保存/ }).waitFor();
|
||||
};
|
||||
try {
|
||||
for (const mode of ['', 'embedded&']) {
|
||||
await open(mode);
|
||||
assert.equal(await dialog().getByRole('combobox', { name: '输入类型', exact: true }).inputValue(), '');
|
||||
await dialog().getByRole('option', { name: /unknown-model/ }).click();
|
||||
await dialog().getByText(/未提供(不代表不支持)/).waitFor();
|
||||
await dialog().getByRole('combobox', { name: '输入类型', exact: true }).selectOption('["text","image"]');
|
||||
await dialog().getByRole('option', { name: /gpt-two/ }).click();
|
||||
await dialog().getByRole('spinbutton', { name: '输出上限(tokens)' }).fill('4096');
|
||||
await dialog().getByRole('combobox', { name: '推理等级', exact: true }).selectOption('custom');
|
||||
await dialog().getByRole('textbox', { name: 'high reasoning_effort' }).fill('custom-high');
|
||||
await dialog().getByRole('button', { name: '供应商设置', exact: true }).click();
|
||||
await dialog().getByRole('combobox', { name: '默认输入类型', exact: true }).selectOption('["text","image"]');
|
||||
await dialog().getByRole('spinbutton', { name: '请求超时(ms)' }).fill('60000');
|
||||
await save();
|
||||
const request = await page.evaluate(() => window.fixtureCalls.filter(c => c.cmd === 'save_deepseek_harness_model_catalog_editor').at(-1).args.request);
|
||||
assert.deepEqual(request.models.find(m => m.id === 'unknown-model').configuration.input, ['text','image']);
|
||||
assert.equal(request.models.find(m => m.id === 'gpt-two').configuration.maxTokens, 4096);
|
||||
assert.equal(request.models.find(m => m.id === 'gpt-two').configuration.reasoningEfforts.high, 'custom-high');
|
||||
assert.equal(request.provider.timeoutMs, 60000);
|
||||
assert.equal(await page.evaluate(() => window.fixtureCalls.some(c => c.cmd === 'update_agent_config')), false);
|
||||
await dialog().getByRole('button', { name: '关闭', exact: true }).click();
|
||||
assert.match(await page.locator('.agent-model-trigger').innerText(), /gpt-one/);
|
||||
await page.evaluate(() => window.fixtureRemount());
|
||||
await page.getByRole('button', { name: '配置模型', exact: true }).click();
|
||||
await dialog().getByRole('option', { name: /unknown-model/ }).click();
|
||||
assert.equal(await dialog().getByRole('combobox', { name: '输入类型', exact: true }).inputValue(), '["text","image"]');
|
||||
await dialog().getByRole('button', { name: '恢复自动', exact: true }).click();
|
||||
await save();
|
||||
assert.equal(await dialog().getByRole('combobox', { name: '输入类型', exact: true }).inputValue(), '');
|
||||
await dialog().getByRole('spinbutton', { name: '上下文窗口(tokens)' }).fill('0');
|
||||
await dialog().getByRole('button', { name: '保存', exact: true }).click();
|
||||
await dialog().getByRole('alert').filter({ hasText: /contextWindow/ }).waitFor();
|
||||
await dialog().getByRole('spinbutton', { name: '上下文窗口(tokens)' }).fill('64000');
|
||||
await page.evaluate(() => window.fixtureFailHarnessSave = true);
|
||||
await dialog().getByRole('button', { name: '保存', exact: true }).click();
|
||||
await dialog().getByRole('alert').filter({ hasText: /模拟模型配置保存失败/ }).waitFor();
|
||||
assert.equal(await dialog().getByRole('spinbutton', { name: '上下文窗口(tokens)' }).inputValue(), '64000');
|
||||
await page.evaluate(() => { window.fixtureFailHarnessSave = false; window.fixtureStaleHarnessSave = true; });
|
||||
await dialog().getByRole('button', { name: '保存', exact: true }).click();
|
||||
await dialog().getByRole('alert').filter({ hasText: /模型列表或配置已变化/ }).waitFor();
|
||||
await page.evaluate(() => window.fixtureStaleHarnessSave = false);
|
||||
await page.keyboard.press('Escape');
|
||||
await page.getByRole('alertdialog').waitFor();
|
||||
await page.getByRole('button', { name: '继续编辑', exact: true }).click();
|
||||
await save();
|
||||
}
|
||||
const screenshots = path.join(os.tmpdir(), 'cpa-harness-catalog-ui');
|
||||
fs.mkdirSync(screenshots, { recursive: true });
|
||||
for (const width of [1280, 540]) for (const theme of ['light', 'dark']) {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await open(`theme=${theme}`);
|
||||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false);
|
||||
await page.screenshot({ path: path.join(screenshots, `${width}-${theme}-model.png`) });
|
||||
await dialog().getByRole('button', { name: '供应商设置', exact: true }).click();
|
||||
assert.equal(await page.evaluate(() => document.querySelector('.codex-catalog-editor').scrollWidth > document.querySelector('.codex-catalog-editor').clientWidth), false);
|
||||
await page.screenshot({ path: path.join(screenshots, `${width}-${theme}-provider.png`) });
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS: default model preserved, catalog overrides, API reset, reasoning, validation, retry, stale save, responsive themes');
|
||||
console.log(screenshots);
|
||||
} finally { await browser.close(); }
|
||||
})().catch(e => { console.error(e); process.exit(1); });
|
||||
Vendored
+22
-1
@@ -12,6 +12,15 @@ localStorage.setItem('cpa-gui.agent-selected-client.v1', params.get('client') ||
|
||||
const ids = ['claude-code','claude-desktop','codex','opencode','openclaw','hermes','deepseek-harness','zcode','kimi-code','grok-build','pi'];
|
||||
let count=0; let backupCount=0; const backups:any[]=[]; let currentModel=params.has('fresh')?null:'gpt-one';
|
||||
let currentOauth=false; const currentMappings:Record<string,any>={};
|
||||
let harnessProvider:Record<string,unknown>={};
|
||||
const harnessConfigurations:Record<string,Record<string,unknown>>={};
|
||||
let harnessRevision=1;
|
||||
const harnessModels=[
|
||||
{id:'gpt-one',defaults:{input:['text','image'],contextWindow:128000}},
|
||||
{id:'gpt-two',defaults:{input:['text']}},
|
||||
{id:'unknown-model',defaults:{}},
|
||||
];
|
||||
const harnessSnapshot=()=>({revision:String(harnessRevision),provider:harnessProvider,baseUrl:'http://127.0.0.1:8317/v1',defaultModel:currentModel,configured:!!currentModel,models:harnessModels.map(m=>({...m,configuration:harnessConfigurations[m.id]??{}}))});
|
||||
let harnessStatus={running:params.has('running'),pid:params.has('running')?100:null as number|null,mode:params.has('running')?params.get('harness-mode')||'web':null as string|null};
|
||||
let appliedCount=0;
|
||||
const calls:any[]=[];(window as any).fixtureCalls=calls;
|
||||
@@ -22,7 +31,10 @@ mockIPC(async (cmd,args:any) => {
|
||||
if(cmd==='plugin:event|listen') return 1;
|
||||
if(cmd==='plugin:event|unlisten'||cmd==='set_app_locale') return null;
|
||||
if(cmd==='get_agent_config_statuses'||cmd==='refresh_agent_config_statuses') return ids.map(id=>({id,name:id,supportedPlatform:true,installed:!params.has('not-installed'),pluginInstalled:!params.has('no-plugin'),launchTargets:params.has('not-installed')?[]:['claude-desktop','zcode'].includes(id)?[{id:'app',label:id,detail:'test desktop'}]:['codex','opencode'].includes(id)&&!params.has('cli-only')?[...(params.has('app-only')?[]:[{id:'cli',label:'CLI',detail:'test CLI'}]),{id:'app',label:'APP',detail:'test desktop'}]:[{id:'cli',label:'CLI',detail:'test CLI'}],version:'1.0',cliVersion:'1.0',appVersion:null,pluginVersion:'1.0',configValid:params.get('state')!=='invalid',connectionState:params.get('state') || (currentModel?'configured':'not-configured'),configured:!!currentModel,configurationSynchronized:!!currentModel,currentModel,oauthConfiguration:id==='codex'&¤tOauth,modificationEnabled:!!currentModel,modificationState:currentModel?'applied':'unconfigured',backupAvailable:false,appliedModel:currentModel,claudeCodeModelMappings:id==='claude-code'?currentMappings[id]??null:null,claudeDesktopModelMappings:id==='claude-desktop'?currentMappings[id]??null:null,warnings:[],error:null}));
|
||||
if(cmd==='get_agent_models') return [{name:'gpt-one'},{name:'gpt-two'}];
|
||||
if(cmd==='get_agent_models') {
|
||||
if(args.client==='deepseek-harness')return harnessModels.map(m=>({name:m.id,inputModalities:m.defaults.input}));
|
||||
return [{name:'gpt-one'},{name:'gpt-two'}];
|
||||
}
|
||||
if(cmd==='list_codex_sessions') {
|
||||
const {offset,limit}=args.request;
|
||||
const sessionIds:string[]=[...(window as any).fixtureSessionIds];
|
||||
@@ -63,6 +75,15 @@ mockIPC(async (cmd,args:any) => {
|
||||
if(cmd==='preview_agent_config_template') return {revision:'template1',files:['C:/test/.codex/config.toml','C:/test/.codex/auth.json','C:/test/.codex/models.json']};
|
||||
if(cmd==='apply_agent_config_template') {currentModel=args.model;return {outcome:'updated'};}
|
||||
if(cmd==='check_pi_provider_update') return {installedVersion:'1.0',latestVersion:params.has('update')?'1.1':'1.0',updateAvailable:params.has('update')};
|
||||
if(cmd==='get_deepseek_harness_model_catalog_editor')return harnessSnapshot();
|
||||
if(cmd==='save_deepseek_harness_model_catalog_editor') {
|
||||
if((window as any).fixtureFailHarnessSave)throw new Error('模拟模型配置保存失败');
|
||||
if((window as any).fixtureStaleHarnessSave)throw new Error('DSH_MODEL_CATALOG_CHANGED');
|
||||
if((window as any).fixtureDeferHarnessSave)await new Promise<void>(resolve=>{(window as any).fixtureFinishHarnessSave=resolve;});
|
||||
harnessProvider=args.request.provider;
|
||||
for(const m of args.request.models)harnessConfigurations[m.id]=m.configuration;
|
||||
harnessRevision++;return harnessSnapshot();
|
||||
}
|
||||
if(cmd==='get_codex_model_catalog_editor') return {models:[],hiddenModels:[],customizations:{}};
|
||||
if(cmd==='check_codex_oauth_login')return null;
|
||||
throw new Error('Unhandled fixture command: '+cmd);
|
||||
|
||||
Reference in new issue
Block a user