@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.
@@ -1,436 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import {
3
- resolveMediaPath,
4
- type Capability,
5
- type CapabilityIntent,
6
- type MediaRef,
7
- type ToolDecl,
8
- } from "@intx/inference-discovery/catalog";
9
-
10
- const TEXT_MODEL = "gemini-2.5-flash";
11
- const IMAGE_MODEL = "gemini-2.5-flash-image";
12
-
13
- const TEXT_MODEL_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([
14
- "plain-text",
15
- "plain-text-streaming",
16
- "function-calling-multi-turn",
17
- "function-calling-multi-turn-streaming",
18
- "function-calling-with-thinking",
19
- "function-calling-with-thinking-streaming",
20
- "vision-input",
21
- "vision-input-streaming",
22
- "audio-input",
23
- "audio-input-streaming",
24
- "video-input",
25
- "video-input-streaming",
26
- "document-input",
27
- "document-input-streaming",
28
- "code-execution",
29
- "code-execution-streaming",
30
- "grounding",
31
- "grounding-streaming",
32
- "files-api-reference",
33
- "files-api-reference-streaming",
34
- "safety-classification",
35
- "safety-classification-streaming",
36
- "structured-output",
37
- "structured-output-streaming",
38
- ]);
39
-
40
- const IMAGE_MODEL_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([
41
- "image-output",
42
- "image-output-streaming",
43
- ]);
44
-
45
- const EXTENSION_TO_MIME_TYPE: Readonly<Record<string, string>> = {
46
- jpg: "image/jpeg",
47
- jpeg: "image/jpeg",
48
- png: "image/png",
49
- gif: "image/gif",
50
- webp: "image/webp",
51
- wav: "audio/wav",
52
- mp3: "audio/mpeg",
53
- ogg: "audio/ogg",
54
- flac: "audio/flac",
55
- mp4: "video/mp4",
56
- mov: "video/quicktime",
57
- webm: "video/webm",
58
- pdf: "application/pdf",
59
- };
60
-
61
- interface GeminiTextPart {
62
- text: string;
63
- }
64
-
65
- interface GeminiInlineDataPart {
66
- inlineData: {
67
- mimeType: string;
68
- data: string;
69
- };
70
- }
71
-
72
- interface GeminiFileDataPart {
73
- fileData: {
74
- mimeType: string;
75
- fileUri: string;
76
- };
77
- }
78
-
79
- type GeminiPart = GeminiTextPart | GeminiInlineDataPart | GeminiFileDataPart;
80
-
81
- interface GeminiContent {
82
- role: "user" | "model";
83
- parts: GeminiPart[];
84
- }
85
-
86
- interface GeminiFunctionDeclaration {
87
- name: string;
88
- description: string;
89
- parameters: ToolDecl["parameters"];
90
- }
91
-
92
- interface GeminiFunctionTool {
93
- functionDeclarations: GeminiFunctionDeclaration[];
94
- }
95
-
96
- interface GeminiCodeExecutionTool {
97
- codeExecution: Record<string, never>;
98
- }
99
-
100
- interface GeminiGoogleSearchTool {
101
- googleSearch: Record<string, never>;
102
- }
103
-
104
- type GeminiTool =
105
- | GeminiFunctionTool
106
- | GeminiCodeExecutionTool
107
- | GeminiGoogleSearchTool;
108
-
109
- interface GeminiThinkingConfig {
110
- thinkingBudget: number;
111
- includeThoughts?: true;
112
- }
113
-
114
- interface GeminiGenerationConfig {
115
- maxOutputTokens?: number;
116
- thinkingConfig?: GeminiThinkingConfig;
117
- responseModalities?: readonly ["TEXT", "IMAGE"];
118
- responseMimeType?: string;
119
- responseSchema?: unknown;
120
- }
121
-
122
- interface GeminiToolConfig {
123
- functionCallingConfig: {
124
- mode: "ANY";
125
- allowedFunctionNames: string[];
126
- };
127
- }
128
-
129
- interface GeminiRequestBody {
130
- contents: GeminiContent[];
131
- tools?: GeminiTool[];
132
- toolConfig?: GeminiToolConfig;
133
- generationConfig?: GeminiGenerationConfig;
134
- }
135
-
136
- function modelSupportsCapability(model: string, capability: Capability): void {
137
- if (model === TEXT_MODEL) {
138
- if (!TEXT_MODEL_CAPABILITIES.has(capability)) {
139
- throw new Error(
140
- `google-genai: model ${model} does not support capability ${capability}`,
141
- );
142
- }
143
- return;
144
- }
145
- if (model === IMAGE_MODEL) {
146
- if (!IMAGE_MODEL_CAPABILITIES.has(capability)) {
147
- throw new Error(
148
- `google-genai: model ${model} does not support capability ${capability}`,
149
- );
150
- }
151
- return;
152
- }
153
- throw new Error(`google-genai: unknown model ${model}`);
154
- }
155
-
156
- function extensionFor(path: string): string {
157
- const dot = path.lastIndexOf(".");
158
- if (dot < 0 || dot === path.length - 1) {
159
- throw new Error(
160
- `google-genai: cannot infer media MIME type, no extension in path: ${path}`,
161
- );
162
- }
163
- return path.slice(dot + 1).toLowerCase();
164
- }
165
-
166
- function mimeTypeFor(ref: MediaRef): string {
167
- const ext = extensionFor(ref.path);
168
- const mime = EXTENSION_TO_MIME_TYPE[ext];
169
- if (mime === undefined) {
170
- throw new Error(
171
- `google-genai: no MIME type mapping for extension .${ext} (path ${ref.path})`,
172
- );
173
- }
174
- return mime;
175
- }
176
-
177
- function readMediaBase64(ref: MediaRef): string {
178
- const absolute = resolveMediaPath(ref);
179
- const bytes = readFileSync(absolute);
180
- return bytes.toString("base64");
181
- }
182
-
183
- function expectSingleMedia(intent: CapabilityIntent): MediaRef {
184
- if (intent.media === undefined || intent.media.length === 0) {
185
- throw new Error(
186
- "google-genai: media-input capability requires intent.media to be non-empty",
187
- );
188
- }
189
- if (intent.media.length !== 1) {
190
- throw new Error(
191
- `google-genai: media-input capability expects exactly one media reference, got ${String(intent.media.length)}`,
192
- );
193
- }
194
- const [media] = intent.media;
195
- if (media === undefined) {
196
- throw new Error(
197
- "google-genai: media-input capability: media[0] is unexpectedly undefined",
198
- );
199
- }
200
- return media;
201
- }
202
-
203
- function expectSingleTool(intent: CapabilityIntent): ToolDecl {
204
- if (intent.tools === undefined || intent.tools.length === 0) {
205
- throw new Error(
206
- "google-genai: function-calling capability requires intent.tools to be non-empty",
207
- );
208
- }
209
- if (intent.tools.length !== 1) {
210
- throw new Error(
211
- `google-genai: function-calling capability expects exactly one tool declaration, got ${String(intent.tools.length)}`,
212
- );
213
- }
214
- const [tool] = intent.tools;
215
- if (tool === undefined) {
216
- throw new Error(
217
- "google-genai: function-calling capability: tools[0] is unexpectedly undefined",
218
- );
219
- }
220
- return tool;
221
- }
222
-
223
- function userTextContent(prompt: string): GeminiContent {
224
- return {
225
- role: "user",
226
- parts: [{ text: prompt }],
227
- };
228
- }
229
-
230
- function plainTextBody(intent: CapabilityIntent): GeminiRequestBody {
231
- return {
232
- contents: [userTextContent(intent.prompt)],
233
- };
234
- }
235
-
236
- function plainTextStreamingBody(intent: CapabilityIntent): GeminiRequestBody {
237
- return {
238
- contents: [userTextContent(intent.prompt)],
239
- generationConfig: {
240
- maxOutputTokens: 400,
241
- thinkingConfig: {
242
- thinkingBudget: 0,
243
- },
244
- },
245
- };
246
- }
247
-
248
- function functionToolFromDecl(decl: ToolDecl): GeminiFunctionTool {
249
- return {
250
- functionDeclarations: [
251
- {
252
- name: decl.name,
253
- description: decl.description,
254
- parameters: decl.parameters,
255
- },
256
- ],
257
- };
258
- }
259
-
260
- function functionCallingBody(
261
- intent: CapabilityIntent,
262
- thinking: { budget: number; includeThoughts: boolean },
263
- ): GeminiRequestBody {
264
- const decl = expectSingleTool(intent);
265
- const thinkingConfig: GeminiThinkingConfig = thinking.includeThoughts
266
- ? { thinkingBudget: thinking.budget, includeThoughts: true }
267
- : { thinkingBudget: thinking.budget };
268
- return {
269
- contents: [userTextContent(intent.prompt)],
270
- tools: [functionToolFromDecl(decl)],
271
- toolConfig: {
272
- functionCallingConfig: {
273
- mode: "ANY",
274
- allowedFunctionNames: [decl.name],
275
- },
276
- },
277
- generationConfig: {
278
- thinkingConfig,
279
- },
280
- };
281
- }
282
-
283
- function inlineMediaBody(intent: CapabilityIntent): GeminiRequestBody {
284
- const media = expectSingleMedia(intent);
285
- return {
286
- contents: [
287
- {
288
- role: "user",
289
- parts: [
290
- { text: intent.prompt },
291
- {
292
- inlineData: {
293
- mimeType: mimeTypeFor(media),
294
- data: readMediaBase64(media),
295
- },
296
- },
297
- ],
298
- },
299
- ],
300
- };
301
- }
302
-
303
- function imageOutputBody(intent: CapabilityIntent): GeminiRequestBody {
304
- return {
305
- contents: [userTextContent(intent.prompt)],
306
- generationConfig: {
307
- responseModalities: ["TEXT", "IMAGE"],
308
- },
309
- };
310
- }
311
-
312
- function codeExecutionBody(intent: CapabilityIntent): GeminiRequestBody {
313
- return {
314
- contents: [userTextContent(intent.prompt)],
315
- tools: [{ codeExecution: {} }],
316
- };
317
- }
318
-
319
- function groundingBody(intent: CapabilityIntent): GeminiRequestBody {
320
- return {
321
- contents: [userTextContent(intent.prompt)],
322
- tools: [{ googleSearch: {} }],
323
- };
324
- }
325
-
326
- // Shape-identical between streaming and non-streaming variants — only the
327
- // endpoint differs (handled by buildEndpointURL). The streaming variant
328
- // deliberately does NOT clamp `thinkingBudget: 0` the way plain-text
329
- // streaming does: the safety classifier's engagement may depend on
330
- // whether the model goes through a thinking phase, and the probe's job
331
- // is to observe natural classifier behavior at default generation
332
- // settings, not constrained ones.
333
- function safetyClassificationBody(intent: CapabilityIntent): GeminiRequestBody {
334
- return {
335
- contents: [userTextContent(intent.prompt)],
336
- };
337
- }
338
-
339
- function structuredOutputBody(intent: CapabilityIntent): GeminiRequestBody {
340
- const format = intent.responseFormat;
341
- if (format === undefined) {
342
- throw new Error(
343
- "google-genai: structured-output intent has no responseFormat",
344
- );
345
- }
346
- const generationConfig: GeminiGenerationConfig = {};
347
- switch (format.kind) {
348
- case "text":
349
- // Free-form text is Gemini's default; emit no responseMimeType.
350
- break;
351
- case "json":
352
- generationConfig.responseMimeType = "application/json";
353
- break;
354
- case "json-schema":
355
- generationConfig.responseMimeType = "application/json";
356
- generationConfig.responseSchema = format.schema;
357
- break;
358
- }
359
- const body: GeminiRequestBody = {
360
- contents: [userTextContent(intent.prompt)],
361
- };
362
- if (Object.keys(generationConfig).length > 0) {
363
- body.generationConfig = generationConfig;
364
- }
365
- return body;
366
- }
367
-
368
- export function buildRequestBody(opts: {
369
- model: string;
370
- capability: Capability;
371
- intent: CapabilityIntent;
372
- }): GeminiRequestBody {
373
- modelSupportsCapability(opts.model, opts.capability);
374
-
375
- switch (opts.capability) {
376
- case "plain-text":
377
- return plainTextBody(opts.intent);
378
- case "plain-text-streaming":
379
- return plainTextStreamingBody(opts.intent);
380
- case "function-calling-multi-turn":
381
- case "function-calling-multi-turn-streaming":
382
- return functionCallingBody(opts.intent, {
383
- budget: 0,
384
- includeThoughts: false,
385
- });
386
- case "function-calling-with-thinking":
387
- case "function-calling-with-thinking-streaming":
388
- return functionCallingBody(opts.intent, {
389
- budget: 1024,
390
- includeThoughts: true,
391
- });
392
- case "vision-input":
393
- case "vision-input-streaming":
394
- case "audio-input":
395
- case "audio-input-streaming":
396
- case "video-input":
397
- case "video-input-streaming":
398
- case "document-input":
399
- case "document-input-streaming":
400
- return inlineMediaBody(opts.intent);
401
- case "image-output":
402
- case "image-output-streaming":
403
- return imageOutputBody(opts.intent);
404
- case "code-execution":
405
- case "code-execution-streaming":
406
- return codeExecutionBody(opts.intent);
407
- case "grounding":
408
- case "grounding-streaming":
409
- return groundingBody(opts.intent);
410
- case "safety-classification":
411
- case "safety-classification-streaming":
412
- return safetyClassificationBody(opts.intent);
413
- case "structured-output":
414
- case "structured-output-streaming":
415
- return structuredOutputBody(opts.intent);
416
- case "files-api-reference":
417
- case "files-api-reference-streaming":
418
- throw new Error(
419
- `google-genai: capability ${opts.capability} is multi-step; use iterateCaptureSteps from the plug-in, not buildRequestBody.`,
420
- );
421
- case "function-calling":
422
- case "reasoning-content":
423
- case "reasoning-content-streaming":
424
- case "redacted-thinking":
425
- case "redacted-thinking-streaming":
426
- throw new Error(
427
- `google-genai: capability ${opts.capability} is not supported by any google-genai model`,
428
- );
429
- default: {
430
- const exhaustive: never = opts.capability;
431
- throw new Error(
432
- `google-genai: unhandled capability ${String(exhaustive)}`,
433
- );
434
- }
435
- }
436
- }
package/tsconfig.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "include": ["src/**/*.ts"]
4
- }