@warlock.js/ai-google 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/index.cjs +767 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/config.type.d.mts +83 -0
- package/esm/config.type.d.mts.map +1 -0
- package/esm/embedder.mjs +103 -0
- package/esm/embedder.mjs.map +1 -0
- package/esm/index.d.mts +3 -0
- package/esm/index.mjs +3 -0
- package/esm/known-vision-models.mjs +39 -0
- package/esm/known-vision-models.mjs.map +1 -0
- package/esm/model.mjs +277 -0
- package/esm/model.mjs.map +1 -0
- package/esm/sdk.d.mts +62 -0
- package/esm/sdk.d.mts.map +1 -0
- package/esm/sdk.mjs +78 -0
- package/esm/sdk.mjs.map +1 -0
- package/esm/utils/index.mjs +6 -0
- package/esm/utils/map-finish-reason.mjs +34 -0
- package/esm/utils/map-finish-reason.mjs.map +1 -0
- package/esm/utils/to-google-contents.mjs +120 -0
- package/esm/utils/to-google-contents.mjs.map +1 -0
- package/esm/utils/to-google-tools.mjs +41 -0
- package/esm/utils/to-google-tools.mjs.map +1 -0
- package/esm/utils/wrap-google-error.mjs +108 -0
- package/esm/utils/wrap-google-error.mjs.map +1 -0
- package/llms-full.txt +154 -0
- package/llms.txt +9 -0
- package/package.json +39 -0
- package/skills/README.md +9 -0
- package/skills/setup-google/SKILL.md +144 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.mjs","names":[],"sources":["../../../../../@warlock.js/ai-google/src/model.ts"],"sourcesContent":["import {\n type Message,\n type ModelCallOptions,\n type ModelCapabilities,\n type ModelContract,\n type ModelPricing,\n type ModelResponse,\n type ModelStreamChunk,\n type ModelToolCallRequest,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type {\n GenerateContentConfig,\n GenerateContentResponse,\n GoogleGenAI,\n Part,\n} from \"@google/genai\";\nimport type { GoogleModelConfig } from \"./config.type\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport { mapFinishReason, toGoogleContents, toGoogleTools, wrapGoogleError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.google\";\n\n/**\n * Google Gemini-backed implementation of `ModelContract`.\n *\n * **Role.** The provider-facing bridge between the vendor-neutral\n * `@warlock.js/ai` agent runtime and the `@google/genai` SDK\n * (`models.generateContent` / `generateContentStream`).\n *\n * **Responsibility.**\n * - Owns: a long-lived `GoogleGenAI` client + frozen `ModelConfig`\n * (name, temperature, maxTokens) used as per-call defaults.\n * - Owns: translating vendor-neutral `Message[]` / `ToolConfig[]` into\n * Gemini shapes (systemInstruction hoisting, `model` role,\n * `functionCall` / `functionResponse` parts, inline image bytes) on\n * the way out, and Gemini's candidate/parts response (text, function\n * calls, finish reason, token usage) back into neutral shapes on the\n * way in.\n * - Does NOT own: dispatching tools, looping, history, retries — those\n * are agent concerns. The model is a per-call protocol adapter.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across calls\"): the `GoogleGenAI` client is reused for the SDK's\n * lifetime.\n *\n * @example\n * import { GoogleGenAI } from \"@google/genai\";\n * const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });\n * const model = new GoogleModel(ai, { name: \"gemini-2.5-flash\" });\n *\n * const myAgent = agent({ model, tools: [searchTool] });\n * const result = await myAgent.execute(\"Summarize today's news.\");\n */\nexport class GoogleModel implements ModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly capabilities: ModelCapabilities;\n public readonly pricing?: ModelPricing;\n\n private readonly ai: GoogleGenAI;\n private readonly config: GoogleModelConfig;\n private readonly logger: Logger = log;\n\n public constructor(ai: GoogleGenAI, config: GoogleModelConfig, provider: string = \"google\") {\n this.ai = ai;\n this.config = config;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n this.capabilities = {\n structuredOutput: config.structuredOutput ?? true,\n vision: config.vision ?? inferVisionCapability(config.name),\n };\n }\n\n /**\n * Single-shot completion. Sends the full message list to\n * `generateContent`, waits for the terminal response, and reshapes\n * it into a vendor-neutral `ModelResponse`. Per-call `options`\n * override the instance defaults for this call only.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting generateContent call\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n const { systemInstruction, contents } = toGoogleContents(messages);\n\n let response: GenerateContentResponse;\n\n try {\n response = await this.ai.models.generateContent({\n model: this.name,\n contents,\n config: this.buildConfig(systemInstruction, options),\n });\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const toolCalls = this.extractToolCalls(response);\n const finishReason = toolCalls\n ? \"tool_calls\"\n : mapFinishReason(response.candidates?.[0]?.finishReason);\n const usage = this.extractUsage(response);\n\n this.logger.debug(LOG_MODULE, \"response\", \"generateContent call succeeded\", {\n finishReason,\n usage,\n });\n\n return {\n content: response.text ?? \"\",\n finishReason,\n usage,\n toolCalls,\n };\n }\n\n /**\n * Incremental streaming completion via `generateContentStream`.\n * Yields neutral `ModelStreamChunk`s — `delta` for text, `tool-call`\n * per function call (Gemini emits a fully-formed call, not partial\n * JSON), and a terminal `done` with the final finish reason + usage.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting generateContentStream call\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n const { systemInstruction, contents } = toGoogleContents(messages);\n\n let iterable: AsyncGenerator<GenerateContentResponse>;\n\n try {\n iterable = await this.ai.models.generateContentStream({\n model: this.name,\n contents,\n config: this.buildConfig(systemInstruction, options),\n });\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n let rawFinishReason: string | undefined;\n let sawToolCall = false;\n const usage: Usage = { input: 0, output: 0, total: 0 };\n\n try {\n for await (const chunk of iterable) {\n const text = chunk.text;\n\n if (text) {\n yield { type: \"delta\", content: text };\n }\n\n for (const part of chunk.candidates?.[0]?.content?.parts ?? []) {\n const toolCall = this.partToToolCall(part);\n\n if (!toolCall) {\n continue;\n }\n\n sawToolCall = true;\n\n yield {\n type: \"tool-call\",\n id: toolCall.id,\n name: toolCall.name,\n input: toolCall.input,\n ...(toolCall.providerMetadata\n ? { providerMetadata: toolCall.providerMetadata }\n : {}),\n };\n }\n\n const candidateFinish = chunk.candidates?.[0]?.finishReason;\n\n if (candidateFinish) {\n rawFinishReason = candidateFinish;\n }\n\n if (chunk.usageMetadata) {\n this.applyUsage(usage, chunk.usageMetadata);\n }\n }\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const finishReason = sawToolCall ? \"tool_calls\" : mapFinishReason(rawFinishReason);\n\n this.logger.debug(LOG_MODULE, \"response\", \"generateContentStream call succeeded\", {\n finishReason,\n usage,\n });\n\n yield { type: \"done\", finishReason, usage };\n }\n\n /**\n * Assemble the `GenerateContentConfig` shared by `complete()` and\n * `stream()`: inference params, hoisted system instruction,\n * cancellation signal, and conditional tools + native structured\n * output.\n */\n private buildConfig(\n systemInstruction: string | undefined,\n options: ModelCallOptions | undefined,\n ): GenerateContentConfig {\n const temperature = options?.temperature ?? this.config.temperature;\n const maxOutputTokens = options?.maxTokens ?? this.config.maxTokens;\n\n return {\n ...(systemInstruction ? { systemInstruction } : {}),\n ...(temperature !== undefined ? { temperature } : {}),\n ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),\n ...(options?.signal ? { abortSignal: options.signal } : {}),\n ...this.buildTools(options?.tools),\n ...this.buildStructuredOutput(options?.responseSchema),\n };\n }\n\n /**\n * Spread-friendly tools fragment. Empty object when no tools were\n * supplied so the caller can unconditionally spread it.\n */\n private buildTools(tools: ModelCallOptions[\"tools\"]): Pick<GenerateContentConfig, \"tools\"> {\n const mapped = toGoogleTools(tools);\n\n return mapped ? { tools: mapped } : {};\n }\n\n /**\n * Translate the neutral `responseSchema` into Gemini's native JSON\n * structured output (`responseMimeType: \"application/json\"` +\n * `responseJsonSchema`, which takes a raw JSON Schema directly).\n * Emitted only when the model is `structuredOutput`-capable and the\n * schema is an object root — otherwise the agent's soft prompt hint\n * + client-side `validate()` carry shape.\n */\n private buildStructuredOutput(\n responseSchema: Record<string, unknown> | undefined,\n ): Pick<GenerateContentConfig, \"responseMimeType\" | \"responseJsonSchema\"> {\n if (!responseSchema || !this.capabilities.structuredOutput) {\n return {};\n }\n\n if (responseSchema.type !== \"object\" || typeof responseSchema.properties !== \"object\") {\n return {};\n }\n\n return {\n responseMimeType: \"application/json\",\n responseJsonSchema: responseSchema,\n };\n }\n\n /**\n * Reshape Gemini's function-call content parts into the neutral\n * `ModelToolCallRequest[]`. Returns `undefined` when the model\n * requested no functions so callers can branch on presence.\n *\n * Reads `candidates[0].content.parts` directly rather than the\n * `response.functionCalls` getter: the getter discards the\n * part-level `thoughtSignature`, and Gemini \"thinking\" models 400\n * the follow-up turn if that signature is not echoed back. See\n * `partToToolCall`.\n */\n private extractToolCalls(\n response: GenerateContentResponse,\n ): ModelToolCallRequest[] | undefined {\n const parts = response.candidates?.[0]?.content?.parts ?? [];\n const toolCalls = parts\n .map((part) => this.partToToolCall(part))\n .filter((call): call is ModelToolCallRequest => call !== undefined);\n\n return toolCalls.length > 0 ? toolCalls : undefined;\n }\n\n /**\n * Map a single Gemini `Part` to a neutral `ModelToolCallRequest`,\n * or `undefined` when the part is not a function call. The part's\n * `thoughtSignature` (opaque, set by thinking models) is carried on\n * `providerMetadata` so `toGoogleContents` can replay it on the\n * assistant turn — Gemini rejects the next request without it.\n */\n private partToToolCall(part: Part): ModelToolCallRequest | undefined {\n if (!part.functionCall) {\n return undefined;\n }\n\n const call = part.functionCall;\n\n return {\n // The Gemini Developer API does not assign function-call ids\n // (only Vertex parallel-calling does). Fall back to the function\n // name so the neutral `toolCallId` is non-empty and the echoed\n // `functionResponse.name` resolves — Gemini matches a result to\n // its call by name. See decisions §49.\n id: call.id ?? call.name ?? \"\",\n name: call.name ?? \"\",\n input: (call.args ?? {}) as Record<string, unknown>,\n ...(part.thoughtSignature\n ? { providerMetadata: { thoughtSignature: part.thoughtSignature } }\n : {}),\n };\n }\n\n /**\n * Normalize Gemini's `usageMetadata` into the neutral `Usage` shape.\n * Cache-read tokens are surfaced as `cachedTokens` only when\n * non-zero. Absent usage collapses to zeros.\n */\n private extractUsage(response: GenerateContentResponse): Usage {\n const usage: Usage = { input: 0, output: 0, total: 0 };\n\n if (response.usageMetadata) {\n this.applyUsage(usage, response.usageMetadata);\n }\n\n return usage;\n }\n\n /**\n * Fold a Gemini `usageMetadata` block into the running neutral\n * `Usage` accumulator. Shared by `complete()` and the streaming\n * loop (where the final chunk carries cumulative totals).\n */\n private applyUsage(\n usage: Usage,\n raw: NonNullable<GenerateContentResponse[\"usageMetadata\"]>,\n ): void {\n usage.input = raw.promptTokenCount ?? usage.input;\n usage.output = raw.candidatesTokenCount ?? usage.output;\n usage.total = raw.totalTokenCount ?? usage.input + usage.output;\n\n const cached = raw.cachedContentTokenCount;\n\n if (cached && cached > 0) {\n usage.cachedTokens = cached;\n }\n }\n\n /**\n * Wrap a thrown provider error into the typed `AIError` hierarchy\n * and emit the standard error log line before it propagates.\n */\n private logAndWrap(thrown: unknown) {\n const wrapped = wrapGoogleError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n return wrapped;\n }\n}\n"],"mappings":"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"}
|
package/esm/sdk.d.mts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { GoogleEmbedderConfig, GoogleModelConfig, GoogleSDKConfig } from "./config.type.mjs";
|
|
2
|
+
import { EmbedderContract, ModelContract, SDKAdapterContract } from "@warlock.js/ai";
|
|
3
|
+
|
|
4
|
+
//#region ../../@warlock.js/ai-google/src/sdk.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Google Gemini-backed implementation of `SDKAdapterContract`.
|
|
7
|
+
*
|
|
8
|
+
* **Role.** The package entry point for Gemini models via the
|
|
9
|
+
* `@google/genai` SDK. A single `GoogleSDK` holds one live
|
|
10
|
+
* `GoogleGenAI` client, shared by every `ModelContract` /
|
|
11
|
+
* `EmbedderContract` it produces. Construct one SDK per
|
|
12
|
+
* account/project and reuse it everywhere.
|
|
13
|
+
*
|
|
14
|
+
* **Responsibility.**
|
|
15
|
+
* - Owns: a long-lived `GoogleGenAI` client (auth, Vertex vs Gemini
|
|
16
|
+
* API) and its lifetime. Factory for `GoogleModel` /
|
|
17
|
+
* `GoogleEmbedder` instances sharing that client.
|
|
18
|
+
* - Does NOT own: anything per-call — those live in `GoogleModel` /
|
|
19
|
+
* `GoogleEmbedder` and the agent runtime.
|
|
20
|
+
*
|
|
21
|
+
* Modeled as a class (see §4.2 of code-style.md — "long-lived state
|
|
22
|
+
* across many calls"), fronted by FP usage like the other adapters.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY! });
|
|
26
|
+
* const model = google.model({ name: "gemini-2.5-flash", temperature: 0.7 });
|
|
27
|
+
* const embedder = google.embedder({ name: "gemini-embedding-001" });
|
|
28
|
+
*/
|
|
29
|
+
declare class GoogleSDK implements SDKAdapterContract {
|
|
30
|
+
private readonly ai;
|
|
31
|
+
private readonly provider;
|
|
32
|
+
private readonly pricing?;
|
|
33
|
+
constructor(config: GoogleSDKConfig);
|
|
34
|
+
/**
|
|
35
|
+
* Build a `GoogleModel` bound to this SDK's client. Each call
|
|
36
|
+
* returns a fresh instance; all instances share the underlying
|
|
37
|
+
* `GoogleGenAI` client. The SDK's `provider` label is forwarded.
|
|
38
|
+
*
|
|
39
|
+
* Pricing resolution: per-model `config.pricing` wins; otherwise the
|
|
40
|
+
* SDK-level registry entry keyed by `config.name`; otherwise
|
|
41
|
+
* `undefined` (no cost computed).
|
|
42
|
+
*/
|
|
43
|
+
model(config: GoogleModelConfig): ModelContract;
|
|
44
|
+
/**
|
|
45
|
+
* Rough token-count estimate. Uses the character-heuristic
|
|
46
|
+
* (`approximateTokenCount`) from the core package — Gemini's
|
|
47
|
+
* `countTokens` is a network round-trip; `count()` is intentionally
|
|
48
|
+
* offline. Good for budgeting/quota guards, not billing.
|
|
49
|
+
*/
|
|
50
|
+
count(text: string, _model?: string): Promise<number>;
|
|
51
|
+
/**
|
|
52
|
+
* Build a `GoogleEmbedder` bound to this SDK's client.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* const embedder = google.embedder({ name: "gemini-embedding-001" });
|
|
56
|
+
* const { vector } = await embedder.embed("Hello world");
|
|
57
|
+
*/
|
|
58
|
+
embedder(config: GoogleEmbedderConfig): EmbedderContract;
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { GoogleSDK };
|
|
62
|
+
//# sourceMappingURL=sdk.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk.d.mts","names":[],"sources":["../../../../../@warlock.js/ai-google/src/sdk.ts"],"mappings":";;;;;;AAwCA;;;;;;;;;;;;;;;;;;;;;;cAAa,SAAA,YAAqB,kBAAA;EAAA,iBACf,EAAA;EAAA,iBACA,QAAA;EAAA,iBACA,OAAA;cAEE,MAAA,EAAQ,eAAA;EA0CpB;;;;;AAAwD;;;;EAzBxD,KAAA,CAAM,MAAA,EAAQ,iBAAA,GAAoB,aAAA;;;;;;;EAc5B,KAAA,CAAM,IAAA,UAAc,MAAA,YAAkB,OAAA;;;;;;;;EAW5C,QAAA,CAAS,MAAA,EAAQ,oBAAA,GAAuB,gBAAA;AAAA"}
|
package/esm/sdk.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { GoogleEmbedder } from "./embedder.mjs";
|
|
2
|
+
import { GoogleModel } from "./model.mjs";
|
|
3
|
+
import { GoogleGenAI } from "@google/genai";
|
|
4
|
+
import { approximateTokenCount } from "@warlock.js/ai";
|
|
5
|
+
|
|
6
|
+
//#region ../../@warlock.js/ai-google/src/sdk.ts
|
|
7
|
+
/**
|
|
8
|
+
* Google Gemini-backed implementation of `SDKAdapterContract`.
|
|
9
|
+
*
|
|
10
|
+
* **Role.** The package entry point for Gemini models via the
|
|
11
|
+
* `@google/genai` SDK. A single `GoogleSDK` holds one live
|
|
12
|
+
* `GoogleGenAI` client, shared by every `ModelContract` /
|
|
13
|
+
* `EmbedderContract` it produces. Construct one SDK per
|
|
14
|
+
* account/project and reuse it everywhere.
|
|
15
|
+
*
|
|
16
|
+
* **Responsibility.**
|
|
17
|
+
* - Owns: a long-lived `GoogleGenAI` client (auth, Vertex vs Gemini
|
|
18
|
+
* API) and its lifetime. Factory for `GoogleModel` /
|
|
19
|
+
* `GoogleEmbedder` instances sharing that client.
|
|
20
|
+
* - Does NOT own: anything per-call — those live in `GoogleModel` /
|
|
21
|
+
* `GoogleEmbedder` and the agent runtime.
|
|
22
|
+
*
|
|
23
|
+
* Modeled as a class (see §4.2 of code-style.md — "long-lived state
|
|
24
|
+
* across many calls"), fronted by FP usage like the other adapters.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY! });
|
|
28
|
+
* const model = google.model({ name: "gemini-2.5-flash", temperature: 0.7 });
|
|
29
|
+
* const embedder = google.embedder({ name: "gemini-embedding-001" });
|
|
30
|
+
*/
|
|
31
|
+
var GoogleSDK = class {
|
|
32
|
+
constructor(config) {
|
|
33
|
+
const { provider, pricing, ...clientOptions } = config;
|
|
34
|
+
this.ai = new GoogleGenAI(clientOptions);
|
|
35
|
+
this.provider = provider ?? "google";
|
|
36
|
+
this.pricing = pricing;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build a `GoogleModel` bound to this SDK's client. Each call
|
|
40
|
+
* returns a fresh instance; all instances share the underlying
|
|
41
|
+
* `GoogleGenAI` client. The SDK's `provider` label is forwarded.
|
|
42
|
+
*
|
|
43
|
+
* Pricing resolution: per-model `config.pricing` wins; otherwise the
|
|
44
|
+
* SDK-level registry entry keyed by `config.name`; otherwise
|
|
45
|
+
* `undefined` (no cost computed).
|
|
46
|
+
*/
|
|
47
|
+
model(config) {
|
|
48
|
+
const resolvedPricing = config.pricing ?? this.pricing?.[config.name];
|
|
49
|
+
const resolvedConfig = resolvedPricing === config.pricing ? config : {
|
|
50
|
+
...config,
|
|
51
|
+
pricing: resolvedPricing
|
|
52
|
+
};
|
|
53
|
+
return new GoogleModel(this.ai, resolvedConfig, this.provider);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Rough token-count estimate. Uses the character-heuristic
|
|
57
|
+
* (`approximateTokenCount`) from the core package — Gemini's
|
|
58
|
+
* `countTokens` is a network round-trip; `count()` is intentionally
|
|
59
|
+
* offline. Good for budgeting/quota guards, not billing.
|
|
60
|
+
*/
|
|
61
|
+
async count(text, _model) {
|
|
62
|
+
return approximateTokenCount(text);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Build a `GoogleEmbedder` bound to this SDK's client.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* const embedder = google.embedder({ name: "gemini-embedding-001" });
|
|
69
|
+
* const { vector } = await embedder.embed("Hello world");
|
|
70
|
+
*/
|
|
71
|
+
embedder(config) {
|
|
72
|
+
return new GoogleEmbedder(this.ai, config, this.provider);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
export { GoogleSDK };
|
|
78
|
+
//# sourceMappingURL=sdk.mjs.map
|
package/esm/sdk.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk.mjs","names":[],"sources":["../../../../../@warlock.js/ai-google/src/sdk.ts"],"sourcesContent":["import { GoogleGenAI } from \"@google/genai\";\nimport type {\n EmbedderContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n} from \"@warlock.js/ai\";\nimport { approximateTokenCount } from \"@warlock.js/ai\";\nimport type {\n GoogleEmbedderConfig,\n GoogleModelConfig,\n GoogleSDKConfig,\n} from \"./config.type\";\nimport { GoogleEmbedder } from \"./embedder\";\nimport { GoogleModel } from \"./model\";\n\n/**\n * Google Gemini-backed implementation of `SDKAdapterContract`.\n *\n * **Role.** The package entry point for Gemini models via the\n * `@google/genai` SDK. A single `GoogleSDK` holds one live\n * `GoogleGenAI` client, shared by every `ModelContract` /\n * `EmbedderContract` it produces. Construct one SDK per\n * account/project and reuse it everywhere.\n *\n * **Responsibility.**\n * - Owns: a long-lived `GoogleGenAI` client (auth, Vertex vs Gemini\n * API) and its lifetime. Factory for `GoogleModel` /\n * `GoogleEmbedder` instances sharing that client.\n * - Does NOT own: anything per-call — those live in `GoogleModel` /\n * `GoogleEmbedder` and the agent runtime.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across many calls\"), fronted by FP usage like the other adapters.\n *\n * @example\n * const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY! });\n * const model = google.model({ name: \"gemini-2.5-flash\", temperature: 0.7 });\n * const embedder = google.embedder({ name: \"gemini-embedding-001\" });\n */\nexport class GoogleSDK implements SDKAdapterContract {\n private readonly ai: GoogleGenAI;\n private readonly provider: string;\n private readonly pricing?: Record<string, ModelPricing>;\n\n public constructor(config: GoogleSDKConfig) {\n const { provider, pricing, ...clientOptions } = config;\n\n this.ai = new GoogleGenAI(clientOptions);\n this.provider = provider ?? \"google\";\n this.pricing = pricing;\n }\n\n /**\n * Build a `GoogleModel` bound to this SDK's client. Each call\n * returns a fresh instance; all instances share the underlying\n * `GoogleGenAI` client. The SDK's `provider` label is forwarded.\n *\n * Pricing resolution: per-model `config.pricing` wins; otherwise the\n * SDK-level registry entry keyed by `config.name`; otherwise\n * `undefined` (no cost computed).\n */\n public model(config: GoogleModelConfig): ModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: GoogleModelConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n return new GoogleModel(this.ai, resolvedConfig, this.provider);\n }\n\n /**\n * Rough token-count estimate. Uses the character-heuristic\n * (`approximateTokenCount`) from the core package — Gemini's\n * `countTokens` is a network round-trip; `count()` is intentionally\n * offline. Good for budgeting/quota guards, not billing.\n */\n public async count(text: string, _model?: string): Promise<number> {\n return approximateTokenCount(text);\n }\n\n /**\n * Build a `GoogleEmbedder` bound to this SDK's client.\n *\n * @example\n * const embedder = google.embedder({ name: \"gemini-embedding-001\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n */\n public embedder(config: GoogleEmbedderConfig): EmbedderContract {\n return new GoogleEmbedder(this.ai, config, this.provider);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,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;AACF"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region ../../@warlock.js/ai-google/src/utils/map-finish-reason.ts
|
|
2
|
+
const finishReasonMap = {
|
|
3
|
+
STOP: "stop",
|
|
4
|
+
MAX_TOKENS: "length"
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Map Gemini's `FinishReason` enum value to the normalized
|
|
8
|
+
* `FinishReason` union.
|
|
9
|
+
*
|
|
10
|
+
* `STOP` is the natural terminal. `MAX_TOKENS` maps to `length`.
|
|
11
|
+
* Everything else — `SAFETY`, `RECITATION`, `BLOCKLIST`,
|
|
12
|
+
* `PROHIBITED_CONTENT`, `SPII`, `MALFORMED_FUNCTION_CALL`,
|
|
13
|
+
* `UNEXPECTED_TOOL_CALL`, `LANGUAGE`, `OTHER`,
|
|
14
|
+
* `FINISH_REASON_UNSPECIFIED`, `null`, or any future value — falls
|
|
15
|
+
* through to `"error"`.
|
|
16
|
+
*
|
|
17
|
+
* Note: Gemini reports `STOP` even when the turn ended in a function
|
|
18
|
+
* call (it has no `tool_use` reason). `GoogleModel` overrides the
|
|
19
|
+
* mapped reason to `"tool_calls"` when the response carries function
|
|
20
|
+
* calls — this map intentionally stays purely about the raw signal.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* mapFinishReason("STOP"); // "stop"
|
|
24
|
+
* mapFinishReason("MAX_TOKENS"); // "length"
|
|
25
|
+
* mapFinishReason("SAFETY"); // "error"
|
|
26
|
+
* mapFinishReason(undefined); // "error"
|
|
27
|
+
*/
|
|
28
|
+
function mapFinishReason(raw) {
|
|
29
|
+
return finishReasonMap[raw ?? ""] ?? "error";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { mapFinishReason };
|
|
34
|
+
//# sourceMappingURL=map-finish-reason.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map-finish-reason.mjs","names":[],"sources":["../../../../../../@warlock.js/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"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { InvalidRequestError, safeJsonParse } from "@warlock.js/ai";
|
|
2
|
+
|
|
3
|
+
//#region ../../@warlock.js/ai-google/src/utils/to-google-contents.ts
|
|
4
|
+
/**
|
|
5
|
+
* Convert vendor-neutral `Message[]` into Gemini's request shape.
|
|
6
|
+
*
|
|
7
|
+
* Gemini specifics this function absorbs:
|
|
8
|
+
*
|
|
9
|
+
* 1. **No `system` role.** System messages concatenate into the
|
|
10
|
+
* separate `systemInstruction` config field.
|
|
11
|
+
* 2. **Role names differ.** Neutral `assistant` → Gemini `"model"`;
|
|
12
|
+
* `user` stays `"user"`.
|
|
13
|
+
* 3. **Tool results are `user` turns.** A neutral `tool` message
|
|
14
|
+
* becomes a `"user"` content with a single `functionResponse` part.
|
|
15
|
+
* 4. **Tool calls are `functionCall` parts.** An assistant message
|
|
16
|
+
* with `toolCalls` becomes a `"model"` content: an optional leading
|
|
17
|
+
* `text` part followed by one `functionCall` part per call.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const { systemInstruction, contents } = toGoogleContents([
|
|
21
|
+
* { role: "system", content: "Be concise." },
|
|
22
|
+
* { role: "user", content: "Hi" },
|
|
23
|
+
* ]);
|
|
24
|
+
*/
|
|
25
|
+
function toGoogleContents(messages) {
|
|
26
|
+
const systemParts = [];
|
|
27
|
+
const contents = [];
|
|
28
|
+
for (const message of messages) {
|
|
29
|
+
if (message.role === "system") {
|
|
30
|
+
systemParts.push(stringifyContent(message.content));
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (message.role === "tool") {
|
|
34
|
+
contents.push({
|
|
35
|
+
role: "user",
|
|
36
|
+
parts: [{ functionResponse: {
|
|
37
|
+
name: message.toolCallId ?? "",
|
|
38
|
+
response: toResponseObject(stringifyContent(message.content))
|
|
39
|
+
} }]
|
|
40
|
+
});
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (message.role === "assistant" && message.toolCalls && message.toolCalls.length > 0) {
|
|
44
|
+
const parts = [];
|
|
45
|
+
const text = stringifyContent(message.content);
|
|
46
|
+
if (text) parts.push({ text });
|
|
47
|
+
for (const toolCall of message.toolCalls) {
|
|
48
|
+
const thoughtSignature = toolCall.providerMetadata?.thoughtSignature;
|
|
49
|
+
parts.push({
|
|
50
|
+
...typeof thoughtSignature === "string" ? { thoughtSignature } : {},
|
|
51
|
+
functionCall: {
|
|
52
|
+
name: toolCall.name,
|
|
53
|
+
args: toolCall.input ?? {}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
contents.push({
|
|
58
|
+
role: "model",
|
|
59
|
+
parts
|
|
60
|
+
});
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (message.role === "user" && Array.isArray(message.content)) {
|
|
64
|
+
contents.push({
|
|
65
|
+
role: "user",
|
|
66
|
+
parts: message.content.map(toGooglePart)
|
|
67
|
+
});
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
contents.push({
|
|
71
|
+
role: message.role === "assistant" ? "model" : "user",
|
|
72
|
+
parts: [{ text: stringifyContent(message.content) }]
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
systemInstruction: systemParts.length > 0 ? systemParts.join("\n\n") : void 0,
|
|
77
|
+
contents
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Multipart content is only meaningful on user messages — for any
|
|
82
|
+
* other role collapse a `ContentPart[]` to concatenated text. Plain
|
|
83
|
+
* strings pass through unchanged.
|
|
84
|
+
*/
|
|
85
|
+
function stringifyContent(content) {
|
|
86
|
+
if (typeof content === "string") return content;
|
|
87
|
+
return content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Gemini's `functionResponse.response` must be a JSON object. Tool
|
|
91
|
+
* results arrive as a string (usually stringified JSON) — parse it
|
|
92
|
+
* when it is a JSON object, otherwise wrap the raw string under a
|
|
93
|
+
* `result` key so the model always receives a well-formed object.
|
|
94
|
+
*/
|
|
95
|
+
function toResponseObject(raw) {
|
|
96
|
+
const parsed = safeJsonParse(raw, void 0);
|
|
97
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
98
|
+
return { result: raw };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Map a resolved `ContentPart` to a Gemini `Part`. Images are sent as
|
|
102
|
+
* inline base64 (`inlineData`). Gemini's `generateContent` does not
|
|
103
|
+
* fetch arbitrary remote URLs (only Files API / GCS URIs via
|
|
104
|
+
* `fileData`), so a neutral `{ url }` image surfaces a typed
|
|
105
|
+
* `InvalidRequestError` upfront rather than a downstream Gemini fault.
|
|
106
|
+
* The agent resolves attachments before this point, so nothing is
|
|
107
|
+
* read or fetched here.
|
|
108
|
+
*/
|
|
109
|
+
function toGooglePart(part) {
|
|
110
|
+
if (part.type === "text") return { text: part.text };
|
|
111
|
+
if ("url" in part.source) throw new InvalidRequestError("Gemini generateContent does not fetch remote-URL images; supply base64 image bytes instead.");
|
|
112
|
+
return { inlineData: {
|
|
113
|
+
mimeType: part.source.mediaType,
|
|
114
|
+
data: part.source.base64
|
|
115
|
+
} };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
export { toGoogleContents };
|
|
120
|
+
//# sourceMappingURL=to-google-contents.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-google-contents.mjs","names":[],"sources":["../../../../../../@warlock.js/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`. Images are sent as\n * inline base64 (`inlineData`). Gemini's `generateContent` does not\n * fetch arbitrary remote URLs (only Files API / GCS URIs via\n * `fileData`), so a neutral `{ url }` image surfaces a typed\n * `InvalidRequestError` upfront rather than a downstream Gemini fault.\n * The agent resolves attachments before this point, so nothing is\n * read or fetched here.\n */\nfunction toGooglePart(part: ContentPart): Part {\n if (part.type === \"text\") {\n return { text: part.text };\n }\n\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"Gemini generateContent does not fetch remote-URL images; supply base64 image bytes instead.\",\n );\n }\n\n return {\n inlineData: { mimeType: part.source.mediaType, data: part.source.base64 },\n };\n}\n"],"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,EAC7E,KAAK,SAAS,KAAK,IAAI,EACvB,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;;;;;;;;;;AAWA,SAAS,aAAa,MAAyB;CAC7C,IAAI,KAAK,SAAS,QAChB,OAAO,EAAE,MAAM,KAAK,KAAK;CAG3B,IAAI,SAAS,KAAK,QAChB,MAAM,IAAI,oBACR,6FACF;CAGF,OAAO,EACL,YAAY;EAAE,UAAU,KAAK,OAAO;EAAW,MAAM,KAAK,OAAO;CAAO,EAC1E;AACF"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { extractJsonSchema } from "@warlock.js/ai";
|
|
2
|
+
|
|
3
|
+
//#region ../../@warlock.js/ai-google/src/utils/to-google-tools.ts
|
|
4
|
+
/**
|
|
5
|
+
* Convert vendor-neutral `ToolConfig[]` into Gemini's `tools` array —
|
|
6
|
+
* a single `Tool` carrying one `functionDeclarations` entry per tool.
|
|
7
|
+
*
|
|
8
|
+
* The input schema is forwarded via `parametersJsonSchema` (raw JSON
|
|
9
|
+
* Schema, mutually exclusive with Gemini's typed `parameters`).
|
|
10
|
+
* Non-object extractions degrade to a parameterless object so
|
|
11
|
+
* registration never fails.
|
|
12
|
+
*
|
|
13
|
+
* Returns `undefined` when there are no tools so the caller can omit
|
|
14
|
+
* `config.tools` entirely.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const tools = toGoogleTools([weatherTool]);
|
|
18
|
+
* await ai.models.generateContent({ model, contents, config: { tools } });
|
|
19
|
+
*/
|
|
20
|
+
function toGoogleTools(tools) {
|
|
21
|
+
if (!tools || tools.length === 0) return;
|
|
22
|
+
return [{ functionDeclarations: tools.map((tool) => ({
|
|
23
|
+
name: tool.name,
|
|
24
|
+
description: tool.description,
|
|
25
|
+
parametersJsonSchema: toJsonSchema(tool.input)
|
|
26
|
+
})) }];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a tool's input schema to a JSON-Schema object. Gemini wants
|
|
30
|
+
* an object root for function parameters; anything else (or a failed
|
|
31
|
+
* extraction) degrades to a parameterless object.
|
|
32
|
+
*/
|
|
33
|
+
function toJsonSchema(input) {
|
|
34
|
+
const schema = extractJsonSchema(input);
|
|
35
|
+
if (schema && schema.type === "object") return schema;
|
|
36
|
+
return { type: "object" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
export { toGoogleTools };
|
|
41
|
+
//# sourceMappingURL=to-google-tools.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-google-tools.mjs","names":[],"sources":["../../../../../../@warlock.js/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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ApiError } from "@google/genai";
|
|
2
|
+
import { AIError, ContextLengthExceededError, InvalidRequestError, ProviderAuthError, ProviderError, ProviderRateLimitError, ProviderTimeoutError } from "@warlock.js/ai";
|
|
3
|
+
|
|
4
|
+
//#region ../../@warlock.js/ai-google/src/utils/wrap-google-error.ts
|
|
5
|
+
/**
|
|
6
|
+
* Wrap any thrown value caught inside the Gemini adapter into the
|
|
7
|
+
* appropriate `@warlock.js/ai` `AIError` subclass.
|
|
8
|
+
*
|
|
9
|
+
* **Dispatch strategy.** Gemini has no machine error `code`; the
|
|
10
|
+
* signals are the HTTP `status` and the canonical status phrase Google
|
|
11
|
+
* embeds in `message` (`PERMISSION_DENIED`, `RESOURCE_EXHAUSTED`,
|
|
12
|
+
* `INVALID_ARGUMENT`, …). Dispatch keys on `status`, using the message
|
|
13
|
+
* phrase as the tie-breaker for the two 400 sub-cases
|
|
14
|
+
* (context-length vs generic) and for status-less auth/quota errors.
|
|
15
|
+
*
|
|
16
|
+
* `AIError` instances pass through unchanged so `catch/throw wrap(e)`
|
|
17
|
+
* pipelines never double-wrap.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* try {
|
|
21
|
+
* return await this.ai.models.generateContent(...);
|
|
22
|
+
* } catch (thrown) {
|
|
23
|
+
* throw wrapGoogleError(thrown);
|
|
24
|
+
* }
|
|
25
|
+
*/
|
|
26
|
+
function wrapGoogleError(thrown) {
|
|
27
|
+
if (thrown instanceof AIError) return thrown;
|
|
28
|
+
const shape = toShape(thrown);
|
|
29
|
+
const context = buildContext(shape);
|
|
30
|
+
const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));
|
|
31
|
+
if (isTimeout(shape)) return new ProviderTimeoutError(message, {
|
|
32
|
+
cause: thrown,
|
|
33
|
+
context
|
|
34
|
+
});
|
|
35
|
+
if (shape.status === 401 || shape.status === 403 || /permission_denied|api key not valid|unauthenticated/i.test(message)) return new ProviderAuthError(message, {
|
|
36
|
+
cause: thrown,
|
|
37
|
+
context
|
|
38
|
+
});
|
|
39
|
+
if (shape.status === 429 || /resource_exhausted|quota/i.test(message)) return new ProviderRateLimitError(message, {
|
|
40
|
+
cause: thrown,
|
|
41
|
+
context
|
|
42
|
+
});
|
|
43
|
+
if (shape.status === 400) {
|
|
44
|
+
if (/token count|context length|exceeds the maximum|input is too long/i.test(message)) return new ContextLengthExceededError(message, {
|
|
45
|
+
cause: thrown,
|
|
46
|
+
context
|
|
47
|
+
});
|
|
48
|
+
return new InvalidRequestError(message, {
|
|
49
|
+
cause: thrown,
|
|
50
|
+
context
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (shape.status === 404 || isClientStatus(shape.status)) return new InvalidRequestError(message, {
|
|
54
|
+
cause: thrown,
|
|
55
|
+
context
|
|
56
|
+
});
|
|
57
|
+
return new ProviderError(message, {
|
|
58
|
+
cause: thrown,
|
|
59
|
+
context
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Read the raw error shape. The Gemini SDK's `ApiError` carries a
|
|
64
|
+
* numeric `status`; flattened/proxied errors may carry it (or `code`)
|
|
65
|
+
* loosely.
|
|
66
|
+
*/
|
|
67
|
+
function toShape(thrown) {
|
|
68
|
+
if (thrown instanceof ApiError) return {
|
|
69
|
+
status: thrown.status,
|
|
70
|
+
message: thrown.message,
|
|
71
|
+
name: thrown.name
|
|
72
|
+
};
|
|
73
|
+
if (typeof thrown === "object" && thrown !== null) {
|
|
74
|
+
const raw = thrown;
|
|
75
|
+
return {
|
|
76
|
+
status: typeof raw.status === "number" ? raw.status : void 0,
|
|
77
|
+
message: typeof raw.message === "string" ? raw.message : void 0,
|
|
78
|
+
name: typeof raw.name === "string" ? raw.name : void 0,
|
|
79
|
+
code: typeof raw.code === "string" ? raw.code : void 0
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Decide whether the error is a timeout. Gemini maps gateway timeouts
|
|
86
|
+
* to HTTP 504 (`DEADLINE_EXCEEDED`); transport aborts surface as
|
|
87
|
+
* `AbortError` / `ETIMEDOUT` / `ECONNABORTED`.
|
|
88
|
+
*/
|
|
89
|
+
function isTimeout(shape) {
|
|
90
|
+
if (shape.status === 504) return true;
|
|
91
|
+
if (shape.name === "AbortError" || /deadline_exceeded/i.test(shape.message ?? "")) return true;
|
|
92
|
+
return shape.code === "ETIMEDOUT" || shape.code === "ECONNABORTED";
|
|
93
|
+
}
|
|
94
|
+
/** True for HTTP 4xx — a client-side request problem, not a server fault. */
|
|
95
|
+
function isClientStatus(status) {
|
|
96
|
+
return typeof status === "number" && status >= 400 && status < 500;
|
|
97
|
+
}
|
|
98
|
+
/** Attach the diagnostic fields to `error.context`. */
|
|
99
|
+
function buildContext(shape) {
|
|
100
|
+
const context = {};
|
|
101
|
+
if (shape.status !== void 0) context.status = shape.status;
|
|
102
|
+
if (shape.name) context.code = shape.name;
|
|
103
|
+
return context;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
//#endregion
|
|
107
|
+
export { wrapGoogleError };
|
|
108
|
+
//# sourceMappingURL=wrap-google-error.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wrap-google-error.mjs","names":[],"sources":["../../../../../../@warlock.js/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"}
|