mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(catalog): address pass-1 review blockers on Databricks model-label PR
- Generator emits both Rust and TypeScript registries in one invocation; both are formatted (cargo fmt, biome) so regenerate is byte-for-byte stable - TS registry uses Map<string, string> — eliminates Object.prototype key hole where 'constructor', '__proto__', 'toString' resolved through Object.prototype instead of passing through as raw IDs - Centralize three-tier resolver (resolveModelLabel) in formatAgentModelLabel.ts; apply to ModelPicker trigger + unsupported-switching state, usePersonaModelDiscovery default rows, and AgentConfigFields defaultModelLabel — all surfaces now show curated names where available instead of raw endpoint IDs - lib.rs module ordering fixed; just fmt-check green - Tests: prototype-key cases (constructor, __proto__, toString, hasOwnProperty), resolver precedence (discovered name wins, blank falls through), DATABRICKS_MODEL_NAMES Map type assertion, Rust/TS parity spot-check; 3794 TS tests pass, 0 fail - AGENTS.md refresh instructions updated: generator emits both files, resolveModelLabel is the single frontend resolver entry point; curl --fail + --max-time 30, explicit JSON shape validation, key/value character validation added to generator Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
co-authored by
Will Pfleger
parent
144ef4d9d0
commit
38558504ca
@@ -1,7 +1,6 @@
|
||||
// GENERATED by scripts/generate-databricks-model-names.py
|
||||
// Source: https://models.dev/api.json -- providers.databricks.models
|
||||
// Refresh: python3 scripts/generate-databricks-model-names.py \
|
||||
// > crates/buzz-agent/src/databricks_model_names.rs
|
||||
// Refresh: python3 scripts/generate-databricks-model-names.py
|
||||
//
|
||||
// Do not hand-edit -- rerun the script to update.
|
||||
|
||||
@@ -22,8 +21,14 @@ pub(crate) static DATABRICKS_MODEL_NAMES: &[(&str, &str)] = &[
|
||||
("databricks-claude-sonnet-4-6", "Claude Sonnet 4.6"),
|
||||
("databricks-gemini-2-5-flash", "Gemini 2.5 Flash"),
|
||||
("databricks-gemini-2-5-pro", "Gemini 2.5 Pro"),
|
||||
("databricks-gemini-3-1-flash-lite", "Gemini 3.1 Flash Lite Preview"),
|
||||
("databricks-gemini-3-1-pro", "Gemini 3.1 Pro Preview Custom Tools"),
|
||||
(
|
||||
"databricks-gemini-3-1-flash-lite",
|
||||
"Gemini 3.1 Flash Lite Preview",
|
||||
),
|
||||
(
|
||||
"databricks-gemini-3-1-pro",
|
||||
"Gemini 3.1 Pro Preview Custom Tools",
|
||||
),
|
||||
("databricks-gemini-3-flash", "Gemini 3 Flash Preview"),
|
||||
("databricks-gemini-3-pro", "Gemini 3 Pro Preview"),
|
||||
("databricks-glm-5-2", "GLM-5.2"),
|
||||
|
||||
@@ -3,8 +3,8 @@ mod agent;
|
||||
pub mod auth;
|
||||
mod builtin;
|
||||
pub mod catalog;
|
||||
pub(crate) mod databricks_model_names;
|
||||
pub mod config;
|
||||
pub(crate) mod databricks_model_names;
|
||||
mod handoff;
|
||||
mod hints;
|
||||
mod llm;
|
||||
|
||||
@@ -148,5 +148,5 @@ treat a config-behavior diff without a matching AGENTS.md diff (or an explicit
|
||||
|
||||
Labels shown in cards, pickers, and popovers follow a three-tier cascade:
|
||||
1. **API/runtime `name`** — `AgentModelInfo.name` from discovery (`AgentModelsResponse`). This is the authoritative source; all providers populate it at discover time (`openai_model_display_name`, Anthropic `display_name`, ACP runtime name, Databricks registry lookup).
|
||||
2. **Table-backed fallback** — for persisted raw Databricks endpoint IDs that render before discovery data is available, `databricksModelName(id)` in `desktop/src/features/agents/lib/databricksModelNames.ts` does a static lookup against the models.dev-seeded registry. Refresh by rerunning `scripts/generate-databricks-model-names.py`.
|
||||
2. **Table-backed fallback** — for persisted raw Databricks endpoint IDs that render before discovery data is available, `resolveModelLabel(id)` in `desktop/src/features/agents/lib/formatAgentModelLabel.ts` does a static lookup against the models.dev-seeded registry (`databricksModelNames.ts`). Both the Rust (`crates/buzz-agent/src/databricks_model_names.rs`) and TypeScript (`desktop/src/features/agents/lib/databricksModelNames.ts`) registries are emitted together by `scripts/generate-databricks-model-names.py` — refresh both by rerunning `python3 scripts/generate-databricks-model-names.py`.
|
||||
3. **Raw ID** — any ID not covered by tiers 1 or 2 renders unchanged. No heuristic string mangling.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { formatAgentModelLabel } from "./formatAgentModelLabel";
|
||||
import { databricksModelName } from "./databricksModelNames";
|
||||
import {
|
||||
formatAgentModelLabel,
|
||||
resolveModelLabel,
|
||||
} from "./formatAgentModelLabel";
|
||||
import type { ManagedAgent } from "@/shared/api/types";
|
||||
|
||||
/**
|
||||
@@ -42,6 +44,6 @@ export function resolveAgentCardModelLabel(input: {
|
||||
export function formatDefaultModelLabel(defaultModel: string) {
|
||||
const model = defaultModel.trim();
|
||||
return model
|
||||
? `Default model (${databricksModelName(model)})`
|
||||
? `Default model (${resolveModelLabel(model)})`
|
||||
: "Default model";
|
||||
}
|
||||
|
||||
@@ -1,45 +1,125 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { DATABRICKS_MODEL_NAMES } from "./databricksModelNames.ts";
|
||||
import {
|
||||
databricksModelName,
|
||||
DATABRICKS_MODEL_NAMES,
|
||||
} from "./databricksModelNames.ts";
|
||||
resolveModelLabel,
|
||||
formatAgentModelLabel,
|
||||
} from "./formatAgentModelLabel.ts";
|
||||
|
||||
test("databricksModelName — known managed endpoint returns curated name", () => {
|
||||
assert.equal(databricksModelName("databricks-gpt-5-5"), "GPT-5.5");
|
||||
// ---------------------------------------------------------------------------
|
||||
// resolveModelLabel — known IDs → curated names
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("resolveModelLabel — known managed endpoint returns curated name", () => {
|
||||
assert.equal(resolveModelLabel("databricks-gpt-5-5"), "GPT-5.5");
|
||||
assert.equal(
|
||||
databricksModelName("databricks-claude-opus-4-7"),
|
||||
resolveModelLabel("databricks-claude-opus-4-7"),
|
||||
"Claude Opus 4.7",
|
||||
);
|
||||
assert.equal(databricksModelName("databricks-gpt-oss-120b"), "GPT OSS 120B");
|
||||
assert.equal(resolveModelLabel("databricks-gpt-oss-120b"), "GPT OSS 120B");
|
||||
});
|
||||
|
||||
test("databricksModelName — unknown custom endpoint returns raw ID unchanged", () => {
|
||||
// A workspace-specific or date-stamped endpoint that isn't in the registry
|
||||
// must never be guessed — return it verbatim.
|
||||
// ---------------------------------------------------------------------------
|
||||
// resolveModelLabel — unknown/custom IDs must pass through unchanged
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("resolveModelLabel — unknown custom endpoint returns raw ID unchanged", () => {
|
||||
assert.equal(
|
||||
databricksModelName("databricks-team-2025-01"),
|
||||
resolveModelLabel("databricks-team-2025-01"),
|
||||
"databricks-team-2025-01",
|
||||
);
|
||||
assert.equal(
|
||||
databricksModelName("databricks-finance-2025-01-30"),
|
||||
resolveModelLabel("databricks-finance-2025-01-30"),
|
||||
"databricks-finance-2025-01-30",
|
||||
);
|
||||
assert.equal(
|
||||
databricksModelName("some-custom-workspace-model"),
|
||||
resolveModelLabel("some-custom-workspace-model"),
|
||||
"some-custom-workspace-model",
|
||||
);
|
||||
});
|
||||
|
||||
test("databricksModelName — empty string returns empty string", () => {
|
||||
assert.equal(databricksModelName(""), "");
|
||||
// ---------------------------------------------------------------------------
|
||||
// resolveModelLabel — Object.prototype key hole: must not resolve through prototype
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("resolveModelLabel — 'constructor' passes through as raw ID", () => {
|
||||
assert.equal(resolveModelLabel("constructor"), "constructor");
|
||||
});
|
||||
|
||||
test("DATABRICKS_MODEL_NAMES — registry is non-empty and all values are non-empty strings", () => {
|
||||
const entries = Object.entries(DATABRICKS_MODEL_NAMES);
|
||||
assert.ok(entries.length > 0, "registry must not be empty");
|
||||
for (const [id, name] of entries) {
|
||||
test("resolveModelLabel — '__proto__' passes through as raw ID", () => {
|
||||
assert.equal(resolveModelLabel("__proto__"), "__proto__");
|
||||
});
|
||||
|
||||
test("resolveModelLabel — 'toString' passes through as raw ID", () => {
|
||||
assert.equal(resolveModelLabel("toString"), "toString");
|
||||
});
|
||||
|
||||
test("resolveModelLabel — 'hasOwnProperty' passes through as raw ID", () => {
|
||||
assert.equal(resolveModelLabel("hasOwnProperty"), "hasOwnProperty");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// resolveModelLabel — discovered name takes precedence over registry and raw ID
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("resolveModelLabel — nonblank discoveredName wins over registry entry", () => {
|
||||
// Even for a known registry ID, a nonblank discovered name wins (tier 1).
|
||||
assert.equal(
|
||||
resolveModelLabel("databricks-gpt-5-5", "My Custom Name"),
|
||||
"My Custom Name",
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveModelLabel — nonblank discoveredName wins over unknown raw ID", () => {
|
||||
assert.equal(
|
||||
resolveModelLabel("databricks-team-2025-01", "Team Model"),
|
||||
"Team Model",
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveModelLabel — blank/null discoveredName falls back to registry then raw ID", () => {
|
||||
assert.equal(resolveModelLabel("databricks-gpt-5-5", null), "GPT-5.5");
|
||||
assert.equal(resolveModelLabel("databricks-gpt-5-5", ""), "GPT-5.5");
|
||||
assert.equal(resolveModelLabel("databricks-gpt-5-5", " "), "GPT-5.5");
|
||||
assert.equal(
|
||||
resolveModelLabel("databricks-team-2025-01", null),
|
||||
"databricks-team-2025-01",
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveModelLabel — empty id returns empty string", () => {
|
||||
assert.equal(resolveModelLabel(""), "");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// formatAgentModelLabel — null/empty → "Auto", non-empty → resolveModelLabel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("formatAgentModelLabel — null or empty returns Auto", () => {
|
||||
assert.equal(formatAgentModelLabel(null), "Auto");
|
||||
assert.equal(formatAgentModelLabel(""), "Auto");
|
||||
assert.equal(formatAgentModelLabel(" "), "Auto");
|
||||
});
|
||||
|
||||
test("formatAgentModelLabel — known Databricks ID returns curated name", () => {
|
||||
assert.equal(formatAgentModelLabel("databricks-gpt-5-5"), "GPT-5.5");
|
||||
});
|
||||
|
||||
test("formatAgentModelLabel — unknown custom Databricks ID returns raw ID unchanged", () => {
|
||||
assert.equal(
|
||||
formatAgentModelLabel("databricks-team-2025-01"),
|
||||
"databricks-team-2025-01",
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DATABRICKS_MODEL_NAMES Map — structural invariants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("DATABRICKS_MODEL_NAMES — registry is non-empty and all entries are valid", () => {
|
||||
assert.ok(DATABRICKS_MODEL_NAMES.size > 0, "registry must not be empty");
|
||||
for (const [id, name] of DATABRICKS_MODEL_NAMES.entries()) {
|
||||
assert.ok(
|
||||
id.startsWith("databricks-"),
|
||||
`ID ${id} must start with 'databricks-'`,
|
||||
@@ -48,3 +128,33 @@ test("DATABRICKS_MODEL_NAMES — registry is non-empty and all values are non-em
|
||||
assert.notEqual(name, id, `curated name for ${id} must differ from raw ID`);
|
||||
}
|
||||
});
|
||||
|
||||
test("DATABRICKS_MODEL_NAMES — is a Map (not a plain object — prototype-key safety)", () => {
|
||||
assert.ok(
|
||||
DATABRICKS_MODEL_NAMES instanceof Map,
|
||||
"must be a Map, not a plain object",
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rust/TS parity: spot-check representative entries from the generated Rust slice
|
||||
// The generator emits both files from the same source, so any key present in Rust
|
||||
// must also be present in the TS Map with the same value.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("DATABRICKS_MODEL_NAMES — parity spot-check: representative entries match Rust slice values", () => {
|
||||
const expected = [
|
||||
["databricks-gpt-5-5", "GPT-5.5"],
|
||||
["databricks-claude-opus-4-7", "Claude Opus 4.7"],
|
||||
["databricks-gpt-oss-120b", "GPT OSS 120B"],
|
||||
["databricks-claude-sonnet-4-5", "Claude Sonnet 4.5 (latest)"],
|
||||
["databricks-gemini-2-5-flash", "Gemini 2.5 Flash"],
|
||||
];
|
||||
for (const [id, name] of expected) {
|
||||
assert.equal(
|
||||
DATABRICKS_MODEL_NAMES.get(id),
|
||||
name,
|
||||
`TS registry entry for '${id}' must match Rust registry`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED by scripts/generate-databricks-model-names.py
|
||||
// Source: https://models.dev/api.json -- providers.databricks.models
|
||||
// Refresh: python3 scripts/generate-databricks-model-names.py (then port to TS)
|
||||
// Refresh: python3 scripts/generate-databricks-model-names.py
|
||||
//
|
||||
// Do not hand-edit -- rerun the script to update.
|
||||
|
||||
@@ -10,45 +10,38 @@
|
||||
* Keys are endpoint IDs returned verbatim by the discovery APIs.
|
||||
* Values are human-readable display names sourced from models.dev.
|
||||
*
|
||||
* Unknown endpoint IDs are displayed as their raw ID -- no guessing.
|
||||
* Unknown endpoint IDs are resolved by resolveModelLabel() as raw IDs.
|
||||
* Use a Map to avoid Object.prototype key collisions.
|
||||
*/
|
||||
export const DATABRICKS_MODEL_NAMES: Record<string, string> = {
|
||||
"databricks-claude-haiku-4-5": "Claude Haiku 4.5 (latest)",
|
||||
"databricks-claude-opus-4-1": "Claude Opus 4.1 (latest)",
|
||||
"databricks-claude-opus-4-5": "Claude Opus 4.5 (latest)",
|
||||
"databricks-claude-opus-4-6": "Claude Opus 4.6",
|
||||
"databricks-claude-opus-4-7": "Claude Opus 4.7",
|
||||
"databricks-claude-sonnet-4": "Claude Sonnet 4.5",
|
||||
"databricks-claude-sonnet-4-5": "Claude Sonnet 4.5 (latest)",
|
||||
"databricks-claude-sonnet-4-6": "Claude Sonnet 4.6",
|
||||
"databricks-gemini-2-5-flash": "Gemini 2.5 Flash",
|
||||
"databricks-gemini-2-5-pro": "Gemini 2.5 Pro",
|
||||
"databricks-gemini-3-1-flash-lite": "Gemini 3.1 Flash Lite Preview",
|
||||
"databricks-gemini-3-1-pro": "Gemini 3.1 Pro Preview Custom Tools",
|
||||
"databricks-gemini-3-flash": "Gemini 3 Flash Preview",
|
||||
"databricks-gemini-3-pro": "Gemini 3 Pro Preview",
|
||||
"databricks-glm-5-2": "GLM-5.2",
|
||||
"databricks-gpt-5": "GPT-5",
|
||||
"databricks-gpt-5-1": "GPT-5.1",
|
||||
"databricks-gpt-5-2": "GPT-5.2",
|
||||
"databricks-gpt-5-4": "GPT-5.4",
|
||||
"databricks-gpt-5-4-mini": "GPT-5.4 mini",
|
||||
"databricks-gpt-5-4-nano": "GPT-5.4 nano",
|
||||
"databricks-gpt-5-5": "GPT-5.5",
|
||||
"databricks-gpt-5-6-luna": "GPT-5.6 Luna",
|
||||
"databricks-gpt-5-6-sol": "GPT-5.6 Sol",
|
||||
"databricks-gpt-5-6-terra": "GPT-5.6 Terra",
|
||||
"databricks-gpt-5-mini": "GPT-5 Mini",
|
||||
"databricks-gpt-5-nano": "GPT-5 Nano",
|
||||
"databricks-gpt-oss-120b": "GPT OSS 120B",
|
||||
"databricks-gpt-oss-20b": "GPT OSS 20B",
|
||||
"databricks-kimi-k2-7-code": "Kimi K2.7 Code",
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the curated display name for a Databricks endpoint ID, or the raw
|
||||
* ID when no entry exists in the registry. No heuristic guessing.
|
||||
*/
|
||||
export function databricksModelName(id: string): string {
|
||||
return DATABRICKS_MODEL_NAMES[id] ?? id;
|
||||
}
|
||||
export const DATABRICKS_MODEL_NAMES: Map<string, string> = new Map([
|
||||
["databricks-claude-haiku-4-5", "Claude Haiku 4.5 (latest)"],
|
||||
["databricks-claude-opus-4-1", "Claude Opus 4.1 (latest)"],
|
||||
["databricks-claude-opus-4-5", "Claude Opus 4.5 (latest)"],
|
||||
["databricks-claude-opus-4-6", "Claude Opus 4.6"],
|
||||
["databricks-claude-opus-4-7", "Claude Opus 4.7"],
|
||||
["databricks-claude-sonnet-4", "Claude Sonnet 4.5"],
|
||||
["databricks-claude-sonnet-4-5", "Claude Sonnet 4.5 (latest)"],
|
||||
["databricks-claude-sonnet-4-6", "Claude Sonnet 4.6"],
|
||||
["databricks-gemini-2-5-flash", "Gemini 2.5 Flash"],
|
||||
["databricks-gemini-2-5-pro", "Gemini 2.5 Pro"],
|
||||
["databricks-gemini-3-1-flash-lite", "Gemini 3.1 Flash Lite Preview"],
|
||||
["databricks-gemini-3-1-pro", "Gemini 3.1 Pro Preview Custom Tools"],
|
||||
["databricks-gemini-3-flash", "Gemini 3 Flash Preview"],
|
||||
["databricks-gemini-3-pro", "Gemini 3 Pro Preview"],
|
||||
["databricks-glm-5-2", "GLM-5.2"],
|
||||
["databricks-gpt-5", "GPT-5"],
|
||||
["databricks-gpt-5-1", "GPT-5.1"],
|
||||
["databricks-gpt-5-2", "GPT-5.2"],
|
||||
["databricks-gpt-5-4", "GPT-5.4"],
|
||||
["databricks-gpt-5-4-mini", "GPT-5.4 mini"],
|
||||
["databricks-gpt-5-4-nano", "GPT-5.4 nano"],
|
||||
["databricks-gpt-5-5", "GPT-5.5"],
|
||||
["databricks-gpt-5-6-luna", "GPT-5.6 Luna"],
|
||||
["databricks-gpt-5-6-sol", "GPT-5.6 Sol"],
|
||||
["databricks-gpt-5-6-terra", "GPT-5.6 Terra"],
|
||||
["databricks-gpt-5-mini", "GPT-5 Mini"],
|
||||
["databricks-gpt-5-nano", "GPT-5 Nano"],
|
||||
["databricks-gpt-oss-120b", "GPT OSS 120B"],
|
||||
["databricks-gpt-oss-20b", "GPT OSS 20B"],
|
||||
["databricks-kimi-k2-7-code", "Kimi K2.7 Code"],
|
||||
]);
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
import { databricksModelName } from "./databricksModelNames";
|
||||
import { DATABRICKS_MODEL_NAMES } from "./databricksModelNames";
|
||||
|
||||
/**
|
||||
* Returns a human-readable model label for an agent or persona, falling back to
|
||||
* "Auto" when no model is set (empty or whitespace-only).
|
||||
* Resolves a human-readable label for a model, following the three-tier
|
||||
* precedence documented in AGENTS.md:
|
||||
*
|
||||
* 1. Nonblank discovered/API name (e.g. from AgentModelInfo.name)
|
||||
* 2. Registry lookup by ID (models.dev-seeded Databricks table)
|
||||
* 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".
|
||||
*/
|
||||
export function resolveModelLabel(
|
||||
id: string,
|
||||
discoveredName?: string | null | undefined,
|
||||
): string {
|
||||
const trimmedName = discoveredName?.trim();
|
||||
if (trimmedName) return trimmedName;
|
||||
const trimmedId = id.trim();
|
||||
if (!trimmedId) return "";
|
||||
return DATABRICKS_MODEL_NAMES.get(trimmedId) ?? trimmedId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable model label for an agent or persona, falling back
|
||||
* to "Auto" when no model is set (empty or whitespace-only).
|
||||
*
|
||||
* For known Databricks managed endpoints the registry-curated name is returned
|
||||
* (e.g. "databricks-gpt-5-5" → "GPT-5.5"). Unknown or custom endpoint IDs are
|
||||
@@ -11,5 +33,5 @@ import { databricksModelName } from "./databricksModelNames";
|
||||
export function formatAgentModelLabel(model: string | null | undefined) {
|
||||
const trimmed = model?.trim();
|
||||
if (!trimmed) return "Auto";
|
||||
return databricksModelName(trimmed);
|
||||
return resolveModelLabel(trimmed);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
} from "@/features/agents/ui/agentConfigControls";
|
||||
import { PersonaProviderApiKeyField } from "@/features/agents/ui/PersonaProviderApiKeyField";
|
||||
import { usePersonaModelDiscovery } from "@/features/agents/ui/usePersonaModelDiscovery";
|
||||
import { resolveModelLabel } from "@/features/agents/lib/formatAgentModelLabel";
|
||||
import {
|
||||
BUZZ_AGENT_THINKING_EFFORT,
|
||||
getProviderEffortConfig,
|
||||
@@ -776,7 +777,9 @@ export function AgentConfigFields({
|
||||
<AgentModelField
|
||||
allowDefaultModel={fallbackModel !== null}
|
||||
defaultModelLabel={
|
||||
fallbackModel ? `Default model (${fallbackModel})` : undefined
|
||||
fallbackModel
|
||||
? `Default model (${resolveModelLabel(fallbackModel)})`
|
||||
: undefined
|
||||
}
|
||||
disableSelectDuringDiscovery={disableModelSelectDuringDiscovery}
|
||||
disabled={dependentFieldsDisabled}
|
||||
|
||||
@@ -27,7 +27,7 @@ import { friendlyAgentLastError } from "@/features/agents/lib/friendlyAgentLastE
|
||||
import { ManagedAgentLogPanel } from "./ManagedAgentLogPanel";
|
||||
import { PubKey } from "@/shared/ui/PubKey";
|
||||
import { SubsectionLabel } from "@/shared/ui/PageHeader";
|
||||
import { databricksModelName } from "@/features/agents/lib/databricksModelNames";
|
||||
import { resolveModelLabel } from "@/features/agents/lib/formatAgentModelLabel";
|
||||
|
||||
export function ManagedAgentRow({
|
||||
agent,
|
||||
@@ -411,7 +411,7 @@ function RuntimeBlock({
|
||||
{runtimeSource || agent.model ? (
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
||||
{runtimeSource ? <span>{runtimeSource}</span> : null}
|
||||
{agent.model ? <span>{databricksModelName(agent.model)}</span> : null}
|
||||
{agent.model ? <span>{resolveModelLabel(agent.model)}</span> : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/ui/dropdown-menu";
|
||||
import { resolveModelLabel } from "@/features/agents/lib/formatAgentModelLabel";
|
||||
|
||||
export function ModelPicker({
|
||||
agent,
|
||||
@@ -82,13 +83,13 @@ export function ModelPicker({
|
||||
);
|
||||
|
||||
const currentValue = agent.model ?? modelsData?.agentDefaultModel ?? "";
|
||||
const displayLabel =
|
||||
agent.model ??
|
||||
(modelsData?.agentDefaultModel
|
||||
? `${modelsData.agentDefaultModel} (default)`
|
||||
const displayLabel = agent.model
|
||||
? resolveModelLabel(agent.model)
|
||||
: modelsData?.agentDefaultModel
|
||||
? `${resolveModelLabel(modelsData.agentDefaultModel)} (default)`
|
||||
: hasRequestedModels && loading
|
||||
? "Loading..."
|
||||
: "Auto");
|
||||
: "Auto";
|
||||
|
||||
// Provenance label shown only for post-spawn agents where the model origin
|
||||
// is known from the config surface and the source is not a user-explicit
|
||||
@@ -221,7 +222,9 @@ export function ModelPicker({
|
||||
<div className="px-3 py-2 text-sm text-muted-foreground">
|
||||
{agent.model ? (
|
||||
<>
|
||||
<p className="font-medium text-foreground">{agent.model}</p>
|
||||
<p className="font-medium text-foreground">
|
||||
{resolveModelLabel(agent.model)}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs">
|
||||
This runtime does not support switching models.
|
||||
</p>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "./personaModelDiscoveryStatus";
|
||||
import type { PersonaModelOption } from "./agentConfigOptions";
|
||||
import { providerRequiresExplicitModel } from "./agentConfigOptions";
|
||||
import { resolveModelLabel } from "@/features/agents/lib/formatAgentModelLabel";
|
||||
|
||||
export const MODEL_DISCOVERY_LOADING_VALUE = "__model_discovery_loading__";
|
||||
|
||||
@@ -64,7 +65,7 @@ export function getDiscoveredPersonaModelOptions(
|
||||
provider === "relay-mesh"
|
||||
? "Default (auto)"
|
||||
: agentDefaultModel
|
||||
? `Default model (${agentDefaultModel})`
|
||||
? `Default model (${resolveModelLabel(agentDefaultModel)})`
|
||||
: "Default model",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -51,7 +51,7 @@ import { BotIdenticon } from "@/features/messages/ui/BotIdenticon";
|
||||
import { useNow } from "@/shared/lib/useNow";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Spinner } from "@/shared/ui/spinner";
|
||||
import { databricksModelName } from "@/features/agents/lib/databricksModelNames";
|
||||
import { resolveModelLabel } from "@/features/agents/lib/formatAgentModelLabel";
|
||||
|
||||
type UserProfilePopoverProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -612,7 +612,7 @@ export function UserProfilePopover({
|
||||
<InfoBadge>{runtimeLabel(relayAgent.agentType)}</InfoBadge>
|
||||
) : null}
|
||||
{managedAgent?.model ? (
|
||||
<InfoBadge>{databricksModelName(managedAgent.model)}</InfoBadge>
|
||||
<InfoBadge>{resolveModelLabel(managedAgent.model)}</InfoBadge>
|
||||
) : null}
|
||||
{managedAgent?.acpCommand ? (
|
||||
<InfoBadge>ACP: {managedAgent.acpCommand}</InfoBadge>
|
||||
|
||||
@@ -1,55 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate crates/buzz-agent/src/databricks_model_names.rs from models.dev.
|
||||
"""Generate Databricks model-name registries from models.dev.
|
||||
|
||||
Usage:
|
||||
python3 scripts/generate-databricks-model-names.py \
|
||||
> crates/buzz-agent/src/databricks_model_names.rs
|
||||
Emits two generated files in one invocation:
|
||||
|
||||
Fetches https://models.dev/api.json, extracts providers.databricks.models (the
|
||||
authoritative curated name registry used by goose and others), and emits a
|
||||
static Rust slice of (id, display_name) pairs sorted by ID.
|
||||
crates/buzz-agent/src/databricks_model_names.rs (Rust)
|
||||
desktop/src/features/agents/lib/databricksModelNames.ts (TypeScript)
|
||||
|
||||
Usage (from repo root):
|
||||
python3 scripts/generate-databricks-model-names.py
|
||||
|
||||
Fetches https://models.dev/api.json, extracts providers.databricks.models,
|
||||
and emits sorted (id, display_name) tables formatted for each language.
|
||||
Re-run whenever Databricks ships a new managed endpoint and commit the diff.
|
||||
|
||||
Both files are kept in sync by this script — never edit them by hand.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
URL = "https://models.dev/api.json"
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
RUST_OUT = REPO_ROOT / "crates/buzz-agent/src/databricks_model_names.rs"
|
||||
TS_OUT = REPO_ROOT / "desktop/src/features/agents/lib/databricksModelNames.ts"
|
||||
|
||||
# Allowed characters in endpoint IDs and curated names.
|
||||
SAFE_ID_RE = re.compile(r"^[a-z0-9][a-z0-9.\-]*$")
|
||||
SAFE_NAME_RE = re.compile(r"^[^\x00-\x1f\"\\<>&]*$")
|
||||
|
||||
|
||||
def fetch(url: str) -> bytes:
|
||||
# urllib blocks with HTTP 403 without a browser User-Agent; use curl when
|
||||
# available so the script works in hermit environments where curl is pinned.
|
||||
"""Fetch URL via curl; raise on HTTP error."""
|
||||
result = subprocess.run(
|
||||
["curl", "-s", "-A", "Mozilla/5.0", url],
|
||||
["curl", "--fail", "--silent", "--max-time", "30", "-A", "Mozilla/5.0", url],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"curl failed (exit {result.returncode}): {result.stderr.decode()}"
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
|
||||
def validate_entries(
|
||||
entries: list[tuple[str, str]],
|
||||
) -> list[tuple[str, str]]:
|
||||
"""Validate all (id, name) pairs and raise on unexpected shapes."""
|
||||
for id_, name in entries:
|
||||
if not isinstance(id_, str) or not isinstance(name, str):
|
||||
raise ValueError(f"Non-string entry: {id_!r} -> {name!r}")
|
||||
if not SAFE_ID_RE.match(id_):
|
||||
raise ValueError(f"Unsafe endpoint ID: {id_!r}")
|
||||
if not SAFE_NAME_RE.match(name):
|
||||
raise ValueError(f"Unsafe display name for {id_!r}: {name!r}")
|
||||
return entries
|
||||
|
||||
|
||||
def rust_str(s: str) -> str:
|
||||
"""Emit a Rust string literal with double quotes."""
|
||||
escaped = s.replace("\\", "\\\\").replace('"', '\\"')
|
||||
return f'"{escaped}"'
|
||||
"""Emit a double-quoted Rust string literal (backslash + quote only)."""
|
||||
return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
||||
|
||||
|
||||
def main() -> None:
|
||||
raw = fetch(URL)
|
||||
data = json.loads(raw)
|
||||
models: dict = data["databricks"]["models"]
|
||||
entries = sorted(
|
||||
(k, v["name"] if isinstance(v, dict) else k) for k, v in models.items()
|
||||
)
|
||||
def ts_str(s: str) -> str:
|
||||
"""Emit a double-quoted TypeScript string literal."""
|
||||
return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
||||
|
||||
|
||||
def write_rust(entries: list[tuple[str, str]]) -> None:
|
||||
"""Write the Rust generated file, then rustfmt it for byte-for-byte stability."""
|
||||
lines = [
|
||||
"// GENERATED by scripts/generate-databricks-model-names.py",
|
||||
"// Source: https://models.dev/api.json -- providers.databricks.models",
|
||||
"// Refresh: python3 scripts/generate-databricks-model-names.py \\",
|
||||
"// > crates/buzz-agent/src/databricks_model_names.rs",
|
||||
"// Refresh: python3 scripts/generate-databricks-model-names.py",
|
||||
"//",
|
||||
"// Do not hand-edit -- rerun the script to update.",
|
||||
"",
|
||||
@@ -64,9 +89,87 @@ def main() -> None:
|
||||
for id_, name in entries:
|
||||
lines.append(f" ({rust_str(id_)}, {rust_str(name)}),")
|
||||
lines += ["];", ""]
|
||||
RUST_OUT.write_text("\n".join(lines))
|
||||
# Run rustfmt so the committed file is always formatter-clean and
|
||||
# a subsequent generator run reproduces it byte-for-byte.
|
||||
result = subprocess.run(
|
||||
["cargo", "fmt", "--", str(RUST_OUT)],
|
||||
cwd=REPO_ROOT,
|
||||
capture_output=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"rustfmt failed: {result.stderr.decode()}"
|
||||
)
|
||||
print(f"Wrote {RUST_OUT.relative_to(REPO_ROOT)}")
|
||||
|
||||
sys.stdout.write("\n".join(lines))
|
||||
|
||||
def write_ts(entries: list[tuple[str, str]]) -> None:
|
||||
"""Write the TypeScript generated file, then biome-format it."""
|
||||
lines = [
|
||||
"// GENERATED by scripts/generate-databricks-model-names.py",
|
||||
"// Source: https://models.dev/api.json -- providers.databricks.models",
|
||||
"// Refresh: python3 scripts/generate-databricks-model-names.py",
|
||||
"//",
|
||||
"// Do not hand-edit -- rerun the script to update.",
|
||||
"",
|
||||
"/**",
|
||||
" * Curated display names for known Databricks AI Gateway endpoints.",
|
||||
" *",
|
||||
" * Keys are endpoint IDs returned verbatim by the discovery APIs.",
|
||||
" * Values are human-readable display names sourced from models.dev.",
|
||||
" *",
|
||||
" * Unknown endpoint IDs are resolved by resolveModelLabel() as raw IDs.",
|
||||
" * Use a Map to avoid Object.prototype key collisions.",
|
||||
" */",
|
||||
"export const DATABRICKS_MODEL_NAMES: Map<string, string> = new Map([",
|
||||
]
|
||||
for id_, name in entries:
|
||||
lines.append(f" [{ts_str(id_)}, {ts_str(name)}],")
|
||||
lines += ["]);\n"]
|
||||
TS_OUT.write_text("\n".join(lines))
|
||||
# biome format for byte-for-byte stability on regenerate.
|
||||
desktop_dir = REPO_ROOT / "desktop"
|
||||
biome_bin = desktop_dir / "node_modules/.bin/biome"
|
||||
if biome_bin.exists():
|
||||
result = subprocess.run(
|
||||
[str(biome_bin), "format", "--write", str(TS_OUT)],
|
||||
cwd=desktop_dir,
|
||||
capture_output=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"biome format failed: {result.stderr.decode()}"
|
||||
)
|
||||
print(f"Wrote {TS_OUT.relative_to(REPO_ROOT)}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
raw = fetch(URL)
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError as e:
|
||||
raise RuntimeError(f"models.dev response is not valid JSON: {e}") from e
|
||||
|
||||
if "databricks" not in data or "models" not in data["databricks"]:
|
||||
raise RuntimeError(
|
||||
"Unexpected models.dev shape: missing data['databricks']['models']"
|
||||
)
|
||||
|
||||
models: dict = data["databricks"]["models"]
|
||||
raw_entries = sorted(
|
||||
(k, v["name"] if isinstance(v, dict) else k) for k, v in models.items()
|
||||
)
|
||||
entries = validate_entries(raw_entries)
|
||||
|
||||
write_rust(entries)
|
||||
write_ts(entries)
|
||||
print(f"Done — {len(entries)} Databricks endpoints.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
try:
|
||||
main()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
Reference in New Issue
Block a user