返回 CodeWhale
build.rs
根目录 / crates / config / build.rs
1 //! Compile immutable provider metadata from its one committed data owner.
2
3 use std::{
4 collections::{BTreeMap, BTreeSet},
5 env, fs,
6 path::PathBuf,
7 };
8
9 use serde_json::Value;
10
11 fn text<'a>(row: &'a Value, key: &str) -> &'a str {
12 row[key]
13 .as_str()
14 .unwrap_or_else(|| panic!("missing string {key}"))
15 }
16
17 fn quoted(value: &str) -> String {
18 format!("{value:?}")
19 }
20
21 fn optional(row: &Value, key: &str) -> String {
22 match row.get(key) {
23 Some(Value::String(value)) => format!("Some({})", quoted(value)),
24 Some(Value::Null) => "None".into(),
25 _ => panic!("missing or malformed optional string {key}"),
26 }
27 }
28
29 fn strings(row: &Value, key: &str) -> String {
30 let values = row[key]
31 .as_array()
32 .unwrap_or_else(|| panic!("missing array {key}"));
33 format!(
34 "&[{}]",
35 values
36 .iter()
37 .map(|value| quoted(value.as_str().expect("string array")))
38 .collect::<Vec<_>>()
39 .join(", ")
40 )
41 }
42
43 fn credential(row: &Value) -> String {
44 let acquisition = match text(row, "acquisition") {
45 "api_key" => "ApiKey",
46 "api_key_or_oauth" => "ApiKeyOrOAuth",
47 "local_optional" => "LocalOptional",
48 "oauth" => "OAuth",
49 "configuration" => "Configuration",
50 other => panic!("unknown credential classification {other}"),
51 };
52 format!(
53 "crate::provider::CredentialHelp {{ acquisition: crate::provider::CredentialAcquisition::{acquisition}, credential_url: {}, docs_url: {}, guidance: {} }}",
54 optional(row, "credential_url"),
55 optional(row, "docs_url"),
56 quoted(text(row, "guidance"))
57 )
58 }
59
60 fn wire(row: &Value) -> &'static str {
61 match text(row, "wire_policy") {
62 "chat_completions" => {
63 "crate::provider::WirePolicy::Fixed(crate::provider::WireFormat::ChatCompletions)"
64 }
65 "responses" => "crate::provider::WirePolicy::Fixed(crate::provider::WireFormat::Responses)",
66 "anthropic_messages" => {
67 "crate::provider::WirePolicy::Fixed(crate::provider::WireFormat::AnthropicMessages)"
68 }
69 "model_aware" => "crate::provider::WirePolicy::ModelAware",
70 other => panic!("unknown wire classification {other}"),
71 }
72 }
73
74 fn main() {
75 println!("cargo:rerun-if-changed=assets/provider_descriptors.json");
76 let file: Value = serde_json::from_str(
77 &fs::read_to_string("assets/provider_descriptors.json").expect("provider data"),
78 )
79 .expect("provider data JSON");
80 assert_eq!(
81 file["schema_version"].as_u64(),
82 Some(3),
83 "unsupported provider schema"
84 );
85 let rows = file["providers"].as_array().expect("built-in rows");
86 let mut ids = BTreeSet::new();
87 let mut kinds = BTreeSet::new();
88 let mut output = String::from("// Generated from provider_descriptors.json. Do not edit.\n");
89 let mut selected = Vec::new();
90 output.push_str("pub(crate) static BUILTIN_DESCRIPTORS: &[BuiltinProviderDescriptor] = &[\n");
91 for row in rows {
92 let id = text(row, "id");
93 let kind = text(row, "kind");
94 assert!(!id.is_empty() && !kind.is_empty(), "empty identity");
95 assert!(ids.insert(id), "duplicate provider id {id}");
96 assert!(kinds.insert(kind), "duplicate provider kind {kind}");
97 assert!(
98 kind.chars().all(|c| c.is_ascii_alphanumeric()),
99 "invalid Rust kind"
100 );
101 let selectable = row["selectable"].as_bool().expect("selectable status");
102 let retired = row["retired"].as_bool().expect("retired status");
103 assert!(
104 !retired || !selectable,
105 "retired provider cannot be selectable"
106 );
107 if kind == "Antigravity" {
108 assert!(retired && !selectable, "Antigravity remains retired");
109 }
110 if selectable {
111 selected.push((
112 row["selection_order"].as_u64().expect("selection order"),
113 format!("crate::ProviderKind::{kind}"),
114 ));
115 } else {
116 assert!(row["selection_order"].is_null(), "non-selectable order");
117 }
118 output.push_str(&format!("BuiltinProviderDescriptor {{ kind: crate::ProviderKind::{kind}, id: {}, label: {}, base_url: {}, default_model: {}, env_vars: {}, aliases: {}, config_key: {}, secret_store_slot: {}, family: {}, selectable: {selectable}, retired: {retired}, wire_policy: {}, credential_help: {} }},\n",
119 quoted(id),quoted(text(row,"label")),quoted(text(row,"base_url")),quoted(text(row,"default_model")),strings(row,"env_vars"),strings(row,"aliases"),quoted(text(row,"config_key")),quoted(text(row,"secret_store_slot")),quoted(text(row,"family")),wire(row),credential(&row["credential_help"])));
120 }
121 output.push_str("];\n");
122 // Generated exhaustive projection makes a new/missing enum variant a
123 // compiler error, rather than a delayed registry panic.
124 output.push_str("pub(crate) const fn builtin_provider_descriptor(kind: crate::ProviderKind) -> &'static BuiltinProviderDescriptor { match kind {\n");
125 for (index, row) in rows.iter().enumerate() {
126 output.push_str(&format!(
127 "crate::ProviderKind::{} => &BUILTIN_DESCRIPTORS[{index}],\n",
128 text(row, "kind")
129 ));
130 }
131 output.push_str("} }\n");
132 selected.sort_by_key(|(order, _)| *order);
133 for (index, (order, _)) in selected.iter().enumerate() {
134 assert_eq!(
135 *order,
136 u64::try_from(index).expect("selection index"),
137 "selection order must be unique and contiguous"
138 );
139 }
140 let selected: Vec<_> = selected.into_iter().map(|(_, kind)| kind).collect();
141 output.push_str(&format!(
142 "pub(crate) const SELECTABLE_PROVIDER_KINDS: [crate::ProviderKind; {}] = [{}];\n",
143 selected.len(),
144 selected.join(", ")
145 ));
146 for row in file["descriptors"].as_array().expect("compatible rows") {
147 assert!(
148 ids.insert(text(row, "id")),
149 "descriptor shadows built-in identity"
150 );
151 for field in ["id", "label", "base_url", "api_key_env", "default_model"] {
152 assert!(!text(row, field).is_empty(), "empty compatible-host field");
153 }
154 assert!(
155 matches!(
156 text(row, "wire"),
157 "openai-compatible" | "anthropic-messages"
158 ),
159 "unknown compatible wire"
160 );
161 assert!(
162 matches!(text(row, "discovery"), "models_endpoint" | "none"),
163 "unknown discovery"
164 );
165 if row.get("aliases").is_some() {
166 let _ = strings(row, "aliases");
167 }
168 for field in ["docs_url", "credential_url", "guidance"] {
169 if row.get(field).is_some() {
170 let _ = optional(row, field);
171 }
172 }
173 }
174 let legacy = &file["legacy_tui"];
175 output.push_str(&format!("/// Exact retained TUI-only compatibility metadata.\npub const LEGACY_DEEPSEEK_CN: LegacyProviderDescriptor = LegacyProviderDescriptor {{ id: {}, label: {}, base_url: {}, default_model: {}, config_key: {}, secret_store_slot: {} }};\n",
176 quoted(text(legacy,"id")),quoted(text(legacy,"label")),quoted(text(legacy,"base_url")),quoted(text(legacy,"default_model")),quoted(text(legacy,"config_key")),quoted(text(legacy,"secret_store_slot"))));
177 // Existing descriptor data owns the released presentation tags and typed
178 // table projection. A missing field or duplicate tag refuses the build.
179 let mut compatibility: Vec<_> = rows.iter().chain(std::iter::once(legacy)).collect();
180 compatibility.sort_by_key(|row| row["tui_order"].as_u64().expect("compatibility order"));
181 let mut tags = BTreeSet::new();
182 let mut compatibility_ids = BTreeSet::new();
183 output.push_str("static PROVIDER_COMPATIBILITY: &[ProviderCompatibility] = &[\n");
184 for (index, row) in compatibility.iter().enumerate() {
185 assert_eq!(
186 row["tui_order"].as_u64(),
187 Some(index as u64),
188 "compatibility order must be contiguous"
189 );
190 let id = text(row, "id");
191 let tag = text(row, "tui_wire_tag");
192 assert!(
193 !tag.is_empty() && tags.insert(tag),
194 "duplicate/empty compatibility tag"
195 );
196 assert!(
197 compatibility_ids.insert(id),
198 "duplicate compatibility identity"
199 );
200 for field in ["catalog_id", "catalog_source_id"] {
201 assert!(
202 rows.iter()
203 .any(|primary| text(primary, "id") == text(row, field))
204 || text(row, field) == id,
205 "unknown catalog projection"
206 );
207 }
208 let field = text(row, "table_field");
209 assert!(
210 field.chars().next().is_some_and(|c| c.is_ascii_lowercase())
211 && field
212 .chars()
213 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'),
214 "invalid Rust table field"
215 );
216 if let Some(key) = row.get("base_url_config_key") {
217 let key = key.as_str().expect("endpoint table key");
218 assert!(
219 rows.iter().any(|row| text(row, "config_key") == key),
220 "unknown endpoint config table"
221 );
222 }
223 output.push_str(&format!("ProviderCompatibility {{ kind: crate::ProviderKind::{}, id: {}, tui_wire_tag: {}, config_key: {}, base_url_config_key: {}, catalog_id: {}, catalog_source_id: {}, subagent_aliases: {}, selector_aliases: {}, label: {}, base_url: {}, default_model: {} }},\n", text(row,"kind"),quoted(id),quoted(tag),quoted(text(row,"config_key")),quoted(row.get("base_url_config_key").map_or_else(|| text(row,"config_key"), |value| value.as_str().expect("endpoint table key"))),quoted(text(row,"catalog_id")),quoted(text(row,"catalog_source_id")),strings(row,"subagent_aliases"),strings(row,"selector_aliases"),quoted(text(row,"label")),quoted(text(row,"base_url")),quoted(text(row,"default_model"))));
224 }
225 output.push_str("];\n");
226 // Expand against the consumer's released typed configuration struct. The
227 // field inventory remains generated; custom/legacy provenance is admitted
228 // by that consumer before this pure projection is called.
229 output.push_str("#[macro_export]\nmacro_rules! provider_config_table {\n");
230 for (mode, borrow) in [("read", "&"), ("write", "&mut ")] {
231 output.push_str(&format!(
232 "(@{mode} $providers:expr, $id:expr) => {{ match $id {{\n"
233 ));
234 for row in &compatibility {
235 if text(row, "kind") != "Custom" {
236 output.push_str(&format!(
237 "{} => Some({borrow}$providers.{}),\n",
238 quoted(text(row, "id")),
239 text(row, "table_field")
240 ));
241 }
242 }
243 output.push_str("_ => None } };\n");
244 }
245 output.push_str("}\n");
246 let mut public_orders = BTreeSet::new();
247 for row in rows {
248 if row["retired"] == false && text(row, "kind") != "Custom" {
249 let order = row["web"]["order"]
250 .as_u64()
251 .expect("public presentation order");
252 assert!(
253 public_orders.insert(order),
254 "duplicate public presentation order"
255 );
256 assert!(
257 row["web"].get("variant").is_none(),
258 "retired enum presentation identity"
259 );
260 }
261 }
262 assert!(
263 public_orders
264 .iter()
265 .enumerate()
266 .all(|(index, order)| *order == index as u64),
267 "public order must be contiguous"
268 );
269 for (key, row) in file["route_credential_help"]
270 .as_object()
271 .expect("route credential help")
272 .iter()
273 .collect::<BTreeMap<_, _>>()
274 {
275 let name = key.replace('-', "_").to_ascii_uppercase();
276 assert!(
277 name.chars().all(|c| c.is_ascii_uppercase() || c == '_'),
278 "invalid route help key"
279 );
280 output.push_str(&format!(
281 "pub(crate) const {name}_CREDENTIAL_HELP: crate::provider::CredentialHelp = {};\n",
282 credential(row)
283 ));
284 }
285 let mut defaults = String::from("// Generated data projections. Do not edit.\n");
286 let refs = file["constant_refs"]
287 .as_object()
288 .expect("constant references");
289 let constants = file["compatibility_constants"]
290 .as_object()
291 .expect("compatibility constants");
292 for (name, value) in refs.iter().collect::<BTreeMap<_, _>>() {
293 assert!(!constants.contains_key(name), "duplicate constant");
294 assert!(
295 name.chars()
296 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_'),
297 "invalid constant name"
298 );
299 let row = rows
300 .iter()
301 .find(|row| row["id"] == value["provider"])
302 .expect("constant row");
303 let field = text(value, "field");
304 assert!(
305 matches!(field, "base_url" | "default_model" | "credential_url"),
306 "unknown constant field"
307 );
308 let source = if field == "credential_url" {
309 &row["credential_help"][field]
310 } else {
311 &row[field]
312 };
313 defaults.push_str(&format!(
314 "/// Projection of the committed provider descriptor.\npub const {name}: &str = {};\n",
315 quoted(source.as_str().expect("constant field string"))
316 ));
317 }
318 for (name, value) in constants.iter().collect::<BTreeMap<_, _>>() {
319 assert!(
320 name.chars()
321 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_'),
322 "invalid constant name"
323 );
324 defaults.push_str(&format!(
325 "/// Retained data-owned compatibility seed.\npub const {name}: &str = {};\n",
326 quoted(value.as_str().expect("compatibility string"))
327 ));
328 }
329 let directory = PathBuf::from(env::var_os("OUT_DIR").expect("build output"));
330 fs::write(directory.join("provider_descriptors.rs"), output).expect("write descriptors");
331 fs::write(directory.join("provider_defaults.rs"), defaults).expect("write defaults");
332
333 println!("cargo:rerun-if-changed=assets/catalog_corrections.json");
334 let catalog: Value = serde_json::from_str(
335 &fs::read_to_string("assets/catalog_corrections.json").expect("catalog data"),
336 )
337 .expect("catalog JSON");
338 let reviewed = &catalog["reviewed"];
339 assert!(!text(reviewed, "revision").is_empty(), "reviewed revision");
340 let mut catalog_constants =
341 String::from("// Generated from catalog_corrections.json.reviewed. Do not edit.\n");
342 for (name, value) in reviewed["constants"]
343 .as_object()
344 .expect("model constants")
345 .iter()
346 .collect::<BTreeMap<_, _>>()
347 {
348 assert!(
349 name.chars()
350 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_'),
351 "invalid catalog constant"
352 );
353 catalog_constants.push_str(&format!(
354 "pub const {name}: &str = {};\n",
355 quoted(value.as_str().expect("model constant string"))
356 ));
357 }
358 for (name, reference) in reviewed["numeric_refs"]
359 .as_object()
360 .expect("numeric contract references")
361 {
362 assert!(
363 name.chars()
364 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_'),
365 "invalid numeric contract name"
366 );
367 let field = text(reference, "field");
368 assert!(
369 matches!(
370 field,
371 "context_window" | "max_output" | "generation_default"
372 ),
373 "unknown numeric contract field"
374 );
375 let value = reviewed["intrinsic"][text(reference, "model")][field]
376 .as_u64()
377 .expect("numeric intrinsic fact");
378 assert!(
379 value > 0 && value <= u64::from(u32::MAX),
380 "numeric intrinsic bound"
381 );
382 catalog_constants.push_str(&format!("pub const {name}: u32 = {value};\n"));
383 }
384 for (name, value) in reviewed["groups"]
385 .as_object()
386 .expect("model rosters")
387 .iter()
388 .collect::<BTreeMap<_, _>>()
389 {
390 assert!(
391 name.chars()
392 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_'),
393 "invalid catalog group"
394 );
395 let values = value
396 .as_array()
397 .expect("model roster array")
398 .iter()
399 .map(|v| quoted(v.as_str().expect("model roster string")))
400 .collect::<Vec<_>>()
401 .join(", ");
402 catalog_constants.push_str(&format!("pub const {name}: &[&str] = &[{values}];\n"));
403 }
404 for (name, provider, endpoint) in [
405 (
406 "OPENCODE_ZEN_RESPONSES_MODELS",
407 "opencode-zen",
408 Some("responses"),
409 ),
410 (
411 "OPENCODE_ZEN_MESSAGES_MODELS",
412 "opencode-zen",
413 Some("messages"),
414 ),
415 ("OPENCODE_ZEN_CHAT_MODELS", "opencode-zen", Some("chat")),
416 ("OPENCODE_GO_MODELS", "opencode-go", None),
417 ("MODELSTUDIO_TEXT_MODELS", "modelstudio-token-plan", None),
418 ("CODEWHALE_FALLBACK_MODELS", "codewhale", None),
419 ] {
420 let values = reviewed["transports"]
421 .as_array()
422 .expect("transport rows")
423 .iter()
424 .filter(|row| {
425 row["provider"] == provider
426 && endpoint.is_none_or(|endpoint| row["endpoint_key"] == endpoint)
427 })
428 .map(|row| quoted(text(row, "id")))
429 .collect::<Vec<_>>()
430 .join(", ");
431 catalog_constants.push_str(&format!("pub const {name}: &[&str] = &[{values}];\n"));
432 }
433 // Alias/completion entries refer to existing defaults or model groups;
434 // the source owner never repeats a descriptor-owned default value.
435 let mut values = BTreeMap::new();
436 for (name, value) in constants {
437 values.insert(name.as_str(), value.as_str().expect("compatibility value"));
438 }
439 for (name, reference) in refs {
440 let provider = rows
441 .iter()
442 .find(|row| row["id"] == reference["provider"])
443 .expect("constant provider");
444 let field = text(reference, "field");
445 let value = if field == "credential_url" {
446 &provider["credential_help"][field]
447 } else {
448 &provider[field]
449 };
450 values.insert(name.as_str(), value.as_str().expect("constant value"));
451 }
452 for (name, value) in reviewed["constants"]
453 .as_object()
454 .expect("catalog constants")
455 {
456 assert!(
457 values
458 .insert(name, value.as_str().expect("catalog value"))
459 .is_none(),
460 "duplicate data constant"
461 );
462 }
463 let resolve = |value: &str| -> String {
464 match value.strip_prefix('$') {
465 Some(name) => values
466 .get(name)
467 .unwrap_or_else(|| panic!("missing constant {name}"))
468 .to_string(),
469 None => value.to_string(),
470 }
471 };
472 let mut groups: BTreeMap<String, Vec<String>> = reviewed["groups"]
473 .as_object()
474 .expect("groups")
475 .iter()
476 .map(|(name, value)| {
477 (
478 name.clone(),
479 value
480 .as_array()
481 .expect("group")
482 .iter()
483 .map(|v| resolve(v.as_str().expect("group entry")))
484 .collect(),
485 )
486 })
487 .collect();
488 for (name, provider, endpoint) in [
489 (
490 "OPENCODE_ZEN_RESPONSES_MODELS",
491 "opencode-zen",
492 Some("responses"),
493 ),
494 (
495 "OPENCODE_ZEN_MESSAGES_MODELS",
496 "opencode-zen",
497 Some("messages"),
498 ),
499 ("OPENCODE_ZEN_CHAT_MODELS", "opencode-zen", Some("chat")),
500 ("OPENCODE_GO_MODELS", "opencode-go", None),
501 ("MODELSTUDIO_TEXT_MODELS", "modelstudio-token-plan", None),
502 ("CODEWHALE_FALLBACK_MODELS", "codewhale", None),
503 ] {
504 groups.insert(
505 name.into(),
506 reviewed["transports"]
507 .as_array()
508 .expect("transports")
509 .iter()
510 .filter(|row| {
511 row["provider"] == provider
512 && endpoint.is_none_or(|endpoint| row["endpoint_key"] == endpoint)
513 })
514 .map(|row| text(row, "id").to_string())
515 .collect(),
516 );
517 }
518 catalog_constants.push_str("#[must_use]\npub fn compatibility_alias(group: &str, model: &str) -> Option<&'static str> { match (group, model) {\n");
519 for (group, aliases) in reviewed["compatibility_aliases"]
520 .as_object()
521 .expect("aliases")
522 {
523 let mut selectors = BTreeSet::new();
524 for (alias, target) in aliases.as_object().expect("alias map") {
525 let alias = resolve(alias);
526 let target = resolve(target.as_str().expect("alias target"));
527 assert!(
528 selectors.insert(alias.clone()),
529 "duplicate resolved selector {alias}"
530 );
531 catalog_constants.push_str(&format!(
532 "({}, {}) => Some({}),\n",
533 quoted(group),
534 quoted(&alias),
535 quoted(&target)
536 ));
537 }
538 }
539 catalog_constants.push_str("_ => None, } }\n#[must_use]\npub fn completion_names(provider: &str) -> &'static [&'static str] { match provider {\n");
540 for (provider, roster) in reviewed["completion_rosters"]
541 .as_object()
542 .expect("completion rosters")
543 {
544 assert!(
545 rows.iter().any(|row| row["id"] == provider.as_str()) || provider == text(legacy, "id"),
546 "unknown completion provider"
547 );
548 let mut list = Vec::new();
549 for entry in roster.as_array().expect("completion roster") {
550 let entry = entry.as_str().expect("completion entry");
551 if let Some(group) = entry.strip_prefix('@') {
552 list.extend(
553 groups
554 .get(group)
555 .unwrap_or_else(|| panic!("missing group {group}"))
556 .iter()
557 .cloned(),
558 );
559 } else {
560 list.push(resolve(entry));
561 }
562 }
563 // Zen's logical default can be an alias of a documented row. Preserve
564 // its original stable, case-insensitive first-occurrence projection.
565 if provider == "opencode-zen" {
566 let mut seen = BTreeSet::new();
567 list.retain(|id| seen.insert(id.to_ascii_lowercase()));
568 }
569 assert!(
570 !matches!(provider.as_str(), "antigravity" | "custom") || list.is_empty(),
571 "retired/custom completion rows"
572 );
573 catalog_constants.push_str(&format!(
574 "{} => &[{}],\n",
575 quoted(provider),
576 list.iter()
577 .map(|v| quoted(v))
578 .collect::<Vec<_>>()
579 .join(", ")
580 ));
581 }
582 catalog_constants.push_str("_ => &[], } }\n");
583 fs::write(directory.join("catalog_constants.rs"), catalog_constants)
584 .expect("write catalog constants");
585 }
586
586 lines RUST