Skip to content

Add Google Gemini as a course-content provider (and fix provider-key gating) - #3

Merged
daTechGuy merged 2 commits into
mainfrom
feat/gemini-provider
Oct 4, 2026
Merged

daTechGuy merged 2 commits into
mainfrom
feat/gemini-provider

Conversation

@daTechGuy

Copy link
Copy Markdown
Owner

Adds Google Gemini as a third course-content provider next to Claude and Ollama Cloud, and fixes a latent bug that blocked non-Claude users.

What it does

  • Backend src/services/llm/gemini.ts — streamGenerateContent over SSE, called directly from the browser (Google answers CORS preflights for the x-goog-api-key header from any origin — verified with a keyless preflight — so no proxy, unlike Ollama). Key goes in a header, never the URL.
  • Setup → API keys: third provider pill, Gemini key + editable model (default gemini-3.8-flash; ids change often). Verify GETs the chosen model, proving the key and the model id. Free-tier privacy note.
  • CLI: --provider gemini, --gemini-model, GEMINI_API_KEY.
  • Handles: thought summaries (requested only with a thinking budget, retried once without if the model rejects it), output-token headroom for thinking, SSE events split across network chunks, blocked prompts/responses (clear message), MAX_TOKENS (returns partial text like the Anthropic path), 429 backoff.
  • Unchanged: "Claude web search" research still needs a Claude key (Gemini ignores Anthropic server tools); Tavily and Wikipedia research work with Gemini as the synthesiser.

Latent bug fixed (also affected Ollama)

Syllabus auto-start, Export "learning outcomes", outline upload and the Research gate all required a Claude key regardless of provider — an Ollama/Gemini user without one was silently blocked (the syllabus never started). They now ask the active provider via a new selectActiveLlm(). friendlyError also learns Google's wording ("API key not valid" is a 400; unknown model 404; RECITATION/BLOCKLIST).

Verification

  • tsc -b, eslint (0 errors, original 7 warnings), vitest --coverage 394 passed (+75), vite build — all green locally.
  • The gating tests were run against the old gating code and fail there (9 failures), pass with the fix.
  • Dialog checked in a real browser (DOM): three equal pills, key/model fields, no overflow.
  • A real call to Google's API (bad-key check in npm run smoke:gemini, needs no key): Google replies 400 INVALID_ARGUMENT: API key not valid…, our parser reads it, friendlyError → "API key rejected".

NOT verified — needs a real key

Unit tests mock fetch, so the success path against the live API hasn't been run (plain text, streaming, thinking, structured-JSON quizzes, model id). Run it yourself — the key is read from your environment and never printed:

GEMINI_API_KEY=AIza... npm run smoke:gemini

Model ids came from Google's docs page; if gemini-3.8-flash isn't available to your key, set another in the dialog (or GEMINI_MODEL=... for the script). Quality on the app's large structured-JSON prompts (quizzes, slides) is the thing to watch — those prompts were tuned on Claude.

Notes

  • Branch protection still requires one approval you can't give yourself (see earlier).

🤖 Generated with Claude Code

…ovider's key

Gemini joins Claude and Ollama Cloud as a course-content provider.

Backend (src/services/llm/gemini.ts)
- streamGenerateContent over SSE, called straight from the browser: Google answers
  CORS preflights for the x-goog-api-key header from any origin (verified with a
  keyless preflight), so no proxy is needed. The key is sent in a header, never the URL.
- Maps Anthropic-style messages to Gemini contents/systemInstruction; requests thought
  summaries only with a thinking budget and retries once without them on a 400 that
  complains about thinking; leaves output-token headroom for thinking (capped at 65,536).
- Streams text and thought summaries; reassembles SSE events split across network
  chunks, CRLF delimiters and a final event with no blank line; explains blocked
  prompts/responses; returns partial text on MAX_TOKENS like the Anthropic path;
  errors keep the HTTP code (so 429 backoff works) and carry .status.
- Router: resolveProvider swaps in the Gemini key/model and ignores the claude-* model ids
  every call site passes (same rule now shared with Ollama).

Store / UI / CLI
- apiStore: geminiApiKey, geminiModel (default gemini-3.8-flash, free-text editable),
  validation state; new selectActiveLlm() = "the key that matters right now".
- Setup keys dialog: third provider pill, Gemini key + model fields; Verify GETs the
  chosen model (proves key AND model id; 404 = unknown model). Free-tier privacy note.
- canvas-course CLI: --provider gemini, --gemini-model, GEMINI_API_KEY.

Fixes a latent bug that also affected Ollama
- Syllabus auto-start, Export learning outcomes, outline upload and the Research gate
  required a *Claude* key regardless of provider, silently blocking anyone using Ollama
  or Gemini without one. They now use selectActiveLlm().
- friendlyError understands Google's wording: "API key not valid" (a 400), unknown model
  (404 → "check the model name"), RECITATION/BLOCKLIST blocks.

Tests (+75): backend contract/streaming/errors, router key+model resolution, store and
persistence upgrade, Setup dialog flows, per-provider gates (verified to fail against the
old gating), error wording. tools/smoke-gemini.ts (npm run smoke:gemini, key from
GEMINI_API_KEY) exercises the live API; its bad-key check ran against Google for real.
Docs: README, CLAUDE.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Oct 3, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 91.25000% with 14 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/services/llm/gemini.ts 90.42% 0 Missing and 9 partials ⚠️
src/pages/SetupPage.tsx 82.60% 0 Missing and 4 partials ⚠️
src/pages/ExportPage.tsx 80.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

…ejection

The test fed a fake .docx to the real parser, which rejects asynchronously; on CI's
timing the rejection escaped the component's try/catch chain and Vitest failed the run
("1 unhandled error") although every test passed. Stub the DOCX parser and LLM
extraction instead, and assert what the test is really about: with only a Gemini /
Ollama / Anthropic key set, extraction runs and receives THAT provider's key.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@daTechGuy
daTechGuy merged commit 7cd2c6b into main Oct 4, 2026
1 check passed
@daTechGuy
daTechGuy deleted the feat/gemini-provider branch October 4, 2026 01:30
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.

2 participants