SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
31.8 KB

# 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: <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

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: <http>, message: <JSON string of the body> } — 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 TYPEs, 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":<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; -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.