@intx/inference-discovery-google-genai 0.1.2 → 0.3.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.
package/src/index.ts DELETED
@@ -1,327 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import {
3
- resolveMediaPath,
4
- type Capability,
5
- type CapabilityIntent,
6
- type MediaRef,
7
- } from "@intx/inference-discovery/catalog";
8
- import type {
9
- CaptureStep,
10
- CapturedResponse,
11
- IterateCaptureStepsOpts,
12
- ProviderPlugin,
13
- } from "@intx/inference-discovery";
14
- import { buildAuthHeaders } from "./auth";
15
- import { buildEndpointURL } from "./endpoint";
16
- import { buildRequestBody } from "./request-body";
17
-
18
- const PROVIDER_NAME = "google-genai";
19
- const MODELS = ["gemini-2.5-flash", "gemini-2.5-flash-image"] as const;
20
- const REDACT_REQUEST_HEADERS = ["x-goog-api-key"] as const;
21
- const REDACT_RESPONSE_HEADERS: readonly string[] = [];
22
-
23
- const FILES_API_UPLOAD_URL =
24
- "https://generativelanguage.googleapis.com/upload/v1beta/files";
25
-
26
- const MULTI_TURN_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([
27
- "function-calling-multi-turn",
28
- "function-calling-multi-turn-streaming",
29
- "function-calling-with-thinking",
30
- "function-calling-with-thinking-streaming",
31
- ]);
32
-
33
- const FILES_API_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([
34
- "files-api-reference",
35
- "files-api-reference-streaming",
36
- ]);
37
-
38
- export interface GoogleGenaiPluginOptions {
39
- apiKey: string;
40
- }
41
-
42
- function isRecord(value: unknown): value is Record<string, unknown> {
43
- return typeof value === "object" && value !== null && !Array.isArray(value);
44
- }
45
-
46
- interface UploadStepDescriptor {
47
- url: string;
48
- mimeType: string;
49
- displayName: string;
50
- bytes: Uint8Array;
51
- }
52
-
53
- function buildUploadDescriptor(intent: CapabilityIntent): UploadStepDescriptor {
54
- const media = intent.media?.[0];
55
- if (media === undefined) {
56
- throw new Error(
57
- "google-genai files-API: intent.media[0] is required; the catalog's " +
58
- "files-api-reference intent must declare the document to upload.",
59
- );
60
- }
61
- const bytes = readFileSync(resolveMediaPath(media));
62
- return {
63
- url: FILES_API_UPLOAD_URL,
64
- mimeType: mimeTypeForMedia(media),
65
- displayName: basename(media.path),
66
- bytes: new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength),
67
- };
68
- }
69
-
70
- function basename(path: string): string {
71
- const slash = path.lastIndexOf("/");
72
- return slash < 0 ? path : path.slice(slash + 1);
73
- }
74
-
75
- function mimeTypeForMedia(ref: MediaRef): string {
76
- if (ref.kind === "document") return "application/pdf";
77
- if (ref.kind === "image") return "image/jpeg";
78
- if (ref.kind === "audio") return "audio/wav";
79
- if (ref.kind === "video") return "video/mp4";
80
- throw new Error(`google-genai: unsupported media kind ${String(ref.kind)}`);
81
- }
82
-
83
- function extractFileUri(parsed: unknown): string {
84
- if (!isRecord(parsed)) {
85
- throw new Error(
86
- "google-genai files-API: upload response is not a JSON object",
87
- );
88
- }
89
- const file = parsed.file;
90
- if (!isRecord(file)) {
91
- throw new Error(
92
- "google-genai files-API: upload response missing 'file' object",
93
- );
94
- }
95
- const uri = file.uri;
96
- if (typeof uri !== "string" || uri.length === 0) {
97
- throw new Error(
98
- "google-genai files-API: upload response has no string 'file.uri'",
99
- );
100
- }
101
- return uri;
102
- }
103
-
104
- function extractMimeTypeFromUpload(parsed: unknown): string {
105
- if (!isRecord(parsed)) {
106
- throw new Error(
107
- "google-genai files-API: upload response is not a JSON object",
108
- );
109
- }
110
- const file = parsed.file;
111
- if (!isRecord(file)) {
112
- throw new Error(
113
- "google-genai files-API: upload response missing 'file' object",
114
- );
115
- }
116
- const mime = file.mimeType;
117
- if (typeof mime !== "string" || mime.length === 0) {
118
- throw new Error(
119
- "google-genai files-API: upload response has no string 'file.mimeType'",
120
- );
121
- }
122
- return mime;
123
- }
124
-
125
- function buildFilesApiGenerateBody(opts: {
126
- intent: CapabilityIntent;
127
- fileUri: string;
128
- mimeType: string;
129
- }): unknown {
130
- return {
131
- contents: [
132
- {
133
- role: "user",
134
- parts: [
135
- { text: opts.intent.prompt },
136
- {
137
- fileData: {
138
- mimeType: opts.mimeType,
139
- fileUri: opts.fileUri,
140
- },
141
- },
142
- ],
143
- },
144
- ],
145
- };
146
- }
147
-
148
- function extractAssistantContent(parsed: unknown): unknown {
149
- if (!isRecord(parsed)) {
150
- throw new Error(
151
- "google-genai multi-turn: turn-1 response is not a JSON object",
152
- );
153
- }
154
- const candidates = parsed.candidates;
155
- if (!Array.isArray(candidates) || candidates.length === 0) {
156
- throw new Error(
157
- "google-genai multi-turn: turn-1 response has no candidates array",
158
- );
159
- }
160
- const first = candidates[0];
161
- if (!isRecord(first)) {
162
- throw new Error(
163
- "google-genai multi-turn: turn-1 response candidates[0] is not an object",
164
- );
165
- }
166
- const content = first.content;
167
- if (!isRecord(content)) {
168
- throw new Error(
169
- "google-genai multi-turn: turn-1 response candidates[0].content is not an object",
170
- );
171
- }
172
- return content;
173
- }
174
-
175
- function deriveToolFollowUp(intent: CapabilityIntent): {
176
- toolName: string;
177
- content: string;
178
- } {
179
- const followUp = intent.followUp;
180
- if (followUp !== undefined) {
181
- for (const step of followUp) {
182
- if (step.role === "tool") {
183
- return { toolName: step.toolName, content: step.content };
184
- }
185
- }
186
- }
187
- const tools = intent.tools;
188
- if (tools === undefined || tools.length === 0) {
189
- throw new Error(
190
- "google-genai multi-turn: intent has neither followUp nor tools",
191
- );
192
- }
193
- const [tool] = tools;
194
- if (tool === undefined) {
195
- throw new Error("google-genai multi-turn: intent.tools[0] is undefined");
196
- }
197
- return { toolName: tool.name, content: "{}" };
198
- }
199
-
200
- function buildMultiTurnTurn2Body(opts: {
201
- capability: Capability;
202
- intent: CapabilityIntent;
203
- turn1Body: unknown;
204
- turn1Response: unknown;
205
- }): unknown {
206
- if (!isRecord(opts.turn1Body)) {
207
- throw new Error("google-genai multi-turn: turn-1 body is not an object");
208
- }
209
- const turn1Contents = opts.turn1Body.contents;
210
- if (!Array.isArray(turn1Contents)) {
211
- throw new Error(
212
- "google-genai multi-turn: turn-1 body.contents is not an array",
213
- );
214
- }
215
- const assistantContent = extractAssistantContent(opts.turn1Response);
216
- const tool = deriveToolFollowUp(opts.intent);
217
- const toolResponseObject: unknown = JSON.parse(tool.content);
218
-
219
- const turn1Tools = opts.turn1Body.tools;
220
- const turn1GenerationConfig = opts.turn1Body.generationConfig;
221
-
222
- const body: Record<string, unknown> = {
223
- contents: [
224
- ...turn1Contents,
225
- assistantContent,
226
- {
227
- role: "user",
228
- parts: [
229
- {
230
- functionResponse: {
231
- name: tool.toolName,
232
- response: toolResponseObject,
233
- },
234
- },
235
- ],
236
- },
237
- ],
238
- };
239
- if (turn1Tools !== undefined) {
240
- body.tools = turn1Tools;
241
- }
242
- if (turn1GenerationConfig !== undefined) {
243
- body.generationConfig = turn1GenerationConfig;
244
- }
245
- return body;
246
- }
247
-
248
- export function* iterateCaptureSteps(
249
- opts: IterateCaptureStepsOpts,
250
- ): Generator<CaptureStep, void, CapturedResponse> {
251
- const { model, capability, intent } = opts;
252
-
253
- if (FILES_API_CAPABILITIES.has(capability)) {
254
- const upload = buildUploadDescriptor(intent);
255
- const uploadResponse = yield {
256
- kind: "raw",
257
- subdir: "upload",
258
- url: upload.url,
259
- method: "POST",
260
- contentType: upload.mimeType,
261
- headers: {
262
- "X-Goog-Upload-Protocol": "raw",
263
- "X-Goog-Upload-File-Name": upload.displayName,
264
- },
265
- body: upload.bytes,
266
- };
267
- const fileUri = extractFileUri(uploadResponse.parsed);
268
- const mimeType = extractMimeTypeFromUpload(uploadResponse.parsed);
269
- const generateBody = buildFilesApiGenerateBody({
270
- intent,
271
- fileUri,
272
- mimeType,
273
- });
274
- yield {
275
- kind: "json",
276
- subdir: "generate",
277
- url: buildEndpointURL({ model, capability }),
278
- body: generateBody,
279
- };
280
- return;
281
- }
282
-
283
- if (MULTI_TURN_CAPABILITIES.has(capability)) {
284
- const turn1Body = buildRequestBody({ model, capability, intent });
285
- const url = buildEndpointURL({ model, capability });
286
- const turn1Response = yield {
287
- kind: "json",
288
- subdir: "turn-1",
289
- url,
290
- body: turn1Body,
291
- };
292
- const turn2Body = buildMultiTurnTurn2Body({
293
- capability,
294
- intent,
295
- turn1Body,
296
- turn1Response: turn1Response.parsed,
297
- });
298
- yield {
299
- kind: "json",
300
- subdir: "turn-2",
301
- url,
302
- body: turn2Body,
303
- };
304
- return;
305
- }
306
-
307
- yield {
308
- kind: "json",
309
- subdir: null,
310
- url: buildEndpointURL({ model, capability }),
311
- body: buildRequestBody({ model, capability, intent }),
312
- };
313
- }
314
-
315
- export function createGoogleGenaiPlugin(
316
- opts: GoogleGenaiPluginOptions,
317
- ): ProviderPlugin {
318
- const apiKey = opts.apiKey;
319
- return {
320
- name: PROVIDER_NAME,
321
- models: MODELS,
322
- redactRequestHeaders: REDACT_REQUEST_HEADERS,
323
- redactResponseHeaders: REDACT_RESPONSE_HEADERS,
324
- buildAuthHeaders: () => buildAuthHeaders(apiKey),
325
- iterateCaptureSteps,
326
- };
327
- }