From 428187009daa4d00ef66f5f1b6ba3a8b406e414b Mon Sep 17 00:00:00 2001 From: npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@sprout-oss.stage.blox.sqprod.co> Date: Mon, 29 Jun 2026 19:12:01 -0400 Subject: [PATCH] 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 Signed-off-by: Will Pfleger --- crates/buzz-agent/src/config.rs | 160 ++++++----------- desktop/src-tauri/build.rs | 41 ++++- .../src-tauri/src/commands/agent_models.rs | 6 +- .../src-tauri/src/managed_agents/runtime.rs | 57 ++++-- .../src/managed_agents/runtime/tests.rs | 84 ++++++++- .../agents/ui/personaDialogPickers.tsx | 166 ++---------------- 6 files changed, 224 insertions(+), 290 deletions(-) diff --git a/crates/buzz-agent/src/config.rs b/crates/buzz-agent/src/config.rs index 8e17d10ca9..d2cbef7717 100644 --- a/crates/buzz-agent/src/config.rs +++ b/crates/buzz-agent/src/config.rs @@ -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 { 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 { - 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 { + 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 { - 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] diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs index 5ffec1a622..f787a0b029 100644 --- a/desktop/src-tauri/build.rs +++ b/desktop/src-tauri/build.rs @@ -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") { diff --git a/desktop/src-tauri/src/commands/agent_models.rs b/desktop/src-tauri/src/commands/agent_models.rs index 2502306b7e..4df4a4fc54 100644 --- a/desktop/src-tauri/src/commands/agent_models.rs +++ b/desktop/src-tauri/src/commands/agent_models.rs @@ -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); diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index 788e38906e..381daa4b52 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -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( diff --git a/desktop/src-tauri/src/managed_agents/runtime/tests.rs b/desktop/src-tauri/src/managed_agents/runtime/tests.rs index 667e8d2414..6c39a3d1c9 100644 --- a/desktop/src-tauri/src/managed_agents/runtime/tests.rs +++ b/desktop/src-tauri/src/managed_agents/runtime/tests.rs @@ -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] diff --git a/desktop/src/features/agents/ui/personaDialogPickers.tsx b/desktop/src/features/agents/ui/personaDialogPickers.tsx index 35cb9db08b..91b414158a 100644 --- a/desktop/src/features/agents/ui/personaDialogPickers.tsx +++ b/desktop/src/features/agents/ui/personaDialogPickers.tsx @@ -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)