Skip to content

feat(agent): add Gemini as a first-class LLM provider - #2053

Open
atishpatel wants to merge 6 commits into
mainfrom
feat/gemini-provider
Open

feat(agent): add Gemini as a first-class LLM provider#2053
atishpatel wants to merge 6 commits into
mainfrom
feat/gemini-provider

Conversation

@atishpatel

Copy link
Copy Markdown
Contributor

Summary

Adds Gemini as a first-class buzz-agent LLM 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 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. API shape pinned to Chat Completions (POST {base}/chat/completions) — never Responses or native generateContent (there is no /responses endpoint on the Google host).
  • BUZZ_AGENT_MODEL overrides GEMINI_MODEL; existing OpenAI/Anthropic/Databricks behavior unchanged.

Changes

Transport (buzz-agent)

  • config.rs: Provider::Gemini variant; resolve_provider + from_env arms read GEMINI_* with the Google default base URL, API pinned to Chat. Extracted GEMINI_DEFAULT_BASE_URL constant.
  • llm.rs: route Gemini through the OpenAI Chat arms + static-bearer token source. Extracted should_use_responses so a Chat-pinned provider can never be routed to /responses regardless of host/auto-upgrade latch.

Desktop (Tauri)

  • agent_models.rs: provider-aware base-URL/key resolution so /models discovery reads GEMINI_BASE_URL/GEMINI_API_KEY with key-redacted errors; Gemini ids skip the GPT text-model filter.
  • readiness.rs: require GEMINI_API_KEY, accept GEMINI_MODEL fallback.

Desktop (frontend)

  • personaDialogPickers.tsx: Gemini provider option, credential mapping, explicit-model requirement, and Goose gating — Gemini is offered/required only for the buzz-agent runtime (Goose has no gemini arm; offering it would demand a GEMINI_API_KEY the Goose backend never reads). A value already persisted with gemini still renders via the (current) tail.
  • bakedEnvHelpers.ts: gemini → GEMINI_MODEL inherited/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 --check clean; cargo clippy --all-targets 0 warnings.
  • Tauri: readiness 53 passed (3 Gemini), agent_models 14 passed (3 Gemini).
  • Frontend: focused suites 180/180; full suite 3127/3127; tsc --noEmit clean; Biome clean; file-size guard PASS.

@atishpatel
atishpatel requested a review from a team as a code owner July 17, 2026 23:02
atishpatel and others added 4 commits July 19, 2026 18:13
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
atishpatel force-pushed the feat/gemini-provider branch from c49b84e to bf25546 Compare July 19, 2026 23:22
atishpatel and others added 2 commits July 19, 2026 21:56
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage-ready Appropriate for agentic review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants