mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 10:36:05 -10:00
feat(config): catalog authority — descriptors not compiled model lists (#5783)
* 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 <[email protected]> * fix(tui): cover Codewhale catalog sources in provenance matches CodewhaleBundled/CodewhaleLive are first-party catalog authority, not user override. rustfmt the descriptor module that failed lint. Signed-off-by: CodeWhale Bot <[email protected]> * fix(tui): keep catalog template docs visible after Command Code The extra descriptor row filled the 16-row list budget and clipped OpenCode Zen's docs URL. Grow the template list and assert the descriptor endpoint plus Command Code. Signed-off-by: CodeWhale Bot <[email protected]> --------- Signed-off-by: CodeWhale Bot <[email protected]> Co-authored-by: CodeWhale Bot <[email protected]>
This commit is contained in:
1 parent
3be41346d3
commit
ceb30eba60
12 files changed
+366
-59
No files matched your search
@@ -12,6 +12,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Public roster language is Pod. `/pod` is the customer surface; fleet remains
|
||||
the internal wire, storage, and migration name (#5776).
|
||||
|
||||
- Provider catalogs: compatible hosts (Baseten, Groq, Cerebras, SenseNova,
|
||||
Command Code) no longer compile a frozen model roster. Descriptors name the
|
||||
wire, URL, and env; live `GET /v1/models` and a Codewhale-owned catalog
|
||||
layer are the offering list. Command Code is a catalog/descriptor row, not
|
||||
a `ProviderKind`. Catalog presence is not an availability, entitlement, or
|
||||
provider-acceptance claim (#5783).
|
||||
|
||||
- Provider selection no longer probes or adopts an external CLI credential on
|
||||
ordinary picker use. Reuse requires an explicit "Use external CLI credentials"
|
||||
choice, exact-path confirmation, and Codewhale-owned revoke (#5772).
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"schema": "codewhale.provider_descriptors.v1",
|
||||
"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.",
|
||||
"descriptors": [
|
||||
{
|
||||
"id": "sensenova",
|
||||
"label": "SenseNova",
|
||||
"wire": "openai-compatible",
|
||||
"base_url": "https://token.sensenova.cn/v1",
|
||||
"api_key_env": "SENSENOVA_API_KEY",
|
||||
"default_model": "deepseek-v4-flash",
|
||||
"discovery": "models_endpoint",
|
||||
"aliases": ["sense-nova", "meituan-sensenova", "meituan-sensenova-cn"]
|
||||
},
|
||||
{
|
||||
"id": "baseten",
|
||||
"label": "Baseten",
|
||||
"wire": "openai-compatible",
|
||||
"base_url": "https://inference.baseten.co/v1",
|
||||
"api_key_env": "BASETEN_API_KEY",
|
||||
"default_model": "deepseek-ai/DeepSeek-V4-Pro",
|
||||
"discovery": "models_endpoint",
|
||||
"docs_url": "https://docs.baseten.co/inference/model-apis/overview",
|
||||
"credential_url": "https://app.baseten.co/settings/api_keys",
|
||||
"aliases": ["base-ten", "base_ten"]
|
||||
},
|
||||
{
|
||||
"id": "groq",
|
||||
"label": "Groq",
|
||||
"wire": "openai-compatible",
|
||||
"base_url": "https://api.groq.com/openai/v1",
|
||||
"api_key_env": "GROQ_API_KEY",
|
||||
"default_model": "llama-3.3-70b-versatile",
|
||||
"discovery": "models_endpoint",
|
||||
"docs_url": "https://console.groq.com/docs/quickstart",
|
||||
"credential_url": "https://console.groq.com/keys"
|
||||
},
|
||||
{
|
||||
"id": "cerebras",
|
||||
"label": "Cerebras",
|
||||
"wire": "openai-compatible",
|
||||
"base_url": "https://api.cerebras.ai/v1",
|
||||
"api_key_env": "CEREBRAS_API_KEY",
|
||||
"default_model": "llama-3.3-70b",
|
||||
"discovery": "models_endpoint",
|
||||
"docs_url": "https://inference-docs.cerebras.ai/quickstart",
|
||||
"credential_url": "https://cloud.cerebras.ai"
|
||||
},
|
||||
{
|
||||
"id": "command-code",
|
||||
"label": "Command Code",
|
||||
"wire": "openai-compatible",
|
||||
"base_url": "https://api.commandcode.ai/provider/v1",
|
||||
"api_key_env": "COMMAND_CODE_API_KEY",
|
||||
"default_model": "deepseek/deepseek-v4-flash",
|
||||
"discovery": "models_endpoint",
|
||||
"docs_url": "https://commandcode.ai/docs/provider",
|
||||
"credential_url": "https://commandcode.ai/provider",
|
||||
"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.",
|
||||
"aliases": ["commandcode", "cmd-code"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,12 +6,15 @@
|
||||
//! refresh) and live [`ProviderCatalogDelta`]s; the HTTP `/models` fetch layer
|
||||
//! lives above this module. Nothing here performs I/O or reads credentials.
|
||||
//!
|
||||
//! Layering (lowest precedence first; #4188):
|
||||
//! Layering (lowest precedence first):
|
||||
//!
|
||||
//! ```text
|
||||
//! bundled Models.dev snapshot (offline/stale fallback only — not competing truth)
|
||||
//! < live Models.dev / provider `/models` cache
|
||||
//! < user / custom overrides (custom endpoints, pinned models, explicit facts)
|
||||
//! bundled Models.dev snapshot (legacy seed, not competing truth)
|
||||
//! < bundled Codewhale catalog (Codewhale-owned offline snapshot)
|
||||
//! < live Models.dev
|
||||
//! < live provider `/v1/models` (credential-scoped workspace list)
|
||||
//! < live Codewhale signed catalog (authority when present)
|
||||
//! < config.toml / user overrides
|
||||
//! ```
|
||||
//!
|
||||
//! After #4187, live Models.dev rows are preferred whenever present. The bundled
|
||||
@@ -61,6 +64,10 @@ pub enum CatalogSource {
|
||||
ModelsDevLive { fetched_at: u64 },
|
||||
/// `config.toml` `[providers.*]` override (layer 30).
|
||||
ConfigOverride,
|
||||
/// Codewhale-owned bundled catalog snapshot (offline authority seed).
|
||||
CodewhaleBundled { revision: String },
|
||||
/// Live Codewhale signed catalog fetched from CWC / `CODEWHALE_CATALOG_URL`.
|
||||
CodewhaleLive { revision: String, fetched_at: u64 },
|
||||
}
|
||||
|
||||
/// One catalog-layer offering row.
|
||||
@@ -625,12 +632,14 @@ impl CatalogSnapshot {
|
||||
/// after every layer and is never overridden:
|
||||
///
|
||||
/// ```text
|
||||
/// 0 bundled committed models.dev-shaped snapshot
|
||||
/// 10 live models.dev models.dev refresh
|
||||
/// 20 provider per-provider /v1/models refresh
|
||||
/// 30 config config.toml [providers.*] overrides
|
||||
/// 40 user user approved set (Phase 3 hook; empty in Phase 1)
|
||||
/// policy DENY last, never overridden
|
||||
/// 0 bundled committed models.dev-shaped snapshot
|
||||
/// 5 codewhale bundled Codewhale-owned offline snapshot
|
||||
/// 10 live models.dev models.dev refresh
|
||||
/// 20 provider per-provider /v1/models refresh
|
||||
/// 25 codewhale live signed CWC catalog (authority)
|
||||
/// 30 config config.toml [providers.*] overrides
|
||||
/// 40 user user approved set
|
||||
/// policy DENY last, never overridden
|
||||
/// ```
|
||||
///
|
||||
/// [`Self::with_live`] remains the combined live bucket so existing callers
|
||||
@@ -639,9 +648,11 @@ impl CatalogSnapshot {
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CatalogCompiler {
|
||||
bundled: Vec<CatalogOffering>,
|
||||
codewhale_bundled: Vec<CatalogOffering>,
|
||||
models_dev_live: Vec<CatalogOffering>,
|
||||
live: Vec<CatalogOffering>,
|
||||
provider_live: Vec<CatalogOffering>,
|
||||
codewhale_live: Vec<CatalogOffering>,
|
||||
config: Vec<CatalogOffering>,
|
||||
overrides: Vec<CatalogOffering>,
|
||||
policy: crate::route::CatalogPolicy,
|
||||
@@ -669,6 +680,20 @@ impl CatalogCompiler {
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Codewhale-owned bundled catalog rows (layer 5).
|
||||
#[must_use]
|
||||
pub fn with_codewhale_bundled(mut self, rows: Vec<CatalogOffering>) -> Self {
|
||||
self.codewhale_bundled.extend(rows);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add live signed Codewhale catalog rows (layer 25; authority when present).
|
||||
#[must_use]
|
||||
pub fn with_codewhale_live(mut self, rows: Vec<CatalogOffering>) -> Self {
|
||||
self.codewhale_live.extend(rows);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add live models.dev refresh rows (layer 10).
|
||||
#[must_use]
|
||||
pub fn with_models_dev_live(mut self, rows: Vec<CatalogOffering>) -> Self {
|
||||
@@ -722,9 +747,11 @@ impl CatalogCompiler {
|
||||
for row in self
|
||||
.bundled
|
||||
.into_iter()
|
||||
.chain(self.codewhale_bundled)
|
||||
.chain(self.models_dev_live)
|
||||
.chain(self.live)
|
||||
.chain(self.provider_live)
|
||||
.chain(self.codewhale_live)
|
||||
.chain(self.config)
|
||||
.chain(self.overrides)
|
||||
{
|
||||
|
||||
@@ -1002,3 +1002,47 @@ fn live_offerings_normalize_models_dev_provider_aliases() {
|
||||
assert!(rows.iter().all(|r| r.provider != "togetherai"));
|
||||
assert!(rows.iter().all(|r| r.provider != "zhipuai"));
|
||||
}
|
||||
|
||||
fn offering(provider: &str, wire: &str, source: CatalogSource) -> CatalogOffering {
|
||||
CatalogOffering {
|
||||
provider: provider.to_string(),
|
||||
wire_model_id: wire.to_string(),
|
||||
endpoint_key: "chat".to_string(),
|
||||
source,
|
||||
..CatalogOffering::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codewhale_live_catalog_beats_models_dev_on_the_same_wire() {
|
||||
let snapshot = CatalogCompiler::new()
|
||||
.with_bundled(vec![offering(
|
||||
"command-code",
|
||||
"deepseek/deepseek-v4-flash",
|
||||
CatalogSource::Bundled,
|
||||
)])
|
||||
.with_models_dev_live(vec![offering(
|
||||
"command-code",
|
||||
"deepseek/deepseek-v4-flash",
|
||||
CatalogSource::ModelsDevLive { fetched_at: 1 },
|
||||
)])
|
||||
.with_codewhale_live(vec![offering(
|
||||
"command-code",
|
||||
"deepseek/deepseek-v4-flash",
|
||||
CatalogSource::CodewhaleLive {
|
||||
revision: "2026-08-31.1".into(),
|
||||
fetched_at: 2,
|
||||
},
|
||||
)])
|
||||
.compile();
|
||||
let row = find(
|
||||
&snapshot.offerings,
|
||||
"command-code",
|
||||
"deepseek/deepseek-v4-flash",
|
||||
);
|
||||
assert!(matches!(
|
||||
row.source,
|
||||
CatalogSource::CodewhaleLive { ref revision, fetched_at: 2 }
|
||||
if revision == "2026-08-31.1"
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//! OMP-style provider descriptors: how to talk to a host.
|
||||
//!
|
||||
//! Model ids are **not** compiled here. A descriptor names the wire, URL, env
|
||||
//! var, and whether authenticated `GET /v1/models` is the catalog authority
|
||||
//! for that host. Offerings come from the Codewhale catalog layers and live
|
||||
//! provider `/models` refreshes.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
const DESCRIPTORS_JSON: &str = include_str!("../assets/provider_descriptors.json");
|
||||
|
||||
/// How this host's model list is discovered.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DescriptorDiscovery {
|
||||
/// Authenticated `GET {base_url}/models` is authoritative for this credential.
|
||||
ModelsEndpoint,
|
||||
/// No live discovery; only catalog/config rows.
|
||||
None,
|
||||
}
|
||||
|
||||
/// Transport used to send turns. Not a brand enum.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum DescriptorWire {
|
||||
OpenaiCompatible,
|
||||
AnthropicMessages,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DescriptorFile {
|
||||
descriptors: Vec<ProviderDescriptor>,
|
||||
}
|
||||
|
||||
/// Data row describing a hosted OpenAI-compatible (or Anthropic Messages) gateway.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub struct ProviderDescriptor {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub wire: DescriptorWire,
|
||||
pub base_url: String,
|
||||
pub api_key_env: String,
|
||||
pub default_model: String,
|
||||
pub discovery: DescriptorDiscovery,
|
||||
#[serde(default)]
|
||||
pub docs_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub credential_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub guidance: Option<String>,
|
||||
#[serde(default)]
|
||||
pub aliases: Vec<String>,
|
||||
}
|
||||
|
||||
impl ProviderDescriptor {
|
||||
#[must_use]
|
||||
pub fn matches(&self, needle: &str) -> bool {
|
||||
let needle = needle.trim().to_ascii_lowercase().replace('_', "-");
|
||||
if needle.is_empty() {
|
||||
return false;
|
||||
}
|
||||
self.id == needle
|
||||
|| self
|
||||
.aliases
|
||||
.iter()
|
||||
.any(|alias| alias.eq_ignore_ascii_case(&needle))
|
||||
}
|
||||
}
|
||||
|
||||
static DESCRIPTORS: OnceLock<Vec<ProviderDescriptor>> = OnceLock::new();
|
||||
|
||||
/// Bundled compatible-host descriptors. Panics only if the committed JSON is invalid.
|
||||
#[must_use]
|
||||
pub fn bundled_provider_descriptors() -> &'static [ProviderDescriptor] {
|
||||
DESCRIPTORS
|
||||
.get_or_init(|| {
|
||||
let file: DescriptorFile = serde_json::from_str(DESCRIPTORS_JSON)
|
||||
.expect("committed provider_descriptors.json must parse");
|
||||
file.descriptors
|
||||
})
|
||||
.as_slice()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn provider_descriptor(id: &str) -> Option<&'static ProviderDescriptor> {
|
||||
bundled_provider_descriptors()
|
||||
.iter()
|
||||
.find(|descriptor| descriptor.matches(id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn descriptors_parse_and_command_code_is_a_row_not_a_kind() {
|
||||
let rows = bundled_provider_descriptors();
|
||||
assert!(
|
||||
rows.len() >= 5,
|
||||
"expected compatible hosts plus command-code"
|
||||
);
|
||||
for row in rows {
|
||||
assert!(row.base_url.starts_with("https://"), "{}", row.id);
|
||||
assert!(!row.api_key_env.is_empty(), "{}", row.id);
|
||||
assert!(!row.default_model.is_empty(), "{}", row.id);
|
||||
assert_eq!(row.discovery, DescriptorDiscovery::ModelsEndpoint);
|
||||
assert_eq!(row.wire, DescriptorWire::OpenaiCompatible);
|
||||
}
|
||||
let cmd = provider_descriptor("command-code").expect("command-code");
|
||||
assert_eq!(cmd.base_url, "https://api.commandcode.ai/provider/v1");
|
||||
assert_eq!(cmd.api_key_env, "COMMAND_CODE_API_KEY");
|
||||
assert_eq!(
|
||||
provider_descriptor("cmd-code").map(|row| row.id.as_str()),
|
||||
Some("command-code")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn descriptors_do_not_embed_model_rosters() {
|
||||
let raw = DESCRIPTORS_JSON;
|
||||
assert!(
|
||||
!raw.contains("moonshotai/Kimi-K2.7-Code"),
|
||||
"do not compile a Baseten/Kimi roster into descriptors"
|
||||
);
|
||||
assert!(
|
||||
!raw.contains("openai/gpt-oss-120b"),
|
||||
"do not compile a Groq roster into descriptors"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ pub mod auth_source;
|
||||
pub mod auto_model;
|
||||
pub mod catalog;
|
||||
mod config_document;
|
||||
pub mod descriptors;
|
||||
pub mod device_code;
|
||||
pub mod external_credentials;
|
||||
mod harness;
|
||||
@@ -31,12 +32,11 @@ pub(crate) use provider_defaults::*;
|
||||
pub use provider_kind::ProviderKind;
|
||||
pub use provider_templates::{
|
||||
AGNES_TEMPLATE_ID, BASETEN_API_KEY_ENV, BASETEN_BASE_URL, BASETEN_DEFAULT_MODEL,
|
||||
BASETEN_MODELS, BASETEN_TEMPLATE_ID, CEREBRAS_API_KEY_ENV, CEREBRAS_BASE_URL,
|
||||
CEREBRAS_DEFAULT_MODEL, CEREBRAS_MODELS, CEREBRAS_TEMPLATE_ID, GROQ_API_KEY_ENV, GROQ_BASE_URL,
|
||||
GROQ_DEFAULT_MODEL, GROQ_MODELS, GROQ_TEMPLATE_ID, ProviderSetupApply, ProviderSetupTemplate,
|
||||
SENSENOVA_API_KEY_ENV, SENSENOVA_BASE_URL, SENSENOVA_DEFAULT_MODEL, SENSENOVA_MODELS,
|
||||
SENSENOVA_TEMPLATE_ID, compatible_provider_setup_templates, provider_setup_template,
|
||||
provider_setup_templates,
|
||||
BASETEN_TEMPLATE_ID, CEREBRAS_API_KEY_ENV, CEREBRAS_BASE_URL, CEREBRAS_DEFAULT_MODEL,
|
||||
CEREBRAS_TEMPLATE_ID, COMMAND_CODE_TEMPLATE_ID, GROQ_API_KEY_ENV, GROQ_BASE_URL,
|
||||
GROQ_DEFAULT_MODEL, GROQ_TEMPLATE_ID, ProviderSetupApply, ProviderSetupTemplate,
|
||||
SENSENOVA_API_KEY_ENV, SENSENOVA_BASE_URL, SENSENOVA_DEFAULT_MODEL, SENSENOVA_TEMPLATE_ID,
|
||||
compatible_provider_setup_templates, provider_setup_template, provider_setup_templates,
|
||||
};
|
||||
pub use setup_state::{
|
||||
ConstitutionAuthoring, ConstitutionChoice, ConstitutionSource, ConstitutionValidity,
|
||||
|
||||
@@ -53,6 +53,8 @@ pub enum PricingProvenance {
|
||||
UserOverride,
|
||||
/// No sourced price.
|
||||
Unknown,
|
||||
/// From the Codewhale-owned signed/bundled catalog.
|
||||
CodewhaleCatalog,
|
||||
}
|
||||
|
||||
impl PricingProvenance {
|
||||
@@ -65,6 +67,7 @@ impl PricingProvenance {
|
||||
Self::ProviderDocs => "provider_docs",
|
||||
Self::UserOverride => "user_override",
|
||||
Self::Unknown => "unknown",
|
||||
Self::CodewhaleCatalog => "codewhale_catalog",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +82,10 @@ impl PricingProvenance {
|
||||
pub fn is_authoritative_without_freshness_check(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::ModelsDevBundled | Self::ProviderDocs | Self::UserOverride
|
||||
Self::ModelsDevBundled
|
||||
| Self::ProviderDocs
|
||||
| Self::UserOverride
|
||||
| Self::CodewhaleCatalog
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -505,17 +511,21 @@ fn provenance_from_source(source: &CatalogSource) -> PricingProvenance {
|
||||
CatalogSource::ConfigOverride | CatalogSource::UserOverride => {
|
||||
PricingProvenance::UserOverride
|
||||
}
|
||||
CatalogSource::CodewhaleBundled { .. } | CatalogSource::CodewhaleLive { .. } => {
|
||||
PricingProvenance::CodewhaleCatalog
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn effective_at_from_source(source: &CatalogSource) -> Option<u64> {
|
||||
match source {
|
||||
CatalogSource::Live { fetched_at, .. } | CatalogSource::ModelsDevLive { fetched_at } => {
|
||||
Some(*fetched_at)
|
||||
}
|
||||
CatalogSource::Bundled | CatalogSource::ConfigOverride | CatalogSource::UserOverride => {
|
||||
None
|
||||
}
|
||||
CatalogSource::Live { fetched_at, .. }
|
||||
| CatalogSource::ModelsDevLive { fetched_at }
|
||||
| CatalogSource::CodewhaleLive { fetched_at, .. } => Some(*fetched_at),
|
||||
CatalogSource::Bundled
|
||||
| CatalogSource::ConfigOverride
|
||||
| CatalogSource::UserOverride
|
||||
| CatalogSource::CodewhaleBundled { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,7 +538,9 @@ fn endpoint_fingerprint_from_source(source: &CatalogSource) -> Option<String> {
|
||||
CatalogSource::Bundled
|
||||
| CatalogSource::ModelsDevLive { .. }
|
||||
| CatalogSource::ConfigOverride
|
||||
| CatalogSource::UserOverride => None,
|
||||
| CatalogSource::UserOverride
|
||||
| CatalogSource::CodewhaleBundled { .. }
|
||||
| CatalogSource::CodewhaleLive { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
//! - first-class gateways users still treat as "paste a Base URL"
|
||||
//! (OpenCode Zen / Go), and
|
||||
//! - named OpenAI-compatible custom routes that are not `ProviderKind`
|
||||
//! variants (SenseNova, Baseten, Groq, Cerebras). Hosted Chat Completions
|
||||
//! backends are data rows here — not new enum variants. Distinct *wires*
|
||||
//! variants (SenseNova, Baseten, Groq, Cerebras, Command Code). Hosted
|
||||
//! Chat Completions backends are descriptor rows — not new enum variants
|
||||
//! and not compiled model rosters. Live `GET /v1/models` and the
|
||||
//! Codewhale catalog are the offering list. Distinct *wires*
|
||||
//! (Anthropic Messages, Codex Responses, Google thought signatures) stay
|
||||
//! on `ProviderKind`.
|
||||
//!
|
||||
@@ -28,7 +30,6 @@ pub const SENSENOVA_TEMPLATE_ID: &str = "sensenova";
|
||||
pub const SENSENOVA_BASE_URL: &str = "https://token.sensenova.cn/v1";
|
||||
pub const SENSENOVA_DEFAULT_MODEL: &str = "deepseek-v4-flash";
|
||||
pub const SENSENOVA_API_KEY_ENV: &str = "SENSENOVA_API_KEY";
|
||||
pub const SENSENOVA_MODELS: &[&str] = &[SENSENOVA_DEFAULT_MODEL];
|
||||
|
||||
/// Agnes is requested by #5350 but has no published OpenAI-compatible
|
||||
/// host in this repository.
|
||||
@@ -39,28 +40,19 @@ pub const BASETEN_TEMPLATE_ID: &str = "baseten";
|
||||
pub const BASETEN_BASE_URL: &str = "https://inference.baseten.co/v1";
|
||||
pub const BASETEN_DEFAULT_MODEL: &str = "deepseek-ai/DeepSeek-V4-Pro";
|
||||
pub const BASETEN_API_KEY_ENV: &str = "BASETEN_API_KEY";
|
||||
pub const BASETEN_MODELS: &[&str] = &[
|
||||
BASETEN_DEFAULT_MODEL,
|
||||
"deepseek-ai/DeepSeek-V4-Flash-0731",
|
||||
"deepseek-ai/DeepSeek-V4-Pro-0813",
|
||||
"zai-org/GLM-5.2",
|
||||
"zai-org/GLM-5.3-Flash",
|
||||
"moonshotai/Kimi-K2.7-Code",
|
||||
];
|
||||
|
||||
/// Groq — OpenAI Chat Completions hosted inference.
|
||||
pub const GROQ_TEMPLATE_ID: &str = "groq";
|
||||
pub const GROQ_BASE_URL: &str = "https://api.groq.com/openai/v1";
|
||||
pub const GROQ_DEFAULT_MODEL: &str = "llama-3.3-70b-versatile";
|
||||
pub const GROQ_API_KEY_ENV: &str = "GROQ_API_KEY";
|
||||
pub const GROQ_MODELS: &[&str] = &[GROQ_DEFAULT_MODEL, "openai/gpt-oss-120b"];
|
||||
|
||||
/// Cerebras — OpenAI Chat Completions hosted inference.
|
||||
pub const CEREBRAS_TEMPLATE_ID: &str = "cerebras";
|
||||
pub const CEREBRAS_BASE_URL: &str = "https://api.cerebras.ai/v1";
|
||||
pub const CEREBRAS_DEFAULT_MODEL: &str = "llama-3.3-70b";
|
||||
pub const CEREBRAS_API_KEY_ENV: &str = "CEREBRAS_API_KEY";
|
||||
pub const CEREBRAS_MODELS: &[&str] = &[CEREBRAS_DEFAULT_MODEL];
|
||||
pub const COMMAND_CODE_TEMPLATE_ID: &str = "command-code";
|
||||
|
||||
/// How a beginner template is applied.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -86,7 +78,6 @@ pub struct ProviderSetupTemplate {
|
||||
pub apply: ProviderSetupApply,
|
||||
base_url: Option<&'static str>,
|
||||
default_model: Option<&'static str>,
|
||||
models: &'static [&'static str],
|
||||
api_key_env: Option<&'static str>,
|
||||
docs_url: Option<&'static str>,
|
||||
credential_url: Option<&'static str>,
|
||||
@@ -116,9 +107,8 @@ impl ProviderSetupTemplate {
|
||||
}
|
||||
}
|
||||
|
||||
/// Models shown in setup / `/model` when the live catalog is empty or
|
||||
/// Models.dev refresh failed. First-class Zen/Go use the curated
|
||||
/// roster already owned by the route catalog.
|
||||
/// Bootstrap model ids when the live catalog has not yet answered.
|
||||
/// Compatible hosts expose only the descriptor default — never a compiled roster.
|
||||
#[must_use]
|
||||
pub fn picker_models(self) -> Vec<&'static str> {
|
||||
match self.apply {
|
||||
@@ -128,11 +118,10 @@ impl ProviderSetupTemplate {
|
||||
ProviderSetupApply::FirstClass(ProviderKind::OpencodeGo) => {
|
||||
OPENCODE_GO_CHAT_MODELS.to_vec()
|
||||
}
|
||||
ProviderSetupApply::FirstClass(_) => self
|
||||
ProviderSetupApply::FirstClass(_) | ProviderSetupApply::Compatible => self
|
||||
.default_model()
|
||||
.map(|model| vec![model])
|
||||
.unwrap_or_default(),
|
||||
ProviderSetupApply::Compatible => self.models.to_vec(),
|
||||
ProviderSetupApply::Unpublished => Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -224,7 +213,6 @@ const TEMPLATES: &[ProviderSetupTemplate] = &[
|
||||
apply: ProviderSetupApply::FirstClass(ProviderKind::OpencodeZen),
|
||||
base_url: None,
|
||||
default_model: None,
|
||||
models: &[],
|
||||
api_key_env: None,
|
||||
docs_url: None,
|
||||
credential_url: None,
|
||||
@@ -236,7 +224,6 @@ const TEMPLATES: &[ProviderSetupTemplate] = &[
|
||||
apply: ProviderSetupApply::FirstClass(ProviderKind::OpencodeGo),
|
||||
base_url: None,
|
||||
default_model: None,
|
||||
models: &[],
|
||||
api_key_env: None,
|
||||
docs_url: None,
|
||||
credential_url: None,
|
||||
@@ -248,7 +235,6 @@ const TEMPLATES: &[ProviderSetupTemplate] = &[
|
||||
apply: ProviderSetupApply::Compatible,
|
||||
base_url: Some(SENSENOVA_BASE_URL),
|
||||
default_model: Some(SENSENOVA_DEFAULT_MODEL),
|
||||
models: SENSENOVA_MODELS,
|
||||
api_key_env: Some(SENSENOVA_API_KEY_ENV),
|
||||
docs_url: None,
|
||||
credential_url: None,
|
||||
@@ -260,7 +246,6 @@ const TEMPLATES: &[ProviderSetupTemplate] = &[
|
||||
apply: ProviderSetupApply::Compatible,
|
||||
base_url: Some(BASETEN_BASE_URL),
|
||||
default_model: Some(BASETEN_DEFAULT_MODEL),
|
||||
models: BASETEN_MODELS,
|
||||
api_key_env: Some(BASETEN_API_KEY_ENV),
|
||||
docs_url: Some("https://docs.baseten.co/inference/model-apis/overview"),
|
||||
credential_url: Some("https://app.baseten.co/settings/api_keys"),
|
||||
@@ -272,7 +257,6 @@ const TEMPLATES: &[ProviderSetupTemplate] = &[
|
||||
apply: ProviderSetupApply::Compatible,
|
||||
base_url: Some(GROQ_BASE_URL),
|
||||
default_model: Some(GROQ_DEFAULT_MODEL),
|
||||
models: GROQ_MODELS,
|
||||
api_key_env: Some(GROQ_API_KEY_ENV),
|
||||
docs_url: Some("https://console.groq.com/docs/quickstart"),
|
||||
credential_url: Some("https://console.groq.com/keys"),
|
||||
@@ -284,19 +268,28 @@ const TEMPLATES: &[ProviderSetupTemplate] = &[
|
||||
apply: ProviderSetupApply::Compatible,
|
||||
base_url: Some(CEREBRAS_BASE_URL),
|
||||
default_model: Some(CEREBRAS_DEFAULT_MODEL),
|
||||
models: CEREBRAS_MODELS,
|
||||
api_key_env: Some(CEREBRAS_API_KEY_ENV),
|
||||
docs_url: Some("https://inference-docs.cerebras.ai/quickstart"),
|
||||
credential_url: Some("https://cloud.cerebras.ai"),
|
||||
guidance: "Cerebras hosted inference. OpenAI Chat Completions. Store CEREBRAS_API_KEY, not a raw key.",
|
||||
},
|
||||
ProviderSetupTemplate {
|
||||
id: COMMAND_CODE_TEMPLATE_ID,
|
||||
display_name: "Command Code",
|
||||
apply: ProviderSetupApply::Compatible,
|
||||
base_url: Some("https://api.commandcode.ai/provider/v1"),
|
||||
default_model: Some("deepseek/deepseek-v4-flash"),
|
||||
api_key_env: Some("COMMAND_CODE_API_KEY"),
|
||||
docs_url: Some("https://commandcode.ai/docs/provider"),
|
||||
credential_url: Some("https://commandcode.ai/provider"),
|
||||
guidance: "Published Provider API. Live GET /v1/models is the roster. Do not import the Command Code CLI login. Store COMMAND_CODE_API_KEY, not a raw key.",
|
||||
},
|
||||
ProviderSetupTemplate {
|
||||
id: AGNES_TEMPLATE_ID,
|
||||
display_name: "Agnes",
|
||||
apply: ProviderSetupApply::Unpublished,
|
||||
base_url: None,
|
||||
default_model: None,
|
||||
models: &[],
|
||||
api_key_env: None,
|
||||
docs_url: None,
|
||||
credential_url: None,
|
||||
@@ -334,6 +327,7 @@ pub fn provider_setup_template(id: &str) -> Option<&'static ProviderSetupTemplat
|
||||
template.id == SENSENOVA_TEMPLATE_ID
|
||||
}
|
||||
"base-ten" | "base_ten" => template.id == BASETEN_TEMPLATE_ID,
|
||||
"commandcode" | "cmd-code" => template.id == COMMAND_CODE_TEMPLATE_ID,
|
||||
_ => false,
|
||||
})
|
||||
})
|
||||
@@ -434,7 +428,7 @@ mod tests {
|
||||
assert_eq!(sense.base_url(), Some(SENSENOVA_BASE_URL));
|
||||
assert_eq!(sense.default_model(), Some(SENSENOVA_DEFAULT_MODEL));
|
||||
assert_eq!(sense.api_key_env(), Some(SENSENOVA_API_KEY_ENV));
|
||||
assert_eq!(sense.picker_models(), SENSENOVA_MODELS);
|
||||
assert_eq!(sense.picker_models(), vec![SENSENOVA_DEFAULT_MODEL]);
|
||||
assert!(sense.docs_url().is_none());
|
||||
assert!(sense.credential_url().is_none());
|
||||
}
|
||||
@@ -455,7 +449,7 @@ mod tests {
|
||||
fn settings_value_names_fillable_then_unpublished() {
|
||||
assert_eq!(
|
||||
ProviderSetupTemplate::settings_value(),
|
||||
"opencode-zen, opencode-go, sensenova, baseten, groq, cerebras; agnes unpublished"
|
||||
"opencode-zen, opencode-go, sensenova, baseten, groq, cerebras, command-code; agnes unpublished"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -493,6 +487,12 @@ mod tests {
|
||||
CEREBRAS_BASE_URL,
|
||||
CEREBRAS_API_KEY_ENV,
|
||||
),
|
||||
(
|
||||
"command-code",
|
||||
COMMAND_CODE_TEMPLATE_ID,
|
||||
"https://api.commandcode.ai/provider/v1",
|
||||
"COMMAND_CODE_API_KEY",
|
||||
),
|
||||
] {
|
||||
let template = provider_setup_template(alias).unwrap_or_else(|| panic!("{alias}"));
|
||||
assert_eq!(template.id, id);
|
||||
|
||||
@@ -12,6 +12,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Public roster language is Pod. `/pod` is the customer surface; fleet remains
|
||||
the internal wire, storage, and migration name (#5776).
|
||||
|
||||
- Provider catalogs: compatible hosts (Baseten, Groq, Cerebras, SenseNova,
|
||||
Command Code) no longer compile a frozen model roster. Descriptors name the
|
||||
wire, URL, and env; live `GET /v1/models` and a Codewhale-owned catalog
|
||||
layer are the offering list. Command Code is a catalog/descriptor row, not
|
||||
a `ProviderKind`. Catalog presence is not an availability, entitlement, or
|
||||
provider-acceptance claim (#5783).
|
||||
|
||||
- Provider selection no longer probes or adopts an external CLI credential on
|
||||
ordinary picker use. Reuse requires an explicit "Use external CLI credentials"
|
||||
choice, exact-path confirmation, and Codewhale-owned revoke (#5772).
|
||||
|
||||
@@ -95,8 +95,10 @@ fn exact_builtin_provider(provider_id: &str) -> Option<ApiProvider> {
|
||||
|
||||
const fn catalog_provenance(source: &CatalogSource) -> &'static str {
|
||||
match source {
|
||||
CatalogSource::Bundled => "bundled catalog",
|
||||
CatalogSource::Live { .. } | CatalogSource::ModelsDevLive { .. } => "live catalog",
|
||||
CatalogSource::Bundled | CatalogSource::CodewhaleBundled { .. } => "bundled catalog",
|
||||
CatalogSource::Live { .. }
|
||||
| CatalogSource::ModelsDevLive { .. }
|
||||
| CatalogSource::CodewhaleLive { .. } => "live catalog",
|
||||
CatalogSource::ConfigOverride | CatalogSource::UserOverride => "override",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2019,7 +2019,10 @@ fn offering_for_row(row: &ModelPickerRow) -> Option<codewhale_config::catalog::C
|
||||
|
||||
fn offering_fetched_at(row: &ModelPickerRow) -> u64 {
|
||||
match offering_for_row(row).map(|o| o.source) {
|
||||
Some(CatalogSource::Live { fetched_at, .. }) => fetched_at,
|
||||
Some(
|
||||
CatalogSource::Live { fetched_at, .. }
|
||||
| CatalogSource::CodewhaleLive { fetched_at, .. },
|
||||
) => fetched_at,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
@@ -2316,10 +2319,14 @@ fn render_picker_model_hint(
|
||||
PickerPricing::Unknown => parts.push("price unknown".to_string()),
|
||||
}
|
||||
match metadata.source.as_ref() {
|
||||
Some(CatalogSource::Live { .. } | CatalogSource::ModelsDevLive { .. }) => {
|
||||
parts.push("live".to_string())
|
||||
Some(
|
||||
CatalogSource::Live { .. }
|
||||
| CatalogSource::ModelsDevLive { .. }
|
||||
| CatalogSource::CodewhaleLive { .. },
|
||||
) => parts.push("live".to_string()),
|
||||
Some(CatalogSource::Bundled | CatalogSource::CodewhaleBundled { .. }) => {
|
||||
parts.push("bundled".to_string())
|
||||
}
|
||||
Some(CatalogSource::Bundled) => parts.push("bundled".to_string()),
|
||||
Some(CatalogSource::ConfigOverride | CatalogSource::UserOverride) => {
|
||||
parts.push("override".to_string())
|
||||
}
|
||||
|
||||
@@ -4418,7 +4418,8 @@ impl ModalView for ProviderPickerView {
|
||||
Stage::StepfunBillingRoute => 11,
|
||||
Stage::Confirm => 10,
|
||||
Stage::CustomForm => 12,
|
||||
Stage::TemplateList => 16,
|
||||
// Catalog rows + descriptor detail (URL, env, default, docs).
|
||||
Stage::TemplateList => 18,
|
||||
};
|
||||
let popup_area = centered_modal_area(area, 120, preferred_height, 64, 8);
|
||||
|
||||
@@ -6108,11 +6109,16 @@ mod tests {
|
||||
assert!(rendered.contains("OpenCode Zen"), "{rendered}");
|
||||
assert!(rendered.contains("OpenCode Go"), "{rendered}");
|
||||
assert!(rendered.contains("SenseNova"), "{rendered}");
|
||||
assert!(rendered.contains("Command Code"), "{rendered}");
|
||||
assert!(rendered.contains("Agnes"), "{rendered}");
|
||||
assert!(
|
||||
rendered.contains("no published") || rendered.contains("unpublished"),
|
||||
"{rendered}"
|
||||
);
|
||||
assert!(
|
||||
rendered.contains("https://opencode.ai/zen/v1"),
|
||||
"descriptor endpoint missing: {rendered}"
|
||||
);
|
||||
assert!(
|
||||
rendered.contains("https://opencode.ai/docs/zen/"),
|
||||
"{rendered}"
|
||||
|
||||
Reference in new issue
Block a user