/** * GeminiProvider — wraps `@google/genai` as an `LLMProvider` (9.13.0). * * Pattern: Adapter (GoF) + Ports-and-Adapters (Cockburn 2005). * Role: Outer ring — translates `LLMRequest`/`LLMResponse` to/from Google's * `generateContent` API. Knows nothing about agents, recorders, or * compositions. * Emits: N/A — providers don't emit; recorders observe via Agent. * * ─── Two doors, one adapter ───────────────────────────────────────── * * The same SDK reaches two different services, and which one you get is a * property of the OPTIONS, not of a separate factory: * * • **Vertex** — `gemini({ project, location })` builds * `new GoogleGenAI({ vertexai: true, project, location, googleAuthOptions })`. * Auth is Application Default Credentials (env key file, gcloud user * credentials, or the metadata server on GCE / Cloud Run / Agent Runtime). * No API key exists on this path. * • **Gemini API (AI Studio)** — `gemini({ apiKey })` builds * `new GoogleGenAI({ apiKey })`. One key, no cloud project. * * Neither is guessed. The factory refuses at construction when it can resolve * neither a project nor a key, naming both doors — because the SDK's own * behaviour in that case is to WARN on stderr, construct anyway, and fail on * the first call, which reads like a network problem rather than a missing * setting. * * ─── Status, per door, because the doors differ ───────────────────── * * • **Vertex: field-validated.** An independent trial ran this adapter, * unchanged since 9.13.0, against a live project through Application Default * Credentials: a two-call tool loop on `gemini-2.5-flash` returning the * expected answer with real token counts, and a stream delivering 10 content * chunks that reconstructed exactly (FINDINGS "Part 1 — native Gemini on * Vertex through ADC", "Part 2C"). `geminiEmbedder` passed on the same door. * • **Gemini API (AI Studio): NOT field-validated, and not for want of * trying.** The trial's key-door calls never reached a model: the old * default model answered 404 for a new account, and a current model answered * 429 for AI Studio prepayment, which is billing SEPARATE from the Google * Cloud account's credit. What that door does with a funded key is * untested here, which is why it has no default model. * • **Tool calling on a 3.x model: field-validated.** The signature round trip * below was built from the trial's captured 400, and the same independent * trial then re-ran it on live Vertex against the published fix * (2026-08-14): `gemini-3.1-flash-lite` called a tool, the tool answered, * the signature went back byte for byte, and the second model call * completed with the right answer — two LLM calls, one tool execution, real * token counts. What it does NOT cover is a signature attached to a TEXT * part with no function call beside it; see below. * * ─── Why native, and not `openai({ baseURL })` ────────────────────── * * Google publishes an OpenAI-compatible endpoint, and it does work — for about * an hour. It authenticates with an OAuth access token that expires (9.29.0 * gives `openai({ apiKey })` a callback form so a long-running process can * refresh one, which is a mitigation and not a fix); its * `tools.function.parameters` is **OpenAPI**, not JSON Schema, so `$ref` / * `oneOf` / `additionalProperties` diverge silently; unsupported parameters are * ignored rather than refused; and its documented response carries no cached- * or reasoning-token counts. This adapter exists because every one of those is * answerable on the native SDK: ADC refreshes itself, tools go over as JSON * Schema untranslated (`parametersJsonSchema`), a forced single tool is * expressible, and `usageMetadata` reports cached and thinking tokens as * separate numbers. * * A field trial measured both halves of that: the native path completed a call * after a forced credential expiry, while the compatible path returned HTTP 401 * on an expired token (FINDINGS "Part 2B"). * * ─── Thought signatures: the round trip a tool loop cannot skip ───── * * A current Gemini model does not merely PREFER its thought signature back — * it refuses the turn without it. An independent field trial on live GCP ran * `gemini-3.1-flash-lite` on Vertex through one ordinary tool loop: the first * call asked for the function, the tool ran, and the second call came back * * 400 INVALID_ARGUMENT — "Function call is missing a thought_signature in * functionCall parts. This is required for tools to work correctly …" * * (FINDINGS "Failure 4"). The cost of that shape is what makes it worth this * paragraph: the failure lands AFTER the tool has already executed, so a * side-effecting tool has run and the answer is unreachable. * * So the signature is CARRIED, not dropped. `Part.thoughtSignature` (a base64 * string on the installed @google/genai 2.16.0 `Part`) is read off the same * part as the `functionCall` it belongs to, parked on the port's neutral * `toolCalls[].providerMeta`, and written back onto the reconstructed * `functionCall` part on the next request — byte for byte, because a signature * is checked and not read. This is the same round trip the Anthropic adapter * does with signed thinking blocks, one field along: there the carrier is * `LLMMessage.thinkingBlocks[].signature`, here it is the tool call itself, * because that is where Google puts it. * * What that does NOT cover, said plainly: a signature attached to a TEXT part * of an answer with no function call. The port's assistant turn is a string, * and a string has nowhere to keep one. The trial's failure was the functionCall * shape; that shape is fixed here, and the other one is named rather than * quietly half-handled. * * ─── Limitations (stated, not discovered) ─────────────────────────── * * • Multi-modal input/output NOT supported — `LLMMessage.content` is `string`, * so this adapter sends and reads text parts only. Inline data, files and * generated images are out of scope for the port as it stands. * • **Thought summaries are not requested.** `usage.thinking` is reported * honestly from `thoughtsTokenCount`, and `req.thinking.budget` is threaded * to `thinkingConfig.thinkingBudget` — but `includeThoughts` is deliberately * left unset, so no `rawThinking` is produced and there is no Gemini * `ThinkingHandler` in this release to normalize thought TEXT. Asking for * content nothing can carry would be a leak, not a feature. Signature * round-tripping (above) is a separate thing and does not need one. * `thinkingConfig.thinkingLevel` has no field on `LLMRequest` and is * likewise not sent. * • `responseJsonSchema` (native structured output) is NOT exposed; use * `.outputSchema(...)`, which goes over as a forced tool — a shape this * adapter DOES carry. * • Grounding tools (Google Search, code execution, URL context, MCP servers) * are not exposed. `tools` carries function declarations only. */ import type { LLMCallHooks, LLMChunk, LLMProvider, LLMRequest, LLMResponse, WireRole } from '../types.js'; import { type GoogleGenAIConnectionOptions } from './googleGenAI.js'; /** One `parts[]` entry. Only the members this adapter reads or writes. */ export interface GeminiPart { readonly text?: string; /** `true` on a thought-summary part. Never joined into visible content. */ readonly thought?: boolean; /** * The model's opaque signature for the thinking behind THIS part — base64, * per `Part.thoughtSignature` on the installed @google/genai 2.16.0 surface. * * Read off a `functionCall` part and written back onto the same part on the * next request. Never parsed, never trimmed, never regenerated: the service * verifies it, so the only correct handling is byte-for-byte. */ readonly thoughtSignature?: string; readonly functionCall?: { readonly id?: string; readonly name?: string; readonly args?: Record; }; readonly functionResponse?: { readonly id?: string; readonly name?: string; readonly response?: Record; }; } /** One conversation turn. Gemini's assistant role is spelled `'model'`. */ export interface GeminiContent { readonly role: 'user' | 'model'; readonly parts: readonly GeminiPart[]; } /** A `FunctionDeclaration`, JSON-Schema flavour. */ export interface GeminiFunctionDeclaration { readonly name: string; readonly description: string; /** * JSON Schema, passed through UNCHANGED. The sibling `parameters` field * takes OpenAPI instead and is never set here — sending both is what makes * an adapter's tool schemas quietly disagree with the ones it was given. */ readonly parametersJsonSchema: Record; } export interface GeminiGenerateConfig { systemInstruction?: string; temperature?: number; maxOutputTokens?: number; stopSequences?: string[]; abortSignal?: AbortSignal; tools?: { functionDeclarations: GeminiFunctionDeclaration[]; }[]; toolConfig?: { functionCallingConfig: { mode: 'AUTO' | 'ANY' | 'NONE'; allowedFunctionNames?: string[]; }; }; thinkingConfig?: { thinkingBudget: number; }; } export interface GeminiGenerateParams { readonly model: string; readonly contents: readonly GeminiContent[]; readonly config?: GeminiGenerateConfig; } export interface GeminiUsageMetadata { readonly promptTokenCount?: number; readonly candidatesTokenCount?: number; readonly cachedContentTokenCount?: number; readonly thoughtsTokenCount?: number; readonly totalTokenCount?: number; } export interface GeminiCandidate { readonly content?: { readonly parts?: readonly GeminiPart[]; }; readonly finishReason?: string; } export interface GeminiGenerateResponse { readonly candidates?: readonly GeminiCandidate[]; readonly usageMetadata?: GeminiUsageMetadata; readonly responseId?: string; } /** * The `models` namespace, narrowed to the two operations this adapter * dispatches. * * `countTokens` is deliberately ABSENT. It exists on the real namespace and it * would be an easy way to fill in a usage number the wire did not report — * which is precisely why it is not here. A count we asked for separately is not * what the call was billed for; see the streaming note on `stream()`. */ export interface GeminiModelsLike { generateContent(params: GeminiGenerateParams): Promise; /** Note the double await: the SDK returns a PROMISE of an async generator. */ generateContentStream(params: GeminiGenerateParams): Promise>; } /** What `new GoogleGenAI(...)` gives us, narrowed to what we use. */ export interface GeminiClientLike { readonly models: GeminiModelsLike; } export interface GeminiProviderOptions extends GoogleGenAIConnectionOptions { /** * Default model used when `LLMRequest.model` is `'gemini'` (the shorthand). * A request naming a full model id wins. * * **Unset behaves differently per door, because the doors behave * differently.** On **Vertex** the shorthand resolves to * `'gemini-2.5-flash'`, which a live field trial ran end to end through ADC. * On the **Gemini API (AI Studio)** door the shorthand is REFUSED by name: * the same trial's key-door call to that exact model returned HTTP 404 — * *"no longer available to new users"* (FINDINGS "Failure 1") — and no model * could be proven working on that door at all, because the account's separate * AI Studio prepayment was empty (FINDINGS "Failure 2", a 429 on * `gemini-3.1-flash-lite`, which at least proves that id resolves there). * * Shipping a second silent default nobody has run would be this library * guessing on a service's behalf; the refusal names the door and the fix. */ readonly defaultModel?: string; /** Default `maxOutputTokens` when the request doesn't set it. Optional. */ readonly defaultMaxTokens?: number; /** @internal Pre-built client for testing. Skips the SDK import entirely. */ readonly _client?: GeminiClientLike; } /** * Build an `LLMProvider` backed by Gemini — on Vertex or on the Gemini API. * * @example Vertex (Application Default Credentials) * ```ts * import { Agent } from 'agentfootprint'; * import { gemini } from 'agentfootprint/providers'; * * const agent = Agent.create({ * provider: gemini({ project: 'my-project', location: 'us-central1' }), * model: 'gemini', * }) * .tool(weatherTool) * .build(); * ``` * * @example Gemini API (one key, no cloud project) * ```ts * const provider = gemini({ apiKey: process.env.GEMINI_API_KEY }); * ``` * * @throws at construction when neither a project nor an API key is resolvable, * and when `@google/genai` is not installed. */ export declare function gemini(options?: GeminiProviderOptions): LLMProvider; /** * Class form for consumers who prefer `new GeminiProvider(...)` over the * `gemini(...)` factory. Identical behavior; trivial wrapper. */ export declare class GeminiProvider implements LLMProvider { readonly name = "gemini"; readonly carriesInMessages: readonly WireRole[]; readonly carriesForcedToolChoice = true; private readonly inner; constructor(options?: GeminiProviderOptions); complete(req: LLMRequest, hooks?: LLMCallHooks): Promise; stream(req: LLMRequest, hooks?: LLMCallHooks): AsyncIterable; } //# sourceMappingURL=GeminiProvider.d.ts.map