mirror of
https://github.com/block/buzz.git
synced 2026-11-07 01:41:06 +00:00
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:
parent
15a73aa27f
commit
428187009d
@@ -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]
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user