Skip to main content

sqlite_graphrag/
backend_choice.rs

1//! LLM / embedding backend CLI choices (Wave C1).
2
3
4/// v1.0.82 (GAP-003): LLM backend for embedding. Accepts `auto` (default —
5/// detects `codex` or `claude` on the PATH), `codex` (forces codex exec), `claude`
6/// (forces claude -p), or `none` (skips embedding; useful for tests).
7#[derive(Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
8pub enum LlmBackendChoice {
9    /// Auto variant.
10    Auto,
11    /// Claude variant.
12    Claude,
13    /// Codex variant.
14    Codex,
15    /// Opencode variant.
16    Opencode,
17    /// Open router variant.
18    OpenRouter,
19    /// None variant.
20    None,
21}
22
23/// v1.0.93: embedding backend selector. Separate from `--llm-backend` which
24/// controls enrichment (entity extraction, body enrichment) via subprocess.
25/// `auto` tries OpenRouter if API key is available, falls back to LLM subprocess.
26/// `openrouter` requires API key (exit 78 if absent).
27/// `llm` forces subprocess (codex/claude/opencode) — legacy behaviour.
28#[derive(Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
29pub enum EmbeddingBackendChoice {
30    /// Auto variant.
31    Auto,
32    /// Openrouter variant.
33    Openrouter,
34    /// LLM variant.
35    Llm,
36}
37
38impl EmbeddingBackendChoice {
39    /// v1.0.93: produces a fallback chain that prepends OpenRouter when
40    /// the client is initialised. Falls back to the LLM subprocess chain.
41    pub fn to_chain(self, llm_choice: LlmBackendChoice) -> Vec<crate::embedder::LlmBackendKind> {
42        use crate::embedder::LlmBackendKind;
43        match self {
44            EmbeddingBackendChoice::Openrouter => vec![LlmBackendKind::OpenRouter],
45            EmbeddingBackendChoice::Llm => llm_choice.to_chain(),
46            EmbeddingBackendChoice::Auto => {
47                if crate::embedder::is_openrouter_initialized() {
48                    let mut chain = vec![LlmBackendKind::OpenRouter];
49                    chain.extend(llm_choice.to_chain());
50                    chain
51                } else {
52                    llm_choice.to_chain()
53                }
54            }
55        }
56    }
57}
58
59impl LlmBackendChoice {
60    /// v1.0.82 (GAP-003): converts the CLI choice into an ordered chain
61    /// of backends that `embedder::embed_with_fallback` iterates. The first
62    /// element of the chain is the preferred backend; subsequent elements
63    /// are fallbacks used when the preferred one fails with `LlmBackendError`.
64    ///
65    /// `Auto` produces `[Codex, Claude, None]` — codex is the default since v1.0.76+,
66    /// claude is the fallback if codex fails (OAuth contention, quota), and
67    /// `None` lets `embed_with_fallback` return an empty vector when
68    /// `skip_on_failure` is active.
69    pub fn to_chain(self) -> Vec<crate::embedder::LlmBackendKind> {
70        use crate::embedder::LlmBackendKind;
71        match self {
72            LlmBackendChoice::Codex => vec![LlmBackendKind::Codex, LlmBackendKind::None],
73            LlmBackendChoice::Claude => vec![LlmBackendKind::Claude, LlmBackendKind::None],
74            LlmBackendChoice::Opencode => vec![
75                LlmBackendKind::Opencode,
76                LlmBackendKind::Codex,
77                LlmBackendKind::Claude,
78                LlmBackendKind::None,
79            ],
80            LlmBackendChoice::OpenRouter => vec![
81                LlmBackendKind::OpenRouter,
82                LlmBackendKind::Codex,
83                LlmBackendKind::None,
84            ],
85            LlmBackendChoice::None => vec![LlmBackendKind::None],
86            LlmBackendChoice::Auto => parse_fallback_chain(
87                &crate::runtime_config::llm_fallback("codex,claude,none"),
88            ),
89        }
90    }
91}
92
93fn parse_fallback_chain(s: &str) -> Vec<crate::embedder::LlmBackendKind> {
94    use crate::embedder::LlmBackendKind;
95    let mut chain: Vec<LlmBackendKind> = s
96        .split(',')
97        .filter_map(|tok| match tok.trim().to_ascii_lowercase().as_str() {
98            "codex" => Some(LlmBackendKind::Codex),
99            "claude" | "claude-code" => Some(LlmBackendKind::Claude),
100            "opencode" => Some(LlmBackendKind::Opencode),
101            "openrouter" => Some(LlmBackendKind::OpenRouter),
102            "none" => Some(LlmBackendKind::None),
103            _ => {
104                tracing::warn!(
105                    token = tok.trim(),
106                    "unknown backend in --llm-fallback, skipping"
107                );
108                Option::None
109            }
110        })
111        .collect();
112    if chain.is_empty() {
113        chain = vec![
114            LlmBackendKind::Codex,
115            LlmBackendKind::Claude,
116            LlmBackendKind::None,
117        ];
118    }
119    chain
120}
121