Skip to main content

loopsmith_core/config/
providers.rs

1//! Provider routing. Every provider is a command template.
2
3use super::graph::Tier;
4use super::yes;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8
9/// Every provider is a command template. `{prompt}`, `{system}`, `{model}`
10/// and `{tier}` are substituted before spawn. This keeps BYOK support out of
11/// the Rust build entirely: if you can invoke it from a shell, loopsmith can
12/// route to it.
13#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
14#[serde(deny_unknown_fields)]
15pub struct ProviderSpec {
16    pub id: String,
17    pub kind: ProviderKind,
18    #[serde(default)]
19    pub tiers: Vec<Tier>,
20    pub command: String,
21    #[serde(default)]
22    pub args: Vec<String>,
23    #[serde(default)]
24    pub model: Option<String>,
25    /// Environment variables that must be present. Values are never read by
26    /// loopsmith, only checked for presence, so keys stay out of the ledger.
27    #[serde(default)]
28    pub requires_env: Vec<String>,
29    #[serde(default)]
30    pub timeout_seconds: Option<u64>,
31    /// Send the prompt on stdin instead of substituting into args.
32    #[serde(default)]
33    pub prompt_on_stdin: bool,
34    /// Regex with one capture group pulling a token count out of the
35    /// provider's own output. Without it, usage is estimated from character
36    /// count, which is enough to make a budget ceiling real but is not exact.
37    #[serde(default)]
38    pub usage_regex: Option<String>,
39    /// Price per thousand tokens, for the cost ceiling.
40    #[serde(default)]
41    pub cost_per_1k_tokens: Option<f64>,
42}
43
44/// Provider families. Each variant accepts the spellings people actually
45/// write, because a config that rejects `openai` in favour of `open_ai` is a
46/// config that wastes the author's afternoon.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
48#[serde(rename_all = "snake_case")]
49pub enum ProviderKind {
50    #[serde(
51        rename = "claude_code",
52        alias = "claude-code",
53        alias = "claude",
54        alias = "claudecode"
55    )]
56    ClaudeCode,
57    #[serde(alias = "Ollama")]
58    Ollama,
59    #[serde(rename = "grok_cli", alias = "grok-cli", alias = "grok")]
60    GrokCli,
61    #[serde(rename = "grok_build", alias = "grok-build")]
62    GrokBuild,
63    #[serde(alias = "Hermes")]
64    Hermes,
65    #[serde(
66        rename = "openai",
67        alias = "open_ai",
68        alias = "open-ai",
69        alias = "OpenAI"
70    )]
71    OpenAi,
72    #[serde(alias = "google_gemini", alias = "google-gemini", alias = "Gemini")]
73    Gemini,
74    /// Any OpenAI-compatible or bespoke endpoint driven by a command.
75    #[serde(alias = "BYOK", alias = "custom")]
76    Byok,
77    /// An MCP server spoken to over stdio.
78    #[serde(alias = "MCP")]
79    Mcp,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
83#[serde(deny_unknown_fields)]
84pub struct ProviderRouting {
85    #[serde(default)]
86    pub providers: Vec<ProviderSpec>,
87    /// Ordered fallback chain per tier. First reachable provider wins.
88    #[serde(default)]
89    pub cascade: BTreeMap<String, Vec<String>>,
90    /// A judge must not run on the provider that produced the work.
91    #[serde(default = "yes")]
92    pub enforce_judge_independence: bool,
93}