Skip to content

Google provider

Standalone — usable in any Node project, no @warlock.js/core required.

@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.

Terminal window
npm install @warlock.js/ai @warlock.js/ai-google

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.

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);
}
google.model({ name: "gemini-2.5-flash", temperature: 0.7 });
google.model({ name: "gemini-2.5-pro" });
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 usage on an embedding result is always { promptTokens: 0, totalTokens: 0 } — an honest absence, not a fabricated estimate.

  • Tool calling — vendor-neutral ToolConfigs map to Gemini function declarations; calls round-trip as functionCall / functionResponse parts. (Thinking models’ opaque thoughtSignature is 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 user turn of functionResponse parts in call order (the shape the @google/genai SDK’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 alternating user and model. Each signed functionCall keeps its own thoughtSignature. countTokens() builds the same contents, so counts match what is sent.

  • Streaming — stream() runs generateContentStream and yields text deltas, a tool-call per function call (Gemini emits a fully-formed call, not partial JSON), and a terminal done with 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-only gemini-1.0-pro is excluded. Override either way with the vision flag.

  • 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 inlineData block (see below). pdf / audio mirror the vision inference; override either with google.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.

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 on capabilities.pdf)
  • { type: "audio", source: { base64, mediaType: "audio/mpeg" } } → { inlineData: { mimeType: "audio/mpeg", data } } (gated on capabilities.audio)

PDF and audio reach the wire only when the model declares the matching capability (inferred for the multimodal Gemini families) — so capability ≡ behavior.

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 idTransportUsage 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.generateImagesnone, 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 same Usage.cost rollup as text.
  • The Gemini path passes token usage through rather than zeroing it: any usageMetadata Google attaches becomes usage.input / output / total (plus cachedTokens / reasoningTokens), and only an absent block collapses to zeros — so price it with { input, output } and check your first live usage. It requests responseModalities: ["TEXT", "IMAGE"] by default (override verbatim with options.responseModalities) and maps aspectRatio / imageSize / personGeneration onto config.imageConfig. count, negativePrompt, and format have no equivalent on this endpoint and are not forwarded.
  • generateImages is deprecated by Google, so the imagen-* 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 a gemini- id for new work.
  • When Google filters the request, the run surfaces a typed ContentFilterError on result.error. On the Gemini path, a response that came back as text instead of an image surfaces a ProviderError quoting 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).

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.

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.

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.