mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): support Goose's native Google Gemini provider
Goose drives Gemini through its own `google` provider (native
`streamGenerateContent`, `x-goog-api-key`), authenticated with
GOOGLE_API_KEY — distinct from buzz-agent's OpenAI-compatible `gemini`
provider (GEMINI_API_KEY). Offer `google` (labelled "Google Gemini") in
the provider picker for the Goose runtime only, and make Goose readiness
require GOOGLE_API_KEY for it.
- agentConfigOptions.tsx: GOOSE_ONLY_PROVIDER_IDS = {google}; add google
to the provider list, credential config, KNOWN_LLM_PROVIDER_IDS, and
providerRequiresExplicitModel. Picker/credential gating is symmetric —
gemini is buzz-agent-only, google is goose-only — and a persisted value
still renders via the `(current)` tail.
- readiness.rs: goose `google` requires GOOGLE_API_KEY from the env layer
only. Goose keeps the key in its own secret store, never in
config.yaml, so the file config cannot silence it. buzz-agent is
unchanged.
- config_bridge/goose.rs: verify the persisted `active_provider: google`
+ `providers.google.model` path parses to provider/model and never
surfaces GOOGLE_API_KEY from the file.
- readiness_tests.rs: the accumulated `mod tests` stays extracted here
(agent_models precedent) so readiness.rs remains under the default
file-size ceiling; check-file-sizes.mjs override retargeted.
No silent GEMINI_API_KEY->GOOGLE_API_KEY migration — the two contracts
stay independent.
Co-authored-by: Atish Patel <atish@squareup.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Atish Patel <atish@squareup.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e573b8f3e1
commit
e77fdcb085
@@ -173,7 +173,12 @@ const overrides = new Map([
|
||||
// Windows Doctor install fix: cli_install_commands_windows field added to test stubs.
|
||||
// team-instructions-first-class: ManagedAgentRecord fixture gains the new
|
||||
// team_id field (+1 line).
|
||||
["src-tauri/src/managed_agents/readiness.rs", 1765],
|
||||
// gemini-goose-provider: the accumulated `mod tests` module was extracted to
|
||||
// readiness_tests.rs (following the agent_models_tests.rs precedent) so the
|
||||
// production file (readiness.rs) is back under the default ceiling. The
|
||||
// historical test debt — plus the goose `google` (native Gemini) provider
|
||||
// readiness coverage — now lives in that test-only file.
|
||||
["src-tauri/src/managed_agents/readiness_tests.rs", 1125],
|
||||
// applyWorkspace reposDir parameter plus the validateReposDir binding,
|
||||
// threaded through Tauri invokes for configurable repos_dir, plus the
|
||||
// harness-persona-sync `harnessOverride` create-input bit — load-bearing
|
||||
|
||||
@@ -199,6 +199,27 @@ providers:
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_nested_google_provider() {
|
||||
// Persisted `active_provider: google` with `providers.google.model` —
|
||||
// Goose's native Gemini provider. The bridge surfaces provider + model
|
||||
// but must NOT parse or infer the GOOGLE_API_KEY secret (Goose keeps it
|
||||
// in its own secret store, not config.yaml).
|
||||
let yaml = r#"
|
||||
active_provider: google
|
||||
providers:
|
||||
google:
|
||||
model: gemini-2.5-pro
|
||||
"#;
|
||||
let cfg = parse_goose_config(yaml).unwrap();
|
||||
assert_eq!(cfg.provider.as_deref(), Some("google"));
|
||||
assert_eq!(cfg.model.as_deref(), Some("gemini-2.5-pro"));
|
||||
assert!(
|
||||
!cfg.extra.contains_key("GOOGLE_API_KEY"),
|
||||
"the file bridge must never surface GOOGLE_API_KEY from config.yaml"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_databricks_provider_uses_provider_host_key() {
|
||||
let yaml = r#"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,18 @@ export const BUZZ_AGENT_ONLY_PROVIDER_IDS: ReadonlySet<string> = new Set([
|
||||
"gemini",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Provider ids that only the `goose` runtime can drive. Goose registers a
|
||||
* native Google Gemini provider under the id `google`, authenticated with
|
||||
* `GOOGLE_API_KEY` (distinct from buzz-agent's OpenAI-compatible `gemini` /
|
||||
* `GEMINI_API_KEY` contract — see `BUZZ_AGENT_ONLY_PROVIDER_IDS`). buzz-agent
|
||||
* has no `google` adapter, so offering it there would advertise a provider the
|
||||
* buzz-agent backend never checks. These ids are suppressed from the picker and
|
||||
* from credential requirements for every runtime except `goose`; a value
|
||||
* already persisted with one of them still renders via the `(current)` tail.
|
||||
*/
|
||||
export const GOOSE_ONLY_PROVIDER_IDS: ReadonlySet<string> = new Set(["google"]);
|
||||
|
||||
export const PERSONA_FIELD_SHELL_CLASS =
|
||||
"rounded-xl border border-input bg-muted/40 transition-colors duration-150 ease-out hover:border-muted-foreground/40 focus-within:border-muted-foreground/50";
|
||||
export const PERSONA_FIELD_CONTROL_CLASS =
|
||||
@@ -51,6 +63,7 @@ const KNOWN_LLM_PROVIDER_IDS = [
|
||||
"databricks",
|
||||
"databricks_v2",
|
||||
"gemini",
|
||||
"google",
|
||||
"openai",
|
||||
"openai-compat",
|
||||
] as const;
|
||||
@@ -110,6 +123,12 @@ const PROVIDER_CREDENTIAL_CONFIG: Partial<
|
||||
requiredEnvKeys: ["GEMINI_API_KEY"],
|
||||
secretEnvVar: "GEMINI_API_KEY",
|
||||
},
|
||||
google: {
|
||||
// Goose's native Gemini provider. GOOGLE_HOST is optional (defaults to the
|
||||
// Google AI host), so only GOOGLE_API_KEY is required.
|
||||
requiredEnvKeys: ["GOOGLE_API_KEY"],
|
||||
secretEnvVar: "GOOGLE_API_KEY",
|
||||
},
|
||||
databricks: {
|
||||
// DATABRICKS_TOKEN is NOT required — OAuth PKCE is the normal path.
|
||||
requiredEnvKeys: ["DATABRICKS_HOST"],
|
||||
@@ -136,6 +155,7 @@ export const PERSONA_LLM_PROVIDER_OPTIONS: readonly PersonaModelOption[] = [
|
||||
{ id: "openai", label: "OpenAI" },
|
||||
{ id: "openai-compat", label: "OpenAI-compatible" },
|
||||
{ id: "gemini", label: "Gemini" },
|
||||
{ id: "google", label: "Google Gemini" },
|
||||
{ id: "relay-mesh", label: "Buzz shared compute" },
|
||||
{ id: "databricks", label: "Databricks" },
|
||||
{ id: "databricks_v2", label: "Databricks v2" },
|
||||
@@ -179,15 +199,23 @@ export function requiredCredentialEnvKeys(
|
||||
return [];
|
||||
}
|
||||
const normalizedProvider = provider.trim().toLowerCase();
|
||||
// buzz-agent-only providers (e.g. gemini) are unsupported by Goose, so they
|
||||
// require no credentials there — keep this in lockstep with the picker gate
|
||||
// in getPersonaProviderOptions so options and requirements never drift.
|
||||
// Runtime-scoped providers require no credentials on other runtimes — keep
|
||||
// this in lockstep with the picker gate in getPersonaProviderOptions so
|
||||
// options and requirements never drift.
|
||||
// buzz-agent-only providers (e.g. gemini) are unsupported by Goose.
|
||||
if (
|
||||
normalizedRuntime !== "buzz-agent" &&
|
||||
BUZZ_AGENT_ONLY_PROVIDER_IDS.has(normalizedProvider)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
// goose-only providers (e.g. google) are unsupported by buzz-agent.
|
||||
if (
|
||||
normalizedRuntime !== "goose" &&
|
||||
GOOSE_ONLY_PROVIDER_IDS.has(normalizedProvider)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
const config = PROVIDER_CREDENTIAL_CONFIG[normalizedProvider];
|
||||
return config?.requiredEnvKeys ?? [];
|
||||
}
|
||||
@@ -285,7 +313,8 @@ export function providerRequiresExplicitModel(
|
||||
trimmedProvider === "anthropic" ||
|
||||
trimmedProvider === "openai" ||
|
||||
trimmedProvider === "openai-compat" ||
|
||||
trimmedProvider === "gemini"
|
||||
trimmedProvider === "gemini" ||
|
||||
trimmedProvider === "google"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -373,15 +402,19 @@ export function getPersonaProviderOptions(
|
||||
const defaultProviderOptions = [
|
||||
{ id: "", label: getDefaultLlmProviderLabel(runtimeId, globalProvider) },
|
||||
];
|
||||
// Suppress buzz-agent-only providers (e.g. gemini) for every other runtime.
|
||||
// A value already persisted with one still renders via the `(current)` tail
|
||||
// below, so an existing agent never loses its saved selection.
|
||||
const isBuzzAgentRuntime = runtimeId.trim() === "buzz-agent";
|
||||
const runtimeVisibleOptions = isBuzzAgentRuntime
|
||||
? PERSONA_LLM_PROVIDER_OPTIONS
|
||||
: PERSONA_LLM_PROVIDER_OPTIONS.filter(
|
||||
(o) => !BUZZ_AGENT_ONLY_PROVIDER_IDS.has(o.id),
|
||||
);
|
||||
// Suppress runtime-scoped providers for runtimes that can't drive them:
|
||||
// buzz-agent-only (e.g. gemini) is hidden everywhere but buzz-agent, and
|
||||
// goose-only (e.g. google) is hidden everywhere but goose. A value already
|
||||
// persisted with one still renders via the `(current)` tail below, so an
|
||||
// existing agent never loses its saved selection.
|
||||
const trimmedRuntimeId = runtimeId.trim();
|
||||
const isBuzzAgentRuntime = trimmedRuntimeId === "buzz-agent";
|
||||
const isGooseRuntime = trimmedRuntimeId === "goose";
|
||||
const runtimeVisibleOptions = PERSONA_LLM_PROVIDER_OPTIONS.filter((o) => {
|
||||
if (BUZZ_AGENT_ONLY_PROVIDER_IDS.has(o.id)) return isBuzzAgentRuntime;
|
||||
if (GOOSE_ONLY_PROVIDER_IDS.has(o.id)) return isGooseRuntime;
|
||||
return true;
|
||||
});
|
||||
const filteredOptions = hideProviderIds?.size
|
||||
? runtimeVisibleOptions.filter((o) => !hideProviderIds.has(o.id))
|
||||
: runtimeVisibleOptions;
|
||||
|
||||
@@ -170,6 +170,46 @@ test("editAgent_gooseSupportedProviderCredentialsUnchanged", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// ── Google Gemini is goose-only — must not be advertised for buzz-agent ──────
|
||||
//
|
||||
// Goose registers a native Gemini provider under the id `google`, authenticated
|
||||
// with GOOGLE_API_KEY (distinct from buzz-agent's OpenAI-compatible `gemini` /
|
||||
// GEMINI_API_KEY). buzz-agent has no `google` adapter, so the picker and
|
||||
// credential-requirement helpers gate `google` to the goose runtime.
|
||||
|
||||
test("editAgent_providerOptions_includesGoogleForGoose", () => {
|
||||
const options = getPersonaProviderOptions("", "goose");
|
||||
const ids = options.map((o) => o.id);
|
||||
assert.ok(
|
||||
ids.includes("google"),
|
||||
"google (Goose's native Gemini provider) must be offered for goose",
|
||||
);
|
||||
});
|
||||
|
||||
test("editAgent_providerOptions_excludesGoogleForBuzzAgent", () => {
|
||||
const options = getPersonaProviderOptions("", "buzz-agent");
|
||||
const ids = options.map((o) => o.id);
|
||||
assert.ok(
|
||||
!ids.includes("google"),
|
||||
"google must not be offered for the buzz-agent runtime",
|
||||
);
|
||||
});
|
||||
|
||||
test("editAgent_google_requiresGoogleApiKeyAndExplicitModelForGoose", () => {
|
||||
assert.deepEqual(requiredCredentialEnvKeys("goose", "google"), [
|
||||
"GOOGLE_API_KEY",
|
||||
]);
|
||||
assert.ok(
|
||||
providerRequiresExplicitModel("google"),
|
||||
"google must require an explicit model",
|
||||
);
|
||||
});
|
||||
|
||||
test("editAgent_google_requiresNoCredentialsForBuzzAgent", () => {
|
||||
// buzz-agent can't drive google, so it requires no google credentials there.
|
||||
assert.deepEqual(requiredCredentialEnvKeys("buzz-agent", "google"), []);
|
||||
});
|
||||
|
||||
test("editAgent_providerOptions_includesDefaultEntry", () => {
|
||||
const options = getPersonaProviderOptions("", "buzz-agent");
|
||||
// The first entry is the default (empty id) — clearing back to runtime default.
|
||||
|
||||
Reference in New Issue
Block a user