nexus_core/app/
backends.rs1use crate::provider::openrouter::OpenRouter;
8use crate::provider::{BackendTag, Model};
9
10#[derive(Clone, Default)]
11pub struct Backends {
12 pub openrouter: Option<OpenRouter>,
13 pub openai: Option<OpenRouter>,
14 pub opencode: Option<OpenRouter>,
15 pub codex: Option<OpenRouter>,
16}
17
18impl Backends {
19 pub const fn any(&self) -> bool {
20 self.openrouter.is_some()
21 || self.openai.is_some()
22 || self.opencode.is_some()
23 || self.codex.is_some()
24 }
25
26 pub const fn configured(&self, tag: BackendTag) -> bool {
28 self.get(tag).is_some()
29 }
30
31 pub const fn get(&self, tag: BackendTag) -> Option<&OpenRouter> {
32 match tag {
33 BackendTag::OpenRouter => self.openrouter.as_ref(),
34 BackendTag::OpenAi => self.openai.as_ref(),
35 BackendTag::OpencodeGo => self.opencode.as_ref(),
36 BackendTag::Codex => self.codex.as_ref(),
37 }
38 }
39
40 pub fn set(&mut self, tag: BackendTag, provider: OpenRouter) {
41 match tag {
42 BackendTag::OpenRouter => self.openrouter = Some(provider),
43 BackendTag::OpenAi => self.openai = Some(provider),
44 BackendTag::OpencodeGo => self.opencode = Some(provider),
45 BackendTag::Codex => self.codex = Some(provider),
46 }
47 }
48
49 pub fn configured_tags(&self) -> Vec<BackendTag> {
52 [
53 BackendTag::OpenRouter,
54 BackendTag::OpenAi,
55 BackendTag::OpencodeGo,
56 BackendTag::Codex,
57 ]
58 .into_iter()
59 .filter(|t| self.get(*t).is_some())
60 .collect()
61 }
62
63 pub fn resolve(&self, composite_id: &str) -> Option<(OpenRouter, String)> {
68 for tag in [
69 BackendTag::OpenAi,
70 BackendTag::OpencodeGo,
71 BackendTag::Codex,
72 ] {
73 if let Some(raw) = composite_id.strip_prefix(tag.key_prefix()) {
74 return self.get(tag).cloned().map(|p| (p, raw.to_string()));
75 }
76 }
77 self.get(BackendTag::OpenRouter)
82 .or_else(|| self.configured_tags().first().and_then(|t| self.get(*t)))
83 .cloned()
84 .map(|p| (p, composite_id.to_string()))
85 }
86}
87
88pub fn composite_id(m: &Model) -> String {
93 format!("{}{}", m.backend.key_prefix(), m.id)
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn resolve_routes_prefixed_models_to_their_backend_even_with_openrouter_configured() {
102 let mut backends = Backends::default();
103 backends.set(
104 BackendTag::OpenRouter,
105 OpenRouter::openrouter_flavor("or".into()),
106 );
107 backends.set(BackendTag::OpenAi, OpenRouter::openai("oa".into()));
108 backends.set(BackendTag::OpencodeGo, OpenRouter::opencode_go("go".into()));
109 backends.set(BackendTag::Codex, OpenRouter::openai_codex("codex".into()));
110
111 let (provider, raw) = backends.resolve("openai:gpt-4.1-mini").unwrap();
112 assert_eq!(provider.backend_tag(), BackendTag::OpenAi);
113 assert_eq!(raw, "gpt-4.1-mini");
114
115 let (provider, raw) = backends.resolve("opencode:deepseek-v4-flash").unwrap();
116 assert_eq!(provider.backend_tag(), BackendTag::OpencodeGo);
117 assert_eq!(raw, "deepseek-v4-flash");
118
119 let (provider, raw) = backends.resolve("codex:gpt-5.4-mini").unwrap();
120 assert_eq!(provider.backend_tag(), BackendTag::Codex);
121 assert_eq!(raw, "gpt-5.4-mini");
122 }
123
124 #[test]
125 fn resolve_keeps_bare_ids_on_openrouter_for_legacy_settings() {
126 let mut backends = Backends::default();
127 backends.set(
128 BackendTag::OpenRouter,
129 OpenRouter::openrouter_flavor("or".into()),
130 );
131 backends.set(BackendTag::OpenAi, OpenRouter::openai("oa".into()));
132
133 let (provider, raw) = backends.resolve("google/gemini-2.5-flash-lite").unwrap();
134 assert_eq!(provider.backend_tag(), BackendTag::OpenRouter);
135 assert_eq!(raw, "google/gemini-2.5-flash-lite");
136 }
137}