Google provider
Standalone — usable in any Node project, no
@warlock.js/corerequired.
@warlock.js/ai-google is the Google provider adapter for
@warlock.js/ai. It wraps the @google/genai SDK
(models.generateContent) and exposes Gemini as a vendor-neutral
ModelContract — the same shape every agent, workflow, and supervisor
already speaks. Works against both the Gemini Developer API and Vertex
AI.
Install
Section titled “Install”npm install @warlock.js/ai @warlock.js/ai-googleyarn add @warlock.js/ai @warlock.js/ai-googlepnpm add @warlock.js/ai @warlock.js/ai-googleConstruct
Section titled “Construct”One GoogleSDK holds one live GoogleGenAI client. Build it once at
boot and reuse it everywhere — every model and embedder it produces
shares the same client and auth.
import { GoogleSDK } from "@warlock.js/ai-google";
const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY!,});The config wraps GoogleGenAIOptions, so the whole object is forwarded
to new GoogleGenAI(...). The common path is the Gemini API with an
apiKey; the same options also drive Vertex AI:
const google = new GoogleSDK({ vertexai: true, project: "my-proj", location: "us-central1",});provider relabels the upstream (defaults to "google") and flows
through to AgentReport.model, logs, and provider-aware middleware.
First call
Section titled “First call”Build a model, hand it to an agent, run it. execute() never throws —
failures land in error as a typed AIError.
import { ai } from "@warlock.js/ai";import { GoogleSDK } from "@warlock.js/ai-google";
const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY! });
const assistant = ai.agent({ model: google.model({ name: "gemini-2.5-flash" }), systemPrompt: "You are a concise senior TypeScript engineer.",});
const { text, usage, error } = await assistant.execute("Why use generics?");
if (error) { console.warn(error.code, error.category);} else { console.log(text, usage.total);}Use as a model
Section titled “Use as a model”google.model({ name: "gemini-2.5-flash", temperature: 0.7 });google.model({ name: "gemini-2.5-pro" });Use as an embedder
Section titled “Use as an embedder”const embedder = google.embedder({ name: "gemini-embedding-001" });const { vector } = await embedder.embed("Hello world");const { vectors } = await embedder.embedMany(["doc 1", "doc 2"]);embedContent accepts an array natively, so embedMany is a single
request — embeddings come back in input order. Pass dimensions to
forward Gemini’s outputDimensionality truncation hint (supported by
2024+ models):
google.embedder({ name: "gemini-embedding-001", dimensions: 768 });Embeddings report no token usage. Gemini’s embed endpoint returns no token counts, so
usageon an embedding result is always{ promptTokens: 0, totalTokens: 0 }— an honest absence, not a fabricated estimate.
Capabilities
Section titled “Capabilities”-
Tool calling — vendor-neutral
ToolConfigs map to Gemini function declarations; calls round-trip asfunctionCall/functionResponseparts. (Thinking models’ opaquethoughtSignatureis carried through automatically so follow-up turns don’t 400.) -
One turn per role — consecutive messages that map to the same Gemini role are sent as one content, with their parts in order. The results of parallel tool calls travel as a single
userturn offunctionResponseparts in call order (the shape the@google/genaiSDK’s own automatic function calling sends), and an agent compaction summary next to a same-role turn (a user-framed summary before the input, or an assistant summary before a tool-call turn) merges into it, so the history keeps alternatinguserandmodel. Each signedfunctionCallkeeps its ownthoughtSignature.countTokens()builds the same contents, so counts match what is sent. -
Streaming —
stream()runsgenerateContentStreamand yields textdeltas, atool-callper function call (Gemini emits a fully-formed call, not partial JSON), and a terminaldonewith the final finish reason and usage. -
Structured output — on by default. A root-object JSON Schema is forwarded to Gemini’s native structured output (
responseMimeType: "application/json"+responseJsonSchema). Otherwise it degrades to the agent’s soft system-prompt hint plus client-side validation. -
Vision — auto-detected from the model id substring. Every Gemini 1.5, 2.x, and 2.5 model is multimodal (plus the legacy
gemini-pro-vision); only the original text-onlygemini-1.0-prois excluded. Override either way with thevisionflag. -
Embeddings —
gemini-embedding-001,text-embedding-004, native batch (see above). -
PDF + audio input — the multimodal Gemini families accept document and audio parts, both mapped to Gemini’s media-agnostic
inlineDatablock (see below).pdf/audiomirror thevisioninference; override either withgoogle.model({ name, pdf: true, audio: false }).
Image attachments accept inlined base64 bytes (with mediaType). Note:
generateContent does not fetch arbitrary remote URLs, so a { source: { url } }
PDF or audio part throws InvalidRequestError — resolve to base64 first.
Multimodal input maps to inlineData
Section titled “Multimodal input maps to inlineData”Gemini’s multimodal input is media-agnostic — every binary modality maps
to one inlineData block keyed by IANA mime type, so image, PDF, and audio
all take the same shape:
{ type: "image", source: { base64, mediaType } }→{ inlineData: { mimeType, data } }{ type: "pdf", source: { base64, mediaType: "application/pdf" } }→{ inlineData: { mimeType: "application/pdf", data } }(gated oncapabilities.pdf){ type: "audio", source: { base64, mediaType: "audio/mpeg" } }→{ inlineData: { mimeType: "audio/mpeg", data } }(gated oncapabilities.audio)
PDF and audio reach the wire only when the model declares the matching capability (inferred for the multimodal Gemini families) — so capability ≡ behavior.
Image generation (Imagen and Gemini)
Section titled “Image generation (Imagen and Gemini)”google.image({ name }) returns an ImageModelContract for the
ai.image() output verb — prompt in, images out, in the same
never-throws { data, error, usage, report } envelope every executable
returns.
The model id picks the transport:
| Model id | Transport | Usage it can report |
|---|---|---|
gemini-* (e.g. gemini-3.1-flash-lite-image) | ai.models.generateContent, responseModalities including "IMAGE" | whatever Google attaches — { input, output } |
anything else (imagen-*, unknown ids) | ai.models.generateImages | none, always zero — { perImage } |
A gemini- id sent to generateImages comes back
404 … is not supported for predict — that call routes to {model}:predict.
generateContent is the SDK’s own named replacement: its runtime deprecation
notice for generateImages reads “Please use the generateContent method with
image models instead.” Routing on the id is what makes both families work
through one factory.
import { ai } from "@warlock.js/ai";import { GoogleSDK } from "@warlock.js/ai-google";
const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY! });const imagen = google.image({ name: "imagen-4.0-generate-001", pricing: { perImage: 0.04 } });
const { data, error } = await ai.image({ model: imagen, prompt: "a watercolor lighthouse at dawn", aspectRatio: "3:4", // Imagen ratio (vs OpenAI's WxH `size`) negativePrompt: "text, watermark", options: { imageSize: "2K", personGeneration: "allow_adult" }, // Imagen passthroughs});
if (error) { console.warn(error.code); // typed AIError — content-filter when every candidate is safety-filtered} else { for (const img of data.images) { save(Buffer.from(img.base64, "base64"), img.mediaType); // Imagen returns base64 bytes }}// Gemini-native — same envelope, same GeneratedImage shape, usage passed through:const gemini = google.image({ name: "gemini-3.1-flash-lite-image", pricing: { input: 0.3, output: 30 }, // token-metered, not per-image});
const { data, usage } = await ai.image({ model: gemini, prompt: "a red bicycle on a white background", aspectRatio: "16:9", // → config.imageConfig.aspectRatio});- Imagen is per-image-metered — price it with
{ perImage }. It returns base64 bytes (no hosted URL, no token usage), and the spend folds into the sameUsage.costrollup as text. - The Gemini path passes token usage through rather than zeroing it: any
usageMetadataGoogle attaches becomesusage.input/output/total(pluscachedTokens/reasoningTokens), and only an absent block collapses to zeros — so price it with{ input, output }and check your first liveusage. It requestsresponseModalities: ["TEXT", "IMAGE"]by default (override verbatim withoptions.responseModalities) and mapsaspectRatio/imageSize/personGenerationontoconfig.imageConfig.count,negativePrompt, andformathave no equivalent on this endpoint and are not forwarded. generateImagesis deprecated by Google, so theimagen-*path is on a clock: “The generateImages method is deprecated and will be removed in the next major release (not before Jan. 1 2027). Please use the generateContent method with image models instead.” Nothing breaks today; prefer agemini-id for new work.- When Google filters the request, the run surfaces a typed
ContentFilterErroronresult.error. On the Gemini path, a response that came back as text instead of an image surfaces aProviderErrorquoting that text — never a silent empty success. - The model id is not validated locally. It selects a route; it is never
refused. An id Google does not serve fails as a typed provider error on
result.error— not with a local throw at construction. Google adds and retires image models on its own schedule; you pick the model, the API rules on it.
See ai.image for the full verb surface (options,
cost-truth, GeneratedImage).
Pricing and usage
Section titled “Pricing and usage”Every response reports token usage (input, output, total), on
model.complete() and the streaming done chunk as well as on agent
reports. Gemini’s promptTokenCount “includes the number of tokens in
the cached content” (UsageMetadata),
so the usage is marked inputIncludesCache: true and cached-content
tokens (cachedTokens) are billed out of input once, at cachedInput.
Thinking tokens surface as reasoningTokens. When the model is priced,
usage.cost holds the per-channel USD breakdown.
Attach a pricing registry — keyed by model name, in USD per million
tokens — to turn tokens into money. Resolution: per-model pricing >
SDK pricing registry > built-in GOOGLE_MODEL_PRICING > unpriced.
import { totalCost } from "@warlock.js/ai";import { GoogleSDK } from "@warlock.js/ai-google";
const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY ?? "", pricing: { // Text / image / video input rate; audio input bills higher on this model. "gemini-2.5-flash": { input: 0.3, output: 2.5, cachedInput: 0.03 }, },});
const { usage } = await google .model({ name: "gemini-2.5-flash" }) .complete([{ role: "user", content: "hi" }]);console.log(totalCost(usage.cost));Built-in list prices. GOOGLE_MODEL_PRICING (lookup:
googlePricingFor(name)) comes from the
Gemini API pricing page
(paid tier, Standard), fetched 2026-10-17. Only models with one flat
token price are built in — today gemini-3.5-flash-lite
({ input: 0.3, output: 2.5, cachedInput: 0.03 }). Left out on purpose:
the Pro models (priced by prompt length, split at 200k prompt tokens),
models whose audio input costs more than text (Usage does not split
input by modality), and models with a scheduled price change. The hourly
context-cache storage charge is not a token rate and is never included.
Errors carry status, retryable and billed. Google states that a
request failing with a 400 or 500 error is not charged
(billing), so those are
billed: false; other failures are "unknown". A 503 UNAVAILABLE
(“temporarily overloaded or down”) surfaces as ProviderOverloadedError
(code: "PROVIDER_OVERLOADED", retryable: true).
Need an offline token estimate? google.count(text) returns a fast
character-heuristic approximation — Gemini’s countTokens is a network
round-trip, so this stays offline for budgeting, not billing.
For the agent/workflow surface these models plug into, see
@warlock.js/ai. For provider-specific notes and the
latest model ids, see the warlock-js-ai-google skill.
Provider config options and input-token counts
Section titled “Provider config options and input-token counts”Native body fields merge after neutral options. When both values are plain
objects, their immediate keys merge and caller-provided keys win; untouched
neutral fields remain. Arrays, scalars and null replace the field. This merge
is one level deep: a nested object explicitly supplied by the caller replaces
that nested value.
providerOptions.google.body adds native GenerateContentConfig fields,
merged after neutral generation options for both completion and streaming:
const options = { providerOptions: { google: { body: { topP: 0.8, topK: 4 } } } };await model.complete(messages, options);The key stays google for custom provider labels. Other provider keys are
ignored. GoogleCallOptions is exported. systemInstruction, tools,
abortSignal and httpOptions are protected and throw InvalidRequestError;
use neutral messages/tools/signal and SDK transport configuration for them.
model.countTokens(messages, options?) counts the converted contents, system
instructions and tools on both the Gemini Developer API (API key) and Vertex AI.
Developer API counting sends the full generateContentRequest, including tool
configuration and generation settings, as documented in the
Gemini token-counting reference. It uses the
existing SDK transport, preserving API-key auth, custom fetch, base URL/proxy
settings, headers, retries and cancellation. Vertex retains its typed SDK path.
Cached-content references still throw InvalidRequestError with
reason: "count-unsupported". Use countInputTokens for budget preflight: it
returns an estimate for this explicit limitation; authentication, network,
rate-limit and ordinary validation errors still propagate. sdk.count(text)
remains a separate offline estimate.
import { countInputTokens } from "@warlock.js/ai";
const messages = [ { role: "system" as const, content: "Be concise." }, { role: "user" as const, content: "Hello" },];const tools = [{ name: "lookup", description: "Look up data", execute: async () => "ok" }];const { tokens, source } = await countInputTokens(model, messages, { tools });// source is "native" for ordinary Developer API and Vertex requests, including tools.Built-in tools and streamed thoughts
Section titled “Built-in tools and streamed thoughts”Gemini’s own tools — { googleSearch: {} }, { codeExecution: {} }, { urlContext: {} } — go in tools through ai.providerTool({ provider: "google", definition }); each is sent verbatim as its own Tool entry after the function declarations. See Provider-defined tools.
const search = ai.providerTool({ provider: "google", definition: { googleSearch: {} } });With streamChunks (on by default under agent.stream()), stream() also yields reasoning for thought parts (they are only sent with providerOptions: { google: { body: { thinkingConfig: { includeThoughts: true } } } }) and usage for every chunk carrying usageMetadata. See Stream reasoning and tool arguments.
Search grounding metadata and fees
Section titled “Search grounding metadata and fees”response.providerMetadata.google.groundingMetadata retains the raw grounding
metadata for each indexed candidate: queries, chunks, supports and search entry
point. Streaming done carries the same metadata. Agent reports retain it per
trip. Hosted search never produces a local function call.
usage.serverToolUse.webSearchRequests counts unique nonempty
groundingMetadata.webSearchQueries; googleWebSearchQueries records the raw
array length, including duplicates and empty entries. Stream snapshots replace
prior metadata per candidate rather than accumulating repeated snapshots.
The official schema
defines the queries array.
Google’s pricing guide
(verified 2026-10-18) bills Gemini 3 and newer per unique nonempty query: $14 per
1,000 queries after the shared monthly free allowance. You can explicitly use
the exported GOOGLE_SERVER_TOOL_PRICING as pricing.serverTools for that
model family. Gemini 2.5 and older bill per grounded prompt; their query count
is not a billable-prompt count, so leave this fee unpriced or account for it
outside this per-query table. Free allowances are not subtracted locally.
No web-fetch/code-execution count is inferred from result parts. See
cost tracking.