feat(agent): add Gemini as a first-class LLM provider#2053
Open
atishpatel wants to merge 6 commits into
Open
Conversation
Adds `gemini` as a first-class buzz-agent provider using Google's OpenAI-compatible surface, so it reuses the existing Chat Completions request/response path rather than introducing a second serializer/parser. Provider contract (RESEARCH/GEMINI_PROVIDER_API_CONTRACT.md): - Provider id `gemini`; env vars `GEMINI_API_KEY`, `GEMINI_MODEL`, optional `GEMINI_BASE_URL` (default https://generativelanguage.googleapis.com/v1beta/openai). - Static `Authorization: Bearer $GEMINI_API_KEY`; `/models` discovery reuses the OpenAI-compatible path with the same bearer and key-redacted errors. - API shape pinned to Chat Completions — never Responses or native generateContent (there is no /responses endpoint on the Google host). Changes: - buzz-agent config.rs: `Provider::Gemini` variant, `resolve_provider` + `from_env` arms reading GEMINI_* with the Google default base URL, API pinned to Chat. Extracted `GEMINI_DEFAULT_BASE_URL` constant. - buzz-agent llm.rs: route Gemini through the OpenAI Chat arms and the static bearer token source. Extracted `should_use_responses` so a Chat-pinned provider can never be routed to /responses regardless of host/auto-upgrade. - desktop agent_models.rs: provider-aware base-URL/key resolution so `/models` discovery reads GEMINI_BASE_URL/GEMINI_API_KEY; Gemini ids pass through the OpenAI text-model filter unchanged. - desktop readiness.rs: require GEMINI_API_KEY and accept GEMINI_MODEL fallback. - frontend personaDialogPickers.tsx: provider option, credential mapping, and explicit-model requirement. Tests: new coverage in config.rs, llm.rs, agent_models_tests.rs, readiness.rs, and editAgentProviderDiscovery.test.mjs. Full local gates green: buzz-agent + desktop Tauri unit tests, desktop TS tests, typecheck, biome, clippy (workspace + Tauri), rustfmt. 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>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
The neutral "API Key" relabel made Playwright's default non-exact
getByLabel("API Key") resolve to two elements — the credential input
and the "Show API key" visibility toggle button — tripping strict-mode
violations across the persona/agent E2E and smoke specs (Desktop E2E
Integration + all four Desktop Smoke shards).
Pin the selector with { exact: true } so it resolves only to the input,
whose accessible name is exactly "API Key" (the required "*" marker is
aria-hidden and excluded from the accessible name). This also resolves
the Biome format error in onboarding-agent-defaults.spec.ts reported by
Desktop Core.
Signed-off-by: Atish Patel <atish@squareup.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to 4e18170. `getByLabel("API Key", { exact: true })` still failed: when the credential is required the field's accessible name is "API Key *" (the required "*" marker is included in the label-derived name despite aria-hidden), so an exact "API Key" match resolves to zero elements and times out. Switch all 16 call sites to getByTestId("persona-provider-api-key") — the stable id on the input across every surface (persona, global config, edit agent). This sidesteps both the show/hide-button collision and the required-marker name variance. Playwright's own strict-mode output named this test id as the canonical locator for the element. Signed-off-by: Atish Patel <atish@squareup.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
atishpatel
force-pushed
the
feat/gemini-provider
branch
from
July 19, 2026 23:22
c49b84e to
bf25546
Compare
Gemini 3.x returns an opaque `thought_signature` inside each tool call's
OpenAI-compat `extra_content` field and *requires* it to be echoed back
with the same tool call on the following turn. buzz-agent's `ToolCall`
only stored id/name/arguments, so the signature was dropped and every
follow-up turn after a tool call was rejected with HTTP 400
("Function call is missing a thought_signature") — surfaced by buzz-acp
as `-32603 Internal error`, which made any agentic (tool-using) Gemini
run fail on its second turn.
Carry the opaque `extra_content` on `ToolCall`: capture it when parsing
the OpenAI-chat response and re-attach it when replaying assistant
history. The payload is treated as an opaque blob, so it is `None` and
omitted for providers that don't emit it (Anthropic, standard OpenAI).
Verified against the live Gemini OpenAI-compat endpoint: a two-turn
tool-calling exchange fails with 400 when the signature is dropped and
succeeds when it is echoed back.
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>
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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds Gemini as a first-class
buzz-agentLLM provider using Google's OpenAI-compatible surface, so it reuses the existing Chat Completions request/response path rather than introducing a second serializer/parser.gemini; env varsGEMINI_API_KEY,GEMINI_MODEL, optionalGEMINI_BASE_URL(defaulthttps://generativelanguage.googleapis.com/v1beta/openai).Authorization: Bearer $GEMINI_API_KEY. API shape pinned to Chat Completions (POST {base}/chat/completions) — never Responses or nativegenerateContent(there is no/responsesendpoint on the Google host).BUZZ_AGENT_MODELoverridesGEMINI_MODEL; existing OpenAI/Anthropic/Databricks behavior unchanged.Changes
Transport (
buzz-agent)config.rs:Provider::Geminivariant;resolve_provider+from_envarms readGEMINI_*with the Google default base URL, API pinned to Chat. ExtractedGEMINI_DEFAULT_BASE_URLconstant.llm.rs: route Gemini through the OpenAI Chat arms + static-bearer token source. Extractedshould_use_responsesso a Chat-pinned provider can never be routed to/responsesregardless of host/auto-upgrade latch.Desktop (Tauri)
agent_models.rs: provider-aware base-URL/key resolution so/modelsdiscovery readsGEMINI_BASE_URL/GEMINI_API_KEYwith key-redacted errors; Gemini ids skip the GPT text-model filter.readiness.rs: requireGEMINI_API_KEY, acceptGEMINI_MODELfallback.Desktop (frontend)
personaDialogPickers.tsx: Gemini provider option, credential mapping, explicit-model requirement, and Goose gating — Gemini is offered/required only for thebuzz-agentruntime (Goose has nogeminiarm; offering it would demand aGEMINI_API_KEYthe Goose backend never reads). A value already persisted withgeministill renders via the(current)tail.bakedEnvHelpers.ts:gemini → GEMINI_MODELinherited/global model fallback.personaModelDiscoveryStatus.ts: Gemini label + tailored "Enter a Gemini API key to load Gemini models." message.Docs
crates/buzz-agent/README.md: run example, env-var table, Providers table row, and Chat-pinned endpoint note.Testing
cargo test -p buzz-agent→ 265 lib + integration suites, 0 failed (incl.chat_pinned_never_uses_responses_regardless_of_host_or_latch);cargo fmt --checkclean;cargo clippy --all-targets0 warnings.readiness53 passed (3 Gemini),agent_models14 passed (3 Gemini).tsc --noEmitclean; Biome clean; file-size guard PASS.