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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant