feat(backend): catalogue de providers OpenCode dynamique + provider personnalisé (#92)
Le catalogue de providers OpenCode lit désormais le cache local ~/.cache/opencode/models.json pour refléter les providers réellement disponibles, avec repli garanti sur le catalogue statique en cas d'absence ou d'erreur de lecture du cache. Ajout d'un champ additif `custom` sur OpenCodeProviderConfig pour permettre à l'utilisateur de déclarer un provider hors catalogue (id + clé API en saisie libre). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -2761,15 +2761,37 @@ fn opencode_provider_config_json(
|
||||
"model".to_owned(),
|
||||
serde_json::Value::String(format!("{}/{}", config.provider_id, config.model)),
|
||||
);
|
||||
let mut options = serde_json::Map::new();
|
||||
options.insert(
|
||||
"apiKey".to_owned(),
|
||||
serde_json::Value::String(api_key.to_owned()),
|
||||
);
|
||||
let mut provider_entry = serde_json::Map::new();
|
||||
if let Some(custom) = config.custom.as_ref() {
|
||||
// Provider outside the OpenCode registry: it needs the AI SDK package
|
||||
// (`npm`), the endpoint (`options.baseURL`), and a `models` block —
|
||||
// OpenCode has no built-in knowledge of this provider otherwise.
|
||||
options.insert(
|
||||
"baseURL".to_owned(),
|
||||
serde_json::Value::String(custom.base_url.clone()),
|
||||
);
|
||||
provider_entry.insert(
|
||||
"npm".to_owned(),
|
||||
serde_json::Value::String(custom.npm.clone()),
|
||||
);
|
||||
let model_label = custom
|
||||
.display_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| config.model.clone());
|
||||
provider_entry.insert(
|
||||
"models".to_owned(),
|
||||
serde_json::json!({ config.model.as_str(): { "name": model_label } }),
|
||||
);
|
||||
}
|
||||
provider_entry.insert("options".to_owned(), serde_json::Value::Object(options));
|
||||
root.insert(
|
||||
"provider".to_owned(),
|
||||
serde_json::json!({
|
||||
config.provider_id.as_str(): {
|
||||
"options": {
|
||||
"apiKey": api_key
|
||||
}
|
||||
}
|
||||
}),
|
||||
serde_json::json!({ config.provider_id.as_str(): provider_entry }),
|
||||
);
|
||||
|
||||
let wiring = mcp_server_wiring(domain::profile::McpTransport::Stdio, runtime);
|
||||
@ -4379,4 +4401,67 @@ command = "idea-mcp"
|
||||
assert!(merged.contains("user_key = \"keep-me\""));
|
||||
assert!(merged.contains("[mcp_servers.idea]\ncommand = \"idea-mcp\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opencode_provider_config_json_known_provider_emits_only_the_api_key_option() {
|
||||
let config = OpenCodeProviderConfig::new(
|
||||
"anthropic",
|
||||
"claude-sonnet-5",
|
||||
domain::ports::SecretRef::new("secret-ref"),
|
||||
)
|
||||
.unwrap();
|
||||
let body = opencode_provider_config_json(&config, "sk-live", "/project", None);
|
||||
let provider = &body["provider"]["anthropic"];
|
||||
assert_eq!(provider["options"]["apiKey"], "sk-live");
|
||||
assert!(provider["options"].get("baseURL").is_none());
|
||||
assert!(provider.get("npm").is_none());
|
||||
assert!(provider.get("models").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opencode_provider_config_json_custom_provider_emits_npm_base_url_and_models() {
|
||||
let custom = domain::profile::CustomProviderConfig::new(
|
||||
"@ai-sdk/openai-compatible",
|
||||
"https://my-endpoint/v1",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let config = OpenCodeProviderConfig::new(
|
||||
"my-custom",
|
||||
"my-model",
|
||||
domain::ports::SecretRef::new("secret-ref"),
|
||||
)
|
||||
.unwrap()
|
||||
.with_custom(custom);
|
||||
|
||||
let body = opencode_provider_config_json(&config, "sk-live", "/project", None);
|
||||
let provider = &body["provider"]["my-custom"];
|
||||
assert_eq!(provider["options"]["apiKey"], "sk-live");
|
||||
assert_eq!(provider["options"]["baseURL"], "https://my-endpoint/v1");
|
||||
assert_eq!(provider["npm"], "@ai-sdk/openai-compatible");
|
||||
assert_eq!(provider["models"]["my-model"]["name"], "my-model");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opencode_provider_config_json_custom_provider_model_name_prefers_display_name() {
|
||||
let custom = domain::profile::CustomProviderConfig::new(
|
||||
"@ai-sdk/openai-compatible",
|
||||
"https://my-endpoint/v1",
|
||||
Some("My Model".to_owned()),
|
||||
)
|
||||
.unwrap();
|
||||
let config = OpenCodeProviderConfig::new(
|
||||
"my-custom",
|
||||
"my-model",
|
||||
domain::ports::SecretRef::new("ref"),
|
||||
)
|
||||
.unwrap()
|
||||
.with_custom(custom);
|
||||
|
||||
let body = opencode_provider_config_json(&config, "sk-live", "/project", None);
|
||||
assert_eq!(
|
||||
body["provider"]["my-custom"]["models"]["my-model"]["name"],
|
||||
"My Model"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user