Gemini joins the provider lineup — and brings the per-claim citation that Claude's dynamic-filter lost
Perplexed now ships a fourth AI provider: Gemini, with Google Search grounding. Gemini's `groundingSupports[]` carries per-segment attribution (text span → source URL) that survives intact, where Claude's `web_search_20260209` dynamic-filter sandbox drops it on the floor. Same modal UX as Ask Claude, same Citations footer shape so cite-wide hex substitution stays provider-agnostic. Two Gemini-specific quirks surfaced when we dissected a real response with curl: chunk URLs are short-lived `vertexaisearch.cloud.google.com` redirects that rot in ~30 days (the plugin resolves them to durable source URLs before writing), and the spec's `searchEntryPoint.renderedContent` is 5KB of inline-styled HTML that Obsidian's Markdown renderer can't display (replaced with a Markdown list of the queries, each linked to google.com/search).
Ask Gemini — fourth provider, first one with surviving per-claim citations
Why care?
If you write research notes in Obsidian, you now have a fourth way to ground a draft in live web sources without leaving the editor:
Ask Gemini — runs Google Search behind the scenes and streams the answer into your note at the cursor, just like Ask Claude and Ask Perplexity.
Per-claim citations actually work. Gemini's response carries a mapping from each cited sentence to the specific URL it came from, which means the
### Citationssection at the bottom of your note is not a flat URL dump — it's a quote-per-source list you can verify by reading.You opt in per-question. A toggle in the Ask Gemini modal turns Google Search grounding on or off without touching settings, so you can compare grounded vs. ungrounded answers on the same prompt.
What's new?
Five settings, all behind one Settings → Perplexed → Gemini (Google) section:
Gemini (Google)
├── Gemini API key (paste from Google AI Studio)
├── Default Gemini model (2.5 pro / 2.5 flash / 2.0 flash)
├── Enable Google search grounding by default (on)
├── Include Google searches list in notes (on — Markdown bullets, not HTML chip)
└── Resolve citation urls (durable, slower) (on — fixes the 30-day URL rot) Ask Geminicommand opens a modal matching the Ask Claude UX — full-width question textarea, model dropdown with taglines, three behavior toggles (grounding / suggestions chip / streaming), Cmd-Enter to submit.Check Gemini service statuscommand reports whether the service initialized and whether an API key is configured.Citations footer mirrors Claude's exactly —
[N]: [Title](url). > cited_text— so cite-wide's hex-substitution pass treats Gemini output the same as everything else.
How it works
The Gemini response shape carries two layers of provenance:
| Layer | Field | What it gives you |
| Page-level | groundingChunks[] | URL + title per page Gemini consulted |
| Segment-level | groundingSupports[] | Text span (segment.text) → indices into groundingChunks[] |
The plugin walks both layers and merges them by URL: page-level entries come in as URL-and-title fallbacks; segment-level entries enrich them with the actual sentence Gemini grounded against. First quote per source wins, on the principle that the earliest segment is closest to the source's lede.
Compare to the Claude path documented in context-v/issues/Getting-Claude-to-Respond-With-Research.md:
"web_search_20260209's dynamic-filtering pass post-processes search results in a code-execution sandbox, and per-claim web_search_result_location citations don't survive that round-trip — text blocks come back with citations: null."
Gemini doesn't have that round-trip. groundingSupports[] ships in the final response unmodified, so the ### Citations section you get is actually attached to the prose above it rather than guessed from string matches.
What the response actually contains (dissected with curl, 2026-05-19)
Two things bit us once we ran a real gemini-2.5-flash request with google_search enabled:
| Surface | Documented as | Actually | What the plugin does |
chunk.web.uri | "source URL" | vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIY… — a Google-controlled redirect that expires ~30 days after the response | resolveCitationUrls() does a parallel fetch(uri, { redirect: 'manual' }) per cited source (3s timeout each), reads the Location header, falls back to the redirect URL on failure |
chunk.web.title | "title" | The source DOMAIN ("nobelprize.org"), not the page title | Rendered as the link text in [domain.com](resolved-url) — acceptable; a future pass could grab <title> during URL resolution |
searchEntryPoint.renderedContent | Google's required Search Suggestions chip | ~5KB of <style>-tag-prefixed inline HTML that Obsidian's Markdown renderer strips, leaving orphan <div> chips | Replaced with a Markdown ### Google Searches section listing webSearchQueries[], each query linked to google.com/search?q=…. Same end-user behavior (re-run the suggested search), Markdown-native rendering |
support.segment.startIndex/endIndex | "text span offsets" | UTF-8 BYTE offsets, not char offsets | Currently unused (we use segment.text directly) but flagged with a code comment for anyone wiring inline [N] markers later |
The grounded-attribution layer (groundingSupports[] → groundingChunkIndices) is exactly what we hoped — segment.text is the verbatim quote, and it points cleanly back to chunk indices. That's the half of this that Just Works.
Architecture
Mirrors the Claude provider exactly, no shared base class:
src/services/geminiService.ts (new — raw fetch, SSE streaming, groundingMetadata parsing,
Citations + Search Queries + Suggestions blocks)
src/modals/GeminiModal.ts (new — Ask Claude shape, gemini-themed)
main.ts
├── PerplexedPluginSettings + DEFAULT_SETTINGS (geminiApiKey, geminiDefaultModel,
│ geminiEnableGrounding,
│ geminiIncludeSearchSuggestions)
├── private geminiService (lifecycle parallel to claudeService)
├── service init blocks (onload + reinitializeServices)
├── registerGeminiCommands (ask-gemini, gemini-service-status)
└── Settings tab — Gemini (Google) section with four rows Zero new dependencies — uses activeWindow.fetch for SSE and Obsidian's request for non-streaming, same pattern as perplexityService.ts. The @google/genai SDK was evaluated and skipped: it pulls in Node-flavored streaming primitives and JSON-schema validators we don't need, and the REST shape is small enough that a hand-rolled parser is more honest about what's on the wire.
Build status
pnpm run build (eslint + tsc + esbuild production) green. ObsidianReviewBot's obsidianmd/ui/sentence-case rule was the only friction — it recognizes "Google" as a proper noun (capitalize) and "URLs" as an acronym (lowercase to "urls"), each of which collides with our usual sentence-case house style. Resolved by following the rule's autosuggestion verbatim; the lint passes and the rule's reasoning is internally consistent even where it looks odd to a human reader.
What's next
Page-title-not-domain in citations. Right now
[nobelprize.org](https://www.nobelprize.org/prizes/physics/2024/summary/)shows the domain as link text because that's whatchunk.web.titlereturns. We're already paying for afetch()round-trip per cited source during URL resolution; one tweak to also parse<title>out of the response body would give us real page titles for free. Future pass.Strict Google grounding-chip compliance. Google's grounding ToS technically says display
searchEntryPoint.renderedContentverbatim. We're displaying the Markdown-equivalent (webSearchQueries[]linked to google.com/search) because the rendered chip doesn't survive Obsidian's Markdown renderer. If Google enforces this strictly we may need a separate "open the chip in a popup" affordance — for now we believe the spirit-of-the-terms is satisfied.Gemini-specific preamble pipeline. Right now
Ask Geminiruns the user's question straight through — noloadPreamble+expandIncludespass like the directory-template service does. Adding it is a one-line refactor (call the same helpers fromdirectoryTemplateService.tsbefore building the request body) and would letpartials/andpreambles/apply to ad-hoc Gemini queries too.Hook Gemini into directory templates. A template that declares
provider: geminiin itscftfence should be routable through the Gemini service the same way the existing templates route through Perplexity. Same template engine, different transport.The editorial-stance partial extraction (deferred from the previous shipping pass — the anti-incumbent stance is still duplicated across
concept-profile.mdandvocabulary-profile.md).
Files touched
src/services/geminiService.ts (new, 320 lines)
src/modals/GeminiModal.ts (new, 140 lines)
main.ts (~80 lines net — types, defaults, init,
commands, settings section)