llm: named-provider registry — judges & topology nodes on GLM/Kimi

Adds a multi-provider registry so judges and topology execution can run on
providers beyond the default. cm-config gains [[llm.providers]] (name, base_url,
api_key_env) — all OpenAI-compatible (GLM, Kimi/Moonshot). The server builds an
Arc<dyn LlmProvider> per entry (OpenAiCompatProvider) keyed by name; a missing
key is skipped with a warning, not a boot failure. cm-runtime RuntimeConfig
carries a ProviderRegistry; Runtime::resolve_provider("<name>:<model>") selects a
registry provider (else the default). The door governor (Runtime::judge) and the
topology compare endpoint both resolve through it, so CLAWMATES_JUDGE_MODEL and
CLAWMATES_TOPOLOGY_EXEC_MODEL accept "glm:glm-4.6" / "kimi:kimi-k2".

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-17 04:17:14 -07:00
co-authored by Claude Opus 4.8
parent 325038e806
commit 2ec8832ef6
5 changed files with 88 additions and 10 deletions
+25
View File
@@ -51,6 +51,29 @@ fn build_provider(config: &AppConfig) -> Result<Arc<dyn LlmProvider>, String> {
}
}
/// Builds the extra named-provider registry (GLM, Kimi, …) from
/// `[[llm.providers]]`. Each is OpenAI-compatible; its key comes from the named
/// env var. A provider whose key env is unset is skipped with a warning, so a
/// missing key degrades that one selector rather than failing boot.
fn build_provider_registry(config: &AppConfig) -> cm_runtime::ProviderRegistry {
let mut map = std::collections::HashMap::new();
for p in &config.llm.providers {
match std::env::var(&p.api_key_env) {
Ok(key) if !key.is_empty() => {
let provider: Arc<dyn LlmProvider> =
Arc::new(OpenAiCompatProvider::new(p.base_url.clone(), Some(key)));
map.insert(p.name.clone(), provider);
println!("clawmates-server: registered LLM provider '{}'", p.name);
}
_ => eprintln!(
"clawmates-server: provider '{}' skipped — {} is unset",
p.name, p.api_key_env
),
}
}
cm_runtime::ProviderRegistry(map)
}
async fn run() -> Result<(), String> {
let config_path = PathBuf::from(
std::env::var("CLAWMATES_CONFIG").unwrap_or_else(|_| "clawmates.toml".into()),
@@ -98,6 +121,7 @@ async fn run() -> Result<(), String> {
}
let provider = build_provider(&config)?;
let provider_registry = build_provider_registry(&config);
let blob: std::sync::Arc<dyn cm_files::BlobStore> = match config.storage.backend {
cm_config::StorageBackend::Local => std::sync::Arc::new(cm_files::LocalBlobStore::new(
PathBuf::from(&config.storage.data_dir),
@@ -154,6 +178,7 @@ async fn run() -> Result<(), String> {
slack_base_url: config.slack.base_url.clone(),
sandboxes,
browser,
providers: provider_registry,
},
blob,
);