export type Message = import("../types").Message; export type ToolDef = import("../types").ToolDef; export type ToolCall = import("../types").ToolCall; export type GenerateResult = import("../types").GenerateResult; export type GeminiOptions = { /** * - Google AI Studio (Gemini) API key. */ apiKey?: string | undefined; /** * - Model ID. */ model?: string | undefined; /** * - API base (override for proxies; posts to `${baseUrl}/models/${model}:generateContent`). */ baseUrl?: string | undefined; /** * - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `generationConfig.thinkingConfig.thinkingBudget` request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`. * Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through). */ thinkingMode?: "on" | "off" | null | undefined; /** * - Attach the full upstream response to `err.body` on HTTP errors (off by default; `err.message` still carries the API error). */ exposeErrorBody?: boolean | undefined; /** * - BA-18: request/idle timeout in ms. Bounds a silent or never-answering socket on inactivity so `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`, `context.bound: 'idle'`) instead of hanging until the OS TCP timeout (~2h). `0`/`Infinity` disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`. */ timeoutMs?: number | undefined; /** * - BA-19: TOTAL call-duration deadline in ms, beside `timeoutMs`. The idle bound resets on any socket activity, so a response that trickles a byte forever never trips it and hangs for hours. This is an absolute, non-resetting ceiling; on trip, `generate()` rejects with a TERMINAL `TimeoutError` (`code: 'EDEADLINE'`, `context.bound: 'deadline'`, `retryable: false`). DISABLED by default; `0`/`Infinity` disable. Overridable per call via `generate(..., { deadlineMs })`. */ deadlineMs?: number | undefined; }; /** * @typedef {object} GeminiOptions * @property {string} [apiKey] - Google AI Studio (Gemini) API key. * @property {string} [model='gemini-2.5-flash'] - Model ID. * @property {string} [baseUrl='https://generativelanguage.googleapis.com/v1beta'] - API base (override for proxies; posts to `${baseUrl}/models/${model}:generateContent`). * @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `generationConfig.thinkingConfig.thinkingBudget` request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`. * Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through). * @property {boolean} [exposeErrorBody=false] - Attach the full upstream response to `err.body` on HTTP errors (off by default; `err.message` still carries the API error). * @property {number} [timeoutMs=600000] - BA-18: request/idle timeout in ms. Bounds a silent or never-answering socket on inactivity so `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`, `context.bound: 'idle'`) instead of hanging until the OS TCP timeout (~2h). `0`/`Infinity` disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`. * @property {number} [deadlineMs=0] - BA-19: TOTAL call-duration deadline in ms, beside `timeoutMs`. The idle bound resets on any socket activity, so a response that trickles a byte forever never trips it and hangs for hours. This is an absolute, non-resetting ceiling; on trip, `generate()` rejects with a TERMINAL `TimeoutError` (`code: 'EDEADLINE'`, `context.bound: 'deadline'`, `retryable: false`). DISABLED by default; `0`/`Infinity` disable. Overridable per call via `generate(..., { deadlineMs })`. */ /** * Google Gemini provider (native `generateContent` API, NOT the OpenAI-compat endpoint — that endpoint * omits the cache token tier, so it can't feed the cost meter; verified by POC). Converts the Loop's * OpenAI-format messages to Gemini `contents`, declares tools as `functionDeclarations`, and normalizes * `usageMetadata` to the neutral Usage shape. Gemini auto-caches (implicit caching on 2.5 models), so the * cache-read tier populates with no opt-in. */ export class GeminiProvider { /** * @param {GeminiOptions} [options] * @when you want Gemini models as the Loop's provider — native generateContent with implicit prompt caching * @fails throws on a missing apiKey; normalizes stopReason (promoting a complete tool call) and usage; a socket idle/deadline/transport cut rejects with a retryable error. * @example * const provider = new GeminiProvider({ apiKey, model: 'gemini-2.5-pro' }); */ constructor(options?: GeminiOptions); apiKey: string | undefined; model: string; baseUrl: string; exposeErrorBody: boolean; thinkingMode: "on" | "off" | null; /** @type {Map} shape that worked per mode+model (no re-probing) */ _thinkingShapes: Map; timeoutMs: number | undefined; deadlineMs: number | undefined; /** * Generate a response from the Gemini API. * @param {Message[]} messages - Conversation messages (OpenAI format, auto-converted). * @param {ToolDef[]} [tools=[]] - Tool definitions. * @param {Record} [options={}] - Options (thinkingMode — per-call override of the constructor's, `null` suppresses; temperature, maxTokens, timeoutMs — a per-call override of the constructor's `timeoutMs`, see BA-18; deadlineMs — a per-call override of the constructor's `deadlineMs`, see BA-19). * @returns {Promise} * @throws {Error} `[GeminiProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response. */ generate(messages: Message[], tools?: ToolDef[], options?: Record): Promise; /** One-time warning (per reason) that this model rejected `temperature` and the request was retried without it (BA-10). */ _warnTemperatureDropped(reason?: string): void; /** * Normalize Gemini `usageMetadata` to the neutral {@link Usage} shape. Like OpenAI, `promptTokenCount` * INCLUDES the cached tokens (`cachedContentTokenCount`), so subtract for the uncached remainder * (verified live). Gemini bills "thinking" (`thoughtsTokenCount`) as output, so fold it into output * (total = prompt + candidates + thoughts — confirmed against live usageMetadata). Implicit caching * has no separate write tier → cacheCreationTokens 0. * @param {any} u - raw `data.usageMetadata` * @returns {import('../types').Usage|null} */ _normalizeUsage(u: any): import("../types").Usage | null; /** * @param {string} path * @param {Record} body * @param {number} [timeoutMs=0] - Idle-socket timeout (ms); 0 disables. See BA-18 / provider-http. * @param {number} [deadlineMs=0] - Total call-duration deadline (ms); 0 disables. See BA-19 / provider-http. * @returns {Promise} */ _request(path: string, body: Record, timeoutMs?: number, deadlineMs?: number): Promise; _warnedInsecure: boolean | undefined; }