# Google Gemini (Gemini Developer API) — provider research for PolyLLM Last documentation audit: **2026-09-08**. SDK probed: `@google/genai` **2.21.0** (Node 25.9, tsx 4.23). Probe scripts and raw outputs: `research/gemini/*.ts`, `research/gemini/out/*.json` (keys redacted). Key used: free-tier `AQ.`-style ("auth") key. > Headline for the adapter: `generateContent` / `streamGenerateContent` remain **fully supported** but Google now labels them legacy and documents everything on the new **Interactions API** (`ai.interactions.create`, GA June 2026, "recommended for all new projects", all new features land there first). This audit targets `generateContent` (what the SDK's `ai.models.*` uses) and notes Interactions where relevant. --- ## 1. Endpoint, auth, headers | Item | Value (verified by probe unless marked docs) | |---|---| | Base URL | `https://generativelanguage.googleapis.com` | | API version | `v1beta` (SDK default; needed for computer use / MCP / preview features). `v1` also works: probed `gemini-3.8-flash` + `thinkingLevel` on `v1` -> OK. Select via `new GoogleGenAI({ apiKey, httpOptions: { apiVersion: "v1" } })`. | | Auth header | `x-goog-api-key: ` (works with `AQ.` keys). `?key=` query param also works (200). **`Authorization: Bearer ` does NOT work**: 401 `UNAUTHENTICATED`, reason `ACCESS_TOKEN_TYPE_UNSUPPORTED` (Bearer is only for the OpenAI-compat endpoint). | | Other headers | `Content-Type: application/json`. No rate-limit headers are returned (only `server-timing`, `vary`, `alt-svc`). | | Key formats | New keys created in AI Studio are "auth keys" bound to a service account (`AQ.` prefix). Docs: **standard (legacy) keys will be rejected after September 2026** — expect BYOK users with old keys to break. Leaked keys: `"Your API key was reported as leaked. Please use another API key."` | | SDK env vars | `GEMINI_API_KEY` or `GOOGLE_API_KEY` (`GOOGLE_API_KEY` wins if both). Pass `apiKey` explicitly in a BYOK app. | | OpenAI-compat | `https://generativelanguage.googleapis.com/v1beta/openai/` with `Authorization: Bearer `; `reasoning_effort` maps to thinking levels (3.x) or budgets 1024/8192/24576 (2.5); Gemini extras via `extra_body.google.*`. Beta. Not probed. | Docs: https://ai.google.dev/gemini-api/docs/api-key , https://ai.google.dev/gemini-api/docs/api-versions , https://ai.google.dev/gemini-api/docs/openai ## 2. SDK ```bash pnpm add @google/genai@latest # 2.21.0 on 2026-09-08; Node >= 20 (3.x will require Node 22+) ``` ```ts import { GoogleGenAI } from "@google/genai"; const ai = new GoogleGenAI({ apiKey, httpOptions: { timeout: 120_000 /* ms */, apiVersion: "v1beta" } }); ``` - `ai.models.generateContent / generateContentStream / countTokens / list / get`, `ai.chats.create` (client-side history helper), `ai.caches`, `ai.files`, `ai.batches`, `ai.live`, `ai.interactions` (new), `mcpToTool()`. - Errors: `ApiError { name: "ApiError", status: , message: }` — message is the raw `{"error":{code,message,status,details}}` JSON; parse it. Retries: `httpOptions.retryOptions` exists (defaults not documented for JS). - `config.abortSignal` supported on generate calls. - Legacy `@google/generative-ai` is deprecated (since 2025-11-30) — do not use. - SDK-side guard: `toolConfig.functionCallingConfig.streamFunctionCallArguments` throws `"streamFunctionCallArguments parameter is only supported in Gemini Enterprise Agent Platform mode, not in Gemini Developer API mode."` (no partial-args streaming on the Developer API). Docs: https://github.com/googleapis/js-genai , https://ai.google.dev/gemini-api/docs/libraries ## 3. generateContent / streamGenerateContent REST: `POST /v1beta/models/{model}:generateContent` and `POST /v1beta/models/{model}:streamGenerateContent?alt=sse`. Request body: `contents[]` (`{role:"user"|"model", parts:[...]}`), `systemInstruction` (`{parts:[{text}]}`), `tools[]`, `toolConfig`, `safetySettings[]`, `generationConfig`, `cachedContent`, `serviceTier`, `store`. Response: `candidates[0].content.parts[]`, `candidates[0].finishReason`, `usageMetadata`, `modelVersion`, `responseId`, `promptFeedback`, `modelStatus`. Part fields seen in probes: `text`, `thought: true` (thought summary), `thoughtSignature` (base64 string, 300–700 chars), `functionCall {name, args, id}`, `functionResponse`, `inlineData {mimeType, data}`, `fileData {fileUri, mimeType}`, `executableCode {language:"PYTHON", code, id}`, `codeExecutionResult {outcome:"OUTCOME_OK", output, id}`. FinishReason enum (SDK 2.21): `STOP, MAX_TOKENS, SAFETY, RECITATION, LANGUAGE, OTHER, BLOCKLIST, PROHIBITED_CONTENT, SPII, MALFORMED_FUNCTION_CALL, IMAGE_SAFETY, UNEXPECTED_TOOL_CALL, TOO_MANY_TOOL_CALLS, IMAGE_PROHIBITED_CONTENT, NO_IMAGE, IMAGE_RECITATION, IMAGE_OTHER`. Docs: https://ai.google.dev/api/generate-content , https://ai.google.dev/gemini-api/docs/text-generation ### 3.1 Streaming protocol (verified) - REST with `alt=sse`: `Content-Type: text/event-stream`; each event is `data: {GenerateContentResponse JSON}` terminated by **CRLF CRLF** (`\r\n\r\n`); no `event:` lines, no `[DONE]` sentinel — the stream simply ends. Errors before the first token come back as a normal HTTP error status (400 etc.) but still with `text/event-stream` content type and a JSON `{error}` body — check `res.ok` before parsing SSE. - REST without `alt=sse`: `application/json` **JSON array** of responses (`[{...},{...}]`) streamed incrementally — avoid. - SDK: `for await (const chunk of await ai.models.generateContentStream({...}))`, each chunk is a full `GenerateContentResponse` (`chunk.text` getter concatenates non-thought text). - Chunk shape observed (Gemini 3.x Flash): 2–4 chunks for short answers. With `includeThoughts: true` the **first chunk** carries `parts:[{text:"…", thought:true}]`; then text chunks; the **last chunk** has `finishReason` and typically an **empty text part carrying `thoughtSignature`** (`{text:"", thoughtSignature:"…"}`) — do not render it as text, but keep it if you replay history. - `usageMetadata` is present on **every** chunk. On 3.5/3.6 Flash the first chunk has only `promptTokenCount/totalTokenCount/promptTokensDetails/serviceTier`; subsequent chunks have the full set (`candidatesTokenCount`, `thoughtsTokenCount`). On 3.7 Flash all chunks were full. Rule: **take usage from the last chunk**. - Function calls stream as a single chunk: `parts:[{functionCall:{name,args,id:"call_…"}, thoughtSignature:"…"}]`, followed by a final chunk with an empty `text` part and `finishReason:"STOP"` (not `FUNCTION_CALL`). Args are complete (never partial). ### 3.2 usageMetadata fields `promptTokenCount`, `candidatesTokenCount`, `thoughtsTokenCount` (absent when 0), `cachedContentTokenCount` (absent when 0), `toolUsePromptTokenCount`, `totalTokenCount` (= prompt + candidates + thoughts), `promptTokensDetails[{modality:"TEXT"|"IMAGE"|…, tokenCount}]`, `cacheTokensDetails[]`, `candidatesTokensDetails[]`, `serviceTier:"standard"`. Thinking tokens are billed as output. ### 3.3 Exact SDK code that worked ```ts const stream = await ai.models.generateContentStream({ model: "gemini-3.5-flash-lite", contents: [{ role: "user", parts: [{ text: "Count from 1 to 12, then say DONE." }] }], config: { systemInstruction: "You are terse.", maxOutputTokens: 2000, // includes thinking tokens! thinkingConfig: { includeThoughts: true, thinkingLevel: "LOW" }, // never combine with thinkingBudget }, }); let usage; for await (const chunk of stream) { for (const part of chunk.candidates?.[0]?.content?.parts ?? []) { if (part.thought) emitReasoning(part.text); else if (part.text) emitText(part.text); // may be "" on the final signature-only part if (part.functionCall) emitToolCall(part.functionCall.id, part.functionCall.name, part.functionCall.args, part.thoughtSignature); } if (chunk.candidates?.[0]?.finishReason) finish = chunk.candidates[0].finishReason; usage = chunk.usageMetadata ?? usage; // last chunk wins } ``` ## 4. generationConfig parameter support (probed matrix) Probed with `maxOutputTokens: 1500` on the models the free-tier key can call. Legend: OK / 400 "…" = exact server message / — = not probed (quota). | param | 3.8-flash | 3.5-flash | 3.5-flash-lite | 3.1-flash-lite | 3-flash-preview | gemma-4-31b-it | |---|---|---|---|---|---|---| | temperature 0 / 1.5 / 2 | OK | OK | OK | OK | OK | 2 OK; 0,1.5 → 500 INTERNAL (flaky) | | temperature 2.5 | 400 `* GenerateContentRequest.generation_config.temperature: temperature must be in the range [0.0, 2.0].` | same | same | same | same | same | | topP 0.9 | OK | OK | OK | OK | OK | 500 INTERNAL (flaky) | | topK 40 | — | OK | OK | OK | OK | OK | | seed 42 | — | OK | OK | OK | OK | OK | | stopSequences ["DONE"] | — | OK (stopped before DONE) | OK | OK | OK | 500 INTERNAL (flaky) | | frequencyPenalty 0.5 | — | 400 `Penalty is not enabled for this model` | same | same | same | same | | presencePenalty 0.5 | — | 400 `Penalty is not enabled for this model` | same | same | same | same | | candidateCount 2 | 400 `Multiple candidates is not enabled for this model` | same | same | same | same | same | | responseMimeType application/json | — | OK | OK | OK | OK | 500 (flaky) | | responseSchema (OpenAPI, `OBJECT/STRING`) | — | OK | OK | OK | OK | OK | | responseJsonSchema (JSON Schema) | — | OK | OK | OK | OK | OK | | responseMimeType text/x.enum + enum schema | — | OK (`blue`) | OK | OK | OK | OK | | thinkingConfig.thinkingBudget 0 | — | OK (0 thoughts) | 400 `Request contains an invalid argument.` | OK | OK | 400 `Thinking budget is not supported for this model.` | | thinkingBudget 1024 / -1 | — | — | OK / OK | OK / OK | OK / OK (-1 → 1436 thought tokens) | 400 same | | includeThoughts true | — | OK (thought parts) | OK | OK | OK | 500 (flaky) | | thinkingLevel MINIMAL | docs: **error** | — | OK (0 thoughts) | OK (0) | OK (0) | 500 (flaky) | | thinkingLevel LOW / MEDIUM / HIGH | — | — | OK | OK | HIGH, MEDIUM OK; LOW — | 400 `Thinking level is not supported for this model.` (HIGH → 500) | | thinkingLevel + thinkingBudget | — | — | 400 `You can only set only one of thinking budget and thinking level.` | same | same | same | | responseLogprobs/logprobs | 400 `Logprobs is not enabled for this model` | — | same | same | — | same | Notes: - **Sampling deprecation**: changelog 2026-07-21 — "The sampling parameters `temperature`, `top_p` and `top_k` are now deprecated" (for 3.6 Flash / 3.5 Flash-Lite onward; the 3.8 guide says "Strip `temperature`, `top_p`, and `top_k` from generation configs"). They are still **accepted** by `generateContent` (probed) but Google recommends not sending them; Gemini 3 guide: "strongly recommend keeping the temperature parameter at its default value of 1.0" (lower values can cause looping). Adapter: default to not sending temperature/topP/topK for Gemini 3.x unless the user overrides. - Default temperature/topP/topK reported by the models endpoint: 1 / 0.95 / 64 (maxTemperature 2) for all Gemini text models. - `maxOutputTokens` **includes thinking tokens**: `gemini-3.8-flash` with `maxOutputTokens: 100` returned empty text, `finishReason: MAX_TOKENS`, `thoughtsTokenCount: 97`. Give thinking models a generous budget (>= 1024 + expected answer) or disable thinking. - `stopSequences` are honoured but the stop string itself is stripped. ## 5. Reasoning controls per family | Family | Default | Controls | Probe evidence | |---|---|---|---| | Gemini 3.8 / 3.7 Flash | `thinkingLevel` **medium** | `low, medium, high`; `minimal` → error (docs). `thinkingBudget` still accepted "for backward compatibility" per Gemini 3 guide but 3.8 guide says replace it. Cannot be disabled. | 3.8: thoughts 199–330 on trivial prompts. | | Gemini 3.6 Flash | medium | `minimal, low, medium, high` | stream has thought part first. | | Gemini 3.5 Flash | medium (docs) | all four levels (docs); `thinkingBudget: 0` **does** disable (probed). | thoughts 136–515 by default. | | Gemini 3.5 Flash-Lite / 3.1 Flash-Lite | **minimal (off)** | all four levels; `thinkingBudget` 1024 / -1 OK; `thinkingBudget: 0` rejected on 3.5-lite (use MINIMAL) but OK on 3.1-lite. | thoughts 0 unless requested. | | Gemini 3 Flash Preview / 3.1 Pro Preview | high (dynamic) | `minimal, low, medium, high`; budgets 0/1024/-1 OK on 3-flash-preview. | | | Gemini 2.5 Pro / Flash / Flash-Lite | on / on / off | `thinkingBudget` (Pro 128–32768 cannot disable; Flash 0–24576; Lite 512–24576; -1 dynamic) + `thinkingLevel` low/medium/high (docs). | **Not callable with new keys (404)** — docs only. | | Gemma 4 | thinks by default | **no control**: both `thinkingBudget` and `thinkingLevel` → 400. | thoughts 47–430. | - `includeThoughts: true` → thought **summaries** as parts with `thought: true` (streamed first). Raw reasoning is never returned. - Thinking tokens: `usageMetadata.thoughtsTokenCount`, billed at the output rate. - SDK enum: `ThinkingLevel.MINIMAL|LOW|MEDIUM|HIGH` (strings `"MINIMAL"`… accepted; Interactions API uses lowercase). Docs: https://ai.google.dev/gemini-api/docs/thinking , https://ai.google.dev/gemini-api/docs/gemini-3 , https://ai.google.dev/gemini-api/docs/latest-model ### 5.1 Thought signatures (critical for tool calling) - Gemini 3.x attaches `thoughtSignature` to the **functionCall part** (and to the last text/empty part of a text answer, and to `executableCode` parts). - **Function calling is strict**: replaying the model turn without the signature → 400 `INVALID_ARGUMENT`: `Function call is missing a thought_signature in functionCall parts. This is required for tools to work correctly, and missing thought_signature may lead to degraded model performance. Additional data, function call `default_api:get_weather` , position 2. Please refer to https://ai.google.dev/gemini-api/docs/thought-signatures for more details.` (probed on gemini-3.7-flash). - Echoing the real signature → OK. The documented escape hatch `thoughtSignature: "skip_thought_signature_validator"` on the functionCall part → accepted (probed) — useful when importing history from another provider. - Text-only multi-turn **without** signatures works (probed): signatures on text parts are optional. - Adapter rule: persist `thoughtSignature` alongside each tool call (and ideally each assistant part) in the conversation store and replay it verbatim in `contents`. ## 6. Tool calling (function declarations) ```ts const tools = [{ functionDeclarations: [{ name: "get_weather", description: "Get weather for a city.", parametersJsonSchema: { type: "object", properties: { city: { type: "string" } }, required: ["city"] }, // JSON Schema (lowercase types) // or legacy: parameters: { type: "OBJECT", properties: { city: { type: "STRING" } }, required: ["city"] } }]}]; // model turn → parts: [{ functionCall: { name, args, id: "call_88210" }, thoughtSignature }] // reply: contents.push({ role: "model", parts: modelPartsVerbatim }); // keep thoughtSignature! contents.push({ role: "user", parts: [{ functionResponse: { name, id, response: { /* any JSON object */ } } }] }); ``` - `toolConfig.functionCallingConfig`: `mode: AUTO | ANY | NONE | VALIDATED`, `allowedFunctionNames[]`. `ANY` + `allowedFunctionNames` probed OK on 3.6 Flash (forced call). - Parallel calls arrive as multiple `functionCall` parts in one turn; send all `functionResponse` parts in one user turn (same order). - `functionCall.id` is present on 3.x (`call_NNNNN`); echo it in `functionResponse.id`. - Built-in tools **can be combined with function declarations on Gemini 3** (not on 2.5). Structured output + tools also allowed on Gemini 3. - SDK automatic function calling exists (`config.automaticFunctionCalling`) — disable it in a UI adapter (`{ disable: true }`) to keep control of the loop. - MCP: `mcpToTool(client)` in the SDK (experimental) and server-side `tools:[{ mcpServers: [...] }]` (v1beta, docs: HTTP transport only). Docs: https://ai.google.dev/gemini-api/docs/function-calling (now Interactions-only; legacy shapes verified by probe and SDK types) ## 7. Structured output / JSON schema - `responseMimeType: "application/json"` alone → valid JSON of free shape (one model returned an array). - `responseJsonSchema` (standard JSON Schema: lowercase types, `additionalProperties`, `anyOf`, `$ref:"#"`, `enum`, `format` date/time, `minimum/maximum`, `items/prefixItems/minItems/maxItems`) → **preferred**; probed OK on all callable models incl. Gemma 4. - `responseSchema` (legacy OpenAPI `Schema` with uppercase `TYPE`s, `propertyOrdering`) → still OK. Don't send both. - `responseMimeType: "text/x.enum"` + `{type:"STRING", enum:[…]}` → bare enum value (probed). - Works together with thinking (JSON is in non-thought parts) and, on Gemini 3, with tools. Docs: https://ai.google.dev/gemini-api/docs/structured-output ## 8. Modalities | Modality | How | Limits / notes | |---|---|---| | Image in | `inlineData {mimeType, data(base64)}` or `fileData {fileUri}`; PNG/JPEG/WebP/HEIC/HEIF; inline request total <= 20 MB; up to 3,600 images | Probed 2x2 PNG → `promptTokensDetails IMAGE 1089` tokens on 3.8/3.5 Flash (Gemini 3 default media resolution is high; docs' "258 tokens" applies to older models / low res). Use `mediaResolution` to cut cost. | | PDF | `inlineData application/pdf` or Files API; <= 50 MB, <= 1000 pages, ~258 tokens/page | Not probed. | | Audio in | wav/mp3/aiff/aac/ogg/flac/m4a/opus/webm…; 32 tokens/s; <= 9.5 h/prompt; >20 MB via Files API | Not probed. | | Video in | inline <100 MB, Files API up to 2 GB (free) / 20 GB (paid), YouTube URLs via `fileData.fileUri`; ~100 tok/s low / 300 tok/s high res; 3.5+ have "agentic" video mode | Not probed. | | Audio out | **Not via chat models.** TTS models (`gemini-3.1-flash-tts-preview`, `gemini-2.5-*-preview-tts`) with `responseModalities:["AUDIO"]` + `speechConfig` → `inlineData audio/L16 24 kHz PCM`; 32k context; Live API (`bidiGenerateContent`, WebSocket) for realtime voice. | Not probed. | | Image out | Image models only (`gemini-3.1-flash-image`, `gemini-3.1-flash-lite-image`, `gemini-3-pro-image`, `gemini-2.5-flash-image`), `responseModalities:["TEXT","IMAGE"]`, `imageConfig {aspectRatio, imageSize "512px"/"1K"/"2K"/"4K"}` → `inlineData image/png`; SynthID watermark; priced per image (~$0.045–0.151). | Not probed. | | Video out | `gemini-omni-1.1-flash` / `gemini-omni-flash-preview` (paid tier only), Veo 3.1 via `predictLongRunning`. | Not chat models. | Docs: image-understanding, document-processing, audio, video-understanding, speech-generation, image-generation, files pages under https://ai.google.dev/gemini-api/docs/ ## 9. Files API `ai.files.upload({file, config:{mimeType}})` → `{uri, mimeType, state}`; poll until `ACTIVE`; reference with `fileData:{fileUri, mimeType}`. 48 h retention, 2 GB/file, 20 GB/project. Use when the request exceeds 20 MB (100 MB for video per newer docs). Not probed. ## 10. System instruction `config.systemInstruction` (string or `Content`) — probed OK on 3.5/3.6/3.7/3.8 Flash ("BLUE" uppercase obeyed). Not probed on Gemma 4 (historically Gemma rejected developer instructions on this API; Gemma 4 docs say the system role is now supported — verify before enabling). ## 11. Conversation state - `generateContent` is **stateless**: send the full `contents` history each turn (`ai.chats` is a client-side helper only). - Server-side state exists only in the **Interactions API** (`previous_interaction_id`, `store` default true; paid tier retains 55 days, free 1 day). Probed `ai.interactions.create({model, input, generation_config:{thinking_level:"low"}, store:false})` → works with this key; response has `steps[]`, `output_text`, `usage{total_input_tokens,total_output_tokens,total_thought_tokens,total_cached_tokens,total_tool_use_tokens}`. Consider it for a later "Gemini v2" adapter; it lacks Batch, explicit caching and custom safety settings today. ## 12. Context caching - **Implicit** caching is on by default for 2.5+ (min 4,096 tokens on 3.x Flash / 3.1 Pro, 2,048 on 2.5); savings show as `usageMetadata.cachedContentTokenCount`; cached input billed at ~10 % (e.g. 3.8 Flash $0.075/M). Put static content first. - **Explicit**: `ai.caches.create({model, config:{contents, systemInstruction, tools, ttl:"300s"|expireTime, displayName}})` → pass `config.cachedContent = cache.name`. Models with `createCachedContent` in `supportedGenerationMethods` (all Gemini text models; not Gemma/omni). Storage $0.50–4.50 /M tokens/hour. Probe on 2.5-flash hit the 404 (model gone); not re-probed. Docs: https://ai.google.dev/gemini-api/docs/caching , https://ai.google.dev/api/caching ## 13. Built-in tools - **Google Search grounding**: `tools:[{googleSearch:{}}]`. Response `candidates[0].groundingMetadata { webSearchQueries[], searchEntryPoint{renderedContent: HTML chip — must be displayed per ToS}, groundingChunks[{web:{uri,title}}], groundingSupports[{segment{startIndex,endIndex,text}, groundingChunkIndices[], confidenceScores[]}] }` (docs + SDK types; probe hit 429 on both attempts — **unverified**). Pricing: 3.x — 5,000 free requests/month shared, then $14/1,000; 2.5 — 1,500 free RPD then $35/1,000. Legacy `googleSearchRetrieval` only for 1.5. - **URL context**: `tools:[{urlContext:{}}]`, <= 20 URLs, 34 MB each, `candidates[0].urlContextMetadata`; billed as input tokens. - **Code execution**: `tools:[{codeExecution:{}}]` probed OK on 3.6 Flash → parts `executableCode{language:"PYTHON", code, id}` + `codeExecutionResult{outcome:"OUTCOME_OK", output, id}` (both also carry `thoughtSignature`), then text. 30 s runtime, matplotlib only for plots, billed as tokens. - **File Search** (RAG): stores + `tools:[{fileSearch:{fileSearchStoreNames:[…]}}]`, citations with `media_id`/page numbers; indexing $0.15/M embedding tokens, storage free. Not probed. - **Computer use**: `tools:[{computerUse:{environment:"browser"|"mobile"|"desktop"}}]` on 3.8/3.7/3.5 Flash(-Lite) (preview, documented for Interactions). Not probed. - **Google Maps grounding**, **MCP servers** tool: v1beta. Not probed. ## 14. Safety settings `safetySettings:[{category, threshold}]`; categories `HARM_CATEGORY_HARASSMENT | HATE_SPEECH | SEXUALLY_EXPLICIT | DANGEROUS_CONTENT` (+ `CIVIC_INTEGRITY` in SDK enum); thresholds `OFF | BLOCK_NONE | BLOCK_ONLY_HIGH | BLOCK_MEDIUM_AND_ABOVE | BLOCK_LOW_AND_ABOVE`. Default for 2.5/3 models is **OFF**. Blocked prompt → `promptFeedback.blockReason` (`SAFETY|OTHER|BLOCKLIST|PROHIBITED_CONTENT|IMAGE_SAFETY`) with no candidates; blocked answer → `finishReason: SAFETY` + `safetyRatings`. Probe was routed to 2.5-flash-lite (404) — not re-probed. Not supported on the Interactions API. ## 15. Rate limits and tiers - Tiers: Free, Tier 1 (billing linked), Tier 2 ($100 spent + 3 days), Tier 3 ($1,000 + 30 days). Dimensions: RPM, TPM (input), RPD (resets midnight Pacific) + rolling 10-minute spend caps ($10/$50/$200). Per-model tables are only shown in AI Studio (`https://aistudio.google.com/rate-limit`), not in docs. - Observed free-tier quotas (from 429 `google.rpc.QuotaFailure` details): `GenerateRequestsPerDayPerProjectPerModel-FreeTier` **quotaValue 20** for `gemini-3.8-flash`; per-minute quota trips after ~5–10 requests; `gemini-3.1-pro*`, `gemini-pro-latest`, `gemini-omni-*` → **limit 0** (paid only). Quotas are **per model**, so parallelising across models is fine. - 429 body includes `details[]`: `google.rpc.Help`, `google.rpc.QuotaFailure{violations[{quotaMetric, quotaId, quotaDimensions{model,location}, quotaValue}]}`, `google.rpc.RetryInfo{retryDelay:"24s"}` → honour `retryDelay`. - 503 `UNAVAILABLE` "This model is currently experiencing high demand…" was frequent on 3.8-flash and gemini-flash-latest (transient; retry with backoff). Gemma 4 returned 500 `INTERNAL` on ~50 % of calls (retry). Docs: https://ai.google.dev/gemini-api/docs/rate-limits ## 16. Errors Body: `{"error":{"code":,"message":"…","status":"","details":[…]}}`. Seen: | HTTP | status | Example | |---|---|---| | 400 | INVALID_ARGUMENT | temperature range, `Penalty is not enabled for this model`, `Multiple candidates is not enabled for this model`, `Logprobs is not enabled for this model`, `You can only set only one of thinking budget and thinking level.`, missing thought_signature, `Request contains an invalid argument.` | | 401 | UNAUTHENTICATED | invalid key: `Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential…`, `details[0].reason: "ACCESS_TOKEN_TYPE_UNSUPPORTED"`. (Docs list 400 `API_KEY_INVALID` for malformed keys — with `AQ.`-style bogus key we got 401.) | | 403 | PERMISSION_DENIED | key lacks permission / wrong project (docs) | | 404 | NOT_FOUND | unknown model: `models/gemini-9-ultra is not found for API version v1beta, or is not supported for generateContent. Call ModelService.ListModels…`; **retired-for-new-users**: `This model models/gemini-2.5-flash is no longer available to new users. Please update your code to use models/gemini-3.6-flash for the latest features and improvements. We recommend you to use the Interactions API.` | | 429 | RESOURCE_EXHAUSTED | quota (see §15) | | 500 | INTERNAL | `Internal error encountered.` (Gemma 4, transient) | | 503 | UNAVAILABLE | high demand | | 504 | DEADLINE_EXCEEDED | docs | Retry: exponential backoff with jitter on 429/500/503/504 (and network), honour `RetryInfo.retryDelay`; never retry 400/401/403/404. Streaming errors mid-stream: SDK throws from the iterator. Timeouts: set `httpOptions.timeout` (ms); long outputs with thinking can take >60 s — use 120 s+ for streaming. Docs: https://ai.google.dev/gemini-api/docs/troubleshooting , https://ai.google.dev/gemini-api/docs/api-errors (now describes Interactions snake_case codes) ## 17. Token counting `ai.models.countTokens({model, contents})` → `{totalTokens}` (free, no quota cost documented). REST `:countTokens` with `generateContentRequest{contents, systemInstruction, tools}` counts everything (probed: 11 tokens text-only → 51 with system + tool) and returns `promptTokensDetails`. Rules of thumb: ~4 chars/token; image 258 tokens (<=384 px) or per 768 px tile — but Gemini 3 high res gave 1,089 for a tiny PNG; audio 32 tok/s; video ~100–300 tok/s. ## 18. Model listing `GET /v1beta/models?pageSize=1000` (paginated via `nextPageToken`; SDK `ai.models.list()` / `ai.models.get({model})`). 54 models on 2026-09-08. Fields: `name`, `version`, `displayName`, `description`, `inputTokenLimit`, `outputTokenLimit`, `supportedGenerationMethods[]` (SDK renames to `supportedActions`), `temperature`, `maxTemperature`, `topP`, `topK`, `thinking` (boolean). Filter chat models with `supportedGenerationMethods.includes("generateContent")` and exclude by name pattern (`-image`, `-tts`, `lyria`, `veo`, `transcribe`, `live`, `native-audio`, `robotics`, `computer-use`, `deep-research`, `antigravity`, `embedding`, `aqa`). **The list includes models the key cannot call** (2.5 family → 404 for new users; paid-only → 429 limit 0), so the picker must tolerate per-model failures. ## 19. Pricing (paid tier, USD per 1M tokens; pricing page 2026-09-08) | Model | Input | Cached | Output (incl. thinking) | |---|---|---|---| | 3.8 / 3.7 / 3.6 Flash | 0.75 (1.50 from 2027-01-01) | 0.075 (0.15) | 3.75 (7.50) | | 3.5 Flash | 1.50 | 0.15 | 9.00 | | 3.5 Flash-Lite | 0.30 | 0.03 | 2.50 | | 3.1 Flash-Lite | 0.25 (audio 0.50) | n/a | 1.50 | | 3 Flash Preview | 0.50 (audio 1.00) | n/a | 3.00 | | 3.1 Pro Preview | 2.00 / 4.00 (>200k) | 0.20 / 0.40 | 12.00 / 18.00 | | 2.5 Pro | 1.25 / 2.50 | 0.125 / 0.25 | 10.00 / 15.00 | | 2.5 Flash | 0.30 (audio 1.00) | 0.03 | 2.50 | | 2.5 Flash-Lite | 0.10 (audio 0.30) | 0.01 | 0.40 | | Omni Flash | 1.50 | n/a | 9.00 text / 17.50 video | | Gemma 4 | free (free tier only; "Not available" on paid) | | | Batch = 50 % off; priority tier = 1.8x; cache storage $0.50–4.50 /M/h; Search grounding see §13. Free tier: prompts may be used for product improvement. Docs: https://ai.google.dev/gemini-api/docs/pricing ## 20. Lifecycle, aliases, deprecations - Stable ids don't change (`gemini-3.6-flash`); previews get >= 2 weeks' notice; `-latest` aliases are hot-swapped with a 2-week email notice: `gemini-flash-latest` → 3.5 Flash (changelog), `gemini-flash-lite-latest` → 3.5 Flash-Lite (probed), `gemini-pro-latest` → 3.1 Pro (quota dimension). - Shut down: all `gemini-2.0-*` (2026-06-01), `gemini-3.1-flash-lite-preview` (2026-05-25; the id still answers as 3.1-flash-lite), 2.5 previews. **Gemini 2.5 Pro/Flash/Flash-Lite are closed to new users** (404) though still listed and priced. - Preview shutdown windows are 3–9 months. Legacy JS SDK deprecated 2025-11-30. Standard API keys rejected after Sept 2026. Docs: https://ai.google.dev/gemini-api/docs/deprecations , https://ai.google.dev/gemini-api/docs/changelog , https://ai.google.dev/gemini-api/docs/models ## 21. Probe results summary | Probe | 3.8-flash | 3.7-flash | 3.6-flash | 3.5-flash | 3.5-flash-lite | 3.1-flash-lite | 3-flash-preview | gemma-4-31b-it | 3.1-pro-preview | 2.5-flash / 2.5-pro | omni-1.1-flash | |---|---|---|---|---|---|---|---|---|---|---|---| | generateContent + systemInstruction | OK | OK | OK | OK | OK (no sysinstr) | OK (no sysinstr) | OK (no sysinstr) | OK (no sysinstr) | 429 limit 0 | 404 gone | 429 limit 0 | | stream (chunk shape, usage) | 503 then quota | OK 3 chunks, usage full each | OK 4 chunks | OK 3 chunks | — | — | — | — | — | 404 | — | | params matrix | partial (RPD 20) | — | — | most | full | full | most | full (flaky 500s) | — | 404 | — | | function call stream + round trip | 503/429 | OK (+signature tests) | OK | OK | OK | OK | — | — | — | 404 | — | | responseJsonSchema | — | — | OK | OK | OK | OK | OK | OK | — | 404 | — | | vision (inline PNG) | OK 1089 img tokens | — | — | OK | — | — | — | — | 429 | 404 | 429 | | googleSearch grounding | 429 | — | 429 | — | — | — | — | — | — | 404 | — | | codeExecution | 429 | — | OK | — | — | — | — | — | — | — | — | | invalid key | 401 UNAUTHENTICATED (REST + SDK) | | | | | | | | | | | | countTokens / models.get / v1 apiVersion / interactions.create | OK / OK / OK / OK | | | | | | | | | | | Not probed (docs only): audio/video/PDF input, Files API, explicit caching, safety settings behaviour, TTS/image output, Live API, File Search, computer use, MCP, Batch, 3.1 Pro & omni anything (paid tier), 2.5 anything (closed to new users). ## 22. Gotchas for the adapter (checklist) 1. Use `x-goog-api-key`, never Bearer. Expect 401 (not 400/403) for bad keys. 2. Filter the model list by `generateContent` + name patterns, and **probe-or-handle 404 "no longer available to new users"** (2.5 family) and 429 "limit: 0" (paid-only) gracefully — hide or badge those models. 3. Gemini 3.x: prefer `thinkingLevel` (uppercase enum in SDK), never send both level and budget, `MINIMAL` errors on 3.7/3.8, Gemma rejects both. Map PolyLLM "reasoning effort" → level; "off" → `MINIMAL` on lite/3.5-flash-lite, `thinkingBudget: 0` on 3.5 Flash / 3.1-lite / 3-flash-preview, and "not disableable" on 3.6+/Pro. 4. `maxOutputTokens` includes thoughts — floor it (e.g. >= 2048) when thinking is on, or you get empty text with `MAX_TOKENS`. 5. Store and replay `thoughtSignature` on tool-call parts (400 otherwise); `skip_thought_signature_validator` is the documented escape hatch. Strip empty signature-only text parts from the UI but keep them in history. 6. Usage: read `usageMetadata` from the last chunk; thoughts = `thoughtsTokenCount`, cache hits = `cachedContentTokenCount`. 7. Penalties, `candidateCount > 1`, logprobs → 400 on every model: don't expose them for Gemini. Temperature/topP/topK are accepted but deprecated for 3.6+; don't send by default. 8. SSE events are CRLF-delimited `data:` lines with no terminator; HTTP error status can arrive with `text/event-stream` content type. 9. Quotas are per model; free tier is tiny (20 RPD on 3.8-flash) — surface `RetryInfo.retryDelay` to the user. 10. Consider a future switch to the Interactions API (server-side state, unified steps); today `generateContent` is stable and complete for chat.