@intx/inference-discovery-google-genai 0.1.2 → 0.2.2
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/LICENSE +176 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +7 -0
- package/dist/endpoint.d.ts +7 -0
- package/dist/endpoint.js +13 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +245 -0
- package/dist/request-body.d.ts +65 -0
- package/dist/request-body.js +297 -0
- package/package.json +14 -4
- package/src/auth.ts +0 -8
- package/src/endpoint.ts +0 -20
- package/src/index.ts +0 -327
- package/src/plugin.test.ts +0 -723
- package/src/request-body.ts +0 -436
- package/tsconfig.json +0 -4
- package/tsconfig.tsbuildinfo +0 -1
package/src/plugin.test.ts
DELETED
|
@@ -1,723 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
6
|
-
INTENTS,
|
|
7
|
-
SUPPORT_MATRIX,
|
|
8
|
-
getFixtureDir,
|
|
9
|
-
type Capability,
|
|
10
|
-
type ToolDecl,
|
|
11
|
-
} from "@intx/inference-discovery/catalog";
|
|
12
|
-
import type { CaptureStep, CapturedResponse } from "@intx/inference-discovery";
|
|
13
|
-
import { createGoogleGenaiPlugin, iterateCaptureSteps } from "./index";
|
|
14
|
-
import { buildRequestBody } from "./request-body";
|
|
15
|
-
import { buildEndpointURL, isStreamingCapability } from "./endpoint";
|
|
16
|
-
|
|
17
|
-
const TEST_API_KEY = "test-key";
|
|
18
|
-
const REPO_ROOT = fileURLToPath(new URL("../../../", import.meta.url));
|
|
19
|
-
|
|
20
|
-
const MULTI_TURN_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([
|
|
21
|
-
"function-calling-multi-turn",
|
|
22
|
-
"function-calling-multi-turn-streaming",
|
|
23
|
-
"function-calling-with-thinking",
|
|
24
|
-
"function-calling-with-thinking-streaming",
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
const FILES_API_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([
|
|
28
|
-
"files-api-reference",
|
|
29
|
-
"files-api-reference-streaming",
|
|
30
|
-
]);
|
|
31
|
-
|
|
32
|
-
function readFixtureJSON(fixtureDir: string, ...parts: string[]): unknown {
|
|
33
|
-
const raw = readFileSync(join(REPO_ROOT, fixtureDir, ...parts), "utf8");
|
|
34
|
-
return JSON.parse(raw);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function readFixtureBytes(fixtureDir: string, ...parts: string[]): Uint8Array {
|
|
38
|
-
const buf = readFileSync(join(REPO_ROOT, fixtureDir, ...parts));
|
|
39
|
-
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function fixtureFileExists(fixtureDir: string, ...parts: string[]): boolean {
|
|
43
|
-
try {
|
|
44
|
-
readFileSync(join(REPO_ROOT, fixtureDir, ...parts));
|
|
45
|
-
return true;
|
|
46
|
-
} catch {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function loadTurn1Response(fixtureDir: string): unknown {
|
|
52
|
-
if (fixtureFileExists(fixtureDir, "turn-1", "response.json")) {
|
|
53
|
-
return readFixtureJSON(fixtureDir, "turn-1", "response.json");
|
|
54
|
-
}
|
|
55
|
-
const turn2Body = readFixtureJSON(fixtureDir, "turn-2", "request.json");
|
|
56
|
-
if (!isPlainObject(turn2Body)) {
|
|
57
|
-
throw new Error(`turn-2 request body is not an object in ${fixtureDir}`);
|
|
58
|
-
}
|
|
59
|
-
const contents = turn2Body.contents;
|
|
60
|
-
if (!Array.isArray(contents) || contents.length < 2) {
|
|
61
|
-
throw new Error(
|
|
62
|
-
`turn-2 request body has no model-role assistant content in ${fixtureDir}`,
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
const assistantContent = contents[1];
|
|
66
|
-
return { candidates: [{ content: assistantContent }] };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
70
|
-
return (
|
|
71
|
-
typeof value === "object" &&
|
|
72
|
-
value !== null &&
|
|
73
|
-
!Array.isArray(value) &&
|
|
74
|
-
Object.getPrototypeOf(value) === Object.prototype
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function normalizeForStructuralComparison(value: unknown): unknown {
|
|
79
|
-
if (Array.isArray(value)) {
|
|
80
|
-
return value.map(normalizeForStructuralComparison);
|
|
81
|
-
}
|
|
82
|
-
if (!isPlainObject(value)) {
|
|
83
|
-
return value;
|
|
84
|
-
}
|
|
85
|
-
const out: Record<string, unknown> = {};
|
|
86
|
-
for (const [key, child] of Object.entries(value)) {
|
|
87
|
-
if (key === "text" && typeof child === "string") {
|
|
88
|
-
out[key] = "<text>";
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (key === "data" && typeof child === "string") {
|
|
92
|
-
out[key] = "<base64>";
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (key === "fileUri" && typeof child === "string") {
|
|
96
|
-
out[key] = "<file-uri>";
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (key === "name" && typeof child === "string") {
|
|
100
|
-
out[key] = "<name>";
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (key === "description" && typeof child === "string") {
|
|
104
|
-
out[key] = "<description>";
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (key === "allowedFunctionNames" && Array.isArray(child)) {
|
|
108
|
-
out[key] = child.map(() => "<name>");
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (key === "thoughtSignature" && typeof child === "string") {
|
|
112
|
-
out[key] = "<thought-signature>";
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (key === "response" && isPlainObject(child)) {
|
|
116
|
-
out[key] = "<response>";
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (key === "assetPath" && typeof child === "string") {
|
|
120
|
-
out[key] = "<asset-path>";
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (key === "contentLength" && typeof child === "number") {
|
|
124
|
-
out[key] = "<content-length>";
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if (key === "displayName" && typeof child === "string") {
|
|
128
|
-
out[key] = "<display-name>";
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
out[key] = normalizeForStructuralComparison(child);
|
|
132
|
-
}
|
|
133
|
-
return out;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function googleGenaiCapturedEntries() {
|
|
137
|
-
return SUPPORT_MATRIX.filter(
|
|
138
|
-
(entry) =>
|
|
139
|
-
entry.provider === "google-genai" && entry.outcome === "captured",
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function firstToolFor(capability: Capability): ToolDecl {
|
|
144
|
-
const tools = INTENTS[capability].tools;
|
|
145
|
-
if (tools === undefined || tools.length === 0) {
|
|
146
|
-
throw new Error(`INTENTS[${capability}] has no tools`);
|
|
147
|
-
}
|
|
148
|
-
const [tool] = tools;
|
|
149
|
-
if (tool === undefined) {
|
|
150
|
-
throw new Error(`INTENTS[${capability}] tools[0] is undefined`);
|
|
151
|
-
}
|
|
152
|
-
return tool;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function collectSteps(opts: {
|
|
156
|
-
model: string;
|
|
157
|
-
capability: Capability;
|
|
158
|
-
responses: readonly CapturedResponse[];
|
|
159
|
-
}): CaptureStep[] {
|
|
160
|
-
const intent = INTENTS[opts.capability];
|
|
161
|
-
const iter = iterateCaptureSteps({
|
|
162
|
-
model: opts.model,
|
|
163
|
-
capability: opts.capability,
|
|
164
|
-
intent,
|
|
165
|
-
});
|
|
166
|
-
const steps: CaptureStep[] = [];
|
|
167
|
-
let i = 0;
|
|
168
|
-
let next = iter.next();
|
|
169
|
-
while (!next.done) {
|
|
170
|
-
steps.push(next.value);
|
|
171
|
-
const response = opts.responses[i];
|
|
172
|
-
i += 1;
|
|
173
|
-
if (response === undefined) {
|
|
174
|
-
// No more responses available; stop pumping the iterator. This is
|
|
175
|
-
// expected for the final step.
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
next = iter.next(response);
|
|
179
|
-
}
|
|
180
|
-
return steps;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
describe("createGoogleGenaiPlugin", () => {
|
|
184
|
-
test("declares provider name, models, and redaction lists", () => {
|
|
185
|
-
const plugin = createGoogleGenaiPlugin({ apiKey: TEST_API_KEY });
|
|
186
|
-
expect(plugin.name).toBe("google-genai");
|
|
187
|
-
expect(plugin.models).toEqual([
|
|
188
|
-
"gemini-2.5-flash",
|
|
189
|
-
"gemini-2.5-flash-image",
|
|
190
|
-
]);
|
|
191
|
-
expect(plugin.redactRequestHeaders).toEqual(["x-goog-api-key"]);
|
|
192
|
-
expect(plugin.redactResponseHeaders).toEqual([]);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
test("buildAuthHeaders returns x-goog-api-key", () => {
|
|
196
|
-
const plugin = createGoogleGenaiPlugin({ apiKey: TEST_API_KEY });
|
|
197
|
-
expect(plugin.buildAuthHeaders()).toEqual({
|
|
198
|
-
"x-goog-api-key": TEST_API_KEY,
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
test("buildAuthHeaders rejects an empty apiKey", () => {
|
|
203
|
-
const plugin = createGoogleGenaiPlugin({ apiKey: "" });
|
|
204
|
-
expect(() => plugin.buildAuthHeaders()).toThrow(/apiKey/);
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
describe("buildEndpointURL", () => {
|
|
209
|
-
test("returns generateContent for non-streaming capabilities", () => {
|
|
210
|
-
expect(
|
|
211
|
-
buildEndpointURL({ model: "gemini-2.5-flash", capability: "plain-text" }),
|
|
212
|
-
).toBe(
|
|
213
|
-
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
|
|
214
|
-
);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
test("returns streamGenerateContent?alt=sse for streaming capabilities", () => {
|
|
218
|
-
expect(
|
|
219
|
-
buildEndpointURL({
|
|
220
|
-
model: "gemini-2.5-flash",
|
|
221
|
-
capability: "plain-text-streaming",
|
|
222
|
-
}),
|
|
223
|
-
).toBe(
|
|
224
|
-
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse",
|
|
225
|
-
);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
test("uses the image model for image-output", () => {
|
|
229
|
-
expect(
|
|
230
|
-
buildEndpointURL({
|
|
231
|
-
model: "gemini-2.5-flash-image",
|
|
232
|
-
capability: "image-output",
|
|
233
|
-
}),
|
|
234
|
-
).toBe(
|
|
235
|
-
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent",
|
|
236
|
-
);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
test("isStreamingCapability matches every -streaming suffix", () => {
|
|
240
|
-
expect(isStreamingCapability("plain-text-streaming")).toBe(true);
|
|
241
|
-
expect(isStreamingCapability("plain-text")).toBe(false);
|
|
242
|
-
expect(isStreamingCapability("image-output-streaming")).toBe(true);
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
describe("buildRequestBody — unsupported pairs", () => {
|
|
247
|
-
test("throws when capability is not in the model's matrix (text model + image-output)", () => {
|
|
248
|
-
expect(() =>
|
|
249
|
-
buildRequestBody({
|
|
250
|
-
model: "gemini-2.5-flash",
|
|
251
|
-
capability: "image-output",
|
|
252
|
-
intent: INTENTS["image-output"],
|
|
253
|
-
}),
|
|
254
|
-
).toThrow(/does not support capability/);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
test("throws when capability is not in the model's matrix (image model + plain-text)", () => {
|
|
258
|
-
expect(() =>
|
|
259
|
-
buildRequestBody({
|
|
260
|
-
model: "gemini-2.5-flash-image",
|
|
261
|
-
capability: "plain-text",
|
|
262
|
-
intent: INTENTS["plain-text"],
|
|
263
|
-
}),
|
|
264
|
-
).toThrow(/does not support capability/);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
test("throws when capability is not in the model's matrix (image model + safety-classification)", () => {
|
|
268
|
-
expect(() =>
|
|
269
|
-
buildRequestBody({
|
|
270
|
-
model: "gemini-2.5-flash-image",
|
|
271
|
-
capability: "safety-classification",
|
|
272
|
-
intent: INTENTS["safety-classification"],
|
|
273
|
-
}),
|
|
274
|
-
).toThrow(/does not support capability/);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
test("throws for an unknown model", () => {
|
|
278
|
-
expect(() =>
|
|
279
|
-
buildRequestBody({
|
|
280
|
-
model: "gemini-99-unknown",
|
|
281
|
-
capability: "plain-text",
|
|
282
|
-
intent: INTENTS["plain-text"],
|
|
283
|
-
}),
|
|
284
|
-
).toThrow(/unknown model/);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
test("throws for capabilities no google-genai model exposes (e.g. function-calling)", () => {
|
|
288
|
-
expect(() =>
|
|
289
|
-
buildRequestBody({
|
|
290
|
-
model: "gemini-2.5-flash",
|
|
291
|
-
capability: "function-calling",
|
|
292
|
-
intent: INTENTS["function-calling"],
|
|
293
|
-
}),
|
|
294
|
-
).toThrow();
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
describe("buildRequestBody — wire-shape spot checks", () => {
|
|
299
|
-
test("plain-text produces a single user turn with the intent prompt", () => {
|
|
300
|
-
const body = buildRequestBody({
|
|
301
|
-
model: "gemini-2.5-flash",
|
|
302
|
-
capability: "plain-text",
|
|
303
|
-
intent: INTENTS["plain-text"],
|
|
304
|
-
});
|
|
305
|
-
expect(body).toEqual({
|
|
306
|
-
contents: [
|
|
307
|
-
{
|
|
308
|
-
role: "user",
|
|
309
|
-
parts: [{ text: INTENTS["plain-text"].prompt }],
|
|
310
|
-
},
|
|
311
|
-
],
|
|
312
|
-
});
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
test("plain-text-streaming adds maxOutputTokens=400 and thinkingBudget=0", () => {
|
|
316
|
-
const body = buildRequestBody({
|
|
317
|
-
model: "gemini-2.5-flash",
|
|
318
|
-
capability: "plain-text-streaming",
|
|
319
|
-
intent: INTENTS["plain-text-streaming"],
|
|
320
|
-
});
|
|
321
|
-
expect(body).toEqual({
|
|
322
|
-
contents: [
|
|
323
|
-
{
|
|
324
|
-
role: "user",
|
|
325
|
-
parts: [{ text: INTENTS["plain-text-streaming"].prompt }],
|
|
326
|
-
},
|
|
327
|
-
],
|
|
328
|
-
generationConfig: {
|
|
329
|
-
maxOutputTokens: 400,
|
|
330
|
-
thinkingConfig: { thinkingBudget: 0 },
|
|
331
|
-
},
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
test("function-calling-with-thinking sets thinkingBudget=1024, includeThoughts=true, mode=ANY", () => {
|
|
336
|
-
const capability: Capability = "function-calling-with-thinking";
|
|
337
|
-
const body = buildRequestBody({
|
|
338
|
-
model: "gemini-2.5-flash",
|
|
339
|
-
capability,
|
|
340
|
-
intent: INTENTS[capability],
|
|
341
|
-
});
|
|
342
|
-
const tool = firstToolFor(capability);
|
|
343
|
-
expect(body).toEqual({
|
|
344
|
-
contents: [
|
|
345
|
-
{
|
|
346
|
-
role: "user",
|
|
347
|
-
parts: [{ text: INTENTS[capability].prompt }],
|
|
348
|
-
},
|
|
349
|
-
],
|
|
350
|
-
tools: [
|
|
351
|
-
{
|
|
352
|
-
functionDeclarations: [
|
|
353
|
-
{
|
|
354
|
-
name: tool.name,
|
|
355
|
-
description: tool.description,
|
|
356
|
-
parameters: tool.parameters,
|
|
357
|
-
},
|
|
358
|
-
],
|
|
359
|
-
},
|
|
360
|
-
],
|
|
361
|
-
toolConfig: {
|
|
362
|
-
functionCallingConfig: {
|
|
363
|
-
mode: "ANY",
|
|
364
|
-
allowedFunctionNames: [tool.name],
|
|
365
|
-
},
|
|
366
|
-
},
|
|
367
|
-
generationConfig: {
|
|
368
|
-
thinkingConfig: { thinkingBudget: 1024, includeThoughts: true },
|
|
369
|
-
},
|
|
370
|
-
});
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
test("function-calling-multi-turn sets thinkingBudget=0 with no includeThoughts", () => {
|
|
374
|
-
const capability: Capability = "function-calling-multi-turn";
|
|
375
|
-
const body = buildRequestBody({
|
|
376
|
-
model: "gemini-2.5-flash",
|
|
377
|
-
capability,
|
|
378
|
-
intent: INTENTS[capability],
|
|
379
|
-
});
|
|
380
|
-
const tool = firstToolFor(capability);
|
|
381
|
-
expect(body).toEqual({
|
|
382
|
-
contents: [
|
|
383
|
-
{
|
|
384
|
-
role: "user",
|
|
385
|
-
parts: [{ text: INTENTS[capability].prompt }],
|
|
386
|
-
},
|
|
387
|
-
],
|
|
388
|
-
tools: [
|
|
389
|
-
{
|
|
390
|
-
functionDeclarations: [
|
|
391
|
-
{
|
|
392
|
-
name: tool.name,
|
|
393
|
-
description: tool.description,
|
|
394
|
-
parameters: tool.parameters,
|
|
395
|
-
},
|
|
396
|
-
],
|
|
397
|
-
},
|
|
398
|
-
],
|
|
399
|
-
toolConfig: {
|
|
400
|
-
functionCallingConfig: {
|
|
401
|
-
mode: "ANY",
|
|
402
|
-
allowedFunctionNames: [tool.name],
|
|
403
|
-
},
|
|
404
|
-
},
|
|
405
|
-
generationConfig: {
|
|
406
|
-
thinkingConfig: { thinkingBudget: 0 },
|
|
407
|
-
},
|
|
408
|
-
});
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
test("code-execution declares the codeExecution tool", () => {
|
|
412
|
-
const body = buildRequestBody({
|
|
413
|
-
model: "gemini-2.5-flash",
|
|
414
|
-
capability: "code-execution",
|
|
415
|
-
intent: INTENTS["code-execution"],
|
|
416
|
-
});
|
|
417
|
-
expect(body).toEqual({
|
|
418
|
-
contents: [
|
|
419
|
-
{
|
|
420
|
-
role: "user",
|
|
421
|
-
parts: [{ text: INTENTS["code-execution"].prompt }],
|
|
422
|
-
},
|
|
423
|
-
],
|
|
424
|
-
tools: [{ codeExecution: {} }],
|
|
425
|
-
});
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
test("safety-classification produces an unconstrained single user turn (no generationConfig)", () => {
|
|
429
|
-
const body = buildRequestBody({
|
|
430
|
-
model: "gemini-2.5-flash",
|
|
431
|
-
capability: "safety-classification",
|
|
432
|
-
intent: INTENTS["safety-classification"],
|
|
433
|
-
});
|
|
434
|
-
expect(body).toEqual({
|
|
435
|
-
contents: [
|
|
436
|
-
{
|
|
437
|
-
role: "user",
|
|
438
|
-
parts: [{ text: INTENTS["safety-classification"].prompt }],
|
|
439
|
-
},
|
|
440
|
-
],
|
|
441
|
-
});
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
test("safety-classification-streaming produces the same body as non-streaming (endpoint differs, not body)", () => {
|
|
445
|
-
const nonStreaming = buildRequestBody({
|
|
446
|
-
model: "gemini-2.5-flash",
|
|
447
|
-
capability: "safety-classification",
|
|
448
|
-
intent: INTENTS["safety-classification"],
|
|
449
|
-
});
|
|
450
|
-
const streaming = buildRequestBody({
|
|
451
|
-
model: "gemini-2.5-flash",
|
|
452
|
-
capability: "safety-classification-streaming",
|
|
453
|
-
intent: INTENTS["safety-classification-streaming"],
|
|
454
|
-
});
|
|
455
|
-
expect(streaming).toEqual(nonStreaming);
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
test("grounding declares the googleSearch tool", () => {
|
|
459
|
-
const body = buildRequestBody({
|
|
460
|
-
model: "gemini-2.5-flash",
|
|
461
|
-
capability: "grounding",
|
|
462
|
-
intent: INTENTS["grounding"],
|
|
463
|
-
});
|
|
464
|
-
expect(body).toEqual({
|
|
465
|
-
contents: [
|
|
466
|
-
{
|
|
467
|
-
role: "user",
|
|
468
|
-
parts: [{ text: INTENTS["grounding"].prompt }],
|
|
469
|
-
},
|
|
470
|
-
],
|
|
471
|
-
tools: [{ googleSearch: {} }],
|
|
472
|
-
});
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
test("image-output sets responseModalities to [TEXT, IMAGE]", () => {
|
|
476
|
-
const body = buildRequestBody({
|
|
477
|
-
model: "gemini-2.5-flash-image",
|
|
478
|
-
capability: "image-output",
|
|
479
|
-
intent: INTENTS["image-output"],
|
|
480
|
-
});
|
|
481
|
-
expect(body).toEqual({
|
|
482
|
-
contents: [
|
|
483
|
-
{
|
|
484
|
-
role: "user",
|
|
485
|
-
parts: [{ text: INTENTS["image-output"].prompt }],
|
|
486
|
-
},
|
|
487
|
-
],
|
|
488
|
-
generationConfig: {
|
|
489
|
-
responseModalities: ["TEXT", "IMAGE"],
|
|
490
|
-
},
|
|
491
|
-
});
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
test("vision-input embeds a base64 inlineData part with image/jpeg", () => {
|
|
495
|
-
const body = buildRequestBody({
|
|
496
|
-
model: "gemini-2.5-flash",
|
|
497
|
-
capability: "vision-input",
|
|
498
|
-
intent: INTENTS["vision-input"],
|
|
499
|
-
});
|
|
500
|
-
const normalized = normalizeForStructuralComparison(body);
|
|
501
|
-
expect(normalized).toEqual({
|
|
502
|
-
contents: [
|
|
503
|
-
{
|
|
504
|
-
role: "user",
|
|
505
|
-
parts: [
|
|
506
|
-
{ text: "<text>" },
|
|
507
|
-
{ inlineData: { mimeType: "image/jpeg", data: "<base64>" } },
|
|
508
|
-
],
|
|
509
|
-
},
|
|
510
|
-
],
|
|
511
|
-
});
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
test("buildRequestBody throws for files-api-reference (multi-step only via iterateCaptureSteps)", () => {
|
|
515
|
-
expect(() =>
|
|
516
|
-
buildRequestBody({
|
|
517
|
-
model: "gemini-2.5-flash",
|
|
518
|
-
capability: "files-api-reference",
|
|
519
|
-
intent: INTENTS["files-api-reference"],
|
|
520
|
-
}),
|
|
521
|
-
).toThrow(/multi-step/);
|
|
522
|
-
expect(() =>
|
|
523
|
-
buildRequestBody({
|
|
524
|
-
model: "gemini-2.5-flash",
|
|
525
|
-
capability: "files-api-reference-streaming",
|
|
526
|
-
intent: INTENTS["files-api-reference-streaming"],
|
|
527
|
-
}),
|
|
528
|
-
).toThrow(/multi-step/);
|
|
529
|
-
});
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
describe("fixture-oracle: iterateCaptureSteps structurally matches every captured step's wire", () => {
|
|
533
|
-
for (const entry of googleGenaiCapturedEntries()) {
|
|
534
|
-
test(`${entry.model} / ${entry.capability}`, () => {
|
|
535
|
-
const fixtureDir = getFixtureDir(entry);
|
|
536
|
-
if (fixtureDir === null) {
|
|
537
|
-
throw new Error(
|
|
538
|
-
`entry ${entry.model}/${entry.capability} has no captured fixture directory`,
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const isMultiTurn = MULTI_TURN_CAPABILITIES.has(entry.capability);
|
|
543
|
-
const isFilesApi = FILES_API_CAPABILITIES.has(entry.capability);
|
|
544
|
-
|
|
545
|
-
if (isMultiTurn) {
|
|
546
|
-
const turn1ParsedResponse = loadTurn1Response(fixtureDir);
|
|
547
|
-
const turn1Response: CapturedResponse = {
|
|
548
|
-
status: 200,
|
|
549
|
-
headers: {},
|
|
550
|
-
parsed: turn1ParsedResponse,
|
|
551
|
-
bytes: null,
|
|
552
|
-
};
|
|
553
|
-
const steps = collectSteps({
|
|
554
|
-
model: entry.model,
|
|
555
|
-
capability: entry.capability,
|
|
556
|
-
responses: [turn1Response],
|
|
557
|
-
});
|
|
558
|
-
expect(steps.length).toBe(2);
|
|
559
|
-
const [step1, step2] = steps;
|
|
560
|
-
if (step1 === undefined || step2 === undefined) {
|
|
561
|
-
throw new Error("expected exactly two steps for multi-turn");
|
|
562
|
-
}
|
|
563
|
-
expect(step1.subdir).toBe("turn-1");
|
|
564
|
-
expect(step2.subdir).toBe("turn-2");
|
|
565
|
-
|
|
566
|
-
const expectedURL = buildEndpointURL({
|
|
567
|
-
model: entry.model,
|
|
568
|
-
capability: entry.capability,
|
|
569
|
-
});
|
|
570
|
-
expect(step1.url).toBe(expectedURL);
|
|
571
|
-
expect(step2.url).toBe(expectedURL);
|
|
572
|
-
|
|
573
|
-
const captured1 = readFixtureJSON(fixtureDir, "turn-1", "request.json");
|
|
574
|
-
const captured2 = readFixtureJSON(fixtureDir, "turn-2", "request.json");
|
|
575
|
-
expect(normalizeForStructuralComparison(step1.body)).toEqual(
|
|
576
|
-
normalizeForStructuralComparison(captured1),
|
|
577
|
-
);
|
|
578
|
-
expect(normalizeForStructuralComparison(step2.body)).toEqual(
|
|
579
|
-
normalizeForStructuralComparison(captured2),
|
|
580
|
-
);
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
if (isFilesApi) {
|
|
585
|
-
const uploadResponse: CapturedResponse = {
|
|
586
|
-
status: 200,
|
|
587
|
-
headers: {},
|
|
588
|
-
parsed: readFixtureJSON(fixtureDir, "upload", "response.json"),
|
|
589
|
-
bytes: null,
|
|
590
|
-
};
|
|
591
|
-
const steps = collectSteps({
|
|
592
|
-
model: entry.model,
|
|
593
|
-
capability: entry.capability,
|
|
594
|
-
responses: [uploadResponse],
|
|
595
|
-
});
|
|
596
|
-
expect(steps.length).toBe(2);
|
|
597
|
-
const [uploadStep, generateStep] = steps;
|
|
598
|
-
if (uploadStep === undefined || generateStep === undefined) {
|
|
599
|
-
throw new Error("expected exactly two steps for files-api");
|
|
600
|
-
}
|
|
601
|
-
expect(uploadStep.subdir).toBe("upload");
|
|
602
|
-
expect(generateStep.subdir).toBe("generate");
|
|
603
|
-
expect(uploadStep.url).toBe(
|
|
604
|
-
"https://generativelanguage.googleapis.com/upload/v1beta/files",
|
|
605
|
-
);
|
|
606
|
-
expect(generateStep.url).toBe(
|
|
607
|
-
buildEndpointURL({
|
|
608
|
-
model: entry.model,
|
|
609
|
-
capability: entry.capability,
|
|
610
|
-
}),
|
|
611
|
-
);
|
|
612
|
-
|
|
613
|
-
if (uploadStep.kind !== "raw") {
|
|
614
|
-
throw new Error("expected files-api upload step to be raw-bytes");
|
|
615
|
-
}
|
|
616
|
-
const capturedUploadBytes = readFixtureBytes(
|
|
617
|
-
fixtureDir,
|
|
618
|
-
"upload",
|
|
619
|
-
"request.bin",
|
|
620
|
-
);
|
|
621
|
-
expect(uploadStep.contentType).toBe("application/pdf");
|
|
622
|
-
expect(Array.from(uploadStep.body)).toEqual(
|
|
623
|
-
Array.from(capturedUploadBytes),
|
|
624
|
-
);
|
|
625
|
-
if (generateStep.kind !== "json") {
|
|
626
|
-
throw new Error("expected files-api generate step to be JSON");
|
|
627
|
-
}
|
|
628
|
-
const capturedGenerate = readFixtureJSON(
|
|
629
|
-
fixtureDir,
|
|
630
|
-
"generate",
|
|
631
|
-
"request.json",
|
|
632
|
-
);
|
|
633
|
-
expect(normalizeForStructuralComparison(generateStep.body)).toEqual(
|
|
634
|
-
normalizeForStructuralComparison(capturedGenerate),
|
|
635
|
-
);
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const steps = collectSteps({
|
|
640
|
-
model: entry.model,
|
|
641
|
-
capability: entry.capability,
|
|
642
|
-
responses: [],
|
|
643
|
-
});
|
|
644
|
-
expect(steps.length).toBe(1);
|
|
645
|
-
const [only] = steps;
|
|
646
|
-
if (only === undefined) {
|
|
647
|
-
throw new Error("expected exactly one step for single-step capability");
|
|
648
|
-
}
|
|
649
|
-
expect(only.subdir).toBeNull();
|
|
650
|
-
expect(only.url).toBe(
|
|
651
|
-
buildEndpointURL({
|
|
652
|
-
model: entry.model,
|
|
653
|
-
capability: entry.capability,
|
|
654
|
-
}),
|
|
655
|
-
);
|
|
656
|
-
const captured = readFixtureJSON(fixtureDir, "request.json");
|
|
657
|
-
expect(normalizeForStructuralComparison(only.body)).toEqual(
|
|
658
|
-
normalizeForStructuralComparison(captured),
|
|
659
|
-
);
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
describe("createGoogleGenaiPlugin — end-to-end through stub fetch", () => {
|
|
665
|
-
test("a stubbed POST round-trips through the plugin", async () => {
|
|
666
|
-
const plugin = createGoogleGenaiPlugin({ apiKey: TEST_API_KEY });
|
|
667
|
-
const iter = plugin.iterateCaptureSteps({
|
|
668
|
-
model: "gemini-2.5-flash",
|
|
669
|
-
capability: "plain-text",
|
|
670
|
-
intent: INTENTS["plain-text"],
|
|
671
|
-
});
|
|
672
|
-
const first = iter.next();
|
|
673
|
-
if (first.done) throw new Error("expected one step");
|
|
674
|
-
const step = first.value;
|
|
675
|
-
const headers = {
|
|
676
|
-
"Content-Type": "application/json",
|
|
677
|
-
...plugin.buildAuthHeaders(),
|
|
678
|
-
};
|
|
679
|
-
|
|
680
|
-
let observedURL = "";
|
|
681
|
-
let observedHeaders: Record<string, string> = {};
|
|
682
|
-
let observedBody = "";
|
|
683
|
-
const stubFetch = (
|
|
684
|
-
input: string,
|
|
685
|
-
init: {
|
|
686
|
-
method: string;
|
|
687
|
-
headers: Record<string, string>;
|
|
688
|
-
body: string;
|
|
689
|
-
},
|
|
690
|
-
): Promise<Response> => {
|
|
691
|
-
observedURL = input;
|
|
692
|
-
observedHeaders = init.headers;
|
|
693
|
-
observedBody = init.body;
|
|
694
|
-
return Promise.resolve(
|
|
695
|
-
new Response(JSON.stringify({ ok: true }), {
|
|
696
|
-
status: 200,
|
|
697
|
-
headers: { "Content-Type": "application/json" },
|
|
698
|
-
}),
|
|
699
|
-
);
|
|
700
|
-
};
|
|
701
|
-
|
|
702
|
-
const response = await stubFetch(step.url, {
|
|
703
|
-
method: "POST",
|
|
704
|
-
headers,
|
|
705
|
-
body: JSON.stringify(step.body),
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
expect(observedURL).toBe(
|
|
709
|
-
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
|
|
710
|
-
);
|
|
711
|
-
expect(observedHeaders["x-goog-api-key"]).toBe(TEST_API_KEY);
|
|
712
|
-
expect(observedHeaders["Content-Type"]).toBe("application/json");
|
|
713
|
-
expect(JSON.parse(observedBody)).toEqual({
|
|
714
|
-
contents: [
|
|
715
|
-
{
|
|
716
|
-
role: "user",
|
|
717
|
-
parts: [{ text: INTENTS["plain-text"].prompt }],
|
|
718
|
-
},
|
|
719
|
-
],
|
|
720
|
-
});
|
|
721
|
-
expect(response.status).toBe(200);
|
|
722
|
-
});
|
|
723
|
-
});
|