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:
@ -436,15 +436,28 @@ fn opencode_provider_config_json(
|
||||
"model".to_owned(),
|
||||
Value::String(format!("{}/{}", config.provider_id, config.model)),
|
||||
);
|
||||
let mut options = Map::new();
|
||||
options.insert("apiKey".to_owned(), Value::String(api_key.to_owned()));
|
||||
let mut provider_entry = 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(), Value::String(custom.base_url.clone()));
|
||||
provider_entry.insert("npm".to_owned(), Value::String(custom.npm.clone()));
|
||||
let model_label = custom
|
||||
.display_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| config.model.clone());
|
||||
provider_entry.insert(
|
||||
"models".to_owned(),
|
||||
json!({ config.model.as_str(): { "name": model_label } }),
|
||||
);
|
||||
}
|
||||
provider_entry.insert("options".to_owned(), Value::Object(options));
|
||||
root.insert(
|
||||
"provider".to_owned(),
|
||||
json!({
|
||||
config.provider_id.as_str(): {
|
||||
"options": {
|
||||
"apiKey": api_key
|
||||
}
|
||||
}
|
||||
}),
|
||||
json!({ config.provider_id.as_str(): provider_entry }),
|
||||
);
|
||||
|
||||
let (command, args) = match runtime {
|
||||
@ -594,3 +607,69 @@ fn parent_dir(base: &ProjectPath, rel: &str) -> String {
|
||||
_ => base.as_str().trim_end_matches(['/', '\\']).to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod opencode_provider_config_json_tests {
|
||||
use domain::profile::CustomProviderConfig;
|
||||
use domain::ports::SecretRef;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn known_provider_config() -> OpenCodeProviderConfig {
|
||||
OpenCodeProviderConfig::new(
|
||||
"anthropic",
|
||||
"claude-sonnet-5",
|
||||
SecretRef::new("secret-ref"),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn custom_provider_config() -> OpenCodeProviderConfig {
|
||||
let custom =
|
||||
CustomProviderConfig::new("@ai-sdk/openai-compatible", "https://my-endpoint/v1", None)
|
||||
.unwrap();
|
||||
OpenCodeProviderConfig::new("my-custom", "my-model", SecretRef::new("secret-ref"))
|
||||
.unwrap()
|
||||
.with_custom(custom)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_provider_emits_only_the_api_key_option() {
|
||||
let config = known_provider_config();
|
||||
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 custom_provider_emits_npm_base_url_and_models() {
|
||||
let config = custom_provider_config();
|
||||
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 custom_provider_model_name_prefers_display_name() {
|
||||
let custom = CustomProviderConfig::new(
|
||||
"@ai-sdk/openai-compatible",
|
||||
"https://my-endpoint/v1",
|
||||
Some("My Model".to_owned()),
|
||||
)
|
||||
.unwrap();
|
||||
let config = OpenCodeProviderConfig::new("my-custom", "my-model", 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