@warlock.js/ai-google 5.29.0 → 5.31.0

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/cjs/index.cjs +206 -31
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/config.type.d.mts +1 -1
  5. package/esm/config.type.d.mts.map +1 -1
  6. package/esm/embedder.mjs +1 -1
  7. package/esm/embedder.mjs.map +1 -1
  8. package/esm/gemini-image.d.mts +1 -1
  9. package/esm/gemini-image.d.mts.map +1 -1
  10. package/esm/gemini-image.mjs +1 -1
  11. package/esm/gemini-image.mjs.map +1 -1
  12. package/esm/image.d.mts +1 -1
  13. package/esm/image.d.mts.map +1 -1
  14. package/esm/image.mjs +1 -1
  15. package/esm/image.mjs.map +1 -1
  16. package/esm/index.d.mts +3 -1
  17. package/esm/index.mjs +2 -1
  18. package/esm/known-pricing.d.mts +33 -0
  19. package/esm/known-pricing.d.mts.map +1 -0
  20. package/esm/known-pricing.mjs +38 -0
  21. package/esm/known-pricing.mjs.map +1 -0
  22. package/esm/known-vision-models.mjs +1 -1
  23. package/esm/known-vision-models.mjs.map +1 -1
  24. package/esm/model.mjs +66 -10
  25. package/esm/model.mjs.map +1 -1
  26. package/esm/provider-options.d.mts +8 -0
  27. package/esm/provider-options.d.mts.map +1 -0
  28. package/esm/provider-options.mjs +36 -0
  29. package/esm/provider-options.mjs.map +1 -0
  30. package/esm/sdk.d.mts +4 -3
  31. package/esm/sdk.d.mts.map +1 -1
  32. package/esm/sdk.mjs +6 -4
  33. package/esm/sdk.mjs.map +1 -1
  34. package/esm/utils/apply-google-usage.mjs +6 -1
  35. package/esm/utils/apply-google-usage.mjs.map +1 -1
  36. package/esm/utils/map-finish-reason.mjs +1 -1
  37. package/esm/utils/map-finish-reason.mjs.map +1 -1
  38. package/esm/utils/to-google-contents.mjs +34 -2
  39. package/esm/utils/to-google-contents.mjs.map +1 -1
  40. package/esm/utils/to-google-tools.mjs +1 -1
  41. package/esm/utils/to-google-tools.mjs.map +1 -1
  42. package/esm/utils/wrap-google-error.mjs +22 -9
  43. package/esm/utils/wrap-google-error.mjs.map +1 -1
  44. package/llms-full.txt +113 -31
  45. package/llms.txt +1 -1
  46. package/package.json +5 -4
  47. package/skills/setup-google/SKILL.md +113 -31
@@ -1 +1 @@
1
- {"version":3,"file":"map-finish-reason.mjs","names":[],"sources":["../../../../../../../ai-google/src/utils/map-finish-reason.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"],"mappings":";AAEA,MAAM,kBAAgD;CACpD,MAAM;CACN,YAAY;AACd;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,gBAAgB,KAA8C;CAC5E,OAAO,gBAAgB,OAAO,OAAO;AACvC"}
1
+ {"version":3,"file":"map-finish-reason.mjs","names":[],"sources":["../../../../../../../../packages/ai-google/src/utils/map-finish-reason.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"],"mappings":";AAEA,MAAM,kBAAgD;CACpD,MAAM;CACN,YAAY;AACd;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,gBAAgB,KAA8C;CAC5E,OAAO,gBAAgB,OAAO,OAAO;AACvC"}
@@ -1,6 +1,6 @@
1
1
  import { InvalidRequestError, safeJsonParse } from "@warlock.js/ai";
2
2
 
3
- //#region ../ai-google/src/utils/to-google-contents.ts
3
+ //#region ../../packages/ai-google/src/utils/to-google-contents.ts
4
4
  /**
5
5
  * Convert vendor-neutral `Message[]` into Gemini's request shape.
6
6
  *
@@ -15,6 +15,12 @@ import { InvalidRequestError, safeJsonParse } from "@warlock.js/ai";
15
15
  * 4. **Tool calls are `functionCall` parts.** An assistant message
16
16
  * with `toolCalls` becomes a `"model"` content: an optional leading
17
17
  * `text` part followed by one `functionCall` part per call.
18
+ * 5. **Adjacent same-role turns merge.** Consecutive contents with the
19
+ * same role become one content with their parts in order, so the
20
+ * results of parallel tool calls travel as one `user` turn (as the
21
+ * `@google/genai` SDK's own automatic function calling sends them)
22
+ * and a compaction summary next to a same-role turn does not break
23
+ * the user/model alternation.
18
24
  *
19
25
  * @example
20
26
  * const { systemInstruction, contents } = toGoogleContents([
@@ -74,10 +80,36 @@ function toGoogleContents(messages) {
74
80
  }
75
81
  return {
76
82
  systemInstruction: systemParts.length > 0 ? systemParts.join("\n\n") : void 0,
77
- contents
83
+ contents: mergeAdjacentContents(contents)
78
84
  };
79
85
  }
80
86
  /**
87
+ * Join consecutive contents that share a role into one, keeping part
88
+ * order: parallel `functionResponse` parts stay in their `functionCall`
89
+ * order, and each `functionCall` keeps its own `thoughtSignature`.
90
+ * Returns new content objects; the input contents are not modified.
91
+ *
92
+ * Source for the single-turn shape: `@google/genai` 2.19.0
93
+ * `Models.generateContent` (automatic function calling) sends every
94
+ * function response of one model turn as a single `{ role: "user",
95
+ * parts }` content, in call order.
96
+ */
97
+ function mergeAdjacentContents(contents) {
98
+ const merged = [];
99
+ for (const content of contents) {
100
+ const previous = merged.at(-1);
101
+ if (previous && previous.role === content.role) {
102
+ merged[merged.length - 1] = {
103
+ ...previous,
104
+ parts: [...previous.parts ?? [], ...content.parts ?? []]
105
+ };
106
+ continue;
107
+ }
108
+ merged.push(content);
109
+ }
110
+ return merged;
111
+ }
112
+ /**
81
113
  * Multipart content is only meaningful on user messages — for any
82
114
  * other role collapse a `ContentPart[]` to concatenated text. Plain
83
115
  * strings pass through unchanged.
@@ -1 +1 @@
1
- {"version":3,"file":"to-google-contents.mjs","names":[],"sources":["../../../../../../../ai-google/src/utils/to-google-contents.ts"],"sourcesContent":["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`. All binary\n * modalities — **image, PDF, and audio** — go to a single\n * `inlineData: { mimeType, data }` block; Gemini's multimodal input is\n * media-agnostic and keys off the IANA `mimeType` (`image/png`,\n * `application/pdf`, `audio/mpeg`, …), so one mapping covers every part\n * type the model's capabilities admit. PDF and audio reach this point\n * only when the model declares the matching capability (`google.model`\n * infers `pdf` / `audio` from the multimodal Gemini families); the\n * agent's modality gate throws upfront otherwise, so capability and\n * behavior stay in lockstep.\n *\n * Gemini's `generateContent` does not fetch arbitrary remote URLs (only\n * Files API / GCS URIs via `fileData`), so a neutral `{ url }` source\n * surfaces a typed `InvalidRequestError` upfront — for any modality —\n * rather than a downstream Gemini fault. The agent resolves attachments\n * before this point, so nothing is 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 cannot fetch remote-URL ${part.type} media; supply base64 bytes instead.`,\n );\n }\n\n return {\n inlineData: { mimeType: part.source.mediaType, data: part.source.base64 },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,iBAAiB,UAAqC;CACpE,MAAM,cAAwB,CAAC;CAC/B,MAAM,WAAsB,CAAC;CAE7B,KAAK,MAAM,WAAW,UAAU;EAC9B,IAAI,QAAQ,SAAS,UAAU;GAC7B,YAAY,KAAK,iBAAiB,QAAQ,OAAO,CAAC;GAElD;EACF;EAEA,IAAI,QAAQ,SAAS,QAAQ;GAC3B,SAAS,KAAK;IACZ,MAAM;IACN,OAAO,CACL,EAME,kBAAkB;KAChB,MAAM,QAAQ,cAAc;KAC5B,UAAU,iBAAiB,iBAAiB,QAAQ,OAAO,CAAC;IAC9D,EACF,CACF;GACF,CAAC;GAED;EACF;EAEA,IAAI,QAAQ,SAAS,eAAe,QAAQ,aAAa,QAAQ,UAAU,SAAS,GAAG;GACrF,MAAM,QAAgB,CAAC;GACvB,MAAM,OAAO,iBAAiB,QAAQ,OAAO;GAE7C,IAAI,MACF,MAAM,KAAK,EAAE,KAAK,CAAC;GAGrB,KAAK,MAAM,YAAY,QAAQ,WAAW;IAMxC,MAAM,mBAAmB,SAAS,kBAAkB;IAEpD,MAAM,KAAK;KACT,GAAI,OAAO,qBAAqB,WAAW,EAAE,iBAAiB,IAAI,CAAC;KAInE,cAAc;MACZ,MAAM,SAAS;MACf,MAAO,SAAS,SAAS,CAAC;KAC5B;IACF,CAAC;GACH;GAEA,SAAS,KAAK;IAAE,MAAM;IAAS;GAAM,CAAC;GAEtC;EACF;EAEA,IAAI,QAAQ,SAAS,UAAU,MAAM,QAAQ,QAAQ,OAAO,GAAG;GAC7D,SAAS,KAAK;IAAE,MAAM;IAAQ,OAAO,QAAQ,QAAQ,IAAI,YAAY;GAAE,CAAC;GAExE;EACF;EAEA,SAAS,KAAK;GACZ,MAAM,QAAQ,SAAS,cAAc,UAAU;GAC/C,OAAO,CAAC,EAAE,MAAM,iBAAiB,QAAQ,OAAO,EAAE,CAAC;EACrD,CAAC;CACH;CAEA,OAAO;EACL,mBAAmB,YAAY,SAAS,IAAI,YAAY,KAAK,MAAM,IAAI;EACvE;CACF;AACF;;;;;;AAOA,SAAS,iBAAiB,SAAyC;CACjE,IAAI,OAAO,YAAY,UACrB,OAAO;CAGT,OAAO,QACJ,QAAQ,SAAiD,KAAK,SAAS,MAAM,CAAC,CAC9E,KAAK,SAAS,KAAK,IAAI,CAAC,CACxB,KAAK,EAAE;AACZ;;;;;;;AAQA,SAAS,iBAAiB,KAAsC;CAC9D,MAAM,SAAS,cAAuB,KAAK,MAAS;CAEpD,IAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,GACxE,OAAO;CAGT,OAAO,EAAE,QAAQ,IAAI;AACvB;;;;;;;;;;;;;;;;;;;AAoBA,SAAS,aAAa,MAAyB;CAC7C,IAAI,KAAK,SAAS,QAChB,OAAO,EAAE,MAAM,KAAK,KAAK;CAG3B,IAAI,SAAS,KAAK,QAChB,MAAM,IAAI,oBACR,kDAAkD,KAAK,KAAK,qCAC9D;CAGF,OAAO,EACL,YAAY;EAAE,UAAU,KAAK,OAAO;EAAW,MAAM,KAAK,OAAO;CAAO,EAC1E;AACF"}
1
+ {"version":3,"file":"to-google-contents.mjs","names":[],"sources":["../../../../../../../../packages/ai-google/src/utils/to-google-contents.ts"],"sourcesContent":["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 * 5. **Adjacent same-role turns merge.** Consecutive contents with the\n * same role become one content with their parts in order, so the\n * results of parallel tool calls travel as one `user` turn (as the\n * `@google/genai` SDK's own automatic function calling sends them)\n * and a compaction summary next to a same-role turn does not break\n * the user/model alternation.\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: mergeAdjacentContents(contents),\n };\n}\n\n/**\n * Join consecutive contents that share a role into one, keeping part\n * order: parallel `functionResponse` parts stay in their `functionCall`\n * order, and each `functionCall` keeps its own `thoughtSignature`.\n * Returns new content objects; the input contents are not modified.\n *\n * Source for the single-turn shape: `@google/genai` 2.19.0\n * `Models.generateContent` (automatic function calling) sends every\n * function response of one model turn as a single `{ role: \"user\",\n * parts }` content, in call order.\n */\nfunction mergeAdjacentContents(contents: Content[]): Content[] {\n const merged: Content[] = [];\n\n for (const content of contents) {\n const previous = merged.at(-1);\n\n if (previous && previous.role === content.role) {\n merged[merged.length - 1] = {\n ...previous,\n parts: [...(previous.parts ?? []), ...(content.parts ?? [])],\n };\n\n continue;\n }\n\n merged.push(content);\n }\n\n return merged;\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`. All binary\n * modalities — **image, PDF, and audio** — go to a single\n * `inlineData: { mimeType, data }` block; Gemini's multimodal input is\n * media-agnostic and keys off the IANA `mimeType` (`image/png`,\n * `application/pdf`, `audio/mpeg`, …), so one mapping covers every part\n * type the model's capabilities admit. PDF and audio reach this point\n * only when the model declares the matching capability (`google.model`\n * infers `pdf` / `audio` from the multimodal Gemini families); the\n * agent's modality gate throws upfront otherwise, so capability and\n * behavior stay in lockstep.\n *\n * Gemini's `generateContent` does not fetch arbitrary remote URLs (only\n * Files API / GCS URIs via `fileData`), so a neutral `{ url }` source\n * surfaces a typed `InvalidRequestError` upfront — for any modality —\n * rather than a downstream Gemini fault. The agent resolves attachments\n * before this point, so nothing is 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 cannot fetch remote-URL ${part.type} media; supply base64 bytes instead.`,\n );\n }\n\n return {\n inlineData: { mimeType: part.source.mediaType, data: part.source.base64 },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAgB,iBAAiB,UAAqC;CACpE,MAAM,cAAwB,CAAC;CAC/B,MAAM,WAAsB,CAAC;CAE7B,KAAK,MAAM,WAAW,UAAU;EAC9B,IAAI,QAAQ,SAAS,UAAU;GAC7B,YAAY,KAAK,iBAAiB,QAAQ,OAAO,CAAC;GAElD;EACF;EAEA,IAAI,QAAQ,SAAS,QAAQ;GAC3B,SAAS,KAAK;IACZ,MAAM;IACN,OAAO,CACL,EAME,kBAAkB;KAChB,MAAM,QAAQ,cAAc;KAC5B,UAAU,iBAAiB,iBAAiB,QAAQ,OAAO,CAAC;IAC9D,EACF,CACF;GACF,CAAC;GAED;EACF;EAEA,IAAI,QAAQ,SAAS,eAAe,QAAQ,aAAa,QAAQ,UAAU,SAAS,GAAG;GACrF,MAAM,QAAgB,CAAC;GACvB,MAAM,OAAO,iBAAiB,QAAQ,OAAO;GAE7C,IAAI,MACF,MAAM,KAAK,EAAE,KAAK,CAAC;GAGrB,KAAK,MAAM,YAAY,QAAQ,WAAW;IAMxC,MAAM,mBAAmB,SAAS,kBAAkB;IAEpD,MAAM,KAAK;KACT,GAAI,OAAO,qBAAqB,WAAW,EAAE,iBAAiB,IAAI,CAAC;KAInE,cAAc;MACZ,MAAM,SAAS;MACf,MAAO,SAAS,SAAS,CAAC;KAC5B;IACF,CAAC;GACH;GAEA,SAAS,KAAK;IAAE,MAAM;IAAS;GAAM,CAAC;GAEtC;EACF;EAEA,IAAI,QAAQ,SAAS,UAAU,MAAM,QAAQ,QAAQ,OAAO,GAAG;GAC7D,SAAS,KAAK;IAAE,MAAM;IAAQ,OAAO,QAAQ,QAAQ,IAAI,YAAY;GAAE,CAAC;GAExE;EACF;EAEA,SAAS,KAAK;GACZ,MAAM,QAAQ,SAAS,cAAc,UAAU;GAC/C,OAAO,CAAC,EAAE,MAAM,iBAAiB,QAAQ,OAAO,EAAE,CAAC;EACrD,CAAC;CACH;CAEA,OAAO;EACL,mBAAmB,YAAY,SAAS,IAAI,YAAY,KAAK,MAAM,IAAI;EACvE,UAAU,sBAAsB,QAAQ;CAC1C;AACF;;;;;;;;;;;;AAaA,SAAS,sBAAsB,UAAgC;CAC7D,MAAM,SAAoB,CAAC;CAE3B,KAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,WAAW,OAAO,GAAG,EAAE;EAE7B,IAAI,YAAY,SAAS,SAAS,QAAQ,MAAM;GAC9C,OAAO,OAAO,SAAS,KAAK;IAC1B,GAAG;IACH,OAAO,CAAC,GAAI,SAAS,SAAS,CAAC,GAAI,GAAI,QAAQ,SAAS,CAAC,CAAE;GAC7D;GAEA;EACF;EAEA,OAAO,KAAK,OAAO;CACrB;CAEA,OAAO;AACT;;;;;;AAOA,SAAS,iBAAiB,SAAyC;CACjE,IAAI,OAAO,YAAY,UACrB,OAAO;CAGT,OAAO,QACJ,QAAQ,SAAiD,KAAK,SAAS,MAAM,CAAC,CAC9E,KAAK,SAAS,KAAK,IAAI,CAAC,CACxB,KAAK,EAAE;AACZ;;;;;;;AAQA,SAAS,iBAAiB,KAAsC;CAC9D,MAAM,SAAS,cAAuB,KAAK,MAAS;CAEpD,IAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,GACxE,OAAO;CAGT,OAAO,EAAE,QAAQ,IAAI;AACvB;;;;;;;;;;;;;;;;;;;AAoBA,SAAS,aAAa,MAAyB;CAC7C,IAAI,KAAK,SAAS,QAChB,OAAO,EAAE,MAAM,KAAK,KAAK;CAG3B,IAAI,SAAS,KAAK,QAChB,MAAM,IAAI,oBACR,kDAAkD,KAAK,KAAK,qCAC9D;CAGF,OAAO,EACL,YAAY;EAAE,UAAU,KAAK,OAAO;EAAW,MAAM,KAAK,OAAO;CAAO,EAC1E;AACF"}
@@ -1,6 +1,6 @@
1
1
  import { extractJsonSchema } from "@warlock.js/ai";
2
2
 
3
- //#region ../ai-google/src/utils/to-google-tools.ts
3
+ //#region ../../packages/ai-google/src/utils/to-google-tools.ts
4
4
  /**
5
5
  * Convert vendor-neutral `ToolConfig[]` into Gemini's `tools` array —
6
6
  * a single `Tool` carrying one `functionDeclarations` entry per tool.
@@ -1 +1 @@
1
- {"version":3,"file":"to-google-tools.mjs","names":[],"sources":["../../../../../../../ai-google/src/utils/to-google-tools.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,cACd,OACoB;CACpB,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B;CAGF,OAAO,CACL,EACE,sBAAsB,MAAM,KAAK,UAAU;EACzC,MAAM,KAAK;EACX,aAAa,KAAK;EAClB,sBAAsB,aAAa,KAAK,KAAK;CAC/C,EAAE,EACJ,CACF;AACF;;;;;;AAOA,SAAS,aAAa,OAAuE;CAC3F,MAAM,SAAS,kBAAkB,KAAK;CAEtC,IAAI,UAAU,OAAO,SAAS,UAC5B,OAAO;CAGT,OAAO,EAAE,MAAM,SAAS;AAC1B"}
1
+ {"version":3,"file":"to-google-tools.mjs","names":[],"sources":["../../../../../../../../packages/ai-google/src/utils/to-google-tools.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,cACd,OACoB;CACpB,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B;CAGF,OAAO,CACL,EACE,sBAAsB,MAAM,KAAK,UAAU;EACzC,MAAM,KAAK;EACX,aAAa,KAAK;EAClB,sBAAsB,aAAa,KAAK,KAAK;CAC/C,EAAE,EACJ,CACF;AACF;;;;;;AAOA,SAAS,aAAa,OAAuE;CAC3F,MAAM,SAAS,kBAAkB,KAAK;CAEtC,IAAI,UAAU,OAAO,SAAS,UAC5B,OAAO;CAGT,OAAO,EAAE,MAAM,SAAS;AAC1B"}
@@ -1,7 +1,7 @@
1
1
  import { ApiError } from "@google/genai";
2
- import { AIError, ContextLengthExceededError, InvalidRequestError, ProviderAuthError, ProviderError, ProviderRateLimitError, ProviderTimeoutError } from "@warlock.js/ai";
2
+ import { AIError, ContextLengthExceededError, InvalidRequestError, ProviderAuthError, ProviderError, ProviderOverloadedError, ProviderRateLimitError, ProviderTimeoutError } from "@warlock.js/ai";
3
3
 
4
- //#region ../ai-google/src/utils/wrap-google-error.ts
4
+ //#region ../../packages/ai-google/src/utils/wrap-google-error.ts
5
5
  /**
6
6
  * Wrap any thrown value caught inside the Gemini adapter into the
7
7
  * appropriate `@warlock.js/ai` `AIError` subclass.
@@ -28,35 +28,48 @@ function wrapGoogleError(thrown) {
28
28
  const shape = toShape(thrown);
29
29
  const context = buildContext(shape);
30
30
  const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));
31
+ const billed = shape.status === 400 || shape.status === 500 ? false : void 0;
31
32
  if (isTimeout(shape)) return new ProviderTimeoutError(message, {
32
33
  cause: thrown,
33
- context
34
+ context,
35
+ billed
34
36
  });
35
37
  if (shape.status === 401 || shape.status === 403 || /permission_denied|api key not valid|unauthenticated/i.test(message)) return new ProviderAuthError(message, {
36
38
  cause: thrown,
37
- context
39
+ context,
40
+ billed
38
41
  });
39
42
  if (shape.status === 429 || /resource_exhausted|quota/i.test(message)) return new ProviderRateLimitError(message, {
40
43
  cause: thrown,
41
- context
44
+ context,
45
+ billed
42
46
  });
43
47
  if (shape.status === 400) {
44
48
  if (/token count|context length|exceeds the maximum|input is too long/i.test(message)) return new ContextLengthExceededError(message, {
45
49
  cause: thrown,
46
- context
50
+ context,
51
+ billed
47
52
  });
48
53
  return new InvalidRequestError(message, {
49
54
  cause: thrown,
50
- context
55
+ context,
56
+ billed
51
57
  });
52
58
  }
53
59
  if (shape.status === 404 || isClientStatus(shape.status)) return new InvalidRequestError(message, {
54
60
  cause: thrown,
55
- context
61
+ context,
62
+ billed
63
+ });
64
+ if (shape.status === 503 || /\bUNAVAILABLE\b/.test(message) || /overloaded/i.test(message)) return new ProviderOverloadedError(message, {
65
+ cause: thrown,
66
+ context,
67
+ billed
56
68
  });
57
69
  return new ProviderError(message, {
58
70
  cause: thrown,
59
- context
71
+ context,
72
+ billed
60
73
  });
61
74
  }
62
75
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"wrap-google-error.mjs","names":[],"sources":["../../../../../../../ai-google/src/utils/wrap-google-error.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,SAAgB,gBAAgB,QAA0B;CACxD,IAAI,kBAAkB,SACpB,OAAO;CAGT,MAAM,QAAQ,QAAQ,MAAM;CAC5B,MAAM,UAAU,aAAa,KAAK;CAClC,MAAM,UAAU,MAAM,YAAY,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM;CAE1F,IAAI,UAAU,KAAK,GACjB,OAAO,IAAI,qBAAqB,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;CAGrE,IACE,MAAM,WAAW,OACjB,MAAM,WAAW,OACjB,uDAAuD,KAAK,OAAO,GAEnE,OAAO,IAAI,kBAAkB,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;CAGlE,IAAI,MAAM,WAAW,OAAO,4BAA4B,KAAK,OAAO,GAClE,OAAO,IAAI,uBAAuB,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;CAGvE,IAAI,MAAM,WAAW,KAAK;EACxB,IAAI,oEAAoE,KAAK,OAAO,GAClF,OAAO,IAAI,2BAA2B,SAAS;GAAE,OAAO;GAAQ;EAAQ,CAAC;EAG3E,OAAO,IAAI,oBAAoB,SAAS;GAAE,OAAO;GAAQ;EAAQ,CAAC;CACpE;CAEA,IAAI,MAAM,WAAW,OAAO,eAAe,MAAM,MAAM,GACrD,OAAO,IAAI,oBAAoB,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;CAGpE,OAAO,IAAI,cAAc,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;AAC9D;;;;;;AAOA,SAAS,QAAQ,QAAmC;CAClD,IAAI,kBAAkB,UACpB,OAAO;EAAE,QAAQ,OAAO;EAAQ,SAAS,OAAO;EAAS,MAAM,OAAO;CAAK;CAG7E,IAAI,OAAO,WAAW,YAAY,WAAW,MAAM;EACjD,MAAM,MAAM;EAEZ,OAAO;GACL,QAAQ,OAAO,IAAI,WAAW,WAAW,IAAI,SAAS;GACtD,SAAS,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU;GACzD,MAAM,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;GAChD,MAAM,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;EAClD;CACF;CAEA,OAAO,CAAC;AACV;;;;;;AAOA,SAAS,UAAU,OAAkC;CACnD,IAAI,MAAM,WAAW,KACnB,OAAO;CAGT,IAAI,MAAM,SAAS,gBAAgB,qBAAqB,KAAK,MAAM,WAAW,EAAE,GAC9E,OAAO;CAGT,OAAO,MAAM,SAAS,eAAe,MAAM,SAAS;AACtD;;AAGA,SAAS,eAAe,QAAqC;CAC3D,OAAO,OAAO,WAAW,YAAY,UAAU,OAAO,SAAS;AACjE;;AAGA,SAAS,aAAa,OAAkD;CACtE,MAAM,UAAmC,CAAC;CAE1C,IAAI,MAAM,WAAW,QACnB,QAAQ,SAAS,MAAM;CAGzB,IAAI,MAAM,MACR,QAAQ,OAAO,MAAM;CAGvB,OAAO;AACT"}
1
+ {"version":3,"file":"wrap-google-error.mjs","names":[],"sources":["../../../../../../../../packages/ai-google/src/utils/wrap-google-error.ts"],"sourcesContent":["import {\n AIError,\n ContextLengthExceededError,\n InvalidRequestError,\n ProviderAuthError,\n ProviderError,\n ProviderOverloadedError,\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 // \"If your request fails with a 400 or 500 error, you won't be charged\n // for the tokens used.\" — https://ai.google.dev/gemini-api/docs/billing\n // (fetched 2026-10-17). Every other failure keeps the default.\n const billed = shape.status === 400 || shape.status === 500 ? false : undefined;\n\n if (isTimeout(shape)) {\n return new ProviderTimeoutError(message, { cause: thrown, context, billed });\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, billed });\n }\n\n if (shape.status === 429 || /resource_exhausted|quota/i.test(message)) {\n return new ProviderRateLimitError(message, { cause: thrown, context, billed });\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, billed });\n }\n\n return new InvalidRequestError(message, { cause: thrown, context, billed });\n }\n\n if (shape.status === 404 || isClientStatus(shape.status)) {\n return new InvalidRequestError(message, { cause: thrown, context, billed });\n }\n\n // 503 UNAVAILABLE — \"temporarily overloaded or down\"\n // (https://ai.google.dev/gemini-api/docs/api-errors).\n if (shape.status === 503 || /\\bUNAVAILABLE\\b/.test(message) || /overloaded/i.test(message)) {\n return new ProviderOverloadedError(message, { cause: thrown, context, billed });\n }\n\n return new ProviderError(message, { cause: thrown, context, billed });\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,SAAgB,gBAAgB,QAA0B;CACxD,IAAI,kBAAkB,SACpB,OAAO;CAGT,MAAM,QAAQ,QAAQ,MAAM;CAC5B,MAAM,UAAU,aAAa,KAAK;CAClC,MAAM,UAAU,MAAM,YAAY,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM;CAI1F,MAAM,SAAS,MAAM,WAAW,OAAO,MAAM,WAAW,MAAM,QAAQ;CAEtE,IAAI,UAAU,KAAK,GACjB,OAAO,IAAI,qBAAqB,SAAS;EAAE,OAAO;EAAQ;EAAS;CAAO,CAAC;CAG7E,IACE,MAAM,WAAW,OACjB,MAAM,WAAW,OACjB,uDAAuD,KAAK,OAAO,GAEnE,OAAO,IAAI,kBAAkB,SAAS;EAAE,OAAO;EAAQ;EAAS;CAAO,CAAC;CAG1E,IAAI,MAAM,WAAW,OAAO,4BAA4B,KAAK,OAAO,GAClE,OAAO,IAAI,uBAAuB,SAAS;EAAE,OAAO;EAAQ;EAAS;CAAO,CAAC;CAG/E,IAAI,MAAM,WAAW,KAAK;EACxB,IAAI,oEAAoE,KAAK,OAAO,GAClF,OAAO,IAAI,2BAA2B,SAAS;GAAE,OAAO;GAAQ;GAAS;EAAO,CAAC;EAGnF,OAAO,IAAI,oBAAoB,SAAS;GAAE,OAAO;GAAQ;GAAS;EAAO,CAAC;CAC5E;CAEA,IAAI,MAAM,WAAW,OAAO,eAAe,MAAM,MAAM,GACrD,OAAO,IAAI,oBAAoB,SAAS;EAAE,OAAO;EAAQ;EAAS;CAAO,CAAC;CAK5E,IAAI,MAAM,WAAW,OAAO,kBAAkB,KAAK,OAAO,KAAK,cAAc,KAAK,OAAO,GACvF,OAAO,IAAI,wBAAwB,SAAS;EAAE,OAAO;EAAQ;EAAS;CAAO,CAAC;CAGhF,OAAO,IAAI,cAAc,SAAS;EAAE,OAAO;EAAQ;EAAS;CAAO,CAAC;AACtE;;;;;;AAOA,SAAS,QAAQ,QAAmC;CAClD,IAAI,kBAAkB,UACpB,OAAO;EAAE,QAAQ,OAAO;EAAQ,SAAS,OAAO;EAAS,MAAM,OAAO;CAAK;CAG7E,IAAI,OAAO,WAAW,YAAY,WAAW,MAAM;EACjD,MAAM,MAAM;EAEZ,OAAO;GACL,QAAQ,OAAO,IAAI,WAAW,WAAW,IAAI,SAAS;GACtD,SAAS,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU;GACzD,MAAM,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;GAChD,MAAM,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;EAClD;CACF;CAEA,OAAO,CAAC;AACV;;;;;;AAOA,SAAS,UAAU,OAAkC;CACnD,IAAI,MAAM,WAAW,KACnB,OAAO;CAGT,IAAI,MAAM,SAAS,gBAAgB,qBAAqB,KAAK,MAAM,WAAW,EAAE,GAC9E,OAAO;CAGT,OAAO,MAAM,SAAS,eAAe,MAAM,SAAS;AACtD;;AAGA,SAAS,eAAe,QAAqC;CAC3D,OAAO,OAAO,WAAW,YAAY,UAAU,OAAO,SAAS;AACjE;;AAGA,SAAS,aAAa,OAAkD;CACtE,MAAM,UAAmC,CAAC;CAE1C,IAAI,MAAM,WAAW,QACnB,QAAQ,SAAS,MAAM;CAGzB,IAAI,MAAM,MACR,QAAQ,OAAO,MAAM;CAGvB,OAAO;AACT"}
package/llms-full.txt CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  ---
10
10
  name: setup-google
11
- description: 'Wire @warlock.js/ai-google — new GoogleSDK({apiKey} | {vertexai, project, location}) for Gemini API + Vertex AI. generateContent / embedContent + thoughtSignature round-trip for thinking models, batched embeddings. .model({name, vision?, reasoning?, audio?, pdf?}) with cost-truth capabilities (PDF + audio input map to Gemini inlineData), .image({name, pricing?}) for image generation via ai.image — the id picks the transport: gemini-* → generateContent + responseModalities IMAGE (usage passed through; generateImages is deprecated by Google), everything else → generateImages (Imagen, per-image, zero usage) — extended thinking via options.reasoning → thinkingConfig.thinkingBudget, usage reasoningTokens (thoughtsTokenCount) / cachedTokens (cachedContentTokenCount). Triggers: `GoogleSDK`, `google.model`, `google.embedder`, `google.image`, `imagen`, `generateImages`, `gemini image`, `nano banana`, `responseModalities`, `imageConfig`, `ai.image`, `inlineData`, `pdf input`, `audio input`, `thoughtSignature`, `responseJsonSchema`, `vertexai`, `reasoning`, `thinkingConfig`, `thinkingBudget`, `thoughtsTokenCount`, `reasoningTokens`, `cachedTokens`, `promptCaching`, `cacheControl`; "use gemini", "wire Vertex AI", "gemini embeddings", "gemini thinking tool calls", "gemini 2.5 thinking budget", "gemini cached content cost", "generate images with imagen", "generate an image with gemini", "gemini image model 404 not supported for predict", "send a pdf / audio to gemini"; import `import { GoogleSDK } from "@warlock.js/ai-google"`. Skip: the ai.image verb surface — the `generate-images` topic of the `warlock-js-ai` skill; agent loop the `run-ai-agent` topic of the `warlock-js-ai` skill; provider picking the `pick-ai-provider` topic of the `warlock-js-ai` skill; embedder usage the `embed-text` topic of the `warlock-js-ai` skill; siblings `@warlock.js/ai-openai`, `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-ollama`; raw `@google/genai`, `@google-cloud/vertexai`, Vercel `@ai-sdk/google`.'
11
+ description: 'Wire @warlock.js/ai-google — new GoogleSDK({apiKey} | {vertexai, project, location}) for Gemini API + Vertex AI. generateContent / embedContent + thoughtSignature round-trip for thinking models, batched embeddings. .model({name, vision?, reasoning?, audio?, pdf?}) with cost-truth capabilities (PDF + audio input map to Gemini inlineData), .image({name, pricing?}) for image generation via ai.image — the id picks the transport: gemini-* → generateContent + responseModalities IMAGE (usage passed through; generateImages is deprecated by Google), everything else → generateImages (Imagen, per-image, zero usage) — extended thinking via options.reasoning → thinkingConfig.thinkingBudget, usage reasoningTokens (thoughtsTokenCount) / cachedTokens (cachedContentTokenCount). Triggers: `providerOptions`, `GoogleCallOptions`, `countTokens`, `GoogleSDK`, `google.model`, `google.embedder`, `google.image`, `imagen`, `generateImages`, `gemini image`, `nano banana`, `responseModalities`, `imageConfig`, `ai.image`, `inlineData`, `pdf input`, `audio input`, `thoughtSignature`, `responseJsonSchema`, `ai.providerTool`, `googleSearch`, `includeThoughts`, `streamChunks`, `vertexai`, `reasoning`, `thinkingConfig`, `thinkingBudget`, `thoughtsTokenCount`, `reasoningTokens`, `cachedTokens`, `inputIncludesCache`, `GOOGLE_MODEL_PRICING`, `googlePricingFor`, `ProviderOverloadedError`, `promptCaching`, `cacheControl`; "use gemini", "wire Vertex AI", "gemini embeddings", "gemini thinking tool calls", "gemini 2.5 thinking budget", "gemini cached content cost", "generate images with imagen", "generate an image with gemini", "gemini image model 404 not supported for predict", "send a pdf / audio to gemini"; import `import { GoogleSDK } from "@warlock.js/ai-google"`. Skip: the ai.image verb surface — the `generate-images` topic of the `warlock-js-ai` skill; agent loop the `run-ai-agent` topic of the `warlock-js-ai` skill; provider picking the `pick-ai-provider` topic of the `warlock-js-ai` skill; embedder usage the `embed-text` topic of the `warlock-js-ai` skill; siblings `@warlock.js/ai-openai`, `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-ollama`; raw `@google/genai`, `@google-cloud/vertexai`, Vercel `@ai-sdk/google`.'
12
12
  ---
13
13
 
14
14
  # `@warlock.js/ai-google`
@@ -37,21 +37,21 @@ const vertex = new GoogleSDK({
37
37
  ## Producing a model
38
38
 
39
39
  ```ts
40
- google.model({ name: "gemini-2.5-flash" })
41
- google.model({ name: "gemini-2.5-pro", temperature: 0.2 })
42
- google.model({ name: "gemini-1.5-flash", maxTokens: 2048 })
40
+ google.model({ name: "gemini-2.5-flash" });
41
+ google.model({ name: "gemini-2.5-pro", temperature: 0.2 });
42
+ google.model({ name: "gemini-1.5-flash", maxTokens: 2048 });
43
43
  ```
44
44
 
45
45
  ## Capabilities — what's auto-set
46
46
 
47
- | Flag | Default |
48
- | --- | --- |
49
- | `structuredOutput` | `true` (via `responseMimeType` + `responseJsonSchema`) |
50
- | `vision` | Inferred from model id substring. `true` when the id contains `gemini-1.5`, `gemini-2` (covers 2.x / 2.5), `gemini-exp`, `gemini-flash`, or `gemini-pro-vision`; `false` for bare `gemini-pro` / `gemini-1.0-pro` and any unknown id. Case-insensitive, tolerates date/preview/vendor-prefix suffixes (`models/gemini-1.5-flash-001`). |
51
- | `reasoning` | `true` — every Gemini 2.5 model thinks; older families harmlessly ignore an empty budget. Set `reasoning: false` to stop forwarding `thinkingConfig` to a model that rejects it. |
52
- | `promptCaching` | Always `true` — Gemini reports cache-read hits (`cachedContentTokenCount`) via implicit caching and accepts explicit context caching. |
53
- | `audio` | Mirrors the `vision` inference (the multimodal Gemini families accept audio parts). Override with `audio: true \| false`. |
54
- | `pdf` | Mirrors the `vision` inference (the multimodal Gemini families accept PDF / document parts). Override with `pdf: true \| false`. |
47
+ | Flag | Default |
48
+ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
49
+ | `structuredOutput` | `true` (via `responseMimeType` + `responseJsonSchema`) |
50
+ | `vision` | Inferred from model id substring. `true` when the id contains `gemini-1.5`, `gemini-2` (covers 2.x / 2.5), `gemini-exp`, `gemini-flash`, or `gemini-pro-vision`; `false` for bare `gemini-pro` / `gemini-1.0-pro` and any unknown id. Case-insensitive, tolerates date/preview/vendor-prefix suffixes (`models/gemini-1.5-flash-001`). |
51
+ | `reasoning` | `true` — every Gemini 2.5 model thinks; older families harmlessly ignore an empty budget. Set `reasoning: false` to stop forwarding `thinkingConfig` to a model that rejects it. |
52
+ | `promptCaching` | Always `true` — Gemini reports cache-read hits (`cachedContentTokenCount`) via implicit caching and accepts explicit context caching. |
53
+ | `audio` | Mirrors the `vision` inference (the multimodal Gemini families accept audio parts). Override with `audio: true \| false`. |
54
+ | `pdf` | Mirrors the `vision` inference (the multimodal Gemini families accept PDF / document parts). Override with `pdf: true \| false`. |
55
55
 
56
56
  Explicit config always wins (`google.model({ name, reasoning: false, audio: false, pdf: true })`).
57
57
 
@@ -64,6 +64,15 @@ Gemini content roles must be `"user"` or `"model"` — there is no `"system"` ro
64
64
  - Neutral `assistant` → Gemini `"model"`; `user` stays `"user"`.
65
65
  - Assistant tool calls → `"model"` content: optional leading `{ text }` + one `{ functionCall: { name, args } }` part per call (`id` deliberately omitted — see below).
66
66
  - Tool results (`role: "tool"`) → a `"user"` content with one `{ functionResponse: { name, response } }` part. `response` must be a JSON object, so a stringified-JSON tool result is parsed back to an object; a non-object result is wrapped as `{ result: <raw> }`.
67
+ - Adjacent contents with the same role merge into one, parts kept in order: parallel tool results become ONE `"user"` content with several `functionResponse` parts in call order (as `@google/genai`'s automatic function calling sends them), and an agent compaction summary merges with a same-role neighbour (user-framed summary + input; assistant summary + the next tool-call turn, text part first, each `functionCall` keeping its `thoughtSignature`). Roles therefore alternate `user`/`model`; `countTokens()` uses the same builder.
68
+
69
+ Neutral history and the `contents` the adapter sends for it:
70
+
71
+ ```text
72
+ user "look both up" → { role: "user", parts: [{ text }] }
73
+ assistant toolCalls [a, b] → { role: "model", parts: [{ functionCall: a }, { functionCall: b }] }
74
+ tool a, tool b → { role: "user", parts: [{ functionResponse: a }, { functionResponse: b }] }
75
+ ```
67
76
 
68
77
  **Gemini matches a result to its call by name, not id** — the Developer API assigns no function-call ids. The adapter sets neutral `id` = tool name so `toolCallId` is non-empty. The wire `id` is omitted from both `functionCall` and `functionResponse` (echoing an empty/synthetic id is rejected by Gemini). **Limitation:** two parallel calls to the same tool in one turn share an id — inherent to the Developer API's id-less design.
69
78
 
@@ -91,12 +100,12 @@ PDF and audio reach the wire only when the model declares the matching capabilit
91
100
 
92
101
  `google.image({ name })` returns an `ImageModelContract` for the `ai.image()` verb. **The model id picks the transport:**
93
102
 
94
- | Model id | Transport | Usage it can report |
95
- | --- | --- | --- |
103
+ | Model id | Transport | Usage it can report |
104
+ | ----------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
96
105
  | `gemini-*` (e.g. `gemini-3.1-flash-lite-image`) | `ai.models.generateContent` with `responseModalities` including `"IMAGE"` | whatever `usageMetadata` Google attaches — price with `{ input, output }` |
97
- | anything else (`imagen-*`, unknown ids) | `ai.models.generateImages` (Imagen) | none, always zero — price with `{ perImage }` |
106
+ | anything else (`imagen-*`, unknown ids) | `ai.models.generateImages` (Imagen) | none, always zero — price with `{ perImage }` |
98
107
 
99
- A `gemini-` id sent to `generateImages` comes back `404 … is not supported for predict` (observed verbatim from Google) — `generateImages` routes to `{model}:predict`. `generateContent` is the SDK's own named replacement: its runtime deprecation notice for `generateImages` says *"Please use the generateContent method with image models instead"*. Routing on the id is what lets both families go through one factory. A leading `models/` resource prefix is tolerated.
108
+ A `gemini-` id sent to `generateImages` comes back `404 … is not supported for predict` (observed verbatim from Google) — `generateImages` routes to `{model}:predict`. `generateContent` is the SDK's own named replacement: its runtime deprecation notice for `generateImages` says _"Please use the generateContent method with image models instead"_. Routing on the id is what lets both families go through one factory. A leading `models/` resource prefix is tolerated.
100
109
 
101
110
  This is **routing, not validation** — no id is refused locally. An id matching neither family takes the `generateImages` route (the pre-existing one) and fails at Google as a typed provider error, not with a local throw.
102
111
 
@@ -107,13 +116,13 @@ This is **routing, not validation** — no id is refused locally. An id matching
107
116
  ```ts
108
117
  const gemini = google.image({
109
118
  name: "gemini-3.1-flash-lite-image",
110
- pricing: { input: 0.3, output: 30 }, // token-metered
119
+ pricing: { input: 0.3, output: 30 }, // token-metered
111
120
  });
112
121
 
113
122
  const { data, error, usage } = await ai.image({
114
123
  model: gemini,
115
124
  prompt: "a red bicycle on a white background",
116
- aspectRatio: "16:9", // → config.imageConfig.aspectRatio
125
+ aspectRatio: "16:9", // → config.imageConfig.aspectRatio
117
126
  options: { imageSize: "2K", personGeneration: "ALLOW_ADULT" }, // → imageConfig passthroughs
118
127
  });
119
128
  ```
@@ -135,7 +144,7 @@ const imagen = google.image({ name: "imagen-4.0-generate-001", pricing: { perIma
135
144
  const { data, error } = await ai.image({
136
145
  model: imagen,
137
146
  prompt: "a watercolor lighthouse at dawn",
138
- aspectRatio: "3:4", // Imagen ratio (vs OpenAI's WxH `size`)
147
+ aspectRatio: "3:4", // Imagen ratio (vs OpenAI's WxH `size`)
139
148
  negativePrompt: "text, watermark",
140
149
  options: { imageSize: "2K", personGeneration: "allow_adult" }, // Imagen passthroughs
141
150
  });
@@ -143,7 +152,7 @@ const { data, error } = await ai.image({
143
152
 
144
153
  - Imagen is **per-image-metered** (price with `{ perImage }`) and returns base64 bytes — no hosted URL, and `usage` is an honest `{ input: 0, output: 0, total: 0 }`.
145
154
  - When every candidate is safety-filtered (`raiFilteredReason`), the run surfaces a typed `ContentFilterError` on `result.error`.
146
- - ⏳ **This path is on a clock.** `@google/genai` warns at runtime: *"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."* This package depends on `@google/genai` `^2.17.1`, which does emit that warning. Nothing breaks today, but new work should prefer a `gemini-*` id.
155
+ - ⏳ **This path is on a clock.** `@google/genai` warns at runtime: _"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."_ This package depends on `@google/genai` `^2.17.1`, which does emit that warning. Nothing breaks today, but new work should prefer a `gemini-*` id.
147
156
 
148
157
  ### Not implemented: the `interactions` API
149
158
 
@@ -155,6 +164,17 @@ The verb surface lives in the `generate-images` topic of the `warlock-js-ai` ski
155
164
 
156
165
  `model.stream()` drains `generateContentStream`. Each chunk's `.text` → `{ type: "delta" }`; `functionCall` parts (read from `candidates[0].content.parts`, not the `.functionCalls` getter — the getter drops `thoughtSignature`) are emitted as `{ type: "tool-call" }` **fully formed** (Gemini streams a complete call with parsed `args`, not partial JSON), carrying `providerMetadata.thoughtSignature` when present. Terminal `{ type: "done", finishReason, usage }` — usage from the final chunk's `usageMetadata`.
157
166
 
167
+ Opt-in chunks (`options.streamChunks`, set by `agent.stream()`): `{ type: "reasoning", delta }` per thought part (`thought: true`; sent only with `providerOptions.google.body.thinkingConfig.includeThoughts: true`) and `{ type: "usage", usage }` per chunk carrying `usageMetadata`. No `tool-args` (calls arrive whole).
168
+
169
+ **Provider-defined tools:** `ai.providerTool({ provider: "google", definition: { googleSearch: {} } })` (also `{ codeExecution: {} }`, `{ urlContext: {} }`) is appended verbatim as its own `Tool` after the function declarations; other providers' tools are skipped with a warning.
170
+
171
+ ```ts
172
+ ai.agent({
173
+ model: google.model({ name: "gemini-2.5-flash" }),
174
+ tools: [ai.providerTool({ provider: "google", definition: { googleSearch: {} } })],
175
+ });
176
+ ```
177
+
158
178
  ## Finish-reason mapping
159
179
 
160
180
  `STOP` → `stop` · `MAX_TOKENS` → `length` · `SAFETY` / `RECITATION` / `BLOCKLIST` / `PROHIBITED_CONTENT` / `MALFORMED_FUNCTION_CALL` / unknown / null → `error`. `tool_calls` is derived from function-call presence.
@@ -164,7 +184,7 @@ The verb surface lives in the `generate-images` topic of the `warlock-js-ai` ski
164
184
  ```ts
165
185
  const embedder = google.embedder({ name: "gemini-embedding-001" });
166
186
  const { vector } = await embedder.embed("Hello world");
167
- const { vectors } = await embedder.embedMany(["a", "b"]); // single batched call
187
+ const { vectors } = await embedder.embedMany(["a", "b"]); // single batched call
168
188
  ```
169
189
 
170
190
  `embedContent` accepts an array natively, so `embedMany` is **one request** (unlike Bedrock/Titan). Pass `dimensions` to forward Gemini's `outputDimensionality` truncation hint.
@@ -180,14 +200,31 @@ Wrapped into the typed `@warlock.js/ai` `AIError` hierarchy. Gemini has no machi
180
200
  - 400 with token/context phrasing → `ContextLengthExceededError`, else `InvalidRequestError`
181
201
  - 404 / any other 4xx → `InvalidRequestError`
182
202
  - 504 / `DEADLINE_EXCEEDED` / `ETIMEDOUT` / `ECONNABORTED` / `AbortError` → `ProviderTimeoutError`
203
+ - 503 / `UNAVAILABLE` / "overloaded" → `ProviderOverloadedError` (`code: "PROVIDER_OVERLOADED"`, `retryable: true`) — Gemini documents 503 as "temporarily overloaded or down" (https://ai.google.dev/gemini-api/docs/api-errors)
183
204
  - anything else (5xx and status-less) → `ProviderError`
184
205
 
206
+ Every wrapped error carries `status`, `retryable` and `billed`. Google states "If your request fails with a 400 or 500 error, you won't be charged for the tokens used" (https://ai.google.dev/gemini-api/docs/billing, checked 2026-10-17), so a 400 or 500 is `billed: false`; 401 / 403 are `false` too (never reached a billable account); everything else is `"unknown"`.
207
+
208
+ ```ts
209
+ import { ai, ProviderError, ProviderOverloadedError } from "@warlock.js/ai";
210
+ import { GoogleSDK } from "@warlock.js/ai-google";
211
+
212
+ const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY ?? "" });
213
+ const result = await ai.agent({ model: google.model({ name: "gemini-2.5-flash" }) }).execute("hi");
214
+
215
+ if (result.error instanceof ProviderOverloadedError) {
216
+ console.log(result.error.status, result.error.retryable); // 503, true — retry or fall back
217
+ } else if (result.error instanceof ProviderError) {
218
+ console.log(result.error.billed); // false for a 400 / 500
219
+ }
220
+ ```
221
+
185
222
  Timeout is checked first, so an aborted/`AbortError` request always classifies as `ProviderTimeoutError` regardless of status. `AIError` instances pass through unwrapped (no double-wrap).
186
223
 
187
224
  ## Token counting
188
225
 
189
226
  ```ts
190
- await google.count("some text") // approximate heuristic, offline
227
+ await google.count("some text"); // approximate heuristic, offline
191
228
  ```
192
229
 
193
230
  `count(text, model?)` accepts an optional model arg for signature parity but **ignores it** — the estimate is `approximateTokenCount` (≈ `ceil(chars / 4)`) from `@warlock.js/ai`, never a `countTokens` network call.
@@ -208,25 +245,70 @@ Gemini's `thinkingBudget` semantics (for direct `maxTokens` use): `0` disables t
208
245
 
209
246
  ## Token usage
210
247
 
211
- `complete()` / `stream()` map Gemini's `usageMetadata` into the neutral `Usage` (`{ input, output, total }`): `promptTokenCount` → `input`, `candidatesTokenCount` → `output`, `totalTokenCount` → `total` (falling back to `input + output` when absent). `cachedContentTokenCount`, when > 0, is surfaced as `usage.cachedTokens`; `thoughtsTokenCount` (a reasoning model's thinking-phase tokens), when > 0, as `usage.reasoningTokens`. Both are omitted when zero/absent. Embeddings have no usage (see above).
248
+ `complete()` / `stream()` map Gemini's `usageMetadata` into the neutral `Usage` (`{ input, output, total }`): `promptTokenCount` → `input`, `candidatesTokenCount` → `output`, `totalTokenCount` → `total` (falling back to `input + output` when absent). `cachedContentTokenCount`, when > 0, is surfaced as `usage.cachedTokens`; `thoughtsTokenCount` (a reasoning model's thinking-phase tokens), when > 0, as `usage.reasoningTokens`. Both are omitted when zero/absent. `promptTokenCount` "includes the number of tokens in the cached content" (https://ai.google.dev/api/generate-content#UsageMetadata), so the usage is marked `inputIncludesCache: true` and cost bills cached tokens out of `input` once. When the model is priced, `usage.cost` is attached on `complete()` and the streaming `done` chunk. Embeddings have no usage (see above).
212
249
 
213
250
  ## Pricing
214
251
 
215
- Optional USD pricing flows in two ways, resolved per `model()` call:
252
+ Resolution per `model()` call: per-model `pricing` > SDK-level registry entry > built-in `GOOGLE_MODEL_PRICING` > `undefined` (no cost computed). The resolved `ModelPricing` is attached to the produced model, and each response's `usage.cost` is computed from it.
216
253
 
217
254
  ```ts
218
- // SDK-level registry, keyed by model name:
255
+ import { totalCost } from "@warlock.js/ai";
256
+ import { GOOGLE_MODEL_PRICING, GoogleSDK, googlePricingFor } from "@warlock.js/ai-google";
257
+
219
258
  const google = new GoogleSDK({
220
- apiKey,
221
- pricing: { "gemini-2.5-flash": { input: 0.3, output: 2.5 } },
259
+ apiKey: process.env.GEMINI_API_KEY ?? "",
260
+ pricing: { "gemini-2.5-flash": { input: 0.3, output: 2.5, cachedInput: 0.03 } }, // text-input rate
222
261
  });
223
262
 
224
- google.model({ name: "gemini-2.5-flash" }); // → inherits the registry entry
225
- google.model({ name: "gemini-2.5-flash", pricing: {...} }); // → per-model pricing wins
226
- google.model({ name: "gemini-2.5-pro" }); // → no entry → pricing undefined (no cost computed)
263
+ google.model({ name: "gemini-2.5-flash" }); // → the registry entry
264
+ google.model({ name: "gemini-3.5-flash-lite" }).pricing; // → built-in { input: 0.3, output: 2.5, cachedInput: 0.03 }
265
+ google.model({ name: "gemini-2.5-pro" }).pricing; // → undefined (prompt-length tiers; pass pricing yourself)
266
+ googlePricingFor("gemini-3.5-flash-lite"); // the same built-in entry
267
+ Object.keys(GOOGLE_MODEL_PRICING); // the priced ids
268
+
269
+ const { usage } = await google
270
+ .model({ name: "gemini-3.5-flash-lite" })
271
+ .complete([{ role: "user", content: "hi" }]);
272
+ console.log(totalCost(usage.cost));
273
+ ```
274
+
275
+ `GOOGLE_MODEL_PRICING` comes from https://ai.google.dev/gemini-api/docs/pricing (paid tier, Standard), fetched 2026-10-17. Only models with a single flat token price are listed. Deliberately unpriced: the Pro models (priced by prompt length, split at 200k prompt tokens), models whose audio input bills higher than text (`Usage` does not split input by modality — price them with the text rate yourself only if you send no audio), and models with a scheduled price change. The hourly context-cache storage charge is not a per-token rate and is never included.
276
+
277
+ ## Provider config options and input-token counts
278
+
279
+ Native body fields merge after neutral options. When both values are plain
280
+ objects, their immediate keys merge and caller-provided keys win; untouched
281
+ neutral fields remain. Arrays, scalars and `null` replace the field. This merge
282
+ is one level deep: a nested object explicitly supplied by the caller replaces
283
+ that nested value.
284
+
285
+ `providerOptions.google.body` adds native `GenerateContentConfig` fields,
286
+ merged after neutral generation options for both completion and streaming:
287
+
288
+ ```ts
289
+ const options = { providerOptions: { google: { body: { topP: 0.8, topK: 4 } } } };
290
+ await model.complete(messages, options);
227
291
  ```
228
292
 
229
- Resolution order: per-model `pricing` > SDK-level registry entry > `undefined`. The resolved `ModelPricing` is attached to the produced model so the agent runtime can compute cost from usage.
293
+ The key stays `google` for custom provider labels. Other provider keys are
294
+ ignored. `GoogleCallOptions` is exported. `systemInstruction`, `tools`,
295
+ `abortSignal` and `httpOptions` are protected and throw `InvalidRequestError`;
296
+ use neutral messages/tools/signal and SDK transport configuration for them.
297
+
298
+ `model.countTokens(messages, options?)` calls the native counting endpoint with
299
+ the same converted contents. In Vertex mode it also counts the same system
300
+ instruction and tools as generation and forwards cancellation. In Developer API
301
+ mode, the installed Google SDK cannot count system instructions or tools through
302
+ its typed count endpoint: requests containing them fail explicitly before a
303
+ network call. Cached-content references also fail explicitly, since this SDK
304
+ count endpoint cannot reproduce them. The adapter never silently counts only a
305
+ subset of a prompt. `sdk.count(text)` remains a separate offline estimate.
306
+
307
+ ```ts
308
+ if (model.countTokens) {
309
+ const inputTokens = await model.countTokens(messages, options);
310
+ }
311
+ ```
230
312
 
231
313
  ## When NOT to use this skill
232
314
 
package/llms.txt CHANGED
@@ -6,4 +6,4 @@
6
6
 
7
7
  ## Skills
8
8
 
9
- - [setup-google](@warlock.js/ai-google/setup-google/SKILL.md): Wire @warlock.js/ai-google — new GoogleSDK({apiKey} | {vertexai, project, location}) for Gemini API + Vertex AI. generateContent / embedContent + thoughtSignature round-trip for thinking models, batched embeddings. .model({name, vision?, reasoning?, audio?, pdf?}) with cost-truth capabilities (PDF + audio input map to Gemini inlineData), .image({name, pricing?}) for image generation via ai.image — the id picks the transport: gemini-* → generateContent + responseModalities IMAGE (usage passed through; generateImages is deprecated by Google), everything else → generateImages (Imagen, per-image, zero usage) — extended thinking via options.reasoning → thinkingConfig.thinkingBudget, usage reasoningTokens (thoughtsTokenCount) / cachedTokens (cachedContentTokenCount). Triggers: `GoogleSDK`, `google.model`, `google.embedder`, `google.image`, `imagen`, `generateImages`, `gemini image`, `nano banana`, `responseModalities`, `imageConfig`, `ai.image`, `inlineData`, `pdf input`, `audio input`, `thoughtSignature`, `responseJsonSchema`, `vertexai`, `reasoning`, `thinkingConfig`, `thinkingBudget`, `thoughtsTokenCount`, `reasoningTokens`, `cachedTokens`, `promptCaching`, `cacheControl`; "use gemini", "wire Vertex AI", "gemini embeddings", "gemini thinking tool calls", "gemini 2.5 thinking budget", "gemini cached content cost", "generate images with imagen", "generate an image with gemini", "gemini image model 404 not supported for predict", "send a pdf / audio to gemini"; import `import { GoogleSDK } from "@warlock.js/ai-google"`. Skip: the ai.image verb surface — the `generate-images` topic of the `warlock-js-ai` skill; agent loop the `run-ai-agent` topic of the `warlock-js-ai` skill; provider picking the `pick-ai-provider` topic of the `warlock-js-ai` skill; embedder usage the `embed-text` topic of the `warlock-js-ai` skill; siblings `@warlock.js/ai-openai`, `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-ollama`; raw `@google/genai`, `@google-cloud/vertexai`, Vercel `@ai-sdk/google`.
9
+ - [setup-google](@warlock.js/ai-google/setup-google/SKILL.md): Wire @warlock.js/ai-google — new GoogleSDK({apiKey} | {vertexai, project, location}) for Gemini API + Vertex AI. generateContent / embedContent + thoughtSignature round-trip for thinking models, batched embeddings. .model({name, vision?, reasoning?, audio?, pdf?}) with cost-truth capabilities (PDF + audio input map to Gemini inlineData), .image({name, pricing?}) for image generation via ai.image — the id picks the transport: gemini-* → generateContent + responseModalities IMAGE (usage passed through; generateImages is deprecated by Google), everything else → generateImages (Imagen, per-image, zero usage) — extended thinking via options.reasoning → thinkingConfig.thinkingBudget, usage reasoningTokens (thoughtsTokenCount) / cachedTokens (cachedContentTokenCount). Triggers: `providerOptions`, `GoogleCallOptions`, `countTokens`, `GoogleSDK`, `google.model`, `google.embedder`, `google.image`, `imagen`, `generateImages`, `gemini image`, `nano banana`, `responseModalities`, `imageConfig`, `ai.image`, `inlineData`, `pdf input`, `audio input`, `thoughtSignature`, `responseJsonSchema`, `ai.providerTool`, `googleSearch`, `includeThoughts`, `streamChunks`, `vertexai`, `reasoning`, `thinkingConfig`, `thinkingBudget`, `thoughtsTokenCount`, `reasoningTokens`, `cachedTokens`, `inputIncludesCache`, `GOOGLE_MODEL_PRICING`, `googlePricingFor`, `ProviderOverloadedError`, `promptCaching`, `cacheControl`; "use gemini", "wire Vertex AI", "gemini embeddings", "gemini thinking tool calls", "gemini 2.5 thinking budget", "gemini cached content cost", "generate images with imagen", "generate an image with gemini", "gemini image model 404 not supported for predict", "send a pdf / audio to gemini"; import `import { GoogleSDK } from "@warlock.js/ai-google"`. Skip: the ai.image verb surface — the `generate-images` topic of the `warlock-js-ai` skill; agent loop the `run-ai-agent` topic of the `warlock-js-ai` skill; provider picking the `pick-ai-provider` topic of the `warlock-js-ai` skill; embedder usage the `embed-text` topic of the `warlock-js-ai` skill; siblings `@warlock.js/ai-openai`, `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-ollama`; raw `@google/genai`, `@google-cloud/vertexai`, Vercel `@ai-sdk/google`.
package/package.json CHANGED
@@ -11,16 +11,17 @@
11
11
  "license": "MIT",
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "https://github.com/warlockjs/ai-google"
14
+ "url": "git+https://github.com/warlockjs/framework.git",
15
+ "directory": "packages/ai-google"
15
16
  },
16
17
  "peerDependencies": {
17
- "@warlock.js/ai": "5.29.0"
18
+ "@warlock.js/ai": "5.31.0"
18
19
  },
19
20
  "dependencies": {
20
21
  "@google/genai": "^2.17.1",
21
- "@warlock.js/logger": "5.29.0"
22
+ "@warlock.js/logger": "5.31.0"
22
23
  },
23
- "version": "5.29.0",
24
+ "version": "5.31.0",
24
25
  "main": "./cjs/index.cjs",
25
26
  "module": "./esm/index.mjs",
26
27
  "types": "./esm/index.d.mts",