Design
Scope
The deck is a thin client for the Gemini API as Google AI Studio serves it (generativelanguage.googleapis.com/v1beta, an API key in the x-goog-api-key header). It covers generateContent with media, a system instruction, function calling and a tool loop, structured output, thinking and safety settings; countTokens; embeddings; the model list; the Files API with its resumable upload; and context caching. call reaches any other endpoint with the same deadline, cap and retries.
It is one of three LLM decks (openai, anthropic, gemini), one per vendor API rather than one generic client: the wire formats differ in every part a client touches (roles and parts, systemInstruction, functionCall arguments as objects, thought signatures, Google's error body), and a common layer would hide what each vendor does well. A provider-neutral facade is planned as its own deck over the three.
Out of scope, on purpose:
- Streaming (
streamGenerateContent):httpcannot read a response incrementally yet. - Vertex AI: OAuth tokens, regional endpoints and Cloud Storage URIs make it a client variant for later; requests, replies and errors keep their shape there.
- The Interactions API, the Live API, the Batch API and file search stores: the plain HTTP ones are reachable with
call. - Multimodal embeddings:
embedsends text; other parts go throughcall. - Google's OpenAI-compatible endpoint: the
openaideck reaches it by base URL; this deck exists for what that endpoint leaves out (files, caching, thought signatures, safety feedback).
Decisions
- Requests and replies are
json.Valuein Google's own shape. Builders (newRequest,withSystem,addUserText,addTools, ...) and readers (text,toolCalls,usage,blockReason, ...) cover the common fields;json.setandwithConfigreach the rest. Nothing the API knows is out of reach, and nothing is translated. - The model is part of the request:
newRequest(model)writes"model": "models/<id>", as Google'sGenerateContentRequestdoes, so one value carries a whole call (chat,countTokens, a cache, the tool loop) and a whole conversation.chatmoves it into the path. - A shared surface with the openai and anthropic decks, aligned in several rounds:
newRequest,withSystem,addUserText,addReply,tool/addTool/addTools,toolError,runTools/runToolsWith,ToolRun,ToolCall{id, name, input}, theUsagecore (inputTokenswith the cache included,outputTokens,totalTokens,cachedTokens,reasoningTokens; gemini addstoolUseTokens),call(c, method, path, body)with the query inpath, one retry rule,withMaxToolInput, and errors withstatus,retryAfterMsandlimits. Gemini has no strict mode for function schemas, so there is nowithStrict. - Tools: every declaration goes into one
tools[]entry withfunctionDeclarations; other tools such asgoogleSearchstay, and a repeated name is refused. A handler that throws ends the loop with its error unchanged;toolError(message)gives the model an error result to recover from instead. A call whose arguments encode to more thanwithMaxToolInput(1 MiB by default) is answered with atoolErrorwithout calling the handler. - The tool loop does not lose the conversation. After
maxTurnsreplies that still call functions it runs those calls, adds their results and returns withToolRun.stoppedset to"maxTurns"; therequestit returns can be sent again as it is. - Thought signatures:
addReplyand the loop add the model's turn exactly as received (thought parts andthoughtSignatures), then everyfunctionResponseas one user turn in call order with its callid. That satisfies Google's two rules: the first parallel call's signature must come back, and all results follow all calls. - Structured output:
withJsonSchemasendsresponseMimeTypeandresponseJsonSchema, the fields Google's SDKs send, and removes aresponseSchemaor the newerresponseFormat, since they are not sent together. - Thinking:
withThinkingLevel(Gemini 3 and later, sent as the reference's uppercase enum) andwithThinkingBudget(Gemini 2.5, -1 to 32768) each clear the other, as the API refuses both together. - Safety is data. A blocked prompt or a finish reason in
BLOCKED_FINISH_REASONSis a 200 reply with ratings, citations and usage, sochatandrunToolsreturn it andblockReasonnames it.textandtextJsonthrow kind"gemini.blocked"so that code skipping the check still learns of it. The openai and anthropic decks only report a refusal. - Retries are the deck's own loop: a 408, 409, 429 or 5xx answer, and a network failure before the whole request was sent (a
net.NetErrorwithsentfalse: refused, unreachable, a failed lookup or handshake, a reset while writing). Once a request was sent in full it is never sent again, whatever the method, since the server may have acted on it. Every method is retried otherwise,generateContenttoo (Google bills only answered requests). The wait isretry-after-ms, elseretry-after(seconds or an HTTP date), orRetryInfo.retryDelay, whichever is longer; a wait overMAX_RETRY_WAIT_MS(a daily quota) is not taken and the error carries it. - Uploads follow Google's resumable protocol: a start request with the key, then one
upload, finalizerequest of the raw bytes to thex-goog-upload-url, without the key. The data step is not retried and has its own deadline (withUploadTimeout; by default 5 minutes plus 1 minute per 100 MB). - Context caches:
updateCacheTtlsendsupdateMask=ttl, the only field the API lets change.
Errors
One struct, GeminiError{kind, message, status, googleStatus, reason, blockReason, retryAfterMs, limits}, serves two kinds:
"gemini": a request the API refused (statusits HTTP status,googleStatusGoogle'serror.status,reasontheErrorInforeason), or one the deck refused or whose answer was malformed (status0)."gemini.blocked":textortextJsonof a reply without an answer;status200,blockReasonthe block or finish reason,reasonPROMPT_BLOCKEDorRESPONSE_BLOCKED,googleStatusempty.
retryAfterMs is the wait the server named (0 when none, at most 1e12); limits holds the answer's rate-limit headers with lower-case names, at most 32. Generic kinds pass through unchanged: "limit" (the reply cap, the 100 MB request, page caps, json's decoder caps), "timeout", "net" (the net.NetError from http, with reason, sent and replied), "closed" (an answer broken off mid-way) and "cancelled". Callers match the kind and read the fields; no code matches a message.
Security and limits
- The API key travels only in
x-goog-api-key: never in a URL, and never in a message (a server that echoes it has it replaced by<api key>). Keys, model names, resource ids, MIME types,callpaths and queries are checked against strict forms before anything is sent, so caller input cannot inject a path segment, a query, a fragment or a header. - Plain
httpis refused unless the host is this machine orwithPlainHttpallows it;withTlstakes a private CA. No request follows a redirect. - The upload URL the server sends must be on the client's own origin, so a hostile or broken server cannot send the bytes elsewhere.
- Every attempt has a deadline (
withTimeout, 5 minutes by default) and every reply a cap (withMaxBytes,DEFAULT_MAX_BYTES32 MiB). A JSON request is at mostMAX_REQUEST_BYTES(Google's 100 MB), a file at mostMAX_FILE_BYTES(2 GB). - Listings stop after
MAX_PAGESpages (MAX_FILE_PAGESfor files), and all pages of one listing together stay under the reply cap. The tool loop stops atMAX_TOOL_TURNS. Server text in a message is cleaned of control and bidi characters and cut short. - Hostile replies were tried: oversized and endless bodies, slow drips, a negative
Content-Length, a redirect to another host, a gzip body, endless page tokens, a hugeretry-after, an upload URL on another host. Each ends with"limit","timeout","net"or"gemini".
Testing and validation
The suite runs offline against loopback fakes written in Jennifer: an httpd fake that replays answers in the shapes of Google's API reference and records what it got (key, upload headers, body), and a raw net fake for connections that are refused, closed before any byte, reset with net.abort before the request was read or after part of an answer, or broken off mid-answer. The examples were run against a local fake. No fixture came from a live call: no API key was available, and one live call remains to be made. The vendor reference was checked on 2026-11-14 and 15 (generate-content, structured output, thinking, thought signatures, files, caching, embeddings, the model list).
Known limitations and open questions
- A file is uploaded from memory, about three times its size at peak, until
httpstreams request bodies. embedManyreturns every vector at once, and alist of floatcosts several hundred bytes per number, so a large corpus is embedded in slices.updateCacheTtl'supdateMaskandembedContent's request body could not be checked against a live server; the reference pages give no example.uploadFiletakes the media asbytes; there is no upload from a path.
Dependencies on the interpreter
- A thrown module struct keeping its identity (M28.56), so a caught
GeminiErrorcan be declared as such. - Transport errors as
net.NetErrorwithsentandreplied(M28.65), which the retry rule is keyed on, andnet.abortfor the tests (M28.71). json's decoder caps throwing"limit"(M28.68).- Later: streaming responses (M30.9), bulk number reads from
json(M29.8), streaming request bodies (M30.10) and a smallerValue(M29.11) would lift the limitations above.
History
- 2026-11-14: built.
- 2026-11-15: validated (a
retry-aftertoo long to read, the listings' shared byte cap), then aligned with the openai and anthropic decks in four rounds (the shared surface,Usage,ToolRun.stopped,withMaxToolInput, the retry rule,limits,blockReason, the ranges, the upload deadline, the file page cap and size), and moved to thenet.NetErrortransport errors.