@warlock.js/ai-google 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["InvalidRequestError","AIError","ProviderTimeoutError","ProviderAuthError","ProviderRateLimitError","ContextLengthExceededError","InvalidRequestError","ProviderError","ApiError","LOG_MODULE","log","log","GoogleGenAI"],"sources":["../../../../../@warlock.js/ai-google/src/utils/map-finish-reason.ts","../../../../../@warlock.js/ai-google/src/utils/to-google-contents.ts","../../../../../@warlock.js/ai-google/src/utils/to-google-tools.ts","../../../../../@warlock.js/ai-google/src/utils/wrap-google-error.ts","../../../../../@warlock.js/ai-google/src/embedder.ts","../../../../../@warlock.js/ai-google/src/known-vision-models.ts","../../../../../@warlock.js/ai-google/src/model.ts","../../../../../@warlock.js/ai-google/src/sdk.ts"],"sourcesContent":["import type { FinishReason } from \"@warlock.js/ai\";\n\nconst finishReasonMap: Record<string, FinishReason> = {\n STOP: \"stop\",\n MAX_TOKENS: \"length\",\n};\n\n/**\n * Map Gemini's `FinishReason` enum value to the normalized\n * `FinishReason` union.\n *\n * `STOP` is the natural terminal. `MAX_TOKENS` maps to `length`.\n * Everything else — `SAFETY`, `RECITATION`, `BLOCKLIST`,\n * `PROHIBITED_CONTENT`, `SPII`, `MALFORMED_FUNCTION_CALL`,\n * `UNEXPECTED_TOOL_CALL`, `LANGUAGE`, `OTHER`,\n * `FINISH_REASON_UNSPECIFIED`, `null`, or any future value — falls\n * through to `\"error\"`.\n *\n * Note: Gemini reports `STOP` even when the turn ended in a function\n * call (it has no `tool_use` reason). `GoogleModel` overrides the\n * mapped reason to `\"tool_calls\"` when the response carries function\n * calls — this map intentionally stays purely about the raw signal.\n *\n * @example\n * mapFinishReason(\"STOP\"); // \"stop\"\n * mapFinishReason(\"MAX_TOKENS\"); // \"length\"\n * mapFinishReason(\"SAFETY\"); // \"error\"\n * mapFinishReason(undefined); // \"error\"\n */\nexport function mapFinishReason(raw: string | null | undefined): FinishReason {\n return finishReasonMap[raw ?? \"\"] ?? \"error\";\n}\n","import { InvalidRequestError, safeJsonParse, type ContentPart, type Message } from \"@warlock.js/ai\";\nimport type { Content, Part } from \"@google/genai\";\n\n/**\n * Result of splitting a vendor-neutral `Message[]` for Gemini's\n * `generateContent`: the system prompt is hoisted to a separate\n * `systemInstruction` string (Gemini has no `\"system\"` role — content\n * roles must be `\"user\"` or `\"model\"`), and the remaining turns map to\n * `Content[]`.\n */\nexport type GoogleContents = {\n systemInstruction: string | undefined;\n contents: Content[];\n};\n\n/**\n * Convert vendor-neutral `Message[]` into Gemini's request shape.\n *\n * Gemini specifics this function absorbs:\n *\n * 1. **No `system` role.** System messages concatenate into the\n * separate `systemInstruction` config field.\n * 2. **Role names differ.** Neutral `assistant` → Gemini `\"model\"`;\n * `user` stays `\"user\"`.\n * 3. **Tool results are `user` turns.** A neutral `tool` message\n * becomes a `\"user\"` content with a single `functionResponse` part.\n * 4. **Tool calls are `functionCall` parts.** An assistant message\n * with `toolCalls` becomes a `\"model\"` content: an optional leading\n * `text` part followed by one `functionCall` part per call.\n *\n * @example\n * const { systemInstruction, contents } = toGoogleContents([\n * { role: \"system\", content: \"Be concise.\" },\n * { role: \"user\", content: \"Hi\" },\n * ]);\n */\nexport function toGoogleContents(messages: Message[]): GoogleContents {\n const systemParts: string[] = [];\n const contents: Content[] = [];\n\n for (const message of messages) {\n if (message.role === \"system\") {\n systemParts.push(stringifyContent(message.content));\n\n continue;\n }\n\n if (message.role === \"tool\") {\n contents.push({\n role: \"user\",\n parts: [\n {\n // Gemini matches a `functionResponse` to its `functionCall`\n // by `name` (the Developer API has no call ids). `name` is\n // the neutral `toolCallId`, which `GoogleModel` set to the\n // function name. The wire `id` is intentionally omitted —\n // an empty/synthetic id is rejected as an invalid argument.\n functionResponse: {\n name: message.toolCallId ?? \"\",\n response: toResponseObject(stringifyContent(message.content)),\n },\n },\n ],\n });\n\n continue;\n }\n\n if (message.role === \"assistant\" && message.toolCalls && message.toolCalls.length > 0) {\n const parts: Part[] = [];\n const text = stringifyContent(message.content);\n\n if (text) {\n parts.push({ text });\n }\n\n for (const toolCall of message.toolCalls) {\n // Replay the opaque `thoughtSignature` Gemini attached to this\n // function call on the original turn. Thinking models reject\n // the follow-up request with a 400 if the signature is missing\n // from the echoed `functionCall` part. Captured by\n // `GoogleModel.partToToolCall` into `providerMetadata`.\n const thoughtSignature = toolCall.providerMetadata?.thoughtSignature;\n\n parts.push({\n ...(typeof thoughtSignature === \"string\" ? { thoughtSignature } : {}),\n // `id` omitted deliberately — Gemini Developer API function\n // calls have no ids; echoing an empty/synthetic one is\n // rejected as an invalid argument. Matched by `name`.\n functionCall: {\n name: toolCall.name,\n args: (toolCall.input ?? {}) as Record<string, unknown>,\n },\n });\n }\n\n contents.push({ role: \"model\", parts });\n\n continue;\n }\n\n if (message.role === \"user\" && Array.isArray(message.content)) {\n contents.push({ role: \"user\", parts: message.content.map(toGooglePart) });\n\n continue;\n }\n\n contents.push({\n role: message.role === \"assistant\" ? \"model\" : \"user\",\n parts: [{ text: stringifyContent(message.content) }],\n });\n }\n\n return {\n systemInstruction: systemParts.length > 0 ? systemParts.join(\"\\n\\n\") : undefined,\n contents,\n };\n}\n\n/**\n * Multipart content is only meaningful on user messages — for any\n * other role collapse a `ContentPart[]` to concatenated text. Plain\n * strings pass through unchanged.\n */\nfunction stringifyContent(content: string | ContentPart[]): string {\n if (typeof content === \"string\") {\n return content;\n }\n\n return content\n .filter((part): part is { type: \"text\"; text: string } => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\");\n}\n\n/**\n * Gemini's `functionResponse.response` must be a JSON object. Tool\n * results arrive as a string (usually stringified JSON) — parse it\n * when it is a JSON object, otherwise wrap the raw string under a\n * `result` key so the model always receives a well-formed object.\n */\nfunction toResponseObject(raw: string): Record<string, unknown> {\n const parsed = safeJsonParse<unknown>(raw, undefined);\n\n if (parsed !== null && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n\n return { result: raw };\n}\n\n/**\n * Map a resolved `ContentPart` to a Gemini `Part`. Images are sent as\n * inline base64 (`inlineData`). Gemini's `generateContent` does not\n * fetch arbitrary remote URLs (only Files API / GCS URIs via\n * `fileData`), so a neutral `{ url }` image surfaces a typed\n * `InvalidRequestError` upfront rather than a downstream Gemini fault.\n * The agent resolves attachments before this point, so nothing is\n * read or fetched here.\n */\nfunction toGooglePart(part: ContentPart): Part {\n if (part.type === \"text\") {\n return { text: part.text };\n }\n\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"Gemini generateContent does not fetch remote-URL images; supply base64 image bytes instead.\",\n );\n }\n\n return {\n inlineData: { mimeType: part.source.mediaType, data: part.source.base64 },\n };\n}\n","import { extractJsonSchema, type ToolConfig } from \"@warlock.js/ai\";\nimport type { Tool } from \"@google/genai\";\n\n/**\n * Convert vendor-neutral `ToolConfig[]` into Gemini's `tools` array —\n * a single `Tool` carrying one `functionDeclarations` entry per tool.\n *\n * The input schema is forwarded via `parametersJsonSchema` (raw JSON\n * Schema, mutually exclusive with Gemini's typed `parameters`).\n * Non-object extractions degrade to a parameterless object so\n * registration never fails.\n *\n * Returns `undefined` when there are no tools so the caller can omit\n * `config.tools` entirely.\n *\n * @example\n * const tools = toGoogleTools([weatherTool]);\n * await ai.models.generateContent({ model, contents, config: { tools } });\n */\nexport function toGoogleTools(\n tools: ToolConfig<unknown, unknown>[] | undefined,\n): Tool[] | undefined {\n if (!tools || tools.length === 0) {\n return undefined;\n }\n\n return [\n {\n functionDeclarations: tools.map((tool) => ({\n name: tool.name,\n description: tool.description,\n parametersJsonSchema: toJsonSchema(tool.input),\n })),\n },\n ];\n}\n\n/**\n * Resolve a tool's input schema to a JSON-Schema object. Gemini wants\n * an object root for function parameters; anything else (or a failed\n * extraction) degrades to a parameterless object.\n */\nfunction toJsonSchema(input: ToolConfig<unknown, unknown>[\"input\"]): Record<string, unknown> {\n const schema = extractJsonSchema(input);\n\n if (schema && schema.type === \"object\") {\n return schema;\n }\n\n return { type: \"object\" };\n}\n","import {\n AIError,\n ContextLengthExceededError,\n InvalidRequestError,\n ProviderAuthError,\n ProviderError,\n ProviderRateLimitError,\n ProviderTimeoutError,\n} from \"@warlock.js/ai\";\nimport { ApiError } from \"@google/genai\";\n\n/**\n * Raw-error fields the wrapper reads off a Gemini SDK error.\n * `@google/genai`'s `ApiError` exposes `status` (HTTP code) +\n * `message`; transport aborts surface as `AbortError` / `ETIMEDOUT`.\n * We duck-type so proxied / re-thrown errors still classify.\n */\ntype GoogleErrorShape = {\n status?: number;\n message?: string;\n name?: string;\n code?: string;\n};\n\n/**\n * Wrap any thrown value caught inside the Gemini adapter into the\n * appropriate `@warlock.js/ai` `AIError` subclass.\n *\n * **Dispatch strategy.** Gemini has no machine error `code`; the\n * signals are the HTTP `status` and the canonical status phrase Google\n * embeds in `message` (`PERMISSION_DENIED`, `RESOURCE_EXHAUSTED`,\n * `INVALID_ARGUMENT`, …). Dispatch keys on `status`, using the message\n * phrase as the tie-breaker for the two 400 sub-cases\n * (context-length vs generic) and for status-less auth/quota errors.\n *\n * `AIError` instances pass through unchanged so `catch/throw wrap(e)`\n * pipelines never double-wrap.\n *\n * @example\n * try {\n * return await this.ai.models.generateContent(...);\n * } catch (thrown) {\n * throw wrapGoogleError(thrown);\n * }\n */\nexport function wrapGoogleError(thrown: unknown): AIError {\n if (thrown instanceof AIError) {\n return thrown;\n }\n\n const shape = toShape(thrown);\n const context = buildContext(shape);\n const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));\n\n if (isTimeout(shape)) {\n return new ProviderTimeoutError(message, { cause: thrown, context });\n }\n\n if (\n shape.status === 401 ||\n shape.status === 403 ||\n /permission_denied|api key not valid|unauthenticated/i.test(message)\n ) {\n return new ProviderAuthError(message, { cause: thrown, context });\n }\n\n if (shape.status === 429 || /resource_exhausted|quota/i.test(message)) {\n return new ProviderRateLimitError(message, { cause: thrown, context });\n }\n\n if (shape.status === 400) {\n if (/token count|context length|exceeds the maximum|input is too long/i.test(message)) {\n return new ContextLengthExceededError(message, { cause: thrown, context });\n }\n\n return new InvalidRequestError(message, { cause: thrown, context });\n }\n\n if (shape.status === 404 || isClientStatus(shape.status)) {\n return new InvalidRequestError(message, { cause: thrown, context });\n }\n\n return new ProviderError(message, { cause: thrown, context });\n}\n\n/**\n * Read the raw error shape. The Gemini SDK's `ApiError` carries a\n * numeric `status`; flattened/proxied errors may carry it (or `code`)\n * loosely.\n */\nfunction toShape(thrown: unknown): GoogleErrorShape {\n if (thrown instanceof ApiError) {\n return { status: thrown.status, message: thrown.message, name: thrown.name };\n }\n\n if (typeof thrown === \"object\" && thrown !== null) {\n const raw = thrown as Record<string, unknown>;\n\n return {\n status: typeof raw.status === \"number\" ? raw.status : undefined,\n message: typeof raw.message === \"string\" ? raw.message : undefined,\n name: typeof raw.name === \"string\" ? raw.name : undefined,\n code: typeof raw.code === \"string\" ? raw.code : undefined,\n };\n }\n\n return {};\n}\n\n/**\n * Decide whether the error is a timeout. Gemini maps gateway timeouts\n * to HTTP 504 (`DEADLINE_EXCEEDED`); transport aborts surface as\n * `AbortError` / `ETIMEDOUT` / `ECONNABORTED`.\n */\nfunction isTimeout(shape: GoogleErrorShape): boolean {\n if (shape.status === 504) {\n return true;\n }\n\n if (shape.name === \"AbortError\" || /deadline_exceeded/i.test(shape.message ?? \"\")) {\n return true;\n }\n\n return shape.code === \"ETIMEDOUT\" || shape.code === \"ECONNABORTED\";\n}\n\n/** True for HTTP 4xx — a client-side request problem, not a server fault. */\nfunction isClientStatus(status: number | undefined): boolean {\n return typeof status === \"number\" && status >= 400 && status < 500;\n}\n\n/** Attach the diagnostic fields to `error.context`. */\nfunction buildContext(shape: GoogleErrorShape): Record<string, unknown> {\n const context: Record<string, unknown> = {};\n\n if (shape.status !== undefined) {\n context.status = shape.status;\n }\n\n if (shape.name) {\n context.code = shape.name;\n }\n\n return context;\n}\n","import {\n type EmbeddingBatchResult,\n type EmbeddingResult,\n type EmbeddingUsage,\n type EmbedderContract,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type { EmbedContentResponse, GoogleGenAI } from \"@google/genai\";\nimport type { GoogleEmbedderConfig } from \"./config.type\";\nimport { wrapGoogleError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.google\";\n\n/**\n * Token usage is not returned by Gemini's `embedContent`, so every\n * embedding result reports a zeroed `EmbeddingUsage` (honest absence,\n * not a fabricated estimate).\n */\nconst NO_USAGE: EmbeddingUsage = { promptTokens: 0, totalTokens: 0 };\n\n/**\n * Google Gemini-backed implementation of `EmbedderContract`\n * (`gemini-embedding-001`, `text-embedding-004`, …) via\n * `models.embedContent`.\n *\n * **Role.** Converts text into floating-point vectors. Standalone\n * primitive — unrelated to generateContent / tools / the agent loop.\n *\n * **Batch is native.** Gemini's `embedContent` accepts an array of\n * inputs and returns embeddings in the same order, so `embedMany` is\n * a single request (unlike the Bedrock/Titan adapter, which has to\n * loop).\n *\n * **No usage.** Gemini's embed endpoint returns no token counts;\n * `usage` is always `{ promptTokens: 0, totalTokens: 0 }`.\n *\n * **Dimensions.** When no `dimensions` override is given,\n * `this.dimensions` starts at `0` and is populated from the first\n * response's vector length, then cached. Passing `dimensions`\n * forwards Gemini's `outputDimensionality` truncation hint and sets\n * the initial value immediately.\n *\n * @example\n * const embedder = new GoogleEmbedder(ai, { name: \"gemini-embedding-001\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n * const { vectors } = await embedder.embedMany([\"doc 1\", \"doc 2\"]);\n */\nexport class GoogleEmbedder implements EmbedderContract {\n public readonly name: string;\n public readonly provider: string;\n public dimensions: number;\n\n private readonly ai: GoogleGenAI;\n private readonly configuredDimensions: number | undefined;\n private readonly logger: Logger = log;\n\n public constructor(\n ai: GoogleGenAI,\n config: GoogleEmbedderConfig,\n provider: string = \"google\",\n ) {\n this.ai = ai;\n this.name = config.name;\n this.provider = provider;\n this.configuredDimensions = config.dimensions;\n this.dimensions = config.dimensions ?? 0;\n }\n\n public async embed(input: string): Promise<EmbeddingResult> {\n const vectors = await this.request([input]);\n\n return { vector: vectors[0], dimensions: this.dimensions, usage: NO_USAGE };\n }\n\n public async embedMany(inputs: string[]): Promise<EmbeddingBatchResult> {\n const vectors = await this.request(inputs);\n\n return { vectors, dimensions: this.dimensions, usage: NO_USAGE };\n }\n\n /**\n * Shared transport: one `embedContent` call for the whole batch,\n * wrap provider errors, cache `dimensions` from the first vector,\n * and return the raw vectors in input order.\n */\n private async request(inputs: string[]): Promise<number[][]> {\n this.logger.debug(LOG_MODULE, \"embedder.request\", \"embedContent\", {\n model: this.name,\n count: inputs.length,\n });\n\n let response: EmbedContentResponse;\n\n try {\n response = await this.ai.models.embedContent({\n model: this.name,\n contents: inputs,\n ...(this.configuredDimensions !== undefined\n ? { config: { outputDimensionality: this.configuredDimensions } }\n : {}),\n });\n } catch (thrown) {\n const wrapped = wrapGoogleError(thrown);\n\n this.logger.error(LOG_MODULE, \"embedder.error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const vectors = (response.embeddings ?? []).map((embedding) => embedding.values ?? []);\n\n if (this.dimensions === 0 && vectors[0]) {\n this.dimensions = vectors[0].length;\n }\n\n this.logger.debug(LOG_MODULE, \"embedder.response\", \"embedContent returned\", {\n count: vectors.length,\n dimensions: this.dimensions,\n });\n\n return vectors;\n }\n}\n","/**\n * Substrings identifying Gemini model ids whose family accepts image\n * input (vision).\n *\n * Every Gemini 1.5, 2.x, and 2.5 model is natively multimodal, as is\n * the legacy `gemini-pro-vision`. Only the original text-only\n * `gemini-pro` / `gemini-1.0-pro` is excluded. A substring match\n * tolerates the date/preview suffixes Google appends\n * (`gemini-2.5-flash-preview-05-20`). Override per-model via\n * `google.model({ name, vision: true | false })`.\n */\nconst VISION_CAPABLE_SUBSTRINGS = [\n \"gemini-1.5\",\n \"gemini-2\",\n \"gemini-exp\",\n \"gemini-pro-vision\",\n \"gemini-flash\",\n];\n\n/**\n * Infer whether a Gemini model id supports vision based on the known\n * multimodal-family substrings. Unknown ids default to `false` so\n * passing an image attachment to an unsupported model surfaces a\n * clear, agent-side capability error instead of an opaque Gemini 400.\n *\n * @example\n * inferVisionCapability(\"gemini-2.5-flash\"); // → true\n * inferVisionCapability(\"gemini-1.5-pro-002\"); // → true\n * inferVisionCapability(\"gemini-1.0-pro\"); // → false\n * inferVisionCapability(\"text-embedding-004\"); // → false\n */\nexport function inferVisionCapability(modelId: string): boolean {\n const normalized = modelId.toLowerCase();\n\n return VISION_CAPABLE_SUBSTRINGS.some((fragment) => normalized.includes(fragment));\n}\n","import {\n type Message,\n type ModelCallOptions,\n type ModelCapabilities,\n type ModelContract,\n type ModelPricing,\n type ModelResponse,\n type ModelStreamChunk,\n type ModelToolCallRequest,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type {\n GenerateContentConfig,\n GenerateContentResponse,\n GoogleGenAI,\n Part,\n} from \"@google/genai\";\nimport type { GoogleModelConfig } from \"./config.type\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport { mapFinishReason, toGoogleContents, toGoogleTools, wrapGoogleError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.google\";\n\n/**\n * Google Gemini-backed implementation of `ModelContract`.\n *\n * **Role.** The provider-facing bridge between the vendor-neutral\n * `@warlock.js/ai` agent runtime and the `@google/genai` SDK\n * (`models.generateContent` / `generateContentStream`).\n *\n * **Responsibility.**\n * - Owns: a long-lived `GoogleGenAI` client + frozen `ModelConfig`\n * (name, temperature, maxTokens) used as per-call defaults.\n * - Owns: translating vendor-neutral `Message[]` / `ToolConfig[]` into\n * Gemini shapes (systemInstruction hoisting, `model` role,\n * `functionCall` / `functionResponse` parts, inline image bytes) on\n * the way out, and Gemini's candidate/parts response (text, function\n * calls, finish reason, token usage) back into neutral shapes on the\n * way in.\n * - Does NOT own: dispatching tools, looping, history, retries — those\n * are agent concerns. The model is a per-call protocol adapter.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across calls\"): the `GoogleGenAI` client is reused for the SDK's\n * lifetime.\n *\n * @example\n * import { GoogleGenAI } from \"@google/genai\";\n * const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });\n * const model = new GoogleModel(ai, { name: \"gemini-2.5-flash\" });\n *\n * const myAgent = agent({ model, tools: [searchTool] });\n * const result = await myAgent.execute(\"Summarize today's news.\");\n */\nexport class GoogleModel implements ModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly capabilities: ModelCapabilities;\n public readonly pricing?: ModelPricing;\n\n private readonly ai: GoogleGenAI;\n private readonly config: GoogleModelConfig;\n private readonly logger: Logger = log;\n\n public constructor(ai: GoogleGenAI, config: GoogleModelConfig, provider: string = \"google\") {\n this.ai = ai;\n this.config = config;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n this.capabilities = {\n structuredOutput: config.structuredOutput ?? true,\n vision: config.vision ?? inferVisionCapability(config.name),\n };\n }\n\n /**\n * Single-shot completion. Sends the full message list to\n * `generateContent`, waits for the terminal response, and reshapes\n * it into a vendor-neutral `ModelResponse`. Per-call `options`\n * override the instance defaults for this call only.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting generateContent call\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n const { systemInstruction, contents } = toGoogleContents(messages);\n\n let response: GenerateContentResponse;\n\n try {\n response = await this.ai.models.generateContent({\n model: this.name,\n contents,\n config: this.buildConfig(systemInstruction, options),\n });\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const toolCalls = this.extractToolCalls(response);\n const finishReason = toolCalls\n ? \"tool_calls\"\n : mapFinishReason(response.candidates?.[0]?.finishReason);\n const usage = this.extractUsage(response);\n\n this.logger.debug(LOG_MODULE, \"response\", \"generateContent call succeeded\", {\n finishReason,\n usage,\n });\n\n return {\n content: response.text ?? \"\",\n finishReason,\n usage,\n toolCalls,\n };\n }\n\n /**\n * Incremental streaming completion via `generateContentStream`.\n * Yields neutral `ModelStreamChunk`s — `delta` for text, `tool-call`\n * per function call (Gemini emits a fully-formed call, not partial\n * JSON), and a terminal `done` with the final finish reason + usage.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting generateContentStream call\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n const { systemInstruction, contents } = toGoogleContents(messages);\n\n let iterable: AsyncGenerator<GenerateContentResponse>;\n\n try {\n iterable = await this.ai.models.generateContentStream({\n model: this.name,\n contents,\n config: this.buildConfig(systemInstruction, options),\n });\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n let rawFinishReason: string | undefined;\n let sawToolCall = false;\n const usage: Usage = { input: 0, output: 0, total: 0 };\n\n try {\n for await (const chunk of iterable) {\n const text = chunk.text;\n\n if (text) {\n yield { type: \"delta\", content: text };\n }\n\n for (const part of chunk.candidates?.[0]?.content?.parts ?? []) {\n const toolCall = this.partToToolCall(part);\n\n if (!toolCall) {\n continue;\n }\n\n sawToolCall = true;\n\n yield {\n type: \"tool-call\",\n id: toolCall.id,\n name: toolCall.name,\n input: toolCall.input,\n ...(toolCall.providerMetadata\n ? { providerMetadata: toolCall.providerMetadata }\n : {}),\n };\n }\n\n const candidateFinish = chunk.candidates?.[0]?.finishReason;\n\n if (candidateFinish) {\n rawFinishReason = candidateFinish;\n }\n\n if (chunk.usageMetadata) {\n this.applyUsage(usage, chunk.usageMetadata);\n }\n }\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const finishReason = sawToolCall ? \"tool_calls\" : mapFinishReason(rawFinishReason);\n\n this.logger.debug(LOG_MODULE, \"response\", \"generateContentStream call succeeded\", {\n finishReason,\n usage,\n });\n\n yield { type: \"done\", finishReason, usage };\n }\n\n /**\n * Assemble the `GenerateContentConfig` shared by `complete()` and\n * `stream()`: inference params, hoisted system instruction,\n * cancellation signal, and conditional tools + native structured\n * output.\n */\n private buildConfig(\n systemInstruction: string | undefined,\n options: ModelCallOptions | undefined,\n ): GenerateContentConfig {\n const temperature = options?.temperature ?? this.config.temperature;\n const maxOutputTokens = options?.maxTokens ?? this.config.maxTokens;\n\n return {\n ...(systemInstruction ? { systemInstruction } : {}),\n ...(temperature !== undefined ? { temperature } : {}),\n ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),\n ...(options?.signal ? { abortSignal: options.signal } : {}),\n ...this.buildTools(options?.tools),\n ...this.buildStructuredOutput(options?.responseSchema),\n };\n }\n\n /**\n * Spread-friendly tools fragment. Empty object when no tools were\n * supplied so the caller can unconditionally spread it.\n */\n private buildTools(tools: ModelCallOptions[\"tools\"]): Pick<GenerateContentConfig, \"tools\"> {\n const mapped = toGoogleTools(tools);\n\n return mapped ? { tools: mapped } : {};\n }\n\n /**\n * Translate the neutral `responseSchema` into Gemini's native JSON\n * structured output (`responseMimeType: \"application/json\"` +\n * `responseJsonSchema`, which takes a raw JSON Schema directly).\n * Emitted only when the model is `structuredOutput`-capable and the\n * schema is an object root — otherwise the agent's soft prompt hint\n * + client-side `validate()` carry shape.\n */\n private buildStructuredOutput(\n responseSchema: Record<string, unknown> | undefined,\n ): Pick<GenerateContentConfig, \"responseMimeType\" | \"responseJsonSchema\"> {\n if (!responseSchema || !this.capabilities.structuredOutput) {\n return {};\n }\n\n if (responseSchema.type !== \"object\" || typeof responseSchema.properties !== \"object\") {\n return {};\n }\n\n return {\n responseMimeType: \"application/json\",\n responseJsonSchema: responseSchema,\n };\n }\n\n /**\n * Reshape Gemini's function-call content parts into the neutral\n * `ModelToolCallRequest[]`. Returns `undefined` when the model\n * requested no functions so callers can branch on presence.\n *\n * Reads `candidates[0].content.parts` directly rather than the\n * `response.functionCalls` getter: the getter discards the\n * part-level `thoughtSignature`, and Gemini \"thinking\" models 400\n * the follow-up turn if that signature is not echoed back. See\n * `partToToolCall`.\n */\n private extractToolCalls(\n response: GenerateContentResponse,\n ): ModelToolCallRequest[] | undefined {\n const parts = response.candidates?.[0]?.content?.parts ?? [];\n const toolCalls = parts\n .map((part) => this.partToToolCall(part))\n .filter((call): call is ModelToolCallRequest => call !== undefined);\n\n return toolCalls.length > 0 ? toolCalls : undefined;\n }\n\n /**\n * Map a single Gemini `Part` to a neutral `ModelToolCallRequest`,\n * or `undefined` when the part is not a function call. The part's\n * `thoughtSignature` (opaque, set by thinking models) is carried on\n * `providerMetadata` so `toGoogleContents` can replay it on the\n * assistant turn — Gemini rejects the next request without it.\n */\n private partToToolCall(part: Part): ModelToolCallRequest | undefined {\n if (!part.functionCall) {\n return undefined;\n }\n\n const call = part.functionCall;\n\n return {\n // The Gemini Developer API does not assign function-call ids\n // (only Vertex parallel-calling does). Fall back to the function\n // name so the neutral `toolCallId` is non-empty and the echoed\n // `functionResponse.name` resolves — Gemini matches a result to\n // its call by name. See decisions §49.\n id: call.id ?? call.name ?? \"\",\n name: call.name ?? \"\",\n input: (call.args ?? {}) as Record<string, unknown>,\n ...(part.thoughtSignature\n ? { providerMetadata: { thoughtSignature: part.thoughtSignature } }\n : {}),\n };\n }\n\n /**\n * Normalize Gemini's `usageMetadata` into the neutral `Usage` shape.\n * Cache-read tokens are surfaced as `cachedTokens` only when\n * non-zero. Absent usage collapses to zeros.\n */\n private extractUsage(response: GenerateContentResponse): Usage {\n const usage: Usage = { input: 0, output: 0, total: 0 };\n\n if (response.usageMetadata) {\n this.applyUsage(usage, response.usageMetadata);\n }\n\n return usage;\n }\n\n /**\n * Fold a Gemini `usageMetadata` block into the running neutral\n * `Usage` accumulator. Shared by `complete()` and the streaming\n * loop (where the final chunk carries cumulative totals).\n */\n private applyUsage(\n usage: Usage,\n raw: NonNullable<GenerateContentResponse[\"usageMetadata\"]>,\n ): void {\n usage.input = raw.promptTokenCount ?? usage.input;\n usage.output = raw.candidatesTokenCount ?? usage.output;\n usage.total = raw.totalTokenCount ?? usage.input + usage.output;\n\n const cached = raw.cachedContentTokenCount;\n\n if (cached && cached > 0) {\n usage.cachedTokens = cached;\n }\n }\n\n /**\n * Wrap a thrown provider error into the typed `AIError` hierarchy\n * and emit the standard error log line before it propagates.\n */\n private logAndWrap(thrown: unknown) {\n const wrapped = wrapGoogleError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n return wrapped;\n }\n}\n","import { GoogleGenAI } from \"@google/genai\";\nimport type {\n EmbedderContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n} from \"@warlock.js/ai\";\nimport { approximateTokenCount } from \"@warlock.js/ai\";\nimport type {\n GoogleEmbedderConfig,\n GoogleModelConfig,\n GoogleSDKConfig,\n} from \"./config.type\";\nimport { GoogleEmbedder } from \"./embedder\";\nimport { GoogleModel } from \"./model\";\n\n/**\n * Google Gemini-backed implementation of `SDKAdapterContract`.\n *\n * **Role.** The package entry point for Gemini models via the\n * `@google/genai` SDK. A single `GoogleSDK` holds one live\n * `GoogleGenAI` client, shared by every `ModelContract` /\n * `EmbedderContract` it produces. Construct one SDK per\n * account/project and reuse it everywhere.\n *\n * **Responsibility.**\n * - Owns: a long-lived `GoogleGenAI` client (auth, Vertex vs Gemini\n * API) and its lifetime. Factory for `GoogleModel` /\n * `GoogleEmbedder` instances sharing that client.\n * - Does NOT own: anything per-call — those live in `GoogleModel` /\n * `GoogleEmbedder` and the agent runtime.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across many calls\"), fronted by FP usage like the other adapters.\n *\n * @example\n * const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY! });\n * const model = google.model({ name: \"gemini-2.5-flash\", temperature: 0.7 });\n * const embedder = google.embedder({ name: \"gemini-embedding-001\" });\n */\nexport class GoogleSDK implements SDKAdapterContract {\n private readonly ai: GoogleGenAI;\n private readonly provider: string;\n private readonly pricing?: Record<string, ModelPricing>;\n\n public constructor(config: GoogleSDKConfig) {\n const { provider, pricing, ...clientOptions } = config;\n\n this.ai = new GoogleGenAI(clientOptions);\n this.provider = provider ?? \"google\";\n this.pricing = pricing;\n }\n\n /**\n * Build a `GoogleModel` bound to this SDK's client. Each call\n * returns a fresh instance; all instances share the underlying\n * `GoogleGenAI` client. The SDK's `provider` label is forwarded.\n *\n * Pricing resolution: per-model `config.pricing` wins; otherwise the\n * SDK-level registry entry keyed by `config.name`; otherwise\n * `undefined` (no cost computed).\n */\n public model(config: GoogleModelConfig): ModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: GoogleModelConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n return new GoogleModel(this.ai, resolvedConfig, this.provider);\n }\n\n /**\n * Rough token-count estimate. Uses the character-heuristic\n * (`approximateTokenCount`) from the core package — Gemini's\n * `countTokens` is a network round-trip; `count()` is intentionally\n * offline. Good for budgeting/quota guards, not billing.\n */\n public async count(text: string, _model?: string): Promise<number> {\n return approximateTokenCount(text);\n }\n\n /**\n * Build a `GoogleEmbedder` bound to this SDK's client.\n *\n * @example\n * const embedder = google.embedder({ name: \"gemini-embedding-001\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n */\n public embedder(config: GoogleEmbedderConfig): EmbedderContract {\n return new GoogleEmbedder(this.ai, config, this.provider);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ import { GoogleGenAIOptions } from "@google/genai";
2
+ import { EmbedderConfig, ModelConfig, ModelPricing } from "@warlock.js/ai";
3
+
4
+ //#region ../../@warlock.js/ai-google/src/config.type.d.ts
5
+ /**
6
+ * Configuration for the Google Gemini SDK adapter.
7
+ *
8
+ * Wraps `GoogleGenAIOptions` from `@google/genai`. The common path is
9
+ * the Gemini API with an `apiKey`; the same options also support
10
+ * Vertex AI (`vertexai: true` + `project` / `location`). The whole
11
+ * object is forwarded to `new GoogleGenAI(...)`, so any client option
12
+ * (`apiVersion`, `httpOptions`) is accepted as-is.
13
+ *
14
+ * `provider` labels the SDK upstream — flows through to
15
+ * `ModelContract.provider`, `AgentReport.model`, logs, and
16
+ * provider-aware middleware. Defaults to `"google"`.
17
+ *
18
+ * `pricing` is an optional SDK-level registry keyed by model name.
19
+ * Resolution at `model()` call time: per-model `pricing` > this SDK
20
+ * registry > `undefined` (no cost computed).
21
+ *
22
+ * @example
23
+ * new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY! });
24
+ *
25
+ * @example
26
+ * // Vertex AI:
27
+ * new GoogleSDK({ vertexai: true, project: "my-proj", location: "us-central1" });
28
+ *
29
+ * @example
30
+ * new GoogleSDK({
31
+ * apiKey,
32
+ * pricing: { "gemini-2.5-flash": { input: 0.3, output: 2.5 } },
33
+ * });
34
+ */
35
+ type GoogleSDKConfig = GoogleGenAIOptions & {
36
+ provider?: string;
37
+ /**
38
+ * Per-model USD pricing registry, keyed by model name. Surfaced onto
39
+ * every `GoogleModel` produced by `model()`; per-model
40
+ * `GoogleModelConfig.pricing` still wins when both are set.
41
+ */
42
+ pricing?: Record<string, ModelPricing>;
43
+ };
44
+ /**
45
+ * Per-model configuration for `GoogleSDK.model()`. `name` is the
46
+ * Gemini model id (e.g. `"gemini-2.5-flash"`, `"gemini-2.5-pro"`).
47
+ *
48
+ * @example
49
+ * google.model({ name: "gemini-2.5-flash" });
50
+ * google.model({ name: "gemini-1.0-pro", vision: false });
51
+ */
52
+ type GoogleModelConfig = ModelConfig & {
53
+ /**
54
+ * Override the auto-inferred vision capability. When omitted, the
55
+ * adapter checks the model id against the known multimodal Gemini
56
+ * families (see `known-vision-models.ts`). Explicit `true`/`false`
57
+ * always wins over inference.
58
+ */
59
+ vision?: boolean;
60
+ /**
61
+ * Override the inferred `structuredOutput` capability. When omitted,
62
+ * the adapter treats the model as capable and forwards
63
+ * `responseSchema` via Gemini's native `responseJsonSchema` +
64
+ * `responseMimeType: "application/json"`. Set `false` for models
65
+ * that don't support it — the agent then re-injects a soft schema
66
+ * hint into the system prompt instead.
67
+ */
68
+ structuredOutput?: boolean;
69
+ };
70
+ /**
71
+ * Per-embedder configuration for `GoogleSDK.embedder()`. `name` is the
72
+ * embeddings model id (e.g. `"gemini-embedding-001"`,
73
+ * `"text-embedding-004"`). `dimensions` is forwarded to Gemini's
74
+ * `outputDimensionality` truncation hint (supported by 2024+ models).
75
+ *
76
+ * @example
77
+ * google.embedder({ name: "gemini-embedding-001" });
78
+ * google.embedder({ name: "gemini-embedding-001", dimensions: 768 });
79
+ */
80
+ type GoogleEmbedderConfig = EmbedderConfig;
81
+ //#endregion
82
+ export { GoogleEmbedderConfig, GoogleModelConfig, GoogleSDKConfig };
83
+ //# sourceMappingURL=config.type.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.type.d.mts","names":[],"sources":["../../../../../@warlock.js/ai-google/src/config.type.ts"],"mappings":";;;;;;AAiCA;;;;;;;;;;;;;;AAOuC;AAWvC;;;;;;;;AAgBkB;AAalB;;;;KA/CY,eAAA,GAAkB,kBAAA;EAC5B,QAAA;;;;;;EAMA,OAAA,GAAU,MAAA,SAAe,YAAA;AAAA;;;;;;;;;KAWf,iBAAA,GAAoB,WAAW;;;;;;;EAOzC,MAAA;;;;;;;;;EASA,gBAAA;AAAA;;;;;;;;;;;KAaU,oBAAA,GAAuB,cAAc"}
@@ -0,0 +1,103 @@
1
+ import { wrapGoogleError } from "./utils/wrap-google-error.mjs";
2
+ import "./utils/index.mjs";
3
+ import { log } from "@warlock.js/logger";
4
+
5
+ //#region ../../@warlock.js/ai-google/src/embedder.ts
6
+ const LOG_MODULE = "ai.google";
7
+ /**
8
+ * Token usage is not returned by Gemini's `embedContent`, so every
9
+ * embedding result reports a zeroed `EmbeddingUsage` (honest absence,
10
+ * not a fabricated estimate).
11
+ */
12
+ const NO_USAGE = {
13
+ promptTokens: 0,
14
+ totalTokens: 0
15
+ };
16
+ /**
17
+ * Google Gemini-backed implementation of `EmbedderContract`
18
+ * (`gemini-embedding-001`, `text-embedding-004`, …) via
19
+ * `models.embedContent`.
20
+ *
21
+ * **Role.** Converts text into floating-point vectors. Standalone
22
+ * primitive — unrelated to generateContent / tools / the agent loop.
23
+ *
24
+ * **Batch is native.** Gemini's `embedContent` accepts an array of
25
+ * inputs and returns embeddings in the same order, so `embedMany` is
26
+ * a single request (unlike the Bedrock/Titan adapter, which has to
27
+ * loop).
28
+ *
29
+ * **No usage.** Gemini's embed endpoint returns no token counts;
30
+ * `usage` is always `{ promptTokens: 0, totalTokens: 0 }`.
31
+ *
32
+ * **Dimensions.** When no `dimensions` override is given,
33
+ * `this.dimensions` starts at `0` and is populated from the first
34
+ * response's vector length, then cached. Passing `dimensions`
35
+ * forwards Gemini's `outputDimensionality` truncation hint and sets
36
+ * the initial value immediately.
37
+ *
38
+ * @example
39
+ * const embedder = new GoogleEmbedder(ai, { name: "gemini-embedding-001" });
40
+ * const { vector } = await embedder.embed("Hello world");
41
+ * const { vectors } = await embedder.embedMany(["doc 1", "doc 2"]);
42
+ */
43
+ var GoogleEmbedder = class {
44
+ constructor(ai, config, provider = "google") {
45
+ this.logger = log;
46
+ this.ai = ai;
47
+ this.name = config.name;
48
+ this.provider = provider;
49
+ this.configuredDimensions = config.dimensions;
50
+ this.dimensions = config.dimensions ?? 0;
51
+ }
52
+ async embed(input) {
53
+ return {
54
+ vector: (await this.request([input]))[0],
55
+ dimensions: this.dimensions,
56
+ usage: NO_USAGE
57
+ };
58
+ }
59
+ async embedMany(inputs) {
60
+ return {
61
+ vectors: await this.request(inputs),
62
+ dimensions: this.dimensions,
63
+ usage: NO_USAGE
64
+ };
65
+ }
66
+ /**
67
+ * Shared transport: one `embedContent` call for the whole batch,
68
+ * wrap provider errors, cache `dimensions` from the first vector,
69
+ * and return the raw vectors in input order.
70
+ */
71
+ async request(inputs) {
72
+ this.logger.debug(LOG_MODULE, "embedder.request", "embedContent", {
73
+ model: this.name,
74
+ count: inputs.length
75
+ });
76
+ let response;
77
+ try {
78
+ response = await this.ai.models.embedContent({
79
+ model: this.name,
80
+ contents: inputs,
81
+ ...this.configuredDimensions !== void 0 ? { config: { outputDimensionality: this.configuredDimensions } } : {}
82
+ });
83
+ } catch (thrown) {
84
+ const wrapped = wrapGoogleError(thrown);
85
+ this.logger.error(LOG_MODULE, "embedder.error", wrapped.message, {
86
+ code: wrapped.code,
87
+ context: wrapped.context
88
+ });
89
+ throw wrapped;
90
+ }
91
+ const vectors = (response.embeddings ?? []).map((embedding) => embedding.values ?? []);
92
+ if (this.dimensions === 0 && vectors[0]) this.dimensions = vectors[0].length;
93
+ this.logger.debug(LOG_MODULE, "embedder.response", "embedContent returned", {
94
+ count: vectors.length,
95
+ dimensions: this.dimensions
96
+ });
97
+ return vectors;
98
+ }
99
+ };
100
+
101
+ //#endregion
102
+ export { GoogleEmbedder };
103
+ //# sourceMappingURL=embedder.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedder.mjs","names":[],"sources":["../../../../../@warlock.js/ai-google/src/embedder.ts"],"sourcesContent":["import {\n type EmbeddingBatchResult,\n type EmbeddingResult,\n type EmbeddingUsage,\n type EmbedderContract,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type { EmbedContentResponse, GoogleGenAI } from \"@google/genai\";\nimport type { GoogleEmbedderConfig } from \"./config.type\";\nimport { wrapGoogleError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.google\";\n\n/**\n * Token usage is not returned by Gemini's `embedContent`, so every\n * embedding result reports a zeroed `EmbeddingUsage` (honest absence,\n * not a fabricated estimate).\n */\nconst NO_USAGE: EmbeddingUsage = { promptTokens: 0, totalTokens: 0 };\n\n/**\n * Google Gemini-backed implementation of `EmbedderContract`\n * (`gemini-embedding-001`, `text-embedding-004`, …) via\n * `models.embedContent`.\n *\n * **Role.** Converts text into floating-point vectors. Standalone\n * primitive — unrelated to generateContent / tools / the agent loop.\n *\n * **Batch is native.** Gemini's `embedContent` accepts an array of\n * inputs and returns embeddings in the same order, so `embedMany` is\n * a single request (unlike the Bedrock/Titan adapter, which has to\n * loop).\n *\n * **No usage.** Gemini's embed endpoint returns no token counts;\n * `usage` is always `{ promptTokens: 0, totalTokens: 0 }`.\n *\n * **Dimensions.** When no `dimensions` override is given,\n * `this.dimensions` starts at `0` and is populated from the first\n * response's vector length, then cached. Passing `dimensions`\n * forwards Gemini's `outputDimensionality` truncation hint and sets\n * the initial value immediately.\n *\n * @example\n * const embedder = new GoogleEmbedder(ai, { name: \"gemini-embedding-001\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n * const { vectors } = await embedder.embedMany([\"doc 1\", \"doc 2\"]);\n */\nexport class GoogleEmbedder implements EmbedderContract {\n public readonly name: string;\n public readonly provider: string;\n public dimensions: number;\n\n private readonly ai: GoogleGenAI;\n private readonly configuredDimensions: number | undefined;\n private readonly logger: Logger = log;\n\n public constructor(\n ai: GoogleGenAI,\n config: GoogleEmbedderConfig,\n provider: string = \"google\",\n ) {\n this.ai = ai;\n this.name = config.name;\n this.provider = provider;\n this.configuredDimensions = config.dimensions;\n this.dimensions = config.dimensions ?? 0;\n }\n\n public async embed(input: string): Promise<EmbeddingResult> {\n const vectors = await this.request([input]);\n\n return { vector: vectors[0], dimensions: this.dimensions, usage: NO_USAGE };\n }\n\n public async embedMany(inputs: string[]): Promise<EmbeddingBatchResult> {\n const vectors = await this.request(inputs);\n\n return { vectors, dimensions: this.dimensions, usage: NO_USAGE };\n }\n\n /**\n * Shared transport: one `embedContent` call for the whole batch,\n * wrap provider errors, cache `dimensions` from the first vector,\n * and return the raw vectors in input order.\n */\n private async request(inputs: string[]): Promise<number[][]> {\n this.logger.debug(LOG_MODULE, \"embedder.request\", \"embedContent\", {\n model: this.name,\n count: inputs.length,\n });\n\n let response: EmbedContentResponse;\n\n try {\n response = await this.ai.models.embedContent({\n model: this.name,\n contents: inputs,\n ...(this.configuredDimensions !== undefined\n ? { config: { outputDimensionality: this.configuredDimensions } }\n : {}),\n });\n } catch (thrown) {\n const wrapped = wrapGoogleError(thrown);\n\n this.logger.error(LOG_MODULE, \"embedder.error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const vectors = (response.embeddings ?? []).map((embedding) => embedding.values ?? []);\n\n if (this.dimensions === 0 && vectors[0]) {\n this.dimensions = vectors[0].length;\n }\n\n this.logger.debug(LOG_MODULE, \"embedder.response\", \"embedContent returned\", {\n count: vectors.length,\n dimensions: this.dimensions,\n });\n\n return vectors;\n }\n}\n"],"mappings":";;;;;AAWA,MAAM,aAAa;;;;;;AAOnB,MAAM,WAA2B;CAAE,cAAc;CAAG,aAAa;AAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BnE,IAAa,iBAAb,MAAwD;CAStD,AAAO,YACL,IACA,QACA,WAAmB,UACnB;gBANgC;EAOhC,KAAK,KAAK;EACV,KAAK,OAAO,OAAO;EACnB,KAAK,WAAW;EAChB,KAAK,uBAAuB,OAAO;EACnC,KAAK,aAAa,OAAO,cAAc;CACzC;CAEA,MAAa,MAAM,OAAyC;EAG1D,OAAO;GAAE,SAAQ,MAFK,KAAK,QAAQ,CAAC,KAAK,CAAC,GAEjB;GAAI,YAAY,KAAK;GAAY,OAAO;EAAS;CAC5E;CAEA,MAAa,UAAU,QAAiD;EAGtE,OAAO;GAAE,eAFa,KAAK,QAAQ,MAAM;GAEvB,YAAY,KAAK;GAAY,OAAO;EAAS;CACjE;;;;;;CAOA,MAAc,QAAQ,QAAuC;EAC3D,KAAK,OAAO,MAAM,YAAY,oBAAoB,gBAAgB;GAChE,OAAO,KAAK;GACZ,OAAO,OAAO;EAChB,CAAC;EAED,IAAI;EAEJ,IAAI;GACF,WAAW,MAAM,KAAK,GAAG,OAAO,aAAa;IAC3C,OAAO,KAAK;IACZ,UAAU;IACV,GAAI,KAAK,yBAAyB,SAC9B,EAAE,QAAQ,EAAE,sBAAsB,KAAK,qBAAqB,EAAE,IAC9D,CAAC;GACP,CAAC;EACH,SAAS,QAAQ;GACf,MAAM,UAAU,gBAAgB,MAAM;GAEtC,KAAK,OAAO,MAAM,YAAY,kBAAkB,QAAQ,SAAS;IAC/D,MAAM,QAAQ;IACd,SAAS,QAAQ;GACnB,CAAC;GAED,MAAM;EACR;EAEA,MAAM,WAAW,SAAS,cAAc,CAAC,GAAG,KAAK,cAAc,UAAU,UAAU,CAAC,CAAC;EAErF,IAAI,KAAK,eAAe,KAAK,QAAQ,IACnC,KAAK,aAAa,QAAQ,GAAG;EAG/B,KAAK,OAAO,MAAM,YAAY,qBAAqB,yBAAyB;GAC1E,OAAO,QAAQ;GACf,YAAY,KAAK;EACnB,CAAC;EAED,OAAO;CACT;AACF"}
@@ -0,0 +1,3 @@
1
+ import { GoogleEmbedderConfig, GoogleModelConfig, GoogleSDKConfig } from "./config.type.mjs";
2
+ import { GoogleSDK } from "./sdk.mjs";
3
+ export { type GoogleEmbedderConfig, type GoogleModelConfig, GoogleSDK, type GoogleSDKConfig };
package/esm/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { GoogleSDK } from "./sdk.mjs";
2
+
3
+ export { GoogleSDK };
@@ -0,0 +1,39 @@
1
+ //#region ../../@warlock.js/ai-google/src/known-vision-models.ts
2
+ /**
3
+ * Substrings identifying Gemini model ids whose family accepts image
4
+ * input (vision).
5
+ *
6
+ * Every Gemini 1.5, 2.x, and 2.5 model is natively multimodal, as is
7
+ * the legacy `gemini-pro-vision`. Only the original text-only
8
+ * `gemini-pro` / `gemini-1.0-pro` is excluded. A substring match
9
+ * tolerates the date/preview suffixes Google appends
10
+ * (`gemini-2.5-flash-preview-05-20`). Override per-model via
11
+ * `google.model({ name, vision: true | false })`.
12
+ */
13
+ const VISION_CAPABLE_SUBSTRINGS = [
14
+ "gemini-1.5",
15
+ "gemini-2",
16
+ "gemini-exp",
17
+ "gemini-pro-vision",
18
+ "gemini-flash"
19
+ ];
20
+ /**
21
+ * Infer whether a Gemini model id supports vision based on the known
22
+ * multimodal-family substrings. Unknown ids default to `false` so
23
+ * passing an image attachment to an unsupported model surfaces a
24
+ * clear, agent-side capability error instead of an opaque Gemini 400.
25
+ *
26
+ * @example
27
+ * inferVisionCapability("gemini-2.5-flash"); // → true
28
+ * inferVisionCapability("gemini-1.5-pro-002"); // → true
29
+ * inferVisionCapability("gemini-1.0-pro"); // → false
30
+ * inferVisionCapability("text-embedding-004"); // → false
31
+ */
32
+ function inferVisionCapability(modelId) {
33
+ const normalized = modelId.toLowerCase();
34
+ return VISION_CAPABLE_SUBSTRINGS.some((fragment) => normalized.includes(fragment));
35
+ }
36
+
37
+ //#endregion
38
+ export { inferVisionCapability };
39
+ //# sourceMappingURL=known-vision-models.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"known-vision-models.mjs","names":[],"sources":["../../../../../@warlock.js/ai-google/src/known-vision-models.ts"],"sourcesContent":["/**\n * Substrings identifying Gemini model ids whose family accepts image\n * input (vision).\n *\n * Every Gemini 1.5, 2.x, and 2.5 model is natively multimodal, as is\n * the legacy `gemini-pro-vision`. Only the original text-only\n * `gemini-pro` / `gemini-1.0-pro` is excluded. A substring match\n * tolerates the date/preview suffixes Google appends\n * (`gemini-2.5-flash-preview-05-20`). Override per-model via\n * `google.model({ name, vision: true | false })`.\n */\nconst VISION_CAPABLE_SUBSTRINGS = [\n \"gemini-1.5\",\n \"gemini-2\",\n \"gemini-exp\",\n \"gemini-pro-vision\",\n \"gemini-flash\",\n];\n\n/**\n * Infer whether a Gemini model id supports vision based on the known\n * multimodal-family substrings. Unknown ids default to `false` so\n * passing an image attachment to an unsupported model surfaces a\n * clear, agent-side capability error instead of an opaque Gemini 400.\n *\n * @example\n * inferVisionCapability(\"gemini-2.5-flash\"); // → true\n * inferVisionCapability(\"gemini-1.5-pro-002\"); // → true\n * inferVisionCapability(\"gemini-1.0-pro\"); // → false\n * inferVisionCapability(\"text-embedding-004\"); // → false\n */\nexport function inferVisionCapability(modelId: string): boolean {\n const normalized = modelId.toLowerCase();\n\n return VISION_CAPABLE_SUBSTRINGS.some((fragment) => normalized.includes(fragment));\n}\n"],"mappings":";;;;;;;;;;;;AAWA,MAAM,4BAA4B;CAChC;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;;AAcA,SAAgB,sBAAsB,SAA0B;CAC9D,MAAM,aAAa,QAAQ,YAAY;CAEvC,OAAO,0BAA0B,MAAM,aAAa,WAAW,SAAS,QAAQ,CAAC;AACnF"}
package/esm/model.mjs ADDED
@@ -0,0 +1,277 @@
1
+ import { mapFinishReason } from "./utils/map-finish-reason.mjs";
2
+ import { toGoogleContents } from "./utils/to-google-contents.mjs";
3
+ import { toGoogleTools } from "./utils/to-google-tools.mjs";
4
+ import { wrapGoogleError } from "./utils/wrap-google-error.mjs";
5
+ import "./utils/index.mjs";
6
+ import { inferVisionCapability } from "./known-vision-models.mjs";
7
+ import { log } from "@warlock.js/logger";
8
+
9
+ //#region ../../@warlock.js/ai-google/src/model.ts
10
+ const LOG_MODULE = "ai.google";
11
+ /**
12
+ * Google Gemini-backed implementation of `ModelContract`.
13
+ *
14
+ * **Role.** The provider-facing bridge between the vendor-neutral
15
+ * `@warlock.js/ai` agent runtime and the `@google/genai` SDK
16
+ * (`models.generateContent` / `generateContentStream`).
17
+ *
18
+ * **Responsibility.**
19
+ * - Owns: a long-lived `GoogleGenAI` client + frozen `ModelConfig`
20
+ * (name, temperature, maxTokens) used as per-call defaults.
21
+ * - Owns: translating vendor-neutral `Message[]` / `ToolConfig[]` into
22
+ * Gemini shapes (systemInstruction hoisting, `model` role,
23
+ * `functionCall` / `functionResponse` parts, inline image bytes) on
24
+ * the way out, and Gemini's candidate/parts response (text, function
25
+ * calls, finish reason, token usage) back into neutral shapes on the
26
+ * way in.
27
+ * - Does NOT own: dispatching tools, looping, history, retries — those
28
+ * are agent concerns. The model is a per-call protocol adapter.
29
+ *
30
+ * Modeled as a class (see §4.2 of code-style.md — "long-lived state
31
+ * across calls"): the `GoogleGenAI` client is reused for the SDK's
32
+ * lifetime.
33
+ *
34
+ * @example
35
+ * import { GoogleGenAI } from "@google/genai";
36
+ * const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
37
+ * const model = new GoogleModel(ai, { name: "gemini-2.5-flash" });
38
+ *
39
+ * const myAgent = agent({ model, tools: [searchTool] });
40
+ * const result = await myAgent.execute("Summarize today's news.");
41
+ */
42
+ var GoogleModel = class {
43
+ constructor(ai, config, provider = "google") {
44
+ this.logger = log;
45
+ this.ai = ai;
46
+ this.config = config;
47
+ this.name = config.name;
48
+ this.provider = provider;
49
+ this.pricing = config.pricing;
50
+ this.capabilities = {
51
+ structuredOutput: config.structuredOutput ?? true,
52
+ vision: config.vision ?? inferVisionCapability(config.name)
53
+ };
54
+ }
55
+ /**
56
+ * Single-shot completion. Sends the full message list to
57
+ * `generateContent`, waits for the terminal response, and reshapes
58
+ * it into a vendor-neutral `ModelResponse`. Per-call `options`
59
+ * override the instance defaults for this call only.
60
+ */
61
+ async complete(messages, options) {
62
+ this.logger.debug(LOG_MODULE, "request", "Starting generateContent call", {
63
+ model: this.name,
64
+ messageCount: messages.length,
65
+ streaming: false,
66
+ toolCount: options?.tools?.length ?? 0
67
+ });
68
+ const { systemInstruction, contents } = toGoogleContents(messages);
69
+ let response;
70
+ try {
71
+ response = await this.ai.models.generateContent({
72
+ model: this.name,
73
+ contents,
74
+ config: this.buildConfig(systemInstruction, options)
75
+ });
76
+ } catch (thrown) {
77
+ throw this.logAndWrap(thrown);
78
+ }
79
+ const toolCalls = this.extractToolCalls(response);
80
+ const finishReason = toolCalls ? "tool_calls" : mapFinishReason(response.candidates?.[0]?.finishReason);
81
+ const usage = this.extractUsage(response);
82
+ this.logger.debug(LOG_MODULE, "response", "generateContent call succeeded", {
83
+ finishReason,
84
+ usage
85
+ });
86
+ return {
87
+ content: response.text ?? "",
88
+ finishReason,
89
+ usage,
90
+ toolCalls
91
+ };
92
+ }
93
+ /**
94
+ * Incremental streaming completion via `generateContentStream`.
95
+ * Yields neutral `ModelStreamChunk`s — `delta` for text, `tool-call`
96
+ * per function call (Gemini emits a fully-formed call, not partial
97
+ * JSON), and a terminal `done` with the final finish reason + usage.
98
+ */
99
+ async *stream(messages, options) {
100
+ this.logger.debug(LOG_MODULE, "request", "Starting generateContentStream call", {
101
+ model: this.name,
102
+ messageCount: messages.length,
103
+ streaming: true,
104
+ toolCount: options?.tools?.length ?? 0
105
+ });
106
+ const { systemInstruction, contents } = toGoogleContents(messages);
107
+ let iterable;
108
+ try {
109
+ iterable = await this.ai.models.generateContentStream({
110
+ model: this.name,
111
+ contents,
112
+ config: this.buildConfig(systemInstruction, options)
113
+ });
114
+ } catch (thrown) {
115
+ throw this.logAndWrap(thrown);
116
+ }
117
+ let rawFinishReason;
118
+ let sawToolCall = false;
119
+ const usage = {
120
+ input: 0,
121
+ output: 0,
122
+ total: 0
123
+ };
124
+ try {
125
+ for await (const chunk of iterable) {
126
+ const text = chunk.text;
127
+ if (text) yield {
128
+ type: "delta",
129
+ content: text
130
+ };
131
+ for (const part of chunk.candidates?.[0]?.content?.parts ?? []) {
132
+ const toolCall = this.partToToolCall(part);
133
+ if (!toolCall) continue;
134
+ sawToolCall = true;
135
+ yield {
136
+ type: "tool-call",
137
+ id: toolCall.id,
138
+ name: toolCall.name,
139
+ input: toolCall.input,
140
+ ...toolCall.providerMetadata ? { providerMetadata: toolCall.providerMetadata } : {}
141
+ };
142
+ }
143
+ const candidateFinish = chunk.candidates?.[0]?.finishReason;
144
+ if (candidateFinish) rawFinishReason = candidateFinish;
145
+ if (chunk.usageMetadata) this.applyUsage(usage, chunk.usageMetadata);
146
+ }
147
+ } catch (thrown) {
148
+ throw this.logAndWrap(thrown);
149
+ }
150
+ const finishReason = sawToolCall ? "tool_calls" : mapFinishReason(rawFinishReason);
151
+ this.logger.debug(LOG_MODULE, "response", "generateContentStream call succeeded", {
152
+ finishReason,
153
+ usage
154
+ });
155
+ yield {
156
+ type: "done",
157
+ finishReason,
158
+ usage
159
+ };
160
+ }
161
+ /**
162
+ * Assemble the `GenerateContentConfig` shared by `complete()` and
163
+ * `stream()`: inference params, hoisted system instruction,
164
+ * cancellation signal, and conditional tools + native structured
165
+ * output.
166
+ */
167
+ buildConfig(systemInstruction, options) {
168
+ const temperature = options?.temperature ?? this.config.temperature;
169
+ const maxOutputTokens = options?.maxTokens ?? this.config.maxTokens;
170
+ return {
171
+ ...systemInstruction ? { systemInstruction } : {},
172
+ ...temperature !== void 0 ? { temperature } : {},
173
+ ...maxOutputTokens !== void 0 ? { maxOutputTokens } : {},
174
+ ...options?.signal ? { abortSignal: options.signal } : {},
175
+ ...this.buildTools(options?.tools),
176
+ ...this.buildStructuredOutput(options?.responseSchema)
177
+ };
178
+ }
179
+ /**
180
+ * Spread-friendly tools fragment. Empty object when no tools were
181
+ * supplied so the caller can unconditionally spread it.
182
+ */
183
+ buildTools(tools) {
184
+ const mapped = toGoogleTools(tools);
185
+ return mapped ? { tools: mapped } : {};
186
+ }
187
+ /**
188
+ * Translate the neutral `responseSchema` into Gemini's native JSON
189
+ * structured output (`responseMimeType: "application/json"` +
190
+ * `responseJsonSchema`, which takes a raw JSON Schema directly).
191
+ * Emitted only when the model is `structuredOutput`-capable and the
192
+ * schema is an object root — otherwise the agent's soft prompt hint
193
+ * + client-side `validate()` carry shape.
194
+ */
195
+ buildStructuredOutput(responseSchema) {
196
+ if (!responseSchema || !this.capabilities.structuredOutput) return {};
197
+ if (responseSchema.type !== "object" || typeof responseSchema.properties !== "object") return {};
198
+ return {
199
+ responseMimeType: "application/json",
200
+ responseJsonSchema: responseSchema
201
+ };
202
+ }
203
+ /**
204
+ * Reshape Gemini's function-call content parts into the neutral
205
+ * `ModelToolCallRequest[]`. Returns `undefined` when the model
206
+ * requested no functions so callers can branch on presence.
207
+ *
208
+ * Reads `candidates[0].content.parts` directly rather than the
209
+ * `response.functionCalls` getter: the getter discards the
210
+ * part-level `thoughtSignature`, and Gemini "thinking" models 400
211
+ * the follow-up turn if that signature is not echoed back. See
212
+ * `partToToolCall`.
213
+ */
214
+ extractToolCalls(response) {
215
+ const toolCalls = (response.candidates?.[0]?.content?.parts ?? []).map((part) => this.partToToolCall(part)).filter((call) => call !== void 0);
216
+ return toolCalls.length > 0 ? toolCalls : void 0;
217
+ }
218
+ /**
219
+ * Map a single Gemini `Part` to a neutral `ModelToolCallRequest`,
220
+ * or `undefined` when the part is not a function call. The part's
221
+ * `thoughtSignature` (opaque, set by thinking models) is carried on
222
+ * `providerMetadata` so `toGoogleContents` can replay it on the
223
+ * assistant turn — Gemini rejects the next request without it.
224
+ */
225
+ partToToolCall(part) {
226
+ if (!part.functionCall) return;
227
+ const call = part.functionCall;
228
+ return {
229
+ id: call.id ?? call.name ?? "",
230
+ name: call.name ?? "",
231
+ input: call.args ?? {},
232
+ ...part.thoughtSignature ? { providerMetadata: { thoughtSignature: part.thoughtSignature } } : {}
233
+ };
234
+ }
235
+ /**
236
+ * Normalize Gemini's `usageMetadata` into the neutral `Usage` shape.
237
+ * Cache-read tokens are surfaced as `cachedTokens` only when
238
+ * non-zero. Absent usage collapses to zeros.
239
+ */
240
+ extractUsage(response) {
241
+ const usage = {
242
+ input: 0,
243
+ output: 0,
244
+ total: 0
245
+ };
246
+ if (response.usageMetadata) this.applyUsage(usage, response.usageMetadata);
247
+ return usage;
248
+ }
249
+ /**
250
+ * Fold a Gemini `usageMetadata` block into the running neutral
251
+ * `Usage` accumulator. Shared by `complete()` and the streaming
252
+ * loop (where the final chunk carries cumulative totals).
253
+ */
254
+ applyUsage(usage, raw) {
255
+ usage.input = raw.promptTokenCount ?? usage.input;
256
+ usage.output = raw.candidatesTokenCount ?? usage.output;
257
+ usage.total = raw.totalTokenCount ?? usage.input + usage.output;
258
+ const cached = raw.cachedContentTokenCount;
259
+ if (cached && cached > 0) usage.cachedTokens = cached;
260
+ }
261
+ /**
262
+ * Wrap a thrown provider error into the typed `AIError` hierarchy
263
+ * and emit the standard error log line before it propagates.
264
+ */
265
+ logAndWrap(thrown) {
266
+ const wrapped = wrapGoogleError(thrown);
267
+ this.logger.error(LOG_MODULE, "error", wrapped.message, {
268
+ code: wrapped.code,
269
+ context: wrapped.context
270
+ });
271
+ return wrapped;
272
+ }
273
+ };
274
+
275
+ //#endregion
276
+ export { GoogleModel };
277
+ //# sourceMappingURL=model.mjs.map