@warlock.js/ai-google 5.30.0 → 5.32.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 (42) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/cjs/index.cjs +273 -21
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/config.type.d.mts +4 -6
  5. package/esm/config.type.d.mts.map +1 -1
  6. package/esm/embedder.mjs.map +1 -1
  7. package/esm/gemini-image.d.mts +1 -3
  8. package/esm/gemini-image.d.mts.map +1 -1
  9. package/esm/gemini-image.mjs +1 -1
  10. package/esm/gemini-image.mjs.map +1 -1
  11. package/esm/image.d.mts +1 -3
  12. package/esm/image.d.mts.map +1 -1
  13. package/esm/image.mjs.map +1 -1
  14. package/esm/index.d.mts +3 -1
  15. package/esm/index.mjs +2 -1
  16. package/esm/known-pricing.d.mts +37 -0
  17. package/esm/known-pricing.d.mts.map +1 -0
  18. package/esm/known-pricing.mjs +42 -0
  19. package/esm/known-pricing.mjs.map +1 -0
  20. package/esm/model.mjs +110 -9
  21. package/esm/model.mjs.map +1 -1
  22. package/esm/provider-options.d.mts +8 -0
  23. package/esm/provider-options.d.mts.map +1 -0
  24. package/esm/provider-options.mjs +36 -0
  25. package/esm/provider-options.mjs.map +1 -0
  26. package/esm/sdk.d.mts +4 -5
  27. package/esm/sdk.d.mts.map +1 -1
  28. package/esm/sdk.mjs +5 -3
  29. package/esm/sdk.mjs.map +1 -1
  30. package/esm/utils/apply-google-usage.mjs +5 -0
  31. package/esm/utils/apply-google-usage.mjs.map +1 -1
  32. package/esm/utils/grounding.mjs +30 -0
  33. package/esm/utils/grounding.mjs.map +1 -0
  34. package/esm/utils/index.mjs +1 -1
  35. package/esm/utils/to-google-contents.mjs +33 -1
  36. package/esm/utils/to-google-contents.mjs.map +1 -1
  37. package/esm/utils/wrap-google-error.mjs +21 -8
  38. package/esm/utils/wrap-google-error.mjs.map +1 -1
  39. package/llms-full.txt +162 -31
  40. package/llms.txt +1 -1
  41. package/package.json +3 -3
  42. package/skills/setup-google/SKILL.md +162 -31
package/esm/sdk.d.mts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { GoogleEmbedderConfig, GoogleImageConfig, GoogleModelConfig, GoogleSDKConfig } from "./config.type.mjs";
2
2
  import { EmbedderContract, ImageModelContract, ModelContract, SDKAdapterContract } from "@warlock.js/ai";
3
-
4
3
  //#region ../../packages/ai-google/src/sdk.d.ts
5
4
  /**
6
5
  * Google Gemini-backed implementation of `SDKAdapterContract`.
@@ -26,7 +25,7 @@ import { EmbedderContract, ImageModelContract, ModelContract, SDKAdapterContract
26
25
  * const model = google.model({ name: "gemini-2.5-flash", temperature: 0.7 });
27
26
  * const embedder = google.embedder({ name: "gemini-embedding-001" });
28
27
  */
29
- declare class GoogleSDK implements SDKAdapterContract {
28
+ export declare class GoogleSDK implements SDKAdapterContract {
30
29
  private readonly ai;
31
30
  private readonly provider;
32
31
  private readonly pricing?;
@@ -37,8 +36,9 @@ declare class GoogleSDK implements SDKAdapterContract {
37
36
  * `GoogleGenAI` client. The SDK's `provider` label is forwarded.
38
37
  *
39
38
  * Pricing resolution: per-model `config.pricing` wins; otherwise the
40
- * SDK-level registry entry keyed by `config.name`; otherwise
41
- * `undefined` (no cost computed).
39
+ * SDK-level registry entry keyed by `config.name`; otherwise the
40
+ * built-in list price (`GOOGLE_MODEL_PRICING`); otherwise `undefined`
41
+ * (no cost computed).
42
42
  */
43
43
  model(config: GoogleModelConfig): ModelContract;
44
44
  /**
@@ -82,5 +82,4 @@ declare class GoogleSDK implements SDKAdapterContract {
82
82
  image(config: GoogleImageConfig): ImageModelContract;
83
83
  }
84
84
  //#endregion
85
- export { GoogleSDK };
86
85
  //# sourceMappingURL=sdk.d.mts.map
package/esm/sdk.d.mts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.d.mts","names":[],"sources":["../../../../../../../packages/ai-google/src/sdk.ts"],"mappings":";;;;;;AA2EA;;;;;;;;;;;;;;;;;;;;;;cAAa,SAAA,YAAqB,kBAAA;EAAA,iBACf,EAAA;EAAA,iBACA,QAAA;EAAA,iBACA,OAAA;cAEE,MAAA,EAAQ,eAAA;EA+BM;;;;;;;;;EAd1B,KAAA,CAAM,MAAA,EAAQ,iBAAA,GAAoB,aAAA;EAoDkB;AAAA;;;;;EAtC9C,KAAA,CAAM,IAAA,UAAc,MAAA,YAAkB,OAAA;;;;;;;;EAW5C,QAAA,CAAS,MAAA,EAAQ,oBAAA,GAAuB,gBAAA;;;;;;;;;;;;;;;;;;;;;;;;EA2BxC,KAAA,CAAM,MAAA,EAAQ,iBAAA,GAAoB,kBAAA;AAAA"}
1
+ {"version":3,"file":"sdk.d.mts","names":[],"sources":["../../../../../../../packages/ai-google/src/sdk.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;qBA+Ea,qBAAqB;mBACf;mBACA;mBACA;EAEjB,YAAmB,QAAQ;;;;;;;;;;;EAkBpB,MAAM,QAAQ,oBAAoB;;;;;;;EAe5B,MAAM,cAAc,kBAAkB;;;;;;;;EAW5C,SAAS,QAAQ,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;EA2BxC,MAAM,QAAQ,oBAAoB"}
package/esm/sdk.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { GoogleEmbedder } from "./embedder.mjs";
2
2
  import { GeminiImageModel } from "./gemini-image.mjs";
3
3
  import { GoogleImageModel } from "./image.mjs";
4
+ import { googlePricingFor } from "./known-pricing.mjs";
4
5
  import { GoogleModel } from "./model.mjs";
5
6
  import { GoogleGenAI } from "@google/genai";
6
7
  import { approximateTokenCount } from "@warlock.js/ai";
@@ -73,11 +74,12 @@ var GoogleSDK = class {
73
74
  * `GoogleGenAI` client. The SDK's `provider` label is forwarded.
74
75
  *
75
76
  * Pricing resolution: per-model `config.pricing` wins; otherwise the
76
- * SDK-level registry entry keyed by `config.name`; otherwise
77
- * `undefined` (no cost computed).
77
+ * SDK-level registry entry keyed by `config.name`; otherwise the
78
+ * built-in list price (`GOOGLE_MODEL_PRICING`); otherwise `undefined`
79
+ * (no cost computed).
78
80
  */
79
81
  model(config) {
80
- const resolvedPricing = config.pricing ?? this.pricing?.[config.name];
82
+ const resolvedPricing = config.pricing ?? this.pricing?.[config.name] ?? googlePricingFor(config.name);
81
83
  const resolvedConfig = resolvedPricing === config.pricing ? config : {
82
84
  ...config,
83
85
  pricing: resolvedPricing
package/esm/sdk.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.mjs","names":[],"sources":["../../../../../../../packages/ai-google/src/sdk.ts"],"sourcesContent":["import { GoogleGenAI } from \"@google/genai\";\nimport type {\n EmbedderContract,\n ImageModelContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n} from \"@warlock.js/ai\";\nimport { approximateTokenCount } from \"@warlock.js/ai\";\nimport type {\n GoogleEmbedderConfig,\n GoogleImageConfig,\n GoogleModelConfig,\n GoogleSDKConfig,\n} from \"./config.type\";\nimport { GoogleEmbedder } from \"./embedder\";\nimport { GeminiImageModel } from \"./gemini-image\";\nimport { GoogleImageModel } from \"./image\";\nimport { GoogleModel } from \"./model\";\n\n/**\n * Pick the transport for an image model id.\n *\n * `ai.models.generateImages` calls `{model}:predict`, and a `gemini-`\n * id sent there comes back `404 … is not supported for predict`\n * (observed verbatim from Google). `generateContent` is what the SDK\n * itself points `generateImages` users at — its deprecation notice\n * reads \"Please use the generateContent method with image models\n * instead\" — so the id has to choose the transport.\n *\n * Runs in this package establish where a `gemini-` id is ACCEPTED, not\n * what it returns: on this transport such an id got as far as a quota\n * error (HTTP 429) instead of the 404. That an image\n * comes back end-to-end once billing is enabled is reported by the\n * maintainer from a locally linked build, not measured here. Whether\n * these models report token usage is still unknown.\n *\n * This is ROUTING, not validation — no id is refused here. An id this\n * function does not recognize takes the `generateImages` route, the\n * only route that existed before Gemini image support landed, so every\n * id that reached Google before still reaches Google the same way and\n * still fails (or succeeds) at the provider.\n *\n * A leading `models/` resource prefix is tolerated, matching the id\n * shapes `inferVisionCapability` already accepts\n * (`models/gemini-1.5-flash-001`).\n */\nfunction usesGeminiImageTransport(name: string): boolean {\n return name.toLowerCase().replace(/^models\\//, \"\").startsWith(\"gemini-\");\n}\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 /**\n * Build an image model bound to this SDK's client for use with\n * `ai.image({ model, prompt })`. `config.name` decides the transport\n * (see {@link usesGeminiImageTransport}) — a `gemini-` id gets the\n * `generateContent` implementation, everything else the Imagen\n * `generateImages` one. No id is rejected locally either way, so an\n * unsupported model fails at Google, not here.\n *\n * The two differ in what usage they can report, which is what the\n * caller must price for: the Imagen path always returns a zero token\n * `Usage` (Imagen reports none — price with `{ perImage }`), while the\n * Gemini path passes through whatever `usageMetadata` Google attaches\n * (price with `{ input, output }` when tokens come back).\n *\n * Pricing resolution mirrors `model()`: per-model `config.pricing`\n * wins, otherwise the SDK-level registry entry keyed by `config.name`,\n * otherwise `undefined`.\n *\n * @example\n * const imagen = google.image({ name: \"imagen-4.0-generate-001\" });\n * const gemini = google.image({ name: \"gemini-3.1-flash-lite-image\" });\n * const { data } = await ai.image({ model: gemini, prompt: \"a red bicycle\" });\n */\n public image(config: GoogleImageConfig): ImageModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: GoogleImageConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n if (usesGeminiImageTransport(config.name)) {\n return new GeminiImageModel(this.ai, resolvedConfig, this.provider);\n }\n\n return new GoogleImageModel(this.ai, resolvedConfig, this.provider);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,SAAS,yBAAyB,MAAuB;CACvD,OAAO,KAAK,YAAY,CAAC,CAAC,QAAQ,aAAa,EAAE,CAAC,CAAC,WAAW,SAAS;AACzE;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,IAAa,YAAb,MAAqD;CAKnD,AAAO,YAAY,QAAyB;EAC1C,MAAM,EAAE,UAAU,SAAS,GAAG,kBAAkB;EAEhD,KAAK,KAAK,IAAI,YAAY,aAAa;EACvC,KAAK,WAAW,YAAY;EAC5B,KAAK,UAAU;CACjB;;;;;;;;;;CAWA,AAAO,MAAM,QAA0C;EACrD,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,OAAO,IAAI,YAAY,KAAK,IAAI,gBAAgB,KAAK,QAAQ;CAC/D;;;;;;;CAQA,MAAa,MAAM,MAAc,QAAkC;EACjE,OAAO,sBAAsB,IAAI;CACnC;;;;;;;;CASA,AAAO,SAAS,QAAgD;EAC9D,OAAO,IAAI,eAAe,KAAK,IAAI,QAAQ,KAAK,QAAQ;CAC1D;;;;;;;;;;;;;;;;;;;;;;;;CAyBA,AAAO,MAAM,QAA+C;EAC1D,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,IAAI,yBAAyB,OAAO,IAAI,GACtC,OAAO,IAAI,iBAAiB,KAAK,IAAI,gBAAgB,KAAK,QAAQ;EAGpE,OAAO,IAAI,iBAAiB,KAAK,IAAI,gBAAgB,KAAK,QAAQ;CACpE;AACF"}
1
+ {"version":3,"file":"sdk.mjs","names":[],"sources":["../../../../../../../packages/ai-google/src/sdk.ts"],"sourcesContent":["import { GoogleGenAI } from \"@google/genai\";\nimport type {\n EmbedderContract,\n ImageModelContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n} from \"@warlock.js/ai\";\nimport { approximateTokenCount } from \"@warlock.js/ai\";\nimport type {\n GoogleEmbedderConfig,\n GoogleImageConfig,\n GoogleModelConfig,\n GoogleSDKConfig,\n} from \"./config.type\";\nimport { GoogleEmbedder } from \"./embedder\";\nimport { GeminiImageModel } from \"./gemini-image\";\nimport { GoogleImageModel } from \"./image\";\nimport { googlePricingFor } from \"./known-pricing\";\nimport { GoogleModel } from \"./model\";\n\n/**\n * Pick the transport for an image model id.\n *\n * `ai.models.generateImages` calls `{model}:predict`, and a `gemini-`\n * id sent there comes back `404 … is not supported for predict`\n * (observed verbatim from Google). `generateContent` is what the SDK\n * itself points `generateImages` users at — its deprecation notice\n * reads \"Please use the generateContent method with image models\n * instead\" — so the id has to choose the transport.\n *\n * Runs in this package establish where a `gemini-` id is ACCEPTED, not\n * what it returns: on this transport such an id got as far as a quota\n * error (HTTP 429) instead of the 404. That an image\n * comes back end-to-end once billing is enabled is reported by the\n * maintainer from a locally linked build, not measured here. Whether\n * these models report token usage is still unknown.\n *\n * This is ROUTING, not validation — no id is refused here. An id this\n * function does not recognize takes the `generateImages` route, the\n * only route that existed before Gemini image support landed, so every\n * id that reached Google before still reaches Google the same way and\n * still fails (or succeeds) at the provider.\n *\n * A leading `models/` resource prefix is tolerated, matching the id\n * shapes `inferVisionCapability` already accepts\n * (`models/gemini-1.5-flash-001`).\n */\nfunction usesGeminiImageTransport(name: string): boolean {\n return name\n .toLowerCase()\n .replace(/^models\\//, \"\")\n .startsWith(\"gemini-\");\n}\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 the\n * built-in list price (`GOOGLE_MODEL_PRICING`); otherwise `undefined`\n * (no cost computed).\n */\n public model(config: GoogleModelConfig): ModelContract {\n const resolvedPricing =\n config.pricing ?? this.pricing?.[config.name] ?? googlePricingFor(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 /**\n * Build an image model bound to this SDK's client for use with\n * `ai.image({ model, prompt })`. `config.name` decides the transport\n * (see {@link usesGeminiImageTransport}) — a `gemini-` id gets the\n * `generateContent` implementation, everything else the Imagen\n * `generateImages` one. No id is rejected locally either way, so an\n * unsupported model fails at Google, not here.\n *\n * The two differ in what usage they can report, which is what the\n * caller must price for: the Imagen path always returns a zero token\n * `Usage` (Imagen reports none — price with `{ perImage }`), while the\n * Gemini path passes through whatever `usageMetadata` Google attaches\n * (price with `{ input, output }` when tokens come back).\n *\n * Pricing resolution mirrors `model()`: per-model `config.pricing`\n * wins, otherwise the SDK-level registry entry keyed by `config.name`,\n * otherwise `undefined`.\n *\n * @example\n * const imagen = google.image({ name: \"imagen-4.0-generate-001\" });\n * const gemini = google.image({ name: \"gemini-3.1-flash-lite-image\" });\n * const { data } = await ai.image({ model: gemini, prompt: \"a red bicycle\" });\n */\n public image(config: GoogleImageConfig): ImageModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: GoogleImageConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n if (usesGeminiImageTransport(config.name)) {\n return new GeminiImageModel(this.ai, resolvedConfig, this.provider);\n }\n\n return new GoogleImageModel(this.ai, resolvedConfig, this.provider);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,SAAS,yBAAyB,MAAuB;CACvD,OAAO,KACJ,YAAY,CAAC,CACb,QAAQ,aAAa,EAAE,CAAC,CACxB,WAAW,SAAS;AACzB;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,IAAa,YAAb,MAAqD;CAKnD,AAAO,YAAY,QAAyB;EAC1C,MAAM,EAAE,UAAU,SAAS,GAAG,kBAAkB;EAEhD,KAAK,KAAK,IAAI,YAAY,aAAa;EACvC,KAAK,WAAW,YAAY;EAC5B,KAAK,UAAU;CACjB;;;;;;;;;;;CAYA,AAAO,MAAM,QAA0C;EACrD,MAAM,kBACJ,OAAO,WAAW,KAAK,UAAU,OAAO,SAAS,iBAAiB,OAAO,IAAI;EAC/E,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,OAAO,IAAI,YAAY,KAAK,IAAI,gBAAgB,KAAK,QAAQ;CAC/D;;;;;;;CAQA,MAAa,MAAM,MAAc,QAAkC;EACjE,OAAO,sBAAsB,IAAI;CACnC;;;;;;;;CASA,AAAO,SAAS,QAAgD;EAC9D,OAAO,IAAI,eAAe,KAAK,IAAI,QAAQ,KAAK,QAAQ;CAC1D;;;;;;;;;;;;;;;;;;;;;;;;CAyBA,AAAO,MAAM,QAA+C;EAC1D,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,IAAI,yBAAyB,OAAO,IAAI,GACtC,OAAO,IAAI,iBAAiB,KAAK,IAAI,gBAAgB,KAAK,QAAQ;EAGpE,OAAO,IAAI,iBAAiB,KAAK,IAAI,gBAAgB,KAAK,QAAQ;CACpE;AACF"}
@@ -14,8 +14,13 @@
14
14
  *
15
15
  * `total` falls back to `input + output` when Google omits
16
16
  * `totalTokenCount`.
17
+ *
18
+ * `promptTokenCount` "includes the number of tokens in the cached
19
+ * content" (https://ai.google.dev/api/generate-content#UsageMetadata), so
20
+ * the usage is marked `inputIncludesCache: true`.
17
21
  */
18
22
  function applyGoogleUsage(usage, raw) {
23
+ usage.inputIncludesCache = true;
19
24
  usage.input = raw.promptTokenCount ?? usage.input;
20
25
  usage.output = raw.candidatesTokenCount ?? usage.output;
21
26
  usage.total = raw.totalTokenCount ?? usage.input + usage.output;
@@ -1 +1 @@
1
- {"version":3,"file":"apply-google-usage.mjs","names":[],"sources":["../../../../../../../../packages/ai-google/src/utils/apply-google-usage.ts"],"sourcesContent":["import type { Usage } from \"@warlock.js/ai\";\nimport type { GenerateContentResponse } from \"@google/genai\";\n\n/** Gemini's per-response token accounting block, as the SDK types it. */\nexport type GoogleUsageMetadata = NonNullable<GenerateContentResponse[\"usageMetadata\"]>;\n\n/**\n * Fold a Gemini `usageMetadata` block into a running neutral `Usage`\n * accumulator. Shared by every `generateContent`-backed surface — the\n * chat model's `complete()`, its streaming loop (where the final chunk\n * carries cumulative totals), and the Gemini image model — so one\n * mapping decides what a Gemini token report means package-wide.\n *\n * Cache-read hits (`cachedContentTokenCount`, implicit or explicit\n * context caching) surface as `cachedTokens`; the thinking-phase tokens\n * of a reasoning model (`thoughtsTokenCount`) surface as\n * `reasoningTokens`. Both are emitted only when reported `> 0` so an\n * absent channel leaves the field undefined rather than a false zero.\n *\n * `total` falls back to `input + output` when Google omits\n * `totalTokenCount`.\n */\nexport function applyGoogleUsage(usage: Usage, raw: GoogleUsageMetadata): 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 const reasoning = raw.thoughtsTokenCount;\n\n if (reasoning && reasoning > 0) {\n usage.reasoningTokens = reasoning;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAsBA,SAAgB,iBAAiB,OAAc,KAAgC;CAC7E,MAAM,QAAQ,IAAI,oBAAoB,MAAM;CAC5C,MAAM,SAAS,IAAI,wBAAwB,MAAM;CACjD,MAAM,QAAQ,IAAI,mBAAmB,MAAM,QAAQ,MAAM;CAEzD,MAAM,SAAS,IAAI;CAEnB,IAAI,UAAU,SAAS,GACrB,MAAM,eAAe;CAGvB,MAAM,YAAY,IAAI;CAEtB,IAAI,aAAa,YAAY,GAC3B,MAAM,kBAAkB;AAE5B"}
1
+ {"version":3,"file":"apply-google-usage.mjs","names":[],"sources":["../../../../../../../../packages/ai-google/src/utils/apply-google-usage.ts"],"sourcesContent":["import type { Usage } from \"@warlock.js/ai\";\nimport type { GenerateContentResponse } from \"@google/genai\";\n\n/** Gemini's per-response token accounting block, as the SDK types it. */\nexport type GoogleUsageMetadata = NonNullable<GenerateContentResponse[\"usageMetadata\"]>;\n\n/**\n * Fold a Gemini `usageMetadata` block into a running neutral `Usage`\n * accumulator. Shared by every `generateContent`-backed surface — the\n * chat model's `complete()`, its streaming loop (where the final chunk\n * carries cumulative totals), and the Gemini image model — so one\n * mapping decides what a Gemini token report means package-wide.\n *\n * Cache-read hits (`cachedContentTokenCount`, implicit or explicit\n * context caching) surface as `cachedTokens`; the thinking-phase tokens\n * of a reasoning model (`thoughtsTokenCount`) surface as\n * `reasoningTokens`. Both are emitted only when reported `> 0` so an\n * absent channel leaves the field undefined rather than a false zero.\n *\n * `total` falls back to `input + output` when Google omits\n * `totalTokenCount`.\n *\n * `promptTokenCount` \"includes the number of tokens in the cached\n * content\" (https://ai.google.dev/api/generate-content#UsageMetadata), so\n * the usage is marked `inputIncludesCache: true`.\n */\nexport function applyGoogleUsage(usage: Usage, raw: GoogleUsageMetadata): void {\n usage.inputIncludesCache = true;\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 const reasoning = raw.thoughtsTokenCount;\n\n if (reasoning && reasoning > 0) {\n usage.reasoningTokens = reasoning;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,iBAAiB,OAAc,KAAgC;CAC7E,MAAM,qBAAqB;CAC3B,MAAM,QAAQ,IAAI,oBAAoB,MAAM;CAC5C,MAAM,SAAS,IAAI,wBAAwB,MAAM;CACjD,MAAM,QAAQ,IAAI,mBAAmB,MAAM,QAAQ,MAAM;CAEzD,MAAM,SAAS,IAAI;CAEnB,IAAI,UAAU,SAAS,GACrB,MAAM,eAAe;CAGvB,MAAM,YAAY,IAAI;CAEtB,IAAI,aAAa,YAAY,GAC3B,MAAM,kBAAkB;AAE5B"}
@@ -0,0 +1,30 @@
1
+ //#region ../../packages/ai-google/src/utils/grounding.ts
2
+ /** Candidate snapshots are replaced, not added: streamed metadata may repeat.
3
+ * Installed @google/genai 2.19.0 GroundingMetadata.webSearchQueries; REST schema:
4
+ * https://ai.google.dev/api/generate-content#GroundingMetadata
5
+ * Billing ignores empty queries and counts unique queries for Gemini 3+:
6
+ * https://ai.google.dev/gemini-api/docs/google-search#pricing
7
+ * Gemini 2.5 and older bill per grounded prompt; this counter is NOT that unit. */
8
+ function applyGrounding(usage, snapshots, response) {
9
+ for (const [position, candidate] of (response.candidates ?? []).entries()) if (candidate.groundingMetadata) snapshots.set(candidate.index ?? position, candidate.groundingMetadata);
10
+ const reported = [...snapshots.values()].filter((metadata) => metadata.webSearchQueries !== void 0);
11
+ if (reported.length > 0) {
12
+ const queries = reported.flatMap((metadata) => metadata.webSearchQueries ?? []);
13
+ usage.serverToolUse = {
14
+ ...usage.serverToolUse,
15
+ webSearchRequests: new Set(queries.filter((query) => query !== "")).size,
16
+ googleWebSearchQueries: queries.length
17
+ };
18
+ }
19
+ }
20
+ /** Retain chunks, supports, search entry point and queries without flattening them. */
21
+ function groundingMetadata(snapshots) {
22
+ return snapshots.size ? { google: { groundingMetadata: [...snapshots].map(([index, metadata]) => ({
23
+ index,
24
+ ...metadata
25
+ })) } } : void 0;
26
+ }
27
+
28
+ //#endregion
29
+ export { applyGrounding, groundingMetadata };
30
+ //# sourceMappingURL=grounding.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grounding.mjs","names":[],"sources":["../../../../../../../../packages/ai-google/src/utils/grounding.ts"],"sourcesContent":["import type { GenerateContentResponse, GroundingMetadata } from \"@google/genai\";\nimport type { Usage } from \"@warlock.js/ai\";\n\n/** Candidate snapshots are replaced, not added: streamed metadata may repeat.\n * Installed @google/genai 2.19.0 GroundingMetadata.webSearchQueries; REST schema:\n * https://ai.google.dev/api/generate-content#GroundingMetadata\n * Billing ignores empty queries and counts unique queries for Gemini 3+:\n * https://ai.google.dev/gemini-api/docs/google-search#pricing\n * Gemini 2.5 and older bill per grounded prompt; this counter is NOT that unit. */\nexport function applyGrounding(\n usage: Usage,\n snapshots: Map<number, GroundingMetadata>,\n response: GenerateContentResponse,\n): void {\n for (const [position, candidate] of (response.candidates ?? []).entries()) {\n if (candidate.groundingMetadata)\n snapshots.set(candidate.index ?? position, candidate.groundingMetadata);\n }\n const reported = [...snapshots.values()].filter(\n (metadata) => metadata.webSearchQueries !== undefined,\n );\n if (reported.length > 0) {\n const queries = reported.flatMap((metadata) => metadata.webSearchQueries ?? []);\n usage.serverToolUse = {\n ...usage.serverToolUse,\n webSearchRequests: new Set(queries.filter((query) => query !== \"\")).size,\n googleWebSearchQueries: queries.length,\n };\n }\n}\n\n/** Retain chunks, supports, search entry point and queries without flattening them. */\nexport function groundingMetadata(\n snapshots: Map<number, GroundingMetadata>,\n): Record<string, unknown> | undefined {\n return snapshots.size\n ? {\n google: {\n groundingMetadata: [...snapshots].map(([index, metadata]) => ({ index, ...metadata })),\n },\n }\n : undefined;\n}\n"],"mappings":";;;;;;;AASA,SAAgB,eACd,OACA,WACA,UACM;CACN,KAAK,MAAM,CAAC,UAAU,eAAe,SAAS,cAAc,CAAC,EAAC,CAAE,QAAQ,GACtE,IAAI,UAAU,mBACZ,UAAU,IAAI,UAAU,SAAS,UAAU,UAAU,iBAAiB;CAE1E,MAAM,WAAW,CAAC,GAAG,UAAU,OAAO,CAAC,CAAC,CAAC,QACtC,aAAa,SAAS,qBAAqB,MAC9C;CACA,IAAI,SAAS,SAAS,GAAG;EACvB,MAAM,UAAU,SAAS,SAAS,aAAa,SAAS,oBAAoB,CAAC,CAAC;EAC9E,MAAM,gBAAgB;GACpB,GAAG,MAAM;GACT,mBAAmB,IAAI,IAAI,QAAQ,QAAQ,UAAU,UAAU,EAAE,CAAC,CAAC,CAAC;GACpE,wBAAwB,QAAQ;EAClC;CACF;AACF;;AAGA,SAAgB,kBACd,WACqC;CACrC,OAAO,UAAU,OACb,EACE,QAAQ,EACN,mBAAmB,CAAC,GAAG,SAAS,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe;EAAE;EAAO,GAAG;CAAS,EAAE,EACvF,EACF,IACA;AACN"}
@@ -4,4 +4,4 @@ import { toGoogleContents } from "./to-google-contents.mjs";
4
4
  import { toGoogleTools } from "./to-google-tools.mjs";
5
5
  import { wrapGoogleError } from "./wrap-google-error.mjs";
6
6
 
7
- export { };
7
+ export { applyGoogleUsage, mapFinishReason, toGoogleContents, toGoogleTools, wrapGoogleError };
@@ -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":["../../../../../../../../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 *\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,5 +1,5 @@
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
4
  //#region ../../packages/ai-google/src/utils/wrap-google-error.ts
5
5
  /**
@@ -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":["../../../../../../../../packages/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"}