walrus_core/config/provider.rs
1//! Remote provider configuration.
2
3use anyhow::{Result, bail};
4use compact_str::CompactString;
5use serde::{Deserialize, Serialize};
6
7/// API protocol standard for remote providers.
8///
9/// Only two wire formats exist: OpenAI-compatible and Anthropic.
10/// Defaults to `OpenAI` when omitted in config.
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum ApiStandard {
14 /// OpenAI-compatible chat completions API (covers DeepSeek, Grok, Qwen, Kimi, Ollama, etc.).
15 #[default]
16 OpenAI,
17 /// Anthropic Messages API.
18 Anthropic,
19}
20
21/// Remote provider configuration.
22///
23/// Any model name is valid — the `standard` field (or auto-detection from
24/// `base_url`) determines which API protocol to use. Local models are handled
25/// by the built-in registry, not by this config.
26#[derive(Debug, Serialize, Deserialize, Clone)]
27pub struct ProviderConfig {
28 /// Model identifier sent to the remote API.
29 pub model: CompactString,
30 /// API key for remote providers. Supports `${ENV_VAR}` expansion at the
31 /// daemon layer.
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub api_key: Option<String>,
34 /// Base URL for the remote provider endpoint.
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub base_url: Option<String>,
37 /// API protocol standard. Defaults to OpenAI if omitted.
38 #[serde(default)]
39 pub standard: ApiStandard,
40}
41
42impl ProviderConfig {
43 /// Resolve the effective API standard.
44 ///
45 /// Returns `Anthropic` if the field is explicitly set to `Anthropic`,
46 /// or if `base_url` contains "anthropic". Otherwise `OpenAI`.
47 pub fn effective_standard(&self) -> ApiStandard {
48 if self.standard == ApiStandard::Anthropic {
49 return ApiStandard::Anthropic;
50 }
51 if let Some(url) = &self.base_url
52 && url.contains("anthropic")
53 {
54 return ApiStandard::Anthropic;
55 }
56 ApiStandard::OpenAI
57 }
58
59 /// Validate field combinations.
60 ///
61 /// Called on startup and on provider add/reload.
62 pub fn validate(&self) -> Result<()> {
63 if self.model.is_empty() {
64 bail!("model is required");
65 }
66 // Remote providers: api_key is required unless base_url is set
67 // (e.g. Ollama which is keyless with a local base_url).
68 if self.api_key.is_none() && self.base_url.is_none() {
69 bail!(
70 "remote provider '{}' requires api_key or base_url",
71 self.model
72 );
73 }
74 Ok(())
75 }
76}