mirror of
https://github.com/block/buzz.git
synced 2026-10-28 04:51:40 +00:00
sprout-agent: make Databricks defaults env-only (#868)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -138,8 +138,8 @@ Everything is environment variables. No flags, no config files. (We are a subpro
|
||||
| `OPENAI_COMPAT_MODEL` | — | Required when provider=openai. |
|
||||
| `OPENAI_COMPAT_BASE_URL` | `https://api.openai.com/v1` | Point at vLLM, llama.cpp, OpenRouter, Ollama, etc. |
|
||||
| `OPENAI_COMPAT_API` | `auto` | `auto` \| `chat` \| `responses`. `auto` picks Responses for `*.openai.com`, Chat Completions everywhere else. |
|
||||
| `DATABRICKS_HOST` | goose config | Required when provider=databricks or when using Databricks fallback. If unset, read from goose's `~/.config/goose/config.yaml`. |
|
||||
| `DATABRICKS_MODEL` | goose config | Required when provider=databricks or when using Databricks fallback. If unset, uses `DATABRICKS_MODEL` from goose config, or `GOOSE_MODEL`/`GOOSE_MODE` when `GOOSE_PROVIDER=databricks`. |
|
||||
| `DATABRICKS_HOST` | — | Required when provider=databricks or when using Databricks fallback. |
|
||||
| `DATABRICKS_MODEL` | — | Required when provider=databricks or when using Databricks fallback. |
|
||||
| `DATABRICKS_TOKEN` | — | Optional static bearer escape hatch. If unset, Databricks uses browser OAuth + refresh cache. |
|
||||
| `SPROUT_AGENT_SYSTEM_PROMPT` | built-in | Inline system prompt. |
|
||||
| `SPROUT_AGENT_SYSTEM_PROMPT_FILE` | — | File path. Mutually exclusive with the above. |
|
||||
@@ -170,7 +170,7 @@ Everything is environment variables. No flags, no config files. (We are a subpro
|
||||
| Block Gateway | `openai` | `POST {base}/chat/completions` | gpt-5, claude |
|
||||
| Databricks | `databricks` | `POST {host}/serving-endpoints/{model}/invocations` | goose-claude-4-6-sonnet |
|
||||
|
||||
If `SPROUT_AGENT_PROVIDER=anthropic` is selected without `ANTHROPIC_API_KEY`, or `SPROUT_AGENT_PROVIDER=openai` is selected without `OPENAI_COMPAT_API_KEY`, the agent automatically falls back to Databricks OAuth when Databricks host/model config is available. The same Databricks fallback applies when `SPROUT_AGENT_PROVIDER` is unset. Host/model can come from env or from goose's config file; explicit Anthropic/OpenAI API keys always win.
|
||||
If `SPROUT_AGENT_PROVIDER=anthropic` is selected without `ANTHROPIC_API_KEY`, or `SPROUT_AGENT_PROVIDER=openai` is selected without `OPENAI_COMPAT_API_KEY`, the agent automatically falls back to Databricks OAuth when `DATABRICKS_HOST` and `DATABRICKS_MODEL` are set. The same Databricks fallback applies when `SPROUT_AGENT_PROVIDER` is unset. Explicit Anthropic/OpenAI API keys always win.
|
||||
|
||||
`provider=openai` speaks two HTTP dialects: the [Responses API](https://platform.openai.com/docs/api-reference/responses) (`/v1/responses`, required for GPT-5 / o-series tool-calling on OpenAI's own service) and the [Chat Completions API](https://platform.openai.com/docs/api-reference/chat) (`/chat/completions`, the broadly-supported OpenAI-compatible wire format).
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{collections::HashMap, path::PathBuf, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 1;
|
||||
|
||||
@@ -84,9 +84,8 @@ pub struct Config {
|
||||
|
||||
impl Config {
|
||||
pub fn from_env() -> Result<Self, String> {
|
||||
let goose_databricks = GooseDatabricksConfig::load_default();
|
||||
let databricks_host = env("DATABRICKS_HOST").or_else(|| goose_databricks.host.clone());
|
||||
let databricks_model = env("DATABRICKS_MODEL").or_else(|| goose_databricks.model.clone());
|
||||
let databricks_host = env("DATABRICKS_HOST");
|
||||
let databricks_model = env("DATABRICKS_MODEL");
|
||||
let provider = resolve_provider(
|
||||
env("SPROUT_AGENT_PROVIDER").as_deref(),
|
||||
env("ANTHROPIC_API_KEY").as_deref(),
|
||||
@@ -115,12 +114,8 @@ impl Config {
|
||||
),
|
||||
Provider::Databricks => (
|
||||
env("DATABRICKS_TOKEN").unwrap_or_default(),
|
||||
databricks_model.ok_or_else(|| {
|
||||
"config: DATABRICKS_MODEL required (or set GOOSE_MODEL in goose config with GOOSE_PROVIDER=databricks)".to_string()
|
||||
})?,
|
||||
databricks_host.ok_or_else(|| {
|
||||
"config: DATABRICKS_HOST required (or set DATABRICKS_HOST in goose config)".to_string()
|
||||
})?,
|
||||
databricks_model.ok_or_else(|| "config: DATABRICKS_MODEL required".to_string())?,
|
||||
databricks_host.ok_or_else(|| "config: DATABRICKS_HOST required".to_string())?,
|
||||
OpenAiApi::Chat, // Databricks invocations is chat-shaped
|
||||
),
|
||||
};
|
||||
@@ -237,66 +232,6 @@ fn req(k: &str) -> Result<String, String> {
|
||||
env(k).ok_or_else(|| format!("config: {k} required"))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GooseDatabricksConfig {
|
||||
host: Option<String>,
|
||||
model: Option<String>,
|
||||
}
|
||||
|
||||
impl GooseDatabricksConfig {
|
||||
fn load_default() -> Self {
|
||||
goose_config_path()
|
||||
.and_then(|p| Self::load_from_path(&p))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn load_from_path(path: &std::path::Path) -> Option<Self> {
|
||||
let raw = std::fs::read_to_string(path).ok()?;
|
||||
let map: HashMap<String, serde_yaml::Value> = serde_yaml::from_str(&raw).ok()?;
|
||||
Some(Self::from_map(&map))
|
||||
}
|
||||
|
||||
fn from_map(map: &HashMap<String, serde_yaml::Value>) -> Self {
|
||||
let host = yaml_string(map, "DATABRICKS_HOST");
|
||||
let explicit_model = yaml_string(map, "DATABRICKS_MODEL");
|
||||
let goose_provider = yaml_string(map, "GOOSE_PROVIDER");
|
||||
let goose_model = yaml_string(map, "GOOSE_MODEL");
|
||||
let goose_mode = yaml_string(map, "GOOSE_MODE");
|
||||
let model = explicit_model.or_else(|| {
|
||||
if goose_provider
|
||||
.as_deref()
|
||||
.is_some_and(|p| p.eq_ignore_ascii_case("databricks"))
|
||||
{
|
||||
goose_model.or(goose_mode)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
Self { host, model }
|
||||
}
|
||||
}
|
||||
|
||||
fn yaml_string(map: &HashMap<String, serde_yaml::Value>, key: &str) -> Option<String> {
|
||||
map.get(key)?
|
||||
.as_str()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
fn goose_config_path() -> Option<PathBuf> {
|
||||
if let Ok(root) = std::env::var("GOOSE_PATH_ROOT") {
|
||||
return Some(PathBuf::from(root).join("config").join("config.yaml"));
|
||||
}
|
||||
let home = std::env::var("HOME").ok()?;
|
||||
Some(
|
||||
PathBuf::from(home)
|
||||
.join(".config")
|
||||
.join("goose")
|
||||
.join("config.yaml"),
|
||||
)
|
||||
}
|
||||
|
||||
fn present_nonempty(v: Option<&str>) -> bool {
|
||||
v.map(str::trim).is_some_and(|s| !s.is_empty())
|
||||
}
|
||||
@@ -555,72 +490,6 @@ mod tests {
|
||||
assert!(err.contains("OPENAI_COMPAT_API=nope"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goose_databricks_config_reads_host_and_model() {
|
||||
let map = HashMap::from([
|
||||
(
|
||||
"DATABRICKS_HOST".to_string(),
|
||||
serde_yaml::Value::String("https://dbc.example".into()),
|
||||
),
|
||||
(
|
||||
"GOOSE_PROVIDER".to_string(),
|
||||
serde_yaml::Value::String("databricks".into()),
|
||||
),
|
||||
(
|
||||
"GOOSE_MODEL".to_string(),
|
||||
serde_yaml::Value::String("goose-claude-4-6-sonnet".into()),
|
||||
),
|
||||
]);
|
||||
let cfg = GooseDatabricksConfig::from_map(&map);
|
||||
assert_eq!(cfg.host.as_deref(), Some("https://dbc.example"));
|
||||
assert_eq!(cfg.model.as_deref(), Some("goose-claude-4-6-sonnet"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goose_databricks_config_prefers_explicit_databricks_model() {
|
||||
let map = HashMap::from([
|
||||
(
|
||||
"DATABRICKS_HOST".to_string(),
|
||||
serde_yaml::Value::String("https://dbc.example".into()),
|
||||
),
|
||||
(
|
||||
"DATABRICKS_MODEL".to_string(),
|
||||
serde_yaml::Value::String("explicit-db-model".into()),
|
||||
),
|
||||
(
|
||||
"GOOSE_PROVIDER".to_string(),
|
||||
serde_yaml::Value::String("databricks".into()),
|
||||
),
|
||||
(
|
||||
"GOOSE_MODEL".to_string(),
|
||||
serde_yaml::Value::String("goose-model".into()),
|
||||
),
|
||||
]);
|
||||
let cfg = GooseDatabricksConfig::from_map(&map);
|
||||
assert_eq!(cfg.model.as_deref(), Some("explicit-db-model"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goose_databricks_config_ignores_goose_model_for_other_provider() {
|
||||
let map = HashMap::from([
|
||||
(
|
||||
"DATABRICKS_HOST".to_string(),
|
||||
serde_yaml::Value::String("https://dbc.example".into()),
|
||||
),
|
||||
(
|
||||
"GOOSE_PROVIDER".to_string(),
|
||||
serde_yaml::Value::String("anthropic".into()),
|
||||
),
|
||||
(
|
||||
"GOOSE_MODEL".to_string(),
|
||||
serde_yaml::Value::String("claude".into()),
|
||||
),
|
||||
]);
|
||||
let cfg = GooseDatabricksConfig::from_map(&map);
|
||||
assert_eq!(cfg.host.as_deref(), Some("https://dbc.example"));
|
||||
assert!(cfg.model.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_provider_keeps_requested_provider_when_token_present() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -3,6 +3,8 @@ fn main() {
|
||||
println!("cargo:rerun-if-env-changed=SPROUT_RELAY_HTTP");
|
||||
println!("cargo:rerun-if-env-changed=SPROUT_UPDATER_PUBLIC_KEY");
|
||||
println!("cargo:rerun-if-env-changed=SPROUT_UPDATER_ENDPOINT");
|
||||
println!("cargo:rerun-if-env-changed=SPROUT_BUILD_DATABRICKS_HOST");
|
||||
println!("cargo:rerun-if-env-changed=SPROUT_BUILD_DATABRICKS_MODEL");
|
||||
println!("cargo:rustc-check-cfg=cfg(sprout_updater_enabled)");
|
||||
|
||||
if let Ok(relay_url) = std::env::var("SPROUT_RELAY_URL") {
|
||||
@@ -13,6 +15,14 @@ fn main() {
|
||||
println!("cargo:rustc-env=SPROUT_DESKTOP_BUILD_RELAY_HTTP={relay_http}");
|
||||
}
|
||||
|
||||
if let Ok(host) = std::env::var("SPROUT_BUILD_DATABRICKS_HOST") {
|
||||
println!("cargo:rustc-env=SPROUT_DESKTOP_BUILD_DATABRICKS_HOST={host}");
|
||||
}
|
||||
|
||||
if let Ok(model) = std::env::var("SPROUT_BUILD_DATABRICKS_MODEL") {
|
||||
println!("cargo:rustc-env=SPROUT_DESKTOP_BUILD_DATABRICKS_MODEL={model}");
|
||||
}
|
||||
|
||||
let updater_public_key = std::env::var("SPROUT_UPDATER_PUBLIC_KEY")
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
|
||||
@@ -998,6 +998,13 @@ pub fn spawn_agent_child(
|
||||
);
|
||||
}
|
||||
|
||||
// Baked-in Databricks defaults for internal builds (sprout-releases sets
|
||||
// SPROUT_BUILD_DATABRICKS_* 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);
|
||||
}
|
||||
|
||||
// ── User env vars: persona first, then per-agent (last wins) ────────
|
||||
//
|
||||
// Precedence: desktop parent env < persona env_vars < agent env_vars.
|
||||
@@ -1051,6 +1058,23 @@ fn child_rust_log_filter() -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Databricks host/model baked in at compile time for internal builds. Empty
|
||||
/// in OSS builds, where the `SPROUT_BUILD_DATABRICKS_*` env is unset.
|
||||
fn build_databricks_defaults() -> Vec<(&'static str, &'static str)> {
|
||||
let mut defaults = Vec::new();
|
||||
if let Some(host) = option_env!("SPROUT_DESKTOP_BUILD_DATABRICKS_HOST") {
|
||||
if !host.is_empty() {
|
||||
defaults.push(("DATABRICKS_HOST", host));
|
||||
}
|
||||
}
|
||||
if let Some(model) = option_env!("SPROUT_DESKTOP_BUILD_DATABRICKS_MODEL") {
|
||||
if !model.is_empty() {
|
||||
defaults.push(("DATABRICKS_MODEL", model));
|
||||
}
|
||||
}
|
||||
defaults
|
||||
}
|
||||
|
||||
pub fn start_managed_agent_process(
|
||||
app: &AppHandle,
|
||||
record: &mut ManagedAgentRecord,
|
||||
@@ -1182,6 +1206,13 @@ mod tests {
|
||||
assert_eq!(p.mcp_command, Some("sprout-dev-mcp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn databricks_defaults_empty_in_oss_build() {
|
||||
// OSS (and normal test) builds set neither SPROUT_BUILD_DATABRICKS_*,
|
||||
// so nothing is baked in and no DATABRICKS_* is injected on spawn.
|
||||
assert!(super::build_databricks_defaults().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sprout_agent_resolved_via_path() {
|
||||
assert!(known_acp_runtime("/usr/local/bin/sprout-agent").is_some_and(|p| p.mcp_hooks));
|
||||
|
||||
Reference in New Issue
Block a user