From e173679c08f0a8e8e77d6a7bb136c8024151f00b Mon Sep 17 00:00:00 2001 From: Duncan Date: Wed, 19 Aug 2026 18:07:56 -0400 Subject: [PATCH 1/4] fix(models): curate Databricks alias labels Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- Justfile | 2 +- crates/buzz-agent/src/model_capabilities.rs | 103 ++++++++++++++---- .../agents/lib/agentCardModelLabel.test.mjs | 26 +++++ .../agents/lib/formatAgentModelLabel.ts | 35 +++--- .../features/agents/ui/modelCapabilities.ts | 46 ++++++++ .../ui/modelCapabilitiesCorpus.test.mjs | 38 ++++++- scripts/model-capabilities.json | 36 ++++++ scripts/normative-corpus.json | 80 ++++++++++++++ scripts/run-tests.sh | 4 +- 9 files changed, 330 insertions(+), 40 deletions(-) diff --git a/Justfile b/Justfile index ce8647cf77c..cd6c081fefa 100644 --- a/Justfile +++ b/Justfile @@ -335,7 +335,7 @@ test-unit: # buzz-agent model-capabilities corpus: the Rust half of the # cross-language drift guard. `model_capabilities.rs` embeds # scripts/model-capabilities.json + scripts/normative-corpus.json via - # include_str! and replays all 103 vectors as pure in-process tests (no + # include_str! and replays the full locked corpus as pure in-process tests (no # infra). Enumerated explicitly because nothing in CI runs # `cargo test --workspace`; without this step a manifest edit that # diverges Rust from the corpus ships green. diff --git a/crates/buzz-agent/src/model_capabilities.rs b/crates/buzz-agent/src/model_capabilities.rs index 81f4b4e3b64..d8f74b6ec61 100644 --- a/crates/buzz-agent/src/model_capabilities.rs +++ b/crates/buzz-agent/src/model_capabilities.rs @@ -375,22 +375,47 @@ pub fn databricks_v2_known_models() -> &'static [String] { } /// Curated display label for a Databricks endpoint id, or `None` when no exact -/// record covers it. Read-only accessor over the same `databricks_v2` exact -/// records `resolve()` consults, with the same case-insensitive id match; used -/// by discovery to curate `ModelEntry.name` (the Databricks API returns no -/// display name of its own). Scoped to `databricks_v2` records only, so it can -/// never surface a curated label for a non-Databricks provider. +/// record covers it. Exact raw-id hits preserve the resolver's current behavior. +/// On an exact miss, aliases share a label only when stripping the manifest's +/// existing family-token prefix from the query and record keys yields exactly one +/// `databricks_v2` record; no or ambiguous stripped matches deliberately remain +/// uncurated. This accessor is discovery-only, so `resolve()` retains its exact- +/// record label contract. pub fn databricks_registry_label(raw_model_id: &str) -> Option<&'static str> { + let m = manifest(); + registry_label_for_databricks_records(raw_model_id, &m.exact_records, &m.family_tokens) +} + +fn registry_label_for_databricks_records<'a>( + raw_model_id: &str, + records: &'a [ExactRecord], + family_tokens: &[String], +) -> Option<&'a str> { if raw_model_id.trim().is_empty() { return None; } - manifest() - .exact_records - .iter() - .find(|rec| { - rec.provider == "databricks_v2" && rec.raw_model_id.eq_ignore_ascii_case(raw_model_id) - }) - .map(|rec| rec.registry_label.as_str()) + + if let Some(rec) = records.iter().find(|rec| { + rec.provider == "databricks_v2" && rec.raw_model_id.eq_ignore_ascii_case(raw_model_id) + }) { + return Some(&rec.registry_label); + } + + let query_lower = raw_model_id.to_ascii_lowercase(); + let stripped_query = strip_catalog_prefix(&query_lower, family_tokens); + if stripped_query == query_lower { + return None; + } + let mut matching_record = None; + for rec in records.iter().filter(|rec| rec.provider == "databricks_v2") { + let record_lower = rec.raw_model_id.to_ascii_lowercase(); + if strip_catalog_prefix(&record_lower, family_tokens) == stripped_query + && matching_record.replace(rec).is_some() + { + return None; + } + } + matching_record.map(|rec| rec.registry_label.as_str()) } /// Semantic invariants that strict typed parsing cannot express. Structural @@ -571,6 +596,10 @@ mod tests { Q::Vector { id: "dbv2-goose-opus-5-prefix-probe", provider: "databricks_v2", raw_model_id: "goose-opus-5", note: Some("Probes a goose- prefix over a bare code-name segment with no leading claude.") }, Q::Section { group: "Resolver-contract probes (plan v4 §Resolver contract)", note: None }, Q::Vector { id: "resolver-exact-raw-id-probe", provider: "databricks_v2", raw_model_id: "databricks-gpt-5-4-mini", note: Some("Probes a raw id that has an exact record.") }, + Q::Vector { id: "dbv2-claude-fable-5-exact-record-probe", provider: "databricks_v2", raw_model_id: "databricks-claude-fable-5", note: Some("Probes the canonical Databricks Fable 5 endpoint record.") }, + Q::Vector { id: "dbv2-goose-claude-fable-5-alias-probe", provider: "databricks_v2", raw_model_id: "goose-claude-fable-5", note: Some("Probes a prefixed alias of the Databricks Fable 5 endpoint.") }, + Q::Vector { id: "dbv2-claude-opus-4-8-exact-record-probe", provider: "databricks_v2", raw_model_id: "databricks-claude-opus-4-8", note: Some("Probes the canonical Databricks Opus 4.8 endpoint record.") }, + Q::Vector { id: "dbv2-goose-claude-opus-4-8-alias-probe", provider: "databricks_v2", raw_model_id: "goose-claude-opus-4-8", note: Some("Probes a prefixed alias of the Databricks Opus 4.8 endpoint.") }, Q::Vector { id: "resolver-prefixed-alias-probe", provider: "databricks_v2", raw_model_id: "team-x-databricks-gpt-5-4-mini", note: Some("Probes a prefixed alias of an exact-record id (raw exact key differs).") }, Q::Vector { id: "resolver-cross-provider-probe", provider: "openai", raw_model_id: "databricks-gpt-5-4-mini", note: Some("Probes the same raw id under a different provider (exact records are provider-scoped).") }, Q::Vector { id: "resolver-exact-record-with-family-route-probe", provider: "databricks_v2", raw_model_id: "databricks-gpt-5-6-sol", note: Some("Exact-vs-family route-axis probe (raw exact key with a covering family rule).") }, @@ -746,7 +775,7 @@ mod tests { } #[test] - fn corpus_has_exactly_103_executable_vectors() { + fn corpus_has_exactly_107_executable_vectors() { // Locks the vector count so a silent INPUTS edit can't quietly drop // coverage; must equal the gate in the TS harness // (modelCapabilitiesCorpus.test.mjs). @@ -755,7 +784,7 @@ mod tests { .filter(|q| matches!(q, Q::Vector { .. })) .count(); assert_eq!( - vectors, 103, + vectors, 107, "corpus executable-vector count changed; update this gate deliberately" ); } @@ -883,17 +912,53 @@ mod tests { #[test] fn test_databricks_registry_label_lookup() { - // Known id → curated label; case-insensitive on the id, matching resolve(). + // Exact raw id remains case-insensitive and unchanged. assert_eq!( - databricks_registry_label("databricks-gpt-5-5"), + databricks_registry_label("DATABRICKS-GPT-5-5"), Some("GPT-5.5") ); + // Aliases reuse the existing family-token stripper. assert_eq!( - databricks_registry_label("DATABRICKS-GPT-5-5"), - Some("GPT-5.5") + databricks_registry_label("goose-gpt-5-6-sol"), + Some("GPT-5.6 Sol") + ); + assert_eq!( + databricks_registry_label("goose-claude-fable-5"), + Some("Claude Fable 5") ); - // Unknown id and blank input → no label. + // Unknown ids, bare family ids, and blanks remain uncurated. assert_eq!(databricks_registry_label("custom-unlisted-endpoint"), None); + assert_eq!(databricks_registry_label("gpt-5"), None); assert_eq!(databricks_registry_label(" "), None); } + + #[test] + fn registry_label_alias_collision_returns_none() { + let record = |raw_model_id: &str, registry_label: &str| ExactRecord { + provider: "databricks_v2".to_string(), + raw_model_id: raw_model_id.to_string(), + registry_label: registry_label.to_string(), + thinking_mode: ThinkingMode::None, + supported_efforts: vec![ThinkingEffort::Medium], + default_effort: Some(ThinkingEffort::Medium), + databricks_v2_wire_route: DatabricksV2Route::MlflowChat, + normalization_policy: NormalizationPolicy::None, + provenance: None, + source: None, + source_alt: None, + reconciliation: None, + reconciliation_note: None, + reconciliation_doc: None, + }; + let records = vec![ + record("databricks-gpt-5-6", "Databricks GPT-5.6"), + record("partner-gpt-5-6", "Partner GPT-5.6"), + ]; + let family_tokens = vec!["gpt-".to_string()]; + + assert_eq!( + registry_label_for_databricks_records("goose-gpt-5-6", &records, &family_tokens), + None + ); + } } diff --git a/desktop/src/features/agents/lib/agentCardModelLabel.test.mjs b/desktop/src/features/agents/lib/agentCardModelLabel.test.mjs index 11d5f74d8f2..a5c55ba29b3 100644 --- a/desktop/src/features/agents/lib/agentCardModelLabel.test.mjs +++ b/desktop/src/features/agents/lib/agentCardModelLabel.test.mjs @@ -60,6 +60,32 @@ test("resolveAgentCardModelLabel — non-inherited agent with a blank resolved m // Databricks registry integration import { formatAgentModelLabel } from "./formatAgentModelLabel.ts"; +test("formatAgentModelLabel — Databricks aliases reuse canonical labels", () => { + assert.equal( + formatAgentModelLabel("goose-gpt-5-6-sol", "databricks_v2"), + "GPT-5.6 Sol", + ); + assert.equal( + formatAgentModelLabel("goose-claude-fable-5", "databricks_v2"), + "Claude Fable 5", + ); + assert.equal( + formatAgentModelLabel("goose-claude-opus-4-8", "databricks_v2"), + "Claude Opus 4.8", + ); +}); + +test("resolveModelLabel — Databricks alias labels stay provider-scoped", () => { + assert.equal( + resolveModelLabel("goose-gpt-5-6-sol", null, "openai"), + "goose-gpt-5-6-sol", + ); +}); + +test("formatAgentModelLabel — bare family IDs remain raw", () => { + assert.equal(formatAgentModelLabel("gpt-5"), "gpt-5"); +}); + test("formatAgentModelLabel — known Databricks managed ID returns curated name", () => { assert.equal(formatAgentModelLabel("databricks-gpt-5-5"), "GPT-5.5"); assert.equal( diff --git a/desktop/src/features/agents/lib/formatAgentModelLabel.ts b/desktop/src/features/agents/lib/formatAgentModelLabel.ts index 7bce26a9b4c..5bc3a0f299d 100644 --- a/desktop/src/features/agents/lib/formatAgentModelLabel.ts +++ b/desktop/src/features/agents/lib/formatAgentModelLabel.ts @@ -1,6 +1,6 @@ import { canonicalizeProvider, - DATABRICKS_MODEL_NAMES, + databricksRegistryLabel, resolveModelCapabilities, } from "../ui/modelCapabilities"; @@ -19,20 +19,22 @@ export { canonicalizeProvider }; * discovery contract (`{id, name: id}`) and any harness/version skew that * echoes the id as the name. * 2. Registry lookup by id: - * - `provider` supplied → provider-qualified exact record only. On a miss - * the raw id is returned; the unscoped `DATABRICKS_MODEL_NAMES` map is - * NOT consulted, so a Databricks endpoint id never leaks a curated label + * - `provider` supplied → Databricks v2 uses alias-aware exact records; + * every other provider uses provider-qualified exact records. On a miss + * the raw id is returned; the providerless registry tier is NOT + * consulted, so a Databricks endpoint id never leaks a curated label * through an anthropic/openai provider context (the P3-B contract). - * - `provider` absent → unscoped `DATABRICKS_MODEL_NAMES` map, for - * legacy/inherited ids with no provider on hand. + * - `provider` absent → alias-aware lookup over `databricks_v2` exact + * records, for legacy/inherited ids with no provider on hand. * 3. Raw id unchanged. * * Returns the empty string when both id and discoveredName are blank; use * `formatAgentModelLabel` when a null/empty id should render "Auto". * - * `resolveModelCapabilities` canonicalizes the provider internally, so callers - * pass the raw provider id. Only exact records carry a `registryLabel`, so a - * family/prefix hit yields `null` and correctly falls back to the raw id. + * `resolveModelCapabilities` canonicalizes the provider internally. The + * providerless registry lookup applies the same family-token stripping and + * unique-match guard as buzz-agent discovery; only unique exact-record aliases + * get a label. */ export function resolveModelLabel( id: string, @@ -46,15 +48,16 @@ export function resolveModelLabel( if (trimmedName && trimmedName !== trimmedId) return trimmedName; if (!trimmedId) return ""; if (provider?.trim()) { - // Provider-qualified exact-record tier (provider-scoped, no unscoped fallback). - const registryLabel = resolveModelCapabilities( - provider, - trimmedId, - ).registryLabel; + // Provider-qualified exact-record tier (provider-scoped, no providerless fallback). + const canonicalProvider = canonicalizeProvider(provider); + const registryLabel = + canonicalProvider === "databricks_v2" + ? databricksRegistryLabel(trimmedId) + : resolveModelCapabilities(provider, trimmedId).registryLabel; return registryLabel ?? trimmedId; } - // Providerless path: unscoped registry map for legacy/inherited ids. - return DATABRICKS_MODEL_NAMES.get(trimmedId) ?? trimmedId; + // Providerless path: alias-aware lookup for legacy/inherited ids. + return databricksRegistryLabel(trimmedId) ?? trimmedId; } /** diff --git a/desktop/src/features/agents/ui/modelCapabilities.ts b/desktop/src/features/agents/ui/modelCapabilities.ts index bd160c7b810..3aa11198875 100644 --- a/desktop/src/features/agents/ui/modelCapabilities.ts +++ b/desktop/src/features/agents/ui/modelCapabilities.ts @@ -358,6 +358,52 @@ export function resolveModelCapabilities( export const DATABRICKS_V2_KNOWN_MODELS: ReadonlyArray = MANIFEST.databricks_v2_known_models; +export type RegistryLabelRecord = { + readonly provider: string; + readonly raw_model_id: string; + readonly registry_label: string; +}; + +export function databricksRegistryLabelForRecords( + rawModelId: string, + records: ReadonlyArray, + familyTokens: ReadonlyArray, +): string | null { + if (!rawModelId.trim()) return null; + + const idLower = rawModelId.toLowerCase(); + const exact = records.find( + (rec) => + rec.provider === "databricks_v2" && + rec.raw_model_id.toLowerCase() === idLower, + ); + if (exact) return exact.registry_label; + + const strippedQuery = stripCatalogPrefix(idLower, familyTokens); + if (strippedQuery === idLower) return null; + let matchingRecord: RegistryLabelRecord | null = null; + for (const rec of records) { + if (rec.provider !== "databricks_v2") continue; + const strippedRecord = stripCatalogPrefix( + rec.raw_model_id.toLowerCase(), + familyTokens, + ); + if (strippedRecord === strippedQuery) { + if (matchingRecord) return null; + matchingRecord = rec; + } + } + return matchingRecord?.registry_label ?? null; +} + +export function databricksRegistryLabel(rawModelId: string): string | null { + return databricksRegistryLabelForRecords( + rawModelId, + MANIFEST.exact_records, + MANIFEST.family_tokens, + ); +} + /** * Databricks endpoint-id → display-name registry, derived at runtime from the * manifest's `databricks_v2` exact records (the only exact records that carry a diff --git a/desktop/src/features/agents/ui/modelCapabilitiesCorpus.test.mjs b/desktop/src/features/agents/ui/modelCapabilitiesCorpus.test.mjs index 52f1f0ecf0e..990d99cedd7 100644 --- a/desktop/src/features/agents/ui/modelCapabilitiesCorpus.test.mjs +++ b/desktop/src/features/agents/ui/modelCapabilitiesCorpus.test.mjs @@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url"; import test from "node:test"; import { + databricksRegistryLabelForRecords, ManifestSchema, resolveModelCapabilities, } from "./modelCapabilities.ts"; @@ -23,10 +24,43 @@ const corpus = JSON.parse(readFileSync(fileURLToPath(corpusUrl), "utf8")); // (`_group`) are skipped. Mirrors the Rust corpus filter. const executable = corpus.filter((entry) => entry.expect != null); -test("corpus has exactly 103 executable vectors", () => { +test("corpus has exactly 107 executable vectors", () => { // Locks the vector count so a silent corpus edit can't quietly drop coverage; // must equal the gate in the Rust suite (model_capabilities.rs). - assert.equal(executable.length, 103); + assert.equal(executable.length, 107); +}); + +test("registry label aliases refuse an unprefixed query", () => { + const records = [ + { + provider: "databricks_v2", + raw_model_id: "databricks-gpt-5", + registry_label: "GPT-5", + }, + ]; + assert.equal( + databricksRegistryLabelForRecords("gpt-5", records, ["gpt-"]), + null, + ); +}); + +test("registry label aliases refuse ambiguous stripped record keys", () => { + const records = [ + { + provider: "databricks_v2", + raw_model_id: "databricks-gpt-5-6", + registry_label: "Databricks GPT-5.6", + }, + { + provider: "databricks_v2", + raw_model_id: "partner-gpt-5-6", + registry_label: "Partner GPT-5.6", + }, + ]; + assert.equal( + databricksRegistryLabelForRecords("goose-gpt-5-6", records, ["gpt-"]), + null, + ); }); test("every executable corpus vector resolves to its expected six-axis profile", () => { diff --git a/scripts/model-capabilities.json b/scripts/model-capabilities.json index 2c78867f13b..efa307cfc3e 100644 --- a/scripts/model-capabilities.json +++ b/scripts/model-capabilities.json @@ -436,6 +436,42 @@ "_reconciliation_note": "models.dev advertises low|medium|high. Same as gpt-5-4-mini. Adopt.", "_reconciliation_doc": "https://models.dev/api.json (retrieved 2026-07-31, SHA-256 d5a4974cd69f19b0f67713acaa6bb3b16e920defdc07ecbdf6b0a936181bb0e0): providers.databricks.models[\"databricks-gpt-5-4-nano\"].reasoning_options=[{\"type\":\"effort\",\"values\":[\"low\",\"medium\",\"high\"]}]" }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-fable-5", + "registry_label": "Claude Fable 5", + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:anthropic-adaptive-xhigh-fable-5", + "_source": "models.dev anthropic catalog label; Databricks workspace endpoint is absent from its provider catalog" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-opus-4-8", + "registry_label": "Claude Opus 4.8", + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:anthropic-adaptive-xhigh-opus-4-8", + "_source": "models.dev anthropic catalog label; Databricks workspace endpoint is absent from its provider catalog" + }, { "provider": "databricks_v2", "raw_model_id": "databricks-gpt-5-6-sol", diff --git a/scripts/normative-corpus.json b/scripts/normative-corpus.json index 2543fd6cbe6..e14f22c16ac 100644 --- a/scripts/normative-corpus.json +++ b/scripts/normative-corpus.json @@ -680,6 +680,86 @@ "registry_label": "GPT-5.4 mini" } }, + { + "id": "dbv2-claude-fable-5-exact-record-probe", + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-fable-5", + "_note": "Probes the canonical Databricks Fable 5 endpoint record.", + "expect": { + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "registry_label": "Claude Fable 5" + } + }, + { + "id": "dbv2-goose-claude-fable-5-alias-probe", + "provider": "databricks_v2", + "raw_model_id": "goose-claude-fable-5", + "_note": "Probes a prefixed alias of the Databricks Fable 5 endpoint.", + "expect": { + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "registry_label": null + } + }, + { + "id": "dbv2-claude-opus-4-8-exact-record-probe", + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-opus-4-8", + "_note": "Probes the canonical Databricks Opus 4.8 endpoint record.", + "expect": { + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "registry_label": "Claude Opus 4.8" + } + }, + { + "id": "dbv2-goose-claude-opus-4-8-alias-probe", + "provider": "databricks_v2", + "raw_model_id": "goose-claude-opus-4-8", + "_note": "Probes a prefixed alias of the Databricks Opus 4.8 endpoint.", + "expect": { + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "registry_label": null + } + }, { "id": "resolver-prefixed-alias-probe", "provider": "databricks_v2", diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 0bbdfca6a4d..9dca8c82c37 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -115,8 +115,8 @@ run_unit_tests() { # buzz-agent model-capabilities corpus: the Rust half of the cross-language # drift guard. model_capabilities.rs embeds scripts/model-capabilities.json + - # scripts/normative-corpus.json via include_str! and replays all 103 vectors - # as pure in-process tests (no infra). Mirrors the nextest path in + # scripts/normative-corpus.json via include_str! and replays the full locked + # corpus as pure in-process tests (no infra). Mirrors the nextest path in # `just test-unit` — the two lists must stay in step. run_test_step "buzz-agent unit tests" \ cargo test -p buzz-agent --lib -- --nocapture From 3461c7e6dd44d7c30963d7663c1307c9145759f0 Mon Sep 17 00:00:00 2001 From: Duncan Date: Thu, 20 Aug 2026 10:54:46 -0400 Subject: [PATCH 2/4] fix(models): add Databricks Claude and Kimi aliases Materialize the confirmed picker IDs with their Databricks capability profiles and labels. Reuse catalog-prefix semantics for alias labels and lock canonical plus prefixed behavior in the cross-language corpus. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- crates/buzz-agent/src/model_capabilities.rs | 34 ++++- .../agents/lib/agentCardModelLabel.test.mjs | 21 ++++ .../ui/modelCapabilitiesCorpus.test.mjs | 4 +- scripts/model-capabilities.json | 76 +++++++++++- scripts/normative-corpus.json | 116 ++++++++++++++++++ 5 files changed, 246 insertions(+), 5 deletions(-) diff --git a/crates/buzz-agent/src/model_capabilities.rs b/crates/buzz-agent/src/model_capabilities.rs index d8f74b6ec61..b299fa61179 100644 --- a/crates/buzz-agent/src/model_capabilities.rs +++ b/crates/buzz-agent/src/model_capabilities.rs @@ -600,6 +600,12 @@ mod tests { Q::Vector { id: "dbv2-goose-claude-fable-5-alias-probe", provider: "databricks_v2", raw_model_id: "goose-claude-fable-5", note: Some("Probes a prefixed alias of the Databricks Fable 5 endpoint.") }, Q::Vector { id: "dbv2-claude-opus-4-8-exact-record-probe", provider: "databricks_v2", raw_model_id: "databricks-claude-opus-4-8", note: Some("Probes the canonical Databricks Opus 4.8 endpoint record.") }, Q::Vector { id: "dbv2-goose-claude-opus-4-8-alias-probe", provider: "databricks_v2", raw_model_id: "goose-claude-opus-4-8", note: Some("Probes a prefixed alias of the Databricks Opus 4.8 endpoint.") }, + Q::Vector { id: "dbv2-claude-opus-5-exact-record-probe", provider: "databricks_v2", raw_model_id: "databricks-claude-opus-5", note: Some("Probes the canonical Databricks Opus 5 endpoint record.") }, + Q::Vector { id: "dbv2-goose-claude-opus-5-alias-probe", provider: "databricks_v2", raw_model_id: "goose-claude-opus-5", note: Some("Probes a prefixed alias of the Databricks Opus 5 endpoint.") }, + Q::Vector { id: "dbv2-claude-sonnet-5-exact-record-probe", provider: "databricks_v2", raw_model_id: "databricks-claude-sonnet-5", note: Some("Probes the canonical Databricks Sonnet 5 endpoint record.") }, + Q::Vector { id: "dbv2-goose-claude-sonnet-5-alias-probe", provider: "databricks_v2", raw_model_id: "goose-claude-sonnet-5", note: Some("Probes a prefixed alias of the Databricks Sonnet 5 endpoint.") }, + Q::Vector { id: "dbv2-kimi-k3-exact-record-probe", provider: "databricks_v2", raw_model_id: "databricks-kimi-k3", note: Some("Probes the canonical Databricks Kimi K3 endpoint record.") }, + Q::Vector { id: "dbv2-goose-kimi-k3-alias-probe", provider: "databricks_v2", raw_model_id: "goose-kimi-k3", note: Some("Probes a prefixed alias of the Databricks Kimi K3 endpoint.") }, Q::Vector { id: "resolver-prefixed-alias-probe", provider: "databricks_v2", raw_model_id: "team-x-databricks-gpt-5-4-mini", note: Some("Probes a prefixed alias of an exact-record id (raw exact key differs).") }, Q::Vector { id: "resolver-cross-provider-probe", provider: "openai", raw_model_id: "databricks-gpt-5-4-mini", note: Some("Probes the same raw id under a different provider (exact records are provider-scoped).") }, Q::Vector { id: "resolver-exact-record-with-family-route-probe", provider: "databricks_v2", raw_model_id: "databricks-gpt-5-6-sol", note: Some("Exact-vs-family route-axis probe (raw exact key with a covering family rule).") }, @@ -775,7 +781,7 @@ mod tests { } #[test] - fn corpus_has_exactly_107_executable_vectors() { + fn corpus_has_exactly_113_executable_vectors() { // Locks the vector count so a silent INPUTS edit can't quietly drop // coverage; must equal the gate in the TS harness // (modelCapabilitiesCorpus.test.mjs). @@ -784,7 +790,7 @@ mod tests { .filter(|q| matches!(q, Q::Vector { .. })) .count(); assert_eq!( - vectors, 107, + vectors, 113, "corpus executable-vector count changed; update this gate deliberately" ); } @@ -917,6 +923,18 @@ mod tests { databricks_registry_label("DATABRICKS-GPT-5-5"), Some("GPT-5.5") ); + // Exact raw ids preserve their canonical labels. + for (model, label) in [ + ("databricks-claude-opus-5", "Claude Opus 5"), + ("databricks-claude-sonnet-5", "Claude Sonnet 5"), + ("databricks-kimi-k3", "Kimi K3"), + ] { + assert_eq!( + databricks_registry_label(model), + Some(label), + "model={model}" + ); + } // Aliases reuse the existing family-token stripper. assert_eq!( databricks_registry_label("goose-gpt-5-6-sol"), @@ -926,6 +944,18 @@ mod tests { databricks_registry_label("goose-claude-fable-5"), Some("Claude Fable 5") ); + for (alias, label) in [ + ("goose-claude-opus-4-8", "Claude Opus 4.8"), + ("goose-claude-opus-5", "Claude Opus 5"), + ("goose-claude-sonnet-5", "Claude Sonnet 5"), + ("goose-kimi-k3", "Kimi K3"), + ] { + assert_eq!( + databricks_registry_label(alias), + Some(label), + "alias={alias}" + ); + } // Unknown ids, bare family ids, and blanks remain uncurated. assert_eq!(databricks_registry_label("custom-unlisted-endpoint"), None); assert_eq!(databricks_registry_label("gpt-5"), None); diff --git a/desktop/src/features/agents/lib/agentCardModelLabel.test.mjs b/desktop/src/features/agents/lib/agentCardModelLabel.test.mjs index a5c55ba29b3..4a1151c9ceb 100644 --- a/desktop/src/features/agents/lib/agentCardModelLabel.test.mjs +++ b/desktop/src/features/agents/lib/agentCardModelLabel.test.mjs @@ -73,6 +73,18 @@ test("formatAgentModelLabel — Databricks aliases reuse canonical labels", () = formatAgentModelLabel("goose-claude-opus-4-8", "databricks_v2"), "Claude Opus 4.8", ); + assert.equal( + formatAgentModelLabel("goose-claude-opus-5", "databricks_v2"), + "Claude Opus 5", + ); + assert.equal( + formatAgentModelLabel("goose-claude-sonnet-5", "databricks_v2"), + "Claude Sonnet 5", + ); + assert.equal( + formatAgentModelLabel("goose-kimi-k3", "databricks_v2"), + "Kimi K3", + ); }); test("resolveModelLabel — Databricks alias labels stay provider-scoped", () => { @@ -92,6 +104,15 @@ test("formatAgentModelLabel — known Databricks managed ID returns curated name formatAgentModelLabel("databricks-claude-opus-4-7"), "Claude Opus 4.7", ); + assert.equal( + formatAgentModelLabel("databricks-claude-opus-5"), + "Claude Opus 5", + ); + assert.equal( + formatAgentModelLabel("databricks-claude-sonnet-5"), + "Claude Sonnet 5", + ); + assert.equal(formatAgentModelLabel("databricks-kimi-k3"), "Kimi K3"); }); test("formatAgentModelLabel — unknown custom Databricks ID returns raw ID unchanged", () => { diff --git a/desktop/src/features/agents/ui/modelCapabilitiesCorpus.test.mjs b/desktop/src/features/agents/ui/modelCapabilitiesCorpus.test.mjs index 990d99cedd7..78c05a4df4b 100644 --- a/desktop/src/features/agents/ui/modelCapabilitiesCorpus.test.mjs +++ b/desktop/src/features/agents/ui/modelCapabilitiesCorpus.test.mjs @@ -24,10 +24,10 @@ const corpus = JSON.parse(readFileSync(fileURLToPath(corpusUrl), "utf8")); // (`_group`) are skipped. Mirrors the Rust corpus filter. const executable = corpus.filter((entry) => entry.expect != null); -test("corpus has exactly 107 executable vectors", () => { +test("corpus has exactly 113 executable vectors", () => { // Locks the vector count so a silent corpus edit can't quietly drop coverage; // must equal the gate in the Rust suite (model_capabilities.rs). - assert.equal(executable.length, 107); + assert.equal(executable.length, 113); }); test("registry label aliases refuse an unprefixed query", () => { diff --git a/scripts/model-capabilities.json b/scripts/model-capabilities.json index efa307cfc3e..321ee418eee 100644 --- a/scripts/model-capabilities.json +++ b/scripts/model-capabilities.json @@ -8,7 +8,8 @@ }, "family_tokens": [ "claude-", - "gpt-" + "gpt-", + "kimi-" ], "family_rules": [ { @@ -371,6 +372,24 @@ "databricks_v2_wire_route": "openai-responses", "normalization_policy": "openai-standard" }, + { + "id": "databricks-kimi-k3", + "_comment": "Databricks Kimi K3 aliases share the exact record's upstream Moonshot capabilities. Kimi is absent from the Databricks models.dev catalog, so retain the established MLflow Chat route; upstream models.dev advertises a reasoning toggle and effort values low|high|max, with no documented default.", + "match_kind": "exact", + "match_value": "kimi-k3", + "providers": [ + "databricks_v2" + ], + "thinking_mode": "none", + "supported_efforts": [ + "low", + "high", + "max" + ], + "default_effort": null, + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-standard" + }, { "id": "dbv2-claude-prefix", "_comment": "DBv2-only broad Claude prefix. Replaces the routing role of the dropped dbv2-claude-code-names-segment 'claude' token: uncurated databricks-claude-* endpoints still route via Anthropic Messages with conservative (omit-fields) effort classification. Bare code-name segments without a leading 'claude-' (e.g. opus-5, goose-opus-5) are deliberately dropped and fall through to the databricks_v2 concrete-unknown fallback (mlflow-chat) — segment-anywhere matching is not expressible as a prefix.", @@ -472,6 +491,42 @@ "_provenance": "all axes materialized from family:anthropic-adaptive-xhigh-opus-4-8", "_source": "models.dev anthropic catalog label; Databricks workspace endpoint is absent from its provider catalog" }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-opus-5", + "registry_label": "Claude Opus 5", + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:anthropic-adaptive-xhigh-opus-5", + "_source": "models.dev anthropic catalog label; Databricks workspace endpoint is absent from its provider catalog" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-sonnet-5", + "registry_label": "Claude Sonnet 5", + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:anthropic-adaptive-xhigh-sonnet-5", + "_source": "models.dev anthropic catalog label; Databricks workspace endpoint is absent from its provider catalog" + }, { "provider": "databricks_v2", "raw_model_id": "databricks-gpt-5-6-sol", @@ -968,6 +1023,25 @@ "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", "_source": "registry_labels" }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-kimi-k3", + "registry_label": "Kimi K3", + "thinking_mode": "none", + "supported_efforts": [ + "low", + "high", + "max" + ], + "default_effort": null, + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-standard", + "_provenance": "databricks_v2_wire_route: provider fallback (databricks_v2/concrete_unknown); thinking_mode+supported_efforts: models.dev Moonshot Kimi K3 catalog; default_effort: no documented default", + "source": "models.dev Moonshot Kimi K3 reasoning_options: toggle + low|high|max", + "_reconciliation": "adopt", + "_reconciliation_note": "models.dev advertises toggleable reasoning with [low, high, max], but no default. The Databricks endpoint is absent from its provider catalog, so retain the established MLflow Chat route, adopt the upstream capability set, and leave the effort unset rather than invent a Databricks default.", + "_reconciliation_doc": "https://models.dev/api.json (retrieved 2026-08-20, SHA-256 7ccb5635f682e4248ad8d39f515fbe3f2bb10e67bbc7fa1b94e66dddb00779e5): providers.moonshotai.models[\"kimi-k3\"].reasoning_options=[{\"type\":\"toggle\"},{\"type\":\"effort\",\"values\":[\"low\",\"high\",\"max\"]}]; Databricks endpoint absent from providers.databricks.models" + }, { "provider": "databricks_v2", "raw_model_id": "databricks-kimi-k2-7-code", diff --git a/scripts/normative-corpus.json b/scripts/normative-corpus.json index e14f22c16ac..fcc848ff480 100644 --- a/scripts/normative-corpus.json +++ b/scripts/normative-corpus.json @@ -760,6 +760,122 @@ "registry_label": null } }, + { + "id": "dbv2-claude-opus-5-exact-record-probe", + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-opus-5", + "_note": "Probes the canonical Databricks Opus 5 endpoint record.", + "expect": { + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "registry_label": "Claude Opus 5" + } + }, + { + "id": "dbv2-goose-claude-opus-5-alias-probe", + "provider": "databricks_v2", + "raw_model_id": "goose-claude-opus-5", + "_note": "Probes a prefixed alias of the Databricks Opus 5 endpoint.", + "expect": { + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "registry_label": null + } + }, + { + "id": "dbv2-claude-sonnet-5-exact-record-probe", + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-sonnet-5", + "_note": "Probes the canonical Databricks Sonnet 5 endpoint record.", + "expect": { + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "registry_label": "Claude Sonnet 5" + } + }, + { + "id": "dbv2-goose-claude-sonnet-5-alias-probe", + "provider": "databricks_v2", + "raw_model_id": "goose-claude-sonnet-5", + "_note": "Probes a prefixed alias of the Databricks Sonnet 5 endpoint.", + "expect": { + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "registry_label": null + } + }, + { + "id": "dbv2-kimi-k3-exact-record-probe", + "provider": "databricks_v2", + "raw_model_id": "databricks-kimi-k3", + "_note": "Probes the canonical Databricks Kimi K3 endpoint record.", + "expect": { + "thinking_mode": "none", + "supported_efforts": [ + "low", + "high", + "max" + ], + "default_effort": null, + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-standard", + "registry_label": "Kimi K3" + } + }, + { + "id": "dbv2-goose-kimi-k3-alias-probe", + "provider": "databricks_v2", + "raw_model_id": "goose-kimi-k3", + "_note": "Probes a prefixed alias of the Databricks Kimi K3 endpoint.", + "expect": { + "thinking_mode": "none", + "supported_efforts": [ + "low", + "high", + "max" + ], + "default_effort": null, + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-standard", + "registry_label": null + } + }, { "id": "resolver-prefixed-alias-probe", "provider": "databricks_v2", From a95643c3d8b5e7a18d931c121b9c35b5703b330b Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Thu, 20 Aug 2026 11:19:45 -0400 Subject: [PATCH 3/4] fix(test): use sentinel var in discovery-provider test to avoid process-env contamination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `effective_discovery_provider_is_none_without_an_explicit_or_env_provider` used `BUZZ_AGENT_PROVIDER` as the test env-var name. In a Databricks dev environment where that var is exported, `env_or_process_value` falls through to `std::env::var` and returns the live process value — failing the assertion that expects `None`. The pre-existing `UNSET_CREDENTIAL` pattern already establishes the fix: use a sentinel name guaranteed not to be set. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- desktop/src-tauri/src/commands/agent_models_tests.rs | 11 ++++++----- scripts/model-capabilities.json | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/desktop/src-tauri/src/commands/agent_models_tests.rs b/desktop/src-tauri/src/commands/agent_models_tests.rs index a9e3b677753..df3849de4a4 100644 --- a/desktop/src-tauri/src/commands/agent_models_tests.rs +++ b/desktop/src-tauri/src/commands/agent_models_tests.rs @@ -306,11 +306,15 @@ fn effective_discovery_provider_recovers_baked_provider_when_record_has_none() { } } +/// A provider env-var name no environment sets, so this test does not depend on +/// what the developer happens to have exported (e.g. `BUZZ_AGENT_PROVIDER`). +const UNSET_PROVIDER_VAR: &str = "BUZZ_TEST_UNSET_DISCOVERY_PROVIDER"; + #[test] fn effective_discovery_provider_is_none_without_an_explicit_or_env_provider() { let env = BTreeMap::new(); assert_eq!( - effective_discovery_provider(None, Some("BUZZ_AGENT_PROVIDER"), &env).as_deref(), + effective_discovery_provider(None, Some(UNSET_PROVIDER_VAR), &env).as_deref(), None ); // A runtime that takes no provider env var has nothing to recover from. @@ -318,10 +322,7 @@ fn effective_discovery_provider_is_none_without_an_explicit_or_env_provider() { effective_discovery_provider( None, None, - &BTreeMap::from([( - "BUZZ_AGENT_PROVIDER".to_string(), - "databricks_v2".to_string() - )]) + &BTreeMap::from([(UNSET_PROVIDER_VAR.to_string(), "databricks_v2".to_string())]) ) .as_deref(), None diff --git a/scripts/model-capabilities.json b/scripts/model-capabilities.json index 321ee418eee..9de2f28a4ca 100644 --- a/scripts/model-capabilities.json +++ b/scripts/model-capabilities.json @@ -1037,7 +1037,7 @@ "databricks_v2_wire_route": "mlflow-chat", "normalization_policy": "openai-standard", "_provenance": "databricks_v2_wire_route: provider fallback (databricks_v2/concrete_unknown); thinking_mode+supported_efforts: models.dev Moonshot Kimi K3 catalog; default_effort: no documented default", - "source": "models.dev Moonshot Kimi K3 reasoning_options: toggle + low|high|max", + "_source": "models.dev Moonshot Kimi K3 reasoning_options: toggle + low|high|max", "_reconciliation": "adopt", "_reconciliation_note": "models.dev advertises toggleable reasoning with [low, high, max], but no default. The Databricks endpoint is absent from its provider catalog, so retain the established MLflow Chat route, adopt the upstream capability set, and leave the effort unset rather than invent a Databricks default.", "_reconciliation_doc": "https://models.dev/api.json (retrieved 2026-08-20, SHA-256 7ccb5635f682e4248ad8d39f515fbe3f2bb10e67bbc7fa1b94e66dddb00779e5): providers.moonshotai.models[\"kimi-k3\"].reasoning_options=[{\"type\":\"toggle\"},{\"type\":\"effort\",\"values\":[\"low\",\"high\",\"max\"]}]; Databricks endpoint absent from providers.databricks.models" From f68686eab1ae9fce1a5b20aae3af13b02fc1c546 Mon Sep 17 00:00:00 2001 From: Duncan Date: Thu, 20 Aug 2026 13:14:44 -0400 Subject: [PATCH 4/4] chore(models): address review nits on Databricks alias-aware labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the Kimi K3 family rule id to dbv2-kimi-k3-exact so it no longer collides verbatim with the exact record's raw model id, making family provenance strings unambiguous. Make the kimi-k3 thinking_mode: none mapping self-justifying — the MLflow Chat request schema cannot express models.dev's reasoning toggle, only reasoning_effort. Drop the unused DATABRICKS_MODEL_NAMES export whose comment falsely claimed it feeds resolveModelLabel's providerless tier (that tier uses databricksRegistryLabel). No behavior change: corpus expect objects and provenance are byte-identical after regen. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- desktop/src/features/agents/ui/modelCapabilities.ts | 13 ------------- scripts/model-capabilities.json | 6 +++--- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/desktop/src/features/agents/ui/modelCapabilities.ts b/desktop/src/features/agents/ui/modelCapabilities.ts index 3aa11198875..bce4af829ac 100644 --- a/desktop/src/features/agents/ui/modelCapabilities.ts +++ b/desktop/src/features/agents/ui/modelCapabilities.ts @@ -403,16 +403,3 @@ export function databricksRegistryLabel(rawModelId: string): string | null { MANIFEST.family_tokens, ); } - -/** - * Databricks endpoint-id → display-name registry, derived at runtime from the - * manifest's `databricks_v2` exact records (the only exact records that carry a - * `registry_label`). Feeds the providerless registry tier of - * `resolveModelLabel`. Derived, not hand-listed — the manifest stays the single - * source of truth, so there is no second table to keep in sync. - */ -export const DATABRICKS_MODEL_NAMES: ReadonlyMap = new Map( - MANIFEST.exact_records - .filter((rec) => rec.provider === "databricks_v2") - .map((rec) => [rec.raw_model_id, rec.registry_label] as const), -); diff --git a/scripts/model-capabilities.json b/scripts/model-capabilities.json index 9de2f28a4ca..e86bde32bc1 100644 --- a/scripts/model-capabilities.json +++ b/scripts/model-capabilities.json @@ -373,8 +373,8 @@ "normalization_policy": "openai-standard" }, { - "id": "databricks-kimi-k3", - "_comment": "Databricks Kimi K3 aliases share the exact record's upstream Moonshot capabilities. Kimi is absent from the Databricks models.dev catalog, so retain the established MLflow Chat route; upstream models.dev advertises a reasoning toggle and effort values low|high|max, with no documented default.", + "id": "dbv2-kimi-k3-exact", + "_comment": "Databricks Kimi K3 aliases share the exact record's upstream Moonshot capabilities. Kimi is absent from the Databricks models.dev catalog, so retain the established MLflow Chat route; upstream models.dev advertises a reasoning toggle and effort values low|high|max, with no documented default. The toggle is not representable on the MLflow Chat request schema (only reasoning_effort is), so thinking_mode stays none, matching the kimi-k2-7-code precedent.", "match_kind": "exact", "match_value": "kimi-k3", "providers": [ @@ -1039,7 +1039,7 @@ "_provenance": "databricks_v2_wire_route: provider fallback (databricks_v2/concrete_unknown); thinking_mode+supported_efforts: models.dev Moonshot Kimi K3 catalog; default_effort: no documented default", "_source": "models.dev Moonshot Kimi K3 reasoning_options: toggle + low|high|max", "_reconciliation": "adopt", - "_reconciliation_note": "models.dev advertises toggleable reasoning with [low, high, max], but no default. The Databricks endpoint is absent from its provider catalog, so retain the established MLflow Chat route, adopt the upstream capability set, and leave the effort unset rather than invent a Databricks default.", + "_reconciliation_note": "models.dev advertises toggleable reasoning with [low, high, max], but no default. The Databricks endpoint is absent from its provider catalog, so retain the established MLflow Chat route, adopt the upstream capability set, and leave the effort unset rather than invent a Databricks default. The MLflow Chat request schema cannot express a reasoning toggle (only reasoning_effort is representable), so thinking_mode maps to none rather than the upstream toggle, matching the kimi-k2-7-code precedent.", "_reconciliation_doc": "https://models.dev/api.json (retrieved 2026-08-20, SHA-256 7ccb5635f682e4248ad8d39f515fbe3f2bb10e67bbc7fa1b94e66dddb00779e5): providers.moonshotai.models[\"kimi-k3\"].reasoning_options=[{\"type\":\"toggle\"},{\"type\":\"effort\",\"values\":[\"low\",\"high\",\"max\"]}]; Databricks endpoint absent from providers.databricks.models" }, {