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/streamGenerateContentremain 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 targetsgenerateContent(what the SDK'sai.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: <key> (works with AQ. keys). ?key= query param also works (200). Authorization: Bearer <api key> 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 <key>; 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
pnpm add @google/genai@latest # 2.21.0 on 2026-09-08; Node >= 20 (3.x will require Node 22+)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: <http>, message: <JSON string of the body> }— message is the raw{"error":{code,message,status,details}}JSON; parse it. Retries:httpOptions.retryOptionsexists (defaults not documented for JS). config.abortSignalsupported on generate calls.- Legacy
@google/generative-aiis deprecated (since 2025-11-30) — do not use. - SDK-side guard:
toolConfig.functionCallingConfig.streamFunctionCallArgumentsthrows"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 isdata: {GenerateContentResponse JSON}terminated by CRLF CRLF (\r\n\r\n); noevent: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 withtext/event-streamcontent type and a JSON{error}body — checkres.okbefore parsing SSE. - REST without
alt=sse:application/jsonJSON array of responses ([{...},{...}]) streamed incrementally — avoid. - SDK:
for await (const chunk of await ai.models.generateContentStream({...})), each chunk is a fullGenerateContentResponse(chunk.textgetter concatenates non-thought text). - Chunk shape observed (Gemini 3.x Flash): 2–4 chunks for short answers. With
includeThoughts: truethe first chunk carriesparts:[{text:"…", thought:true}]; then text chunks; the last chunk hasfinishReasonand typically an empty text part carryingthoughtSignature({text:"", thoughtSignature:"…"}) — do not render it as text, but keep it if you replay history. usageMetadatais present on every chunk. On 3.5/3.6 Flash the first chunk has onlypromptTokenCount/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 emptytextpart andfinishReason:"STOP"(notFUNCTION_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
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_pandtop_kare now deprecated" (for 3.6 Flash / 3.5 Flash-Lite onward; the 3.8 guide says "Striptemperature,top_p, andtop_kfrom generation configs"). They are still accepted bygenerateContent(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.
maxOutputTokensincludes thinking tokens:gemini-3.8-flashwithmaxOutputTokens: 100returned empty text,finishReason: MAX_TOKENS,thoughtsTokenCount: 97. Give thinking models a generous budget (>= 1024 + expected answer) or disable thinking.stopSequencesare 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 withthought: 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
thoughtSignatureto the functionCall part (and to the last text/empty part of a text answer, and toexecutableCodeparts). - 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 calldefault_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
thoughtSignaturealongside each tool call (and ideally each assistant part) in the conversation store and replay it verbatim incontents.
6. Tool calling (function declarations)
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+allowedFunctionNamesprobed OK on 3.6 Flash (forced call).- Parallel calls arrive as multiple
functionCallparts in one turn; send allfunctionResponseparts in one user turn (same order). functionCall.idis present on 3.x (call_NNNNN); echo it infunctionResponse.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-sidetools:[{ 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,formatdate/time,minimum/maximum,items/prefixItems/minItems/maxItems) → preferred; probed OK on all callable models incl. Gemma 4.responseSchema(legacy OpenAPISchemawith uppercaseTYPEs,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. |
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
generateContentis stateless: send the fullcontentshistory each turn (ai.chatsis a client-side helper only).- Server-side state exists only in the Interactions API (
previous_interaction_id,storedefault true; paid tier retains 55 days, free 1 day). Probedai.interactions.create({model, input, generation_config:{thinking_level:"low"}, store:false})→ works with this key; response hassteps[],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}})→ passconfig.cachedContent = cache.name. Models withcreateCachedContentinsupportedGenerationMethods(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:{}}]. Responsecandidates[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. LegacygoogleSearchRetrievalonly 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 → partsexecutableCode{language:"PYTHON", code, id}+codeExecutionResult{outcome:"OUTCOME_OK", output, id}(both also carrythoughtSignature), then text. 30 s runtime, matplotlib only for plots, billed as tokens. - File Search (RAG): stores +
tools:[{fileSearch:{fileSearchStoreNames:[…]}}], citations withmedia_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.QuotaFailuredetails):GenerateRequestsPerDayPerProjectPerModel-FreeTierquotaValue 20 forgemini-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"}→ honourretryDelay. - 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 500INTERNALon ~50 % of calls (retry).
Docs: https://ai.google.dev/gemini-api/docs/rate-limits
16. Errors
Body: {"error":{"code":<http>,"message":"…","status":"<grpc 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;-latestaliases 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)
- Use
x-goog-api-key, never Bearer. Expect 401 (not 400/403) for bad keys. - 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. - Gemini 3.x: prefer
thinkingLevel(uppercase enum in SDK), never send both level and budget,MINIMALerrors on 3.7/3.8, Gemma rejects both. Map PolyLLM "reasoning effort" → level; "off" →MINIMALon lite/3.5-flash-lite,thinkingBudget: 0on 3.5 Flash / 3.1-lite / 3-flash-preview, and "not disableable" on 3.6+/Pro. maxOutputTokensincludes thoughts — floor it (e.g. >= 2048) when thinking is on, or you get empty text withMAX_TOKENS.- Store and replay
thoughtSignatureon tool-call parts (400 otherwise);skip_thought_signature_validatoris the documented escape hatch. Strip empty signature-only text parts from the UI but keep them in history. - Usage: read
usageMetadatafrom the last chunk; thoughts =thoughtsTokenCount, cache hits =cachedContentTokenCount. - 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. - SSE events are CRLF-delimited
data:lines with no terminator; HTTP error status can arrive withtext/event-streamcontent type. - Quotas are per model; free tier is tiny (20 RPD on 3.8-flash) — surface
RetryInfo.retryDelayto the user. - Consider a future switch to the Interactions API (server-side state, unified steps); today
generateContentis stable and complete for chat.