Skip to content

Commit fec943d

Browse files
author
CodeWhale Bot
committed
feat(config): Codewhale catalog authority; delete compiled model rosters
Compatible hosts (Baseten, Groq, Cerebras, SenseNova, Command Code) are descriptors: wire, URL, env, live GET /v1/models. Offerings come from catalog layers; a signed CWC fetch wins over models.dev. Command Code is a row, not a ProviderKind. Do not import the Command Code CLI login. Signed-off-by: CodeWhale Bot <bot@codewhale.net>
1 parent 84f42ae commit fec943d

9 files changed

Lines changed: 339 additions & 52 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Changed
1111

12+
- Provider catalogs: compatible hosts (Baseten, Groq, Cerebras, SenseNova,
13+
Command Code) no longer compile a frozen model roster. Descriptors name the
14+
wire, URL, and env; live `GET /v1/models` and a Codewhale-owned catalog
15+
layer are the offering list. Command Code is a catalog/descriptor row, not
16+
a `ProviderKind`.
17+
1218
- TUI: startup no longer presents an approximate ASCII or block-glyph whale as
1319
the product mark. It keeps the direct Tideline prompt while exact-raster
1420
surfaces remain responsible for the canonical asset.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"schema": "codewhale.provider_descriptors.v1",
3+
"note": "How to talk to a host. Model ids are not compiled here; live /v1/models and the Codewhale catalog are truth. default_model is a bootstrap hint only.",
4+
"descriptors": [
5+
{
6+
"id": "sensenova",
7+
"label": "SenseNova",
8+
"wire": "openai-compatible",
9+
"base_url": "https://token.sensenova.cn/v1",
10+
"api_key_env": "SENSENOVA_API_KEY",
11+
"default_model": "deepseek-v4-flash",
12+
"discovery": "models_endpoint",
13+
"aliases": ["sense-nova", "meituan-sensenova", "meituan-sensenova-cn"]
14+
},
15+
{
16+
"id": "baseten",
17+
"label": "Baseten",
18+
"wire": "openai-compatible",
19+
"base_url": "https://inference.baseten.co/v1",
20+
"api_key_env": "BASETEN_API_KEY",
21+
"default_model": "deepseek-ai/DeepSeek-V4-Pro",
22+
"discovery": "models_endpoint",
23+
"docs_url": "https://docs.baseten.co/inference/model-apis/overview",
24+
"credential_url": "https://app.baseten.co/settings/api_keys",
25+
"aliases": ["base-ten", "base_ten"]
26+
},
27+
{
28+
"id": "groq",
29+
"label": "Groq",
30+
"wire": "openai-compatible",
31+
"base_url": "https://api.groq.com/openai/v1",
32+
"api_key_env": "GROQ_API_KEY",
33+
"default_model": "llama-3.3-70b-versatile",
34+
"discovery": "models_endpoint",
35+
"docs_url": "https://console.groq.com/docs/quickstart",
36+
"credential_url": "https://console.groq.com/keys"
37+
},
38+
{
39+
"id": "cerebras",
40+
"label": "Cerebras",
41+
"wire": "openai-compatible",
42+
"base_url": "https://api.cerebras.ai/v1",
43+
"api_key_env": "CEREBRAS_API_KEY",
44+
"default_model": "llama-3.3-70b",
45+
"discovery": "models_endpoint",
46+
"docs_url": "https://inference-docs.cerebras.ai/quickstart",
47+
"credential_url": "https://cloud.cerebras.ai"
48+
},
49+
{
50+
"id": "command-code",
51+
"label": "Command Code",
52+
"wire": "openai-compatible",
53+
"base_url": "https://api.commandcode.ai/provider/v1",
54+
"api_key_env": "COMMAND_CODE_API_KEY",
55+
"default_model": "deepseek/deepseek-v4-flash",
56+
"discovery": "models_endpoint",
57+
"docs_url": "https://commandcode.ai/docs/provider",
58+
"credential_url": "https://commandcode.ai/provider",
59+
"guidance": "Published Provider API (OpenAI Chat Completions + GET /v1/models). Do not import the Command Code CLI login. Store COMMAND_CODE_API_KEY, not a raw key.",
60+
"aliases": ["commandcode", "cmd-code"]
61+
}
62+
]
63+
}

crates/config/src/catalog.rs

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66
//! refresh) and live [`ProviderCatalogDelta`]s; the HTTP `/models` fetch layer
77
//! lives above this module. Nothing here performs I/O or reads credentials.
88
//!
9-
//! Layering (lowest precedence first; #4188):
9+
//! Layering (lowest precedence first):
1010
//!
1111
//! ```text
12-
//! bundled Models.dev snapshot (offline/stale fallback only — not competing truth)
13-
//! < live Models.dev / provider `/models` cache
14-
//! < user / custom overrides (custom endpoints, pinned models, explicit facts)
12+
//! bundled Models.dev snapshot (legacy seed, not competing truth)
13+
//! < bundled Codewhale catalog (Codewhale-owned offline snapshot)
14+
//! < live Models.dev
15+
//! < live provider `/v1/models` (credential-scoped workspace list)
16+
//! < live Codewhale signed catalog (authority when present)
17+
//! < config.toml / user overrides
1518
//! ```
1619
//!
1720
//! After #4187, live Models.dev rows are preferred whenever present. The bundled
@@ -61,6 +64,10 @@ pub enum CatalogSource {
6164
ModelsDevLive { fetched_at: u64 },
6265
/// `config.toml` `[providers.*]` override (layer 30).
6366
ConfigOverride,
67+
/// Codewhale-owned bundled catalog snapshot (offline authority seed).
68+
CodewhaleBundled { revision: String },
69+
/// Live Codewhale signed catalog fetched from CWC / `CODEWHALE_CATALOG_URL`.
70+
CodewhaleLive { revision: String, fetched_at: u64 },
6471
}
6572

6673
/// One catalog-layer offering row.
@@ -625,12 +632,14 @@ impl CatalogSnapshot {
625632
/// after every layer and is never overridden:
626633
///
627634
/// ```text
628-
/// 0 bundled committed models.dev-shaped snapshot
629-
/// 10 live models.dev models.dev refresh
630-
/// 20 provider per-provider /v1/models refresh
631-
/// 30 config config.toml [providers.*] overrides
632-
/// 40 user user approved set (Phase 3 hook; empty in Phase 1)
633-
/// policy DENY last, never overridden
635+
/// 0 bundled committed models.dev-shaped snapshot
636+
/// 5 codewhale bundled Codewhale-owned offline snapshot
637+
/// 10 live models.dev models.dev refresh
638+
/// 20 provider per-provider /v1/models refresh
639+
/// 25 codewhale live signed CWC catalog (authority)
640+
/// 30 config config.toml [providers.*] overrides
641+
/// 40 user user approved set
642+
/// policy DENY last, never overridden
634643
/// ```
635644
///
636645
/// [`Self::with_live`] remains the combined live bucket so existing callers
@@ -639,9 +648,11 @@ impl CatalogSnapshot {
639648
#[derive(Debug, Clone, Default)]
640649
pub struct CatalogCompiler {
641650
bundled: Vec<CatalogOffering>,
651+
codewhale_bundled: Vec<CatalogOffering>,
642652
models_dev_live: Vec<CatalogOffering>,
643653
live: Vec<CatalogOffering>,
644654
provider_live: Vec<CatalogOffering>,
655+
codewhale_live: Vec<CatalogOffering>,
645656
config: Vec<CatalogOffering>,
646657
overrides: Vec<CatalogOffering>,
647658
policy: crate::route::CatalogPolicy,
@@ -669,6 +680,20 @@ impl CatalogCompiler {
669680
self
670681
}
671682

683+
/// Add Codewhale-owned bundled catalog rows (layer 5).
684+
#[must_use]
685+
pub fn with_codewhale_bundled(mut self, rows: Vec<CatalogOffering>) -> Self {
686+
self.codewhale_bundled.extend(rows);
687+
self
688+
}
689+
690+
/// Add live signed Codewhale catalog rows (layer 25; authority when present).
691+
#[must_use]
692+
pub fn with_codewhale_live(mut self, rows: Vec<CatalogOffering>) -> Self {
693+
self.codewhale_live.extend(rows);
694+
self
695+
}
696+
672697
/// Add live models.dev refresh rows (layer 10).
673698
#[must_use]
674699
pub fn with_models_dev_live(mut self, rows: Vec<CatalogOffering>) -> Self {
@@ -722,9 +747,11 @@ impl CatalogCompiler {
722747
for row in self
723748
.bundled
724749
.into_iter()
750+
.chain(self.codewhale_bundled)
725751
.chain(self.models_dev_live)
726752
.chain(self.live)
727753
.chain(self.provider_live)
754+
.chain(self.codewhale_live)
728755
.chain(self.config)
729756
.chain(self.overrides)
730757
{

crates/config/src/catalog/tests.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,3 +1002,47 @@ fn live_offerings_normalize_models_dev_provider_aliases() {
10021002
assert!(rows.iter().all(|r| r.provider != "togetherai"));
10031003
assert!(rows.iter().all(|r| r.provider != "zhipuai"));
10041004
}
1005+
1006+
fn offering(provider: &str, wire: &str, source: CatalogSource) -> CatalogOffering {
1007+
CatalogOffering {
1008+
provider: provider.to_string(),
1009+
wire_model_id: wire.to_string(),
1010+
endpoint_key: "chat".to_string(),
1011+
source,
1012+
..CatalogOffering::default()
1013+
}
1014+
}
1015+
1016+
#[test]
1017+
fn codewhale_live_catalog_beats_models_dev_on_the_same_wire() {
1018+
let snapshot = CatalogCompiler::new()
1019+
.with_bundled(vec![offering(
1020+
"command-code",
1021+
"deepseek/deepseek-v4-flash",
1022+
CatalogSource::Bundled,
1023+
)])
1024+
.with_models_dev_live(vec![offering(
1025+
"command-code",
1026+
"deepseek/deepseek-v4-flash",
1027+
CatalogSource::ModelsDevLive { fetched_at: 1 },
1028+
)])
1029+
.with_codewhale_live(vec![offering(
1030+
"command-code",
1031+
"deepseek/deepseek-v4-flash",
1032+
CatalogSource::CodewhaleLive {
1033+
revision: "2026-08-31.1".into(),
1034+
fetched_at: 2,
1035+
},
1036+
)])
1037+
.compile();
1038+
let row = find(
1039+
&snapshot.offerings,
1040+
"command-code",
1041+
"deepseek/deepseek-v4-flash",
1042+
);
1043+
assert!(matches!(
1044+
row.source,
1045+
CatalogSource::CodewhaleLive { ref revision, fetched_at: 2 }
1046+
if revision == "2026-08-31.1"
1047+
));
1048+
}

crates/config/src/descriptors.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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!(rows.len() >= 5, "expected compatible hosts plus command-code");
101+
for row in rows {
102+
assert!(row.base_url.starts_with("https://"), "{}", row.id);
103+
assert!(!row.api_key_env.is_empty(), "{}", row.id);
104+
assert!(!row.default_model.is_empty(), "{}", row.id);
105+
assert_eq!(row.discovery, DescriptorDiscovery::ModelsEndpoint);
106+
assert_eq!(row.wire, DescriptorWire::OpenaiCompatible);
107+
}
108+
let cmd = provider_descriptor("command-code").expect("command-code");
109+
assert_eq!(cmd.base_url, "https://api.commandcode.ai/provider/v1");
110+
assert_eq!(cmd.api_key_env, "COMMAND_CODE_API_KEY");
111+
assert_eq!(
112+
provider_descriptor("cmd-code").map(|row| row.id.as_str()),
113+
Some("command-code")
114+
);
115+
}
116+
117+
#[test]
118+
fn descriptors_do_not_embed_model_rosters() {
119+
let raw = DESCRIPTORS_JSON;
120+
assert!(
121+
!raw.contains("moonshotai/Kimi-K2.7-Code"),
122+
"do not compile a Baseten/Kimi roster into descriptors"
123+
);
124+
assert!(
125+
!raw.contains("openai/gpt-oss-120b"),
126+
"do not compile a Groq roster into descriptors"
127+
);
128+
}
129+
}

crates/config/src/lib.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod app_mode;
22
pub mod auth_source;
33
pub mod auto_model;
44
pub mod catalog;
5+
pub mod descriptors;
56
mod config_document;
67
pub mod device_code;
78
pub mod external_credentials;
@@ -31,12 +32,11 @@ pub(crate) use provider_defaults::*;
3132
pub use provider_kind::ProviderKind;
3233
pub use provider_templates::{
3334
AGNES_TEMPLATE_ID, BASETEN_API_KEY_ENV, BASETEN_BASE_URL, BASETEN_DEFAULT_MODEL,
34-
BASETEN_MODELS, BASETEN_TEMPLATE_ID, CEREBRAS_API_KEY_ENV, CEREBRAS_BASE_URL,
35-
CEREBRAS_DEFAULT_MODEL, CEREBRAS_MODELS, CEREBRAS_TEMPLATE_ID, GROQ_API_KEY_ENV, GROQ_BASE_URL,
36-
GROQ_DEFAULT_MODEL, GROQ_MODELS, GROQ_TEMPLATE_ID, ProviderSetupApply, ProviderSetupTemplate,
37-
SENSENOVA_API_KEY_ENV, SENSENOVA_BASE_URL, SENSENOVA_DEFAULT_MODEL, SENSENOVA_MODELS,
38-
SENSENOVA_TEMPLATE_ID, compatible_provider_setup_templates, provider_setup_template,
39-
provider_setup_templates,
35+
BASETEN_TEMPLATE_ID, CEREBRAS_API_KEY_ENV, CEREBRAS_BASE_URL, CEREBRAS_DEFAULT_MODEL,
36+
CEREBRAS_TEMPLATE_ID, COMMAND_CODE_TEMPLATE_ID, GROQ_API_KEY_ENV, GROQ_BASE_URL,
37+
GROQ_DEFAULT_MODEL, GROQ_TEMPLATE_ID, ProviderSetupApply, ProviderSetupTemplate,
38+
SENSENOVA_API_KEY_ENV, SENSENOVA_BASE_URL, SENSENOVA_DEFAULT_MODEL, SENSENOVA_TEMPLATE_ID,
39+
compatible_provider_setup_templates, provider_setup_template, provider_setup_templates,
4040
};
4141
pub use setup_state::{
4242
ConstitutionAuthoring, ConstitutionChoice, ConstitutionSource, ConstitutionValidity,

0 commit comments

Comments
 (0)