返回 CodeWhale
descriptors.rs
根目录 / crates / config / src / descriptors.rs
1 //! OMP-style provider descriptors: how to talk to a host.
2 //!
3 //! Model ids are **not** compiled here. A descriptor names the wire, URL, env
4 //! var, and whether authenticated `GET /v1/models` is the catalog authority
5 //! for that host. Offerings come from the Codewhale catalog layers and live
6 //! provider `/models` refreshes.
7
8 use std::sync::OnceLock;
9
10 use serde::Deserialize;
11
12 const DESCRIPTORS_JSON: &str = include_str!("../assets/provider_descriptors.json");
13
14 /// How this host's model list is discovered.
15 #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
16 #[serde(rename_all = "snake_case")]
17 pub enum DescriptorDiscovery {
18 /// Authenticated `GET {base_url}/models` is authoritative for this credential.
19 ModelsEndpoint,
20 /// No live discovery; only catalog/config rows.
21 None,
22 }
23
24 /// Transport used to send turns. Not a brand enum.
25 #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
26 #[serde(rename_all = "kebab-case")]
27 pub enum DescriptorWire {
28 OpenaiCompatible,
29 AnthropicMessages,
30 }
31
32 #[derive(Debug, Deserialize)]
33 struct DescriptorFile {
34 descriptors: Vec<ProviderDescriptor>,
35 }
36
37 /// Data row describing a hosted OpenAI-compatible (or Anthropic Messages) gateway.
38 #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
39 pub struct ProviderDescriptor {
40 pub id: String,
41 pub label: String,
42 pub wire: DescriptorWire,
43 pub base_url: String,
44 pub api_key_env: String,
45 pub default_model: String,
46 pub discovery: DescriptorDiscovery,
47 #[serde(default)]
48 pub docs_url: Option<String>,
49 #[serde(default)]
50 pub credential_url: Option<String>,
51 #[serde(default)]
52 pub guidance: Option<String>,
53 #[serde(default)]
54 pub aliases: Vec<String>,
55 }
56
57 impl ProviderDescriptor {
58 #[must_use]
59 pub fn matches(&self, needle: &str) -> bool {
60 let needle = needle.trim().to_ascii_lowercase().replace('_', "-");
61 if needle.is_empty() {
62 return false;
63 }
64 self.id == needle
65 || self
66 .aliases
67 .iter()
68 .any(|alias| alias.eq_ignore_ascii_case(&needle))
69 }
70 }
71
72 static DESCRIPTORS: OnceLock<Vec<ProviderDescriptor>> = OnceLock::new();
73
74 /// Bundled compatible-host descriptors. Panics only if the committed JSON is invalid.
75 #[must_use]
76 pub fn bundled_provider_descriptors() -> &'static [ProviderDescriptor] {
77 DESCRIPTORS
78 .get_or_init(|| {
79 let file: DescriptorFile = serde_json::from_str(DESCRIPTORS_JSON)
80 .expect("committed provider_descriptors.json must parse");
81 file.descriptors
82 })
83 .as_slice()
84 }
85
86 #[must_use]
87 pub fn provider_descriptor(id: &str) -> Option<&'static ProviderDescriptor> {
88 bundled_provider_descriptors()
89 .iter()
90 .find(|descriptor| descriptor.matches(id))
91 }
92
93 #[cfg(test)]
94 mod tests {
95 use super::*;
96
97 #[test]
98 fn descriptors_parse_and_command_code_is_a_row_not_a_kind() {
99 let rows = bundled_provider_descriptors();
100 assert!(
101 rows.len() >= 6,
102 "expected compatible hosts plus command-code and dashscope"
103 );
104 for row in rows {
105 assert!(row.base_url.starts_with("https://"), "{}", row.id);
106 assert!(!row.api_key_env.is_empty(), "{}", row.id);
107 assert!(!row.default_model.is_empty(), "{}", row.id);
108 assert_eq!(row.discovery, DescriptorDiscovery::ModelsEndpoint);
109 assert_eq!(row.wire, DescriptorWire::OpenaiCompatible);
110 }
111 let cmd = provider_descriptor("command-code").expect("command-code");
112 assert_eq!(cmd.base_url, "https://api.commandcode.ai/provider/v1");
113 assert_eq!(cmd.api_key_env, "COMMAND_CODE_API_KEY");
114 assert_eq!(
115 provider_descriptor("cmd-code").map(|row| row.id.as_str()),
116 Some("command-code")
117 );
118 // Alibaba Model Studio is a data-driven row: live /v1/models is the
119 // Qwen model authority, never a compiled roster.
120 let dashscope = provider_descriptor("dashscope").expect("dashscope");
121 assert_eq!(
122 dashscope.base_url,
123 "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
124 );
125 assert_eq!(dashscope.api_key_env, "DASHSCOPE_API_KEY");
126 assert_eq!(
127 provider_descriptor("qwen").map(|row| row.id.as_str()),
128 Some("dashscope"),
129 "the founder's `qwen` name resolves to the DashScope row"
130 );
131 }
132
133 #[test]
134 fn descriptors_do_not_embed_model_rosters() {
135 let raw = DESCRIPTORS_JSON;
136 assert!(
137 !raw.contains("moonshotai/Kimi-K2.7-Code"),
138 "do not compile a Baseten/Kimi roster into descriptors"
139 );
140 assert!(
141 !raw.contains("openai/gpt-oss-120b"),
142 "do not compile a Groq roster into descriptors"
143 );
144 }
145 }
146
146 lines RUST