refactor(desktop): make agent provider/model selection provider-agnostic

OSS Buzz must stay provider/model-agnostic; Block-internal defaults belong
in buzz-releases via env vars, not baked into the OSS tree. This removes the
implicit Databricks fallback and the hardcoded Block-specific contamination
introduced by #1201, across three layers:

- buzz-agent config: resolve_provider/resolve_model no longer auto-infer
  Databricks. BUZZ_AGENT_PROVIDER is required; BUZZ_AGENT_MODEL wins over
  provider-specific env vars.
- desktop build/runtime: add a generic BUZZ_BUILD_AGENT_ENV passthrough
  (newline-delimited KEY=VALUE). build.rs validates and emits
  BUZZ_DESKTOP_BUILD_AGENT_ENV; runtime.rs parses it via parse_agent_env_lines()
  and injects each pair onto spawned agents (user env still wins). This lets
  internal builds bake DATABRICKS_HOST without naming Databricks in OSS; OSS
  builds leave it unset and inject nothing.
- persona dialog UI: delete the hardcoded Block model registry (goose-claude-*,
  databricks-gpt-*, etc.) sourced from squareup/goose-releases. Models now come
  exclusively from live discovery (usePersonaModelDiscovery -> discoverAgentModels);
  the only static fallback is the generic 'Default model' + freeform custom input.
  Drops the Databricks-default behavior. databricks_v2 remains a selectable
  provider option (parallel to the Rust enum), not a default.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw
2026-06-29 19:12:01 -04:00
parent 15a73aa27f
commit 428187009d
6 changed files with 224 additions and 290 deletions

View File

@@ -104,13 +104,12 @@ impl Config {
env("BUZZ_AGENT_PROVIDER").as_deref(),
env("ANTHROPIC_API_KEY").as_deref(),
env("OPENAI_COMPAT_API_KEY").as_deref(),
databricks_host.as_deref(),
databricks_model.as_deref(),
)?;
// Universal model override — any provider will use this when its own
// model env var is absent. Useful for wrapper scripts that set a single
// var regardless of which provider is active.
// Universal model override — takes priority over provider-specific model
// env vars (ANTHROPIC_MODEL, OPENAI_COMPAT_MODEL, DATABRICKS_MODEL) when
// present. Set by the desktop from the persona/record to express explicit
// user intent; provider-specific vars serve as defaults for CLI/standalone use.
let buzz_agent_model = env("BUZZ_AGENT_MODEL");
// OPENAI_COMPAT_API is only read when provider=openai, so a stray
@@ -123,8 +122,8 @@ impl Config {
Provider::Anthropic => (
req("ANTHROPIC_API_KEY")?,
resolve_model(
env("ANTHROPIC_MODEL").as_deref(),
buzz_agent_model.as_deref(),
env("ANTHROPIC_MODEL").as_deref(),
)
.ok_or_else(|| "config: ANTHROPIC_MODEL required".to_string())?,
env_or("ANTHROPIC_BASE_URL", "https://api.anthropic.com"),
@@ -133,8 +132,8 @@ impl Config {
Provider::OpenAi => (
req("OPENAI_COMPAT_API_KEY")?,
resolve_model(
env("OPENAI_COMPAT_MODEL").as_deref(),
buzz_agent_model.as_deref(),
env("OPENAI_COMPAT_MODEL").as_deref(),
)
.ok_or_else(|| "config: OPENAI_COMPAT_MODEL required".to_string())?,
env_or("OPENAI_COMPAT_BASE_URL", "https://api.openai.com/v1"),
@@ -142,7 +141,7 @@ impl Config {
),
Provider::Databricks | Provider::DatabricksV2 => (
env("DATABRICKS_TOKEN").unwrap_or_default(),
resolve_model(databricks_model.as_deref(), buzz_agent_model.as_deref())
resolve_model(buzz_agent_model.as_deref(), databricks_model.as_deref())
.ok_or_else(|| "config: DATABRICKS_MODEL required".to_string())?,
databricks_host.ok_or_else(|| "config: DATABRICKS_HOST required".to_string())?,
OpenAiApi::Chat, // only read by OpenAI/legacy Databricks dispatch
@@ -271,54 +270,38 @@ fn req(k: &str) -> Result<String, String> {
env(k).ok_or_else(|| format!("config: {k} required"))
}
/// Returns the first of `provider_model` or `universal_fallback` that is
/// `Some`, converting to an owned `String`. Returns `None` when both are
/// absent so the caller can supply a provider-specific error message.
fn resolve_model(provider_model: Option<&str>, universal_fallback: Option<&str>) -> Option<String> {
provider_model.or(universal_fallback).map(str::to_owned)
/// Returns the first present value. `explicit_override` (BUZZ_AGENT_MODEL,
/// set by the desktop from the persona/record) wins over `provider_default`
/// (provider-specific env var that may be inherited from the shell).
/// Returns `None` when both are absent so the caller can supply a
/// provider-specific error message.
fn resolve_model(
explicit_override: Option<&str>,
provider_default: Option<&str>,
) -> Option<String> {
explicit_override.or(provider_default).map(str::to_owned)
}
fn present_nonempty(v: Option<&str>) -> bool {
v.map(str::trim).is_some_and(|s| !s.is_empty())
}
fn databricks_available(host: Option<&str>, model: Option<&str>) -> bool {
present_nonempty(host) && present_nonempty(model)
}
fn resolve_provider(
requested: Option<&str>,
anthropic_key: Option<&str>,
openai_key: Option<&str>,
databricks_host: Option<&str>,
databricks_model: Option<&str>,
) -> Result<Provider, String> {
let databricks_ready = databricks_available(databricks_host, databricks_model);
match requested.map(str::trim).filter(|s| !s.is_empty()) {
Some(raw) => {
let normalized = raw.to_ascii_lowercase();
match normalized.as_str() {
"anthropic" if present_nonempty(anthropic_key) => Ok(Provider::Anthropic),
"anthropic" if databricks_ready => {
tracing::warn!(
requested = raw,
"API key missing for requested provider; falling back to Databricks OAuth"
);
Ok(Provider::Databricks)
}
"anthropic" => Err(
"config: ANTHROPIC_API_KEY required (or set DATABRICKS_HOST and DATABRICKS_MODEL for Databricks OAuth fallback)".into(),
"config: ANTHROPIC_API_KEY required".into(),
),
"openai" | "openai-compat" if present_nonempty(openai_key) => Ok(Provider::OpenAi),
"openai" | "openai-compat" if databricks_ready => {
tracing::warn!(
requested = raw,
"API key missing for requested provider; falling back to Databricks OAuth"
);
Ok(Provider::Databricks)
}
"openai" | "openai-compat" => Err(
"config: OPENAI_COMPAT_API_KEY required (or set DATABRICKS_HOST and DATABRICKS_MODEL for Databricks OAuth fallback)".into(),
"config: OPENAI_COMPAT_API_KEY required".into(),
),
"databricks" => Ok(Provider::Databricks),
"databricks_v2" | "databricks-v2" => Ok(Provider::DatabricksV2),
@@ -327,9 +310,8 @@ fn resolve_provider(
)),
}
}
None if databricks_ready => Ok(Provider::Databricks),
None => Err(
"config: BUZZ_AGENT_PROVIDER required (or set DATABRICKS_HOST and DATABRICKS_MODEL for Databricks OAuth fallback)".into(),
"config: BUZZ_AGENT_PROVIDER is required — set it to your provider (e.g. anthropic, openai, databricks)".into(),
),
}
}
@@ -540,89 +522,51 @@ mod tests {
#[test]
fn resolve_provider_keeps_requested_provider_when_token_present() {
assert_eq!(
resolve_provider(
Some("anthropic"),
Some("sk-ant"),
None,
Some("https://dbc.example"),
Some("db-model")
)
.unwrap(),
resolve_provider(Some("anthropic"), Some("sk-ant"), None,).unwrap(),
Provider::Anthropic
);
assert_eq!(
resolve_provider(
Some("openai"),
None,
Some("sk-openai"),
Some("https://dbc.example"),
Some("db-model")
)
.unwrap(),
resolve_provider(Some("openai"), None, Some("sk-openai"),).unwrap(),
Provider::OpenAi
);
}
#[test]
fn resolve_provider_falls_back_to_databricks_when_requested_token_missing() {
assert_eq!(
resolve_provider(
Some("anthropic"),
None,
None,
Some("https://dbc.example"),
Some("goose-claude-4-6-sonnet")
)
.unwrap(),
Provider::Databricks
);
assert_eq!(
resolve_provider(
Some("openai-compat"),
None,
Some(" "),
Some("https://dbc.example"),
Some("goose-claude-4-6-sonnet")
)
.unwrap(),
Provider::Databricks
);
fn resolve_provider_errors_when_requested_provider_key_missing() {
// No fallback — missing key returns an error regardless of Databricks availability.
let err = resolve_provider(Some("anthropic"), None, None).unwrap_err();
assert!(err.contains("ANTHROPIC_API_KEY required"), "{err}");
let err = resolve_provider(Some("openai-compat"), None, Some(" ")).unwrap_err();
assert!(err.contains("OPENAI_COMPAT_API_KEY required"), "{err}");
}
#[test]
fn resolve_provider_can_auto_select_databricks_without_explicit_provider() {
assert_eq!(
resolve_provider(
None,
None,
None,
Some("https://dbc.example"),
Some("goose-claude-4-6-sonnet")
)
.unwrap(),
Provider::Databricks
);
fn resolve_provider_errors_when_provider_env_absent() {
// No implicit inference — absent BUZZ_AGENT_PROVIDER is an error.
let err = resolve_provider(None, None, None).unwrap_err();
assert!(err.contains("BUZZ_AGENT_PROVIDER is required"), "{err}");
}
#[test]
fn resolve_provider_requires_databricks_host_and_model_for_fallback() {
let err = resolve_provider(
Some("openai"),
None,
None,
Some("https://dbc.example"),
None,
)
.unwrap_err();
assert!(err.contains("OPENAI_COMPAT_API_KEY required"));
let err =
resolve_provider(None, None, None, Some("https://dbc.example"), None).unwrap_err();
assert!(err.contains("BUZZ_AGENT_PROVIDER required"));
// Renamed: verify the explicit databricks provider path works correctly.
// When BUZZ_AGENT_PROVIDER=databricks, resolve_provider succeeds regardless
// of DATABRICKS_HOST/MODEL (those are validated later in from_env()).
assert_eq!(
resolve_provider(Some("databricks"), None, None).unwrap(),
Provider::Databricks
);
// Missing key for other providers still errors — no Databricks fallback.
let err = resolve_provider(Some("openai"), None, None).unwrap_err();
assert!(err.contains("OPENAI_COMPAT_API_KEY required"), "{err}");
let err = resolve_provider(None, None, None).unwrap_err();
assert!(err.contains("BUZZ_AGENT_PROVIDER is required"), "{err}");
}
#[test]
fn resolve_provider_unsupported_error_preserves_user_casing() {
let err = resolve_provider(Some("OpenAIish"), None, None, None, None).unwrap_err();
let err = resolve_provider(Some("OpenAIish"), None, None).unwrap_err();
assert!(err.contains("BUZZ_AGENT_PROVIDER=OpenAIish"));
}
@@ -645,15 +589,15 @@ mod tests {
}
#[test]
fn resolve_model_prefers_provider_specific() {
let result = resolve_model(Some("anthropic-model"), Some("universal-model"));
assert_eq!(result.as_deref(), Some("anthropic-model"));
fn resolve_model_prefers_explicit_override() {
let result = resolve_model(Some("override-model"), Some("provider-model"));
assert_eq!(result.as_deref(), Some("override-model"));
}
#[test]
fn resolve_model_falls_back_to_universal() {
let result = resolve_model(None, Some("universal-model"));
assert_eq!(result.as_deref(), Some("universal-model"));
fn resolve_model_falls_back_to_provider_default() {
let result = resolve_model(None, Some("provider-model"));
assert_eq!(result.as_deref(), Some("provider-model"));
}
#[test]

View File

@@ -7,8 +7,9 @@ fn main() {
println!("cargo:rerun-if-env-changed=BUZZ_RELAY_HTTP");
println!("cargo:rerun-if-env-changed=BUZZ_UPDATER_PUBLIC_KEY");
println!("cargo:rerun-if-env-changed=BUZZ_UPDATER_ENDPOINT");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_DATABRICKS_HOST");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_DATABRICKS_MODEL");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_BUZZ_AGENT_PROVIDER");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_BUZZ_AGENT_MODEL");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_AGENT_ENV");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_RELAY_RECONNECT_CMD");
println!("cargo:rustc-check-cfg=cfg(buzz_updater_enabled)");
@@ -20,12 +21,40 @@ fn main() {
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_RELAY_HTTP={relay_http}");
}
if let Ok(host) = std::env::var("BUZZ_BUILD_DATABRICKS_HOST") {
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_DATABRICKS_HOST={host}");
if let Ok(provider) = std::env::var("BUZZ_BUILD_BUZZ_AGENT_PROVIDER") {
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_BUZZ_AGENT_PROVIDER={provider}");
}
if let Ok(model) = std::env::var("BUZZ_BUILD_DATABRICKS_MODEL") {
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_DATABRICKS_MODEL={model}");
if let Ok(model) = std::env::var("BUZZ_BUILD_BUZZ_AGENT_MODEL") {
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_BUZZ_AGENT_MODEL={model}");
}
// Generic KEY=VALUE pairs to inject into every spawned agent process.
// Newline-delimited; each line must be non-empty and contain exactly one
// `=` separator with a non-empty key. OSS builds leave this unset.
if let Ok(raw) = std::env::var("BUZZ_BUILD_AGENT_ENV") {
for (line_no, line) in raw.lines().enumerate() {
let line = line.trim();
if line.is_empty() {
continue;
}
let eq = line.find('=').unwrap_or_else(|| {
panic!(
"BUZZ_BUILD_AGENT_ENV line {}: missing '=' separator in {:?}",
line_no + 1,
line
)
});
let key = &line[..eq];
if key.is_empty() {
panic!(
"BUZZ_BUILD_AGENT_ENV line {}: key must not be empty in {:?}",
line_no + 1,
line
);
}
}
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_AGENT_ENV={raw}");
}
if let Ok(val) = std::env::var("BUZZ_BUILD_RELAY_RECONNECT_CMD") {

View File

@@ -637,11 +637,9 @@ async fn run_agent_models_command(
}
}
}
// Mirror runtime spawn: internal builds may bake Databricks host/model
// Mirror runtime spawn: internal builds may bake provider/model
// defaults. User-provided env below still wins.
for (key, value) in crate::managed_agents::build_databricks_defaults() {
cmd.env(key, value);
}
crate::managed_agents::build_buzz_agent_provider_defaults(&mut cmd);
// User env layering — written LAST so it overrides any Buzz-set env above.
for (k, v) in &merged_env {
cmd.env(k, v);

View File

@@ -1870,12 +1870,10 @@ pub fn spawn_agent_child(
);
}
// Baked-in Databricks defaults for internal builds (buzz-releases sets
// BUZZ_BUILD_DATABRICKS_* at compile time; OSS builds bake nothing).
// Baked-in provider defaults for internal builds (buzz-releases sets
// BUZZ_BUILD_BUZZ_AGENT_* at compile time; OSS builds bake nothing).
// Written BEFORE user env_vars so a GUI/persona override still wins.
for (key, value) in build_databricks_defaults() {
command.env(key, value);
}
build_buzz_agent_provider_defaults(&mut command);
// ── User env vars: the record snapshot ─────────────────────────────
//
@@ -1953,21 +1951,50 @@ fn child_rust_log_filter() -> String {
}
}
/// Databricks host/model baked in at compile time for internal builds. Empty
/// in OSS builds, where the `BUZZ_BUILD_DATABRICKS_*` env is unset.
pub(crate) fn build_databricks_defaults() -> Vec<(&'static str, &'static str)> {
let mut defaults = Vec::new();
if let Some(host) = option_env!("BUZZ_DESKTOP_BUILD_DATABRICKS_HOST") {
if !host.is_empty() {
defaults.push(("DATABRICKS_HOST", host));
/// Provider/model baked in at compile time for internal builds. Empty
/// in OSS builds, where the `BUZZ_BUILD_BUZZ_AGENT_*` env is unset.
///
/// Also injects any arbitrary `KEY=VALUE` pairs baked via `BUZZ_BUILD_AGENT_ENV`
/// (newline-delimited). This is how provider-specific vars like `DATABRICKS_HOST`
/// reach the agent in internal builds without naming them in OSS code.
/// User-supplied env vars (written after this call) always win.
pub(crate) fn build_buzz_agent_provider_defaults(cmd: &mut std::process::Command) {
if let Some(provider) = option_env!("BUZZ_DESKTOP_BUILD_BUZZ_AGENT_PROVIDER") {
if !provider.is_empty() {
cmd.env("BUZZ_AGENT_PROVIDER", provider);
}
}
if let Some(model) = option_env!("BUZZ_DESKTOP_BUILD_DATABRICKS_MODEL") {
if let Some(model) = option_env!("BUZZ_DESKTOP_BUILD_BUZZ_AGENT_MODEL") {
if !model.is_empty() {
defaults.push(("DATABRICKS_MODEL", model));
cmd.env("BUZZ_AGENT_MODEL", model);
}
}
defaults
if let Some(raw) = option_env!("BUZZ_DESKTOP_BUILD_AGENT_ENV") {
for (key, value) in parse_agent_env_lines(raw) {
cmd.env(key, value);
}
}
}
/// Parse newline-delimited `KEY=VALUE` lines from a baked env blob.
/// Blank lines are skipped. Each non-blank line must contain `=`; the key
/// is everything before the first `=`, the value is everything after (values
/// may themselves contain `=`). Lines with an empty key are skipped.
pub(crate) fn parse_agent_env_lines(raw: &str) -> Vec<(&str, &str)> {
raw.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() {
return None;
}
let eq = line.find('=')?;
let key = &line[..eq];
if key.is_empty() {
return None;
}
Some((key, &line[eq + 1..]))
})
.collect()
}
pub fn start_managed_agent_process(

View File

@@ -87,10 +87,86 @@ fn buzz_agent_has_mcp_hooks() {
}
#[test]
fn databricks_defaults_empty_in_oss_build() {
// OSS (and normal test) builds set neither BUZZ_BUILD_DATABRICKS_*,
// so nothing is baked in and no DATABRICKS_* is injected on spawn.
assert!(super::build_databricks_defaults().is_empty());
fn buzz_agent_provider_defaults_empty_in_oss_build() {
// OSS (and normal test) builds set neither BUZZ_BUILD_BUZZ_AGENT_*,
// so nothing is baked in and no BUZZ_AGENT_* is injected on spawn.
let mut cmd = std::process::Command::new("env");
super::build_buzz_agent_provider_defaults(&mut cmd);
// Verify the function injects nothing when the compile-time vars are absent.
let output = cmd.output().expect("env should run");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("BUZZ_AGENT_PROVIDER="),
"BUZZ_AGENT_PROVIDER should not be injected in OSS builds"
);
assert!(
!stdout.contains("BUZZ_AGENT_MODEL="),
"BUZZ_AGENT_MODEL should not be injected in OSS builds"
);
// BUZZ_DESKTOP_BUILD_AGENT_ENV is also absent in OSS builds — no
// arbitrary keys should appear from the generic passthrough either.
assert!(
!stdout.contains("DATABRICKS_HOST="),
"DATABRICKS_HOST should not be injected in OSS builds"
);
}
// ── parse_agent_env_lines tests ──────────────────────────────────────────
use super::parse_agent_env_lines;
#[test]
fn parse_agent_env_lines_splits_on_first_equals() {
// Value may itself contain `=` — only the first `=` is the separator.
let pairs = parse_agent_env_lines("DATABRICKS_HOST=https://host.example.com/path?a=1");
assert_eq!(
pairs,
vec![("DATABRICKS_HOST", "https://host.example.com/path?a=1")]
);
}
#[test]
fn parse_agent_env_lines_multiple_pairs() {
let raw = "KEY_A=value_a\nKEY_B=value_b";
let pairs = parse_agent_env_lines(raw);
assert_eq!(pairs, vec![("KEY_A", "value_a"), ("KEY_B", "value_b")]);
}
#[test]
fn parse_agent_env_lines_skips_blank_lines() {
let raw = "KEY_A=val_a\n\n \nKEY_B=val_b";
let pairs = parse_agent_env_lines(raw);
assert_eq!(pairs, vec![("KEY_A", "val_a"), ("KEY_B", "val_b")]);
}
#[test]
fn parse_agent_env_lines_skips_line_without_equals() {
// A malformed line (no `=`) is silently skipped — build.rs validates at
// compile time; runtime parsing is defensive.
let raw = "NO_EQUALS_HERE\nGOOD=value";
let pairs = parse_agent_env_lines(raw);
assert_eq!(pairs, vec![("GOOD", "value")]);
}
#[test]
fn parse_agent_env_lines_skips_empty_key() {
// `=value` has an empty key — skip it.
let raw = "=orphan_value\nGOOD=value";
let pairs = parse_agent_env_lines(raw);
assert_eq!(pairs, vec![("GOOD", "value")]);
}
#[test]
fn parse_agent_env_lines_empty_value_is_allowed() {
// `KEY=` is valid — empty value is intentional (clears an env var).
let pairs = parse_agent_env_lines("EMPTY=");
assert_eq!(pairs, vec![("EMPTY", "")]);
}
#[test]
fn parse_agent_env_lines_empty_input_returns_empty() {
assert!(parse_agent_env_lines("").is_empty());
assert!(parse_agent_env_lines(" \n \n").is_empty());
}
#[test]

View File

@@ -16,6 +16,7 @@ export const NO_RUNTIME_DROPDOWN_VALUE = "__no_runtime__";
const KNOWN_LLM_PROVIDER_IDS = [
"anthropic",
"databricks",
"databricks_v2",
"openai",
"openai-compat",
] as const;
@@ -45,144 +46,20 @@ const DEFAULT_MODEL_OPTION: PersonaModelOption = {
label: "Default model",
};
const DATABRICKS_DEFAULT_MODEL_ID = "goose-claude-4-8-opus";
const DATABRICKS_DEFAULT_MODEL_LABEL = "Claude Opus 4.8";
const DATABRICKS_DEFAULT_MODEL_OPTION: PersonaModelOption = {
id: "",
label: `${DATABRICKS_DEFAULT_MODEL_LABEL} (default)`,
};
// Databricks IDs are sourced from squareup/goose-releases goose_models.json.
// `goose-claude-4-8-opus` is also the current Buzz internal build default in
// squareup/buzz-releases, though it is ahead of that registry today.
const BUZZ_AGENT_MODEL_OPTIONS: readonly PersonaModelOption[] = [
DATABRICKS_DEFAULT_MODEL_OPTION,
{
id: "goose-claude-4-8-opus",
label: "Claude Opus 4.8",
providers: ["databricks"],
},
{
id: "goose-claude-4-7-opus",
label: "Claude Opus 4.7",
providers: ["databricks"],
},
{
id: "goose-claude-4-6-opus",
label: "Claude Opus 4.6",
providers: ["anthropic", "databricks"],
},
{
id: "goose-claude-4-6-sonnet",
label: "Claude Sonnet 4.6",
providers: ["anthropic", "databricks"],
},
{
id: "goose-claude-4-5-opus",
label: "Claude Opus 4.5",
providers: ["databricks"],
},
{
id: "goose-claude-4-5-sonnet",
label: "Claude Sonnet 4.5",
providers: ["databricks"],
},
{
id: "goose-claude-4-5-haiku",
label: "Claude Haiku 4.5",
providers: ["databricks"],
},
{
id: "goose-gpt-5-2",
label: "GPT-5.2",
providers: ["databricks"],
},
{
id: "databricks-gpt-5-5-pro",
label: "GPT-5.5 Pro",
providers: ["databricks"],
},
{
id: "databricks-gpt-5-5",
label: "GPT-5.5",
providers: ["databricks"],
},
{
id: "gpt-5.5",
label: "GPT-5.5",
providers: ["openai", "openai-compat"],
},
{
id: "gpt-5.4",
label: "GPT-5.4",
providers: ["openai", "openai-compat"],
},
{
id: "gpt-5.4-mini",
label: "GPT-5.4 mini",
providers: ["openai", "openai-compat"],
},
{
id: "gpt-5.4-nano",
label: "GPT-5.4 nano",
providers: ["openai", "openai-compat"],
},
{
id: "gpt-5",
label: "GPT-5",
providers: ["openai", "openai-compat"],
},
{
id: "gpt-5-mini",
label: "GPT-5 mini",
providers: ["openai", "openai-compat"],
},
{
id: "goose-gemini-3-5-flash",
label: "Gemini 3.5 Flash",
providers: ["databricks"],
},
{
id: "goose-gemini-3-1-pro",
label: "Gemini 3.1 Pro",
providers: ["databricks"],
},
{
id: "goose-gemini-3-1-flash-lite",
label: "Gemini 3.1 Flash Lite",
providers: ["databricks"],
},
{
id: "goose-gemini-2-5-pro",
label: "Gemini 2.5 Pro",
providers: ["databricks"],
},
{
id: "gemini-2.5-pro",
label: "Gemini 2.5 Pro",
providers: ["openai-compat"],
},
{
id: "gemini-2.5-flash",
label: "Gemini 2.5 Flash",
providers: ["openai-compat"],
},
];
const PERSONA_LLM_PROVIDER_OPTIONS: readonly PersonaModelOption[] = [
{ id: "anthropic", label: "Anthropic" },
{ id: "openai", label: "OpenAI" },
{ id: "openai-compat", label: "OpenAI-compatible" },
{ id: "databricks", label: "Databricks" },
{ id: "databricks_v2", label: "Databricks v2" },
];
const PERSONA_MODEL_OPTIONS_BY_RUNTIME: Record<
string,
readonly PersonaModelOption[]
> = {
goose: BUZZ_AGENT_MODEL_OPTIONS,
"buzz-agent": BUZZ_AGENT_MODEL_OPTIONS,
goose: [DEFAULT_MODEL_OPTION],
"buzz-agent": [DEFAULT_MODEL_OPTION],
claude: [DEFAULT_MODEL_OPTION],
codex: [DEFAULT_MODEL_OPTION],
};
@@ -199,12 +76,8 @@ function isKnownLlmProvider(
return (KNOWN_LLM_PROVIDER_IDS as readonly string[]).includes(providerId);
}
function runtimeDefaultsToDatabricks(runtimeId: string) {
return runtimeId === "buzz-agent" || runtimeId === "goose";
}
export function runtimeSupportsLlmProviderSelection(runtimeId: string) {
return runtimeDefaultsToDatabricks(runtimeId);
return runtimeId === "buzz-agent" || runtimeId === "goose";
}
function effectiveModelProviderForOptions(
@@ -218,11 +91,7 @@ function effectiveModelProviderForOptions(
return "";
}
const trimmedProvider = providerId?.trim() ?? "";
if (trimmedProvider.length === 0 && runtimeDefaultsToDatabricks(runtimeId)) {
return "databricks";
}
return trimmedProvider;
return providerId?.trim() ?? "";
}
export function getPersonaModelOptions(
@@ -245,8 +114,7 @@ export function getPersonaModelOptions(
(option) =>
(option.id.length === 0 &&
!providerRequiresExplicitModel(trimmedProvider)) ||
(option.id !== DATABRICKS_DEFAULT_MODEL_ID &&
option.providers?.includes(trimmedProvider)),
option.providers?.includes(trimmedProvider),
);
}
@@ -299,10 +167,8 @@ export function providerRequiresExplicitModel(
);
}
export function getDefaultLlmProviderLabel(runtimeId: string) {
return runtimeDefaultsToDatabricks(runtimeId)
? "Databricks (default)"
: "Default";
export function getDefaultLlmProviderLabel(_runtimeId: string) {
return "Default";
}
export function getPersonaProviderOptions(
@@ -310,16 +176,10 @@ export function getPersonaProviderOptions(
runtimeId: string,
): readonly PersonaModelOption[] {
const trimmedProvider = currentProvider.trim();
const providerOptions = runtimeDefaultsToDatabricks(runtimeId)
? PERSONA_LLM_PROVIDER_OPTIONS.filter((option) =>
trimmedProvider === "databricks" ? true : option.id !== "databricks",
)
: PERSONA_LLM_PROVIDER_OPTIONS;
const defaultProviderOptions =
runtimeDefaultsToDatabricks(runtimeId) && trimmedProvider === "databricks"
? []
: [{ id: "", label: getDefaultLlmProviderLabel(runtimeId) }];
const options = [...defaultProviderOptions, ...providerOptions];
const defaultProviderOptions = [
{ id: "", label: getDefaultLlmProviderLabel(runtimeId) },
];
const options = [...defaultProviderOptions, ...PERSONA_LLM_PROVIDER_OPTIONS];
if (
trimmedProvider.length === 0 ||
options.some((option) => option.id === trimmedProvider)