@intx/inference-discovery-google-genai 0.2.2 → 0.4.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/README.md +13 -7
- package/dist/index.d.ts +8 -2
- package/dist/index.js +24 -12
- package/dist/request-body.d.ts +5 -0
- package/dist/request-body.js +62 -22
- package/dist/sse.d.ts +1 -0
- package/dist/sse.js +109 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -9,12 +9,17 @@ for the runtime, the plug-in contract, and the `discover` CLI.
|
|
|
9
9
|
|
|
10
10
|
## Models
|
|
11
11
|
|
|
12
|
-
- `gemini-2.5-flash` — text
|
|
13
|
-
|
|
14
|
-
grounding,
|
|
15
|
-
variants of each.
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
- `gemini-2.5-flash` / `gemini-3.5-flash` — text multimodal surface
|
|
13
|
+
(vision, audio, video, document, function calling, code execution,
|
|
14
|
+
grounding, safety classification, structured output, files API).
|
|
15
|
+
Streaming and non-streaming variants of each. These models accept
|
|
16
|
+
a zero thinking budget when the probe wants thinking suppressed.
|
|
17
|
+
- `gemini-3.6-flash` / `gemini-2.5-pro` — same text multimodal
|
|
18
|
+
surface, but they reject a zero thinking budget, so the request
|
|
19
|
+
builder uses the dynamic thinking budget (`-1`) on the suppress
|
|
20
|
+
path.
|
|
21
|
+
- `gemini-2.5-flash-image` / `gemini-3.1-flash-image` — image
|
|
22
|
+
output, streaming and non-streaming.
|
|
18
23
|
|
|
19
24
|
The full per-capability list is in `SUPPORT_MATRIX` in
|
|
20
25
|
`@intx/inference-discovery/catalog`.
|
|
@@ -57,7 +62,8 @@ runner writes the bundle:
|
|
|
57
62
|
`function-calling-with-thinking`,
|
|
58
63
|
`function-calling-with-thinking-streaming`) — turn 1 is sent as
|
|
59
64
|
usual; the plug-in extracts the model's assistant content from
|
|
60
|
-
the parsed response
|
|
65
|
+
the parsed response (reconstructing it from the SSE stream when
|
|
66
|
+
turn-1 is streamed), derives a tool follow-up from the intent's
|
|
61
67
|
`followUp` (or synthesises one from the intent's tools), and
|
|
62
68
|
sends a turn-2 body that echoes the assistant turn verbatim and
|
|
63
69
|
appends the tool response. Each turn writes its own `turn-1/` and
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CaptureStep, type CapturedResponse, type IterateCaptureStepsOpts, type ProviderPlugin } from "@intx/inference-discovery";
|
|
2
|
+
import { type GeminiModelClass } from "./request-body.js";
|
|
3
|
+
export type { GeminiModelClass } from "./request-body.js";
|
|
4
|
+
export { isKnownModel } from "./request-body.js";
|
|
2
5
|
export interface GoogleGenaiPluginOptions {
|
|
3
6
|
apiKey: string;
|
|
7
|
+
modelClass?: GeminiModelClass | undefined;
|
|
4
8
|
}
|
|
5
|
-
export declare function iterateCaptureSteps(opts: IterateCaptureStepsOpts
|
|
9
|
+
export declare function iterateCaptureSteps(opts: IterateCaptureStepsOpts & {
|
|
10
|
+
modelClass?: GeminiModelClass | undefined;
|
|
11
|
+
}): Generator<CaptureStep, void, CapturedResponse>;
|
|
6
12
|
export declare function createGoogleGenaiPlugin(opts: GoogleGenaiPluginOptions): ProviderPlugin;
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { resolveMediaPath, } from "@intx/inference-discovery/catalog";
|
|
3
|
+
import { resolveTurn1Response, } from "@intx/inference-discovery";
|
|
3
4
|
import { buildAuthHeaders } from "./auth.js";
|
|
4
5
|
import { buildEndpointURL } from "./endpoint.js";
|
|
5
6
|
import { buildRequestBody } from "./request-body.js";
|
|
7
|
+
import { reconstructResponseFromSSE } from "./sse.js";
|
|
8
|
+
export { isKnownModel } from "./request-body.js";
|
|
6
9
|
const PROVIDER_NAME = "google-genai";
|
|
7
|
-
const MODELS = [
|
|
10
|
+
const MODELS = [
|
|
11
|
+
"gemini-2.5-flash",
|
|
12
|
+
"gemini-2.5-flash-image",
|
|
13
|
+
"gemini-2.5-pro",
|
|
14
|
+
"gemini-3.6-flash",
|
|
15
|
+
"gemini-3.5-flash",
|
|
16
|
+
"gemini-3.1-flash-image",
|
|
17
|
+
"gemini-3-flash-preview",
|
|
18
|
+
"gemini-3.1-pro-preview",
|
|
19
|
+
];
|
|
8
20
|
const REDACT_REQUEST_HEADERS = ["x-goog-api-key"];
|
|
9
21
|
const REDACT_RESPONSE_HEADERS = [];
|
|
10
22
|
const FILES_API_UPLOAD_URL = "https://generativelanguage.googleapis.com/upload/v1beta/files";
|
|
@@ -172,12 +184,11 @@ function buildMultiTurnTurn2Body(opts) {
|
|
|
172
184
|
return body;
|
|
173
185
|
}
|
|
174
186
|
export function* iterateCaptureSteps(opts) {
|
|
175
|
-
const { model, capability, intent } = opts;
|
|
187
|
+
const { model, capability, intent, modelClass } = opts;
|
|
176
188
|
if (FILES_API_CAPABILITIES.has(capability)) {
|
|
177
189
|
const upload = buildUploadDescriptor(intent);
|
|
178
190
|
const uploadResponse = yield {
|
|
179
191
|
kind: "raw",
|
|
180
|
-
subdir: "upload",
|
|
181
192
|
url: upload.url,
|
|
182
193
|
method: "POST",
|
|
183
194
|
contentType: upload.mimeType,
|
|
@@ -196,18 +207,21 @@ export function* iterateCaptureSteps(opts) {
|
|
|
196
207
|
});
|
|
197
208
|
yield {
|
|
198
209
|
kind: "json",
|
|
199
|
-
subdir: "generate",
|
|
200
210
|
url: buildEndpointURL({ model, capability }),
|
|
201
211
|
body: generateBody,
|
|
202
212
|
};
|
|
203
213
|
return;
|
|
204
214
|
}
|
|
205
215
|
if (MULTI_TURN_CAPABILITIES.has(capability)) {
|
|
206
|
-
const turn1Body = buildRequestBody({
|
|
216
|
+
const turn1Body = buildRequestBody({
|
|
217
|
+
model,
|
|
218
|
+
capability,
|
|
219
|
+
intent,
|
|
220
|
+
modelClass,
|
|
221
|
+
});
|
|
207
222
|
const url = buildEndpointURL({ model, capability });
|
|
208
223
|
const turn1Response = yield {
|
|
209
224
|
kind: "json",
|
|
210
|
-
subdir: "turn-1",
|
|
211
225
|
url,
|
|
212
226
|
body: turn1Body,
|
|
213
227
|
};
|
|
@@ -215,11 +229,10 @@ export function* iterateCaptureSteps(opts) {
|
|
|
215
229
|
capability,
|
|
216
230
|
intent,
|
|
217
231
|
turn1Body,
|
|
218
|
-
turn1Response: turn1Response
|
|
232
|
+
turn1Response: resolveTurn1Response(turn1Response, reconstructResponseFromSSE),
|
|
219
233
|
});
|
|
220
234
|
yield {
|
|
221
235
|
kind: "json",
|
|
222
|
-
subdir: "turn-2",
|
|
223
236
|
url,
|
|
224
237
|
body: turn2Body,
|
|
225
238
|
};
|
|
@@ -227,19 +240,18 @@ export function* iterateCaptureSteps(opts) {
|
|
|
227
240
|
}
|
|
228
241
|
yield {
|
|
229
242
|
kind: "json",
|
|
230
|
-
subdir: null,
|
|
231
243
|
url: buildEndpointURL({ model, capability }),
|
|
232
|
-
body: buildRequestBody({ model, capability, intent }),
|
|
244
|
+
body: buildRequestBody({ model, capability, intent, modelClass }),
|
|
233
245
|
};
|
|
234
246
|
}
|
|
235
247
|
export function createGoogleGenaiPlugin(opts) {
|
|
236
|
-
const apiKey = opts
|
|
248
|
+
const { apiKey, modelClass } = opts;
|
|
237
249
|
return {
|
|
238
250
|
name: PROVIDER_NAME,
|
|
239
251
|
models: MODELS,
|
|
240
252
|
redactRequestHeaders: REDACT_REQUEST_HEADERS,
|
|
241
253
|
redactResponseHeaders: REDACT_RESPONSE_HEADERS,
|
|
242
254
|
buildAuthHeaders: () => buildAuthHeaders(apiKey),
|
|
243
|
-
iterateCaptureSteps,
|
|
255
|
+
iterateCaptureSteps: (stepOpts) => iterateCaptureSteps({ ...stepOpts, modelClass }),
|
|
244
256
|
};
|
|
245
257
|
}
|
package/dist/request-body.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type Capability, type CapabilityIntent, type ToolDecl } from "@intx/inference-discovery/catalog";
|
|
2
|
+
export declare const TEXT_MODELS: ReadonlySet<string>;
|
|
3
|
+
export declare const IMAGE_MODELS: ReadonlySet<string>;
|
|
2
4
|
interface GeminiTextPart {
|
|
3
5
|
text: string;
|
|
4
6
|
}
|
|
@@ -57,9 +59,12 @@ interface GeminiRequestBody {
|
|
|
57
59
|
toolConfig?: GeminiToolConfig;
|
|
58
60
|
generationConfig?: GeminiGenerationConfig;
|
|
59
61
|
}
|
|
62
|
+
export type GeminiModelClass = "text" | "image";
|
|
63
|
+
export declare function isKnownModel(model: string): boolean;
|
|
60
64
|
export declare function buildRequestBody(opts: {
|
|
61
65
|
model: string;
|
|
62
66
|
capability: Capability;
|
|
63
67
|
intent: CapabilityIntent;
|
|
68
|
+
modelClass?: GeminiModelClass | undefined;
|
|
64
69
|
}): GeminiRequestBody;
|
|
65
70
|
export {};
|
package/dist/request-body.js
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { resolveMediaPath, } from "@intx/inference-discovery/catalog";
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { CapabilityNotBuildableError, resolveMediaPath, } from "@intx/inference-discovery/catalog";
|
|
3
|
+
// Text models share the full multimodal text capability surface; only
|
|
4
|
+
// thinking-budget handling differs for models that cannot disable thinking
|
|
5
|
+
// (see minimalThinkingBudget). Image models are output-only.
|
|
6
|
+
export const TEXT_MODELS = new Set([
|
|
7
|
+
"gemini-2.5-flash",
|
|
8
|
+
"gemini-2.5-pro",
|
|
9
|
+
"gemini-3.6-flash",
|
|
10
|
+
"gemini-3.5-flash",
|
|
11
|
+
"gemini-3-flash-preview",
|
|
12
|
+
"gemini-3.1-pro-preview",
|
|
13
|
+
]);
|
|
14
|
+
export const IMAGE_MODELS = new Set([
|
|
15
|
+
"gemini-2.5-flash-image",
|
|
16
|
+
"gemini-3.1-flash-image",
|
|
17
|
+
]);
|
|
5
18
|
const TEXT_MODEL_CAPABILITIES = new Set([
|
|
6
19
|
"plain-text",
|
|
7
20
|
"plain-text-streaming",
|
|
@@ -47,20 +60,26 @@ const EXTENSION_TO_MIME_TYPE = {
|
|
|
47
60
|
webm: "video/webm",
|
|
48
61
|
pdf: "application/pdf",
|
|
49
62
|
};
|
|
50
|
-
function
|
|
51
|
-
if (model
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
function classifyModel(model, override) {
|
|
64
|
+
if (TEXT_MODELS.has(model))
|
|
65
|
+
return "text";
|
|
66
|
+
if (IMAGE_MODELS.has(model))
|
|
67
|
+
return "image";
|
|
68
|
+
return override ?? "text";
|
|
69
|
+
}
|
|
70
|
+
// True when the model's class is known from set membership, so its request
|
|
71
|
+
// shape is not a guess. A caller (the probe) uses this to tell an operator when
|
|
72
|
+
// it is defaulting an unknown model to the text class rather than classifying a
|
|
73
|
+
// known one — the default is deliberate, but it should never be silent.
|
|
74
|
+
export function isKnownModel(model) {
|
|
75
|
+
return TEXT_MODELS.has(model) || IMAGE_MODELS.has(model);
|
|
76
|
+
}
|
|
77
|
+
function assertCapabilityBuildable(model, capability, modelClass) {
|
|
78
|
+
const resolved = classifyModel(model, modelClass);
|
|
79
|
+
const supported = resolved === "image" ? IMAGE_MODEL_CAPABILITIES : TEXT_MODEL_CAPABILITIES;
|
|
80
|
+
if (!supported.has(capability)) {
|
|
81
|
+
throw new CapabilityNotBuildableError(capability, `google-genai: ${resolved}-class model ${model} does not support capability ${capability}`);
|
|
62
82
|
}
|
|
63
|
-
throw new Error(`google-genai: unknown model ${model}`);
|
|
64
83
|
}
|
|
65
84
|
function extensionFor(path) {
|
|
66
85
|
const dot = path.lastIndexOf(".");
|
|
@@ -114,18 +133,38 @@ function userTextContent(prompt) {
|
|
|
114
133
|
parts: [{ text: prompt }],
|
|
115
134
|
};
|
|
116
135
|
}
|
|
136
|
+
// Gemini's "dynamic" thinking budget sentinel: the model decides how much to
|
|
137
|
+
// think and no cap is imposed.
|
|
138
|
+
const DYNAMIC_THINKING_BUDGET = -1;
|
|
139
|
+
// gemini-2.5-pro rejects thinkingConfig.thinkingBudget: 0 with HTTP 400
|
|
140
|
+
// "Budget 0 is invalid. This model only works in thinking mode." The budget can
|
|
141
|
+
// only be chosen by model identity, because the API surfaces the constraint
|
|
142
|
+
// solely as a runtime 400 with no build-time signal. Add a model here when its
|
|
143
|
+
// API rejects a zero thinking budget.
|
|
144
|
+
const THINKING_MANDATORY_MODELS = new Set([
|
|
145
|
+
"gemini-2.5-pro",
|
|
146
|
+
"gemini-3.6-flash",
|
|
147
|
+
"gemini-3.1-pro-preview",
|
|
148
|
+
]);
|
|
149
|
+
// The thinking budget to request when a probe wants thinking suppressed: 0
|
|
150
|
+
// (fully off) for models that allow it, or the dynamic budget for models that
|
|
151
|
+
// cannot be set to 0. includeThoughts stays false in both cases, so no thought
|
|
152
|
+
// parts are returned either way.
|
|
153
|
+
function minimalThinkingBudget(model) {
|
|
154
|
+
return THINKING_MANDATORY_MODELS.has(model) ? DYNAMIC_THINKING_BUDGET : 0;
|
|
155
|
+
}
|
|
117
156
|
function plainTextBody(intent) {
|
|
118
157
|
return {
|
|
119
158
|
contents: [userTextContent(intent.prompt)],
|
|
120
159
|
};
|
|
121
160
|
}
|
|
122
|
-
function plainTextStreamingBody(intent) {
|
|
161
|
+
function plainTextStreamingBody(intent, model) {
|
|
123
162
|
return {
|
|
124
163
|
contents: [userTextContent(intent.prompt)],
|
|
125
164
|
generationConfig: {
|
|
126
165
|
maxOutputTokens: 400,
|
|
127
166
|
thinkingConfig: {
|
|
128
|
-
thinkingBudget:
|
|
167
|
+
thinkingBudget: minimalThinkingBudget(model),
|
|
129
168
|
},
|
|
130
169
|
},
|
|
131
170
|
};
|
|
@@ -238,16 +277,16 @@ function structuredOutputBody(intent) {
|
|
|
238
277
|
return body;
|
|
239
278
|
}
|
|
240
279
|
export function buildRequestBody(opts) {
|
|
241
|
-
|
|
280
|
+
assertCapabilityBuildable(opts.model, opts.capability, opts.modelClass);
|
|
242
281
|
switch (opts.capability) {
|
|
243
282
|
case "plain-text":
|
|
244
283
|
return plainTextBody(opts.intent);
|
|
245
284
|
case "plain-text-streaming":
|
|
246
|
-
return plainTextStreamingBody(opts.intent);
|
|
285
|
+
return plainTextStreamingBody(opts.intent, opts.model);
|
|
247
286
|
case "function-calling-multi-turn":
|
|
248
287
|
case "function-calling-multi-turn-streaming":
|
|
249
288
|
return functionCallingBody(opts.intent, {
|
|
250
|
-
budget:
|
|
289
|
+
budget: minimalThinkingBudget(opts.model),
|
|
251
290
|
includeThoughts: false,
|
|
252
291
|
});
|
|
253
292
|
case "function-calling-with-thinking":
|
|
@@ -288,7 +327,8 @@ export function buildRequestBody(opts) {
|
|
|
288
327
|
case "reasoning-content-streaming":
|
|
289
328
|
case "redacted-thinking":
|
|
290
329
|
case "redacted-thinking-streaming":
|
|
291
|
-
|
|
330
|
+
case "structured-output-refusal-streaming":
|
|
331
|
+
throw new CapabilityNotBuildableError(opts.capability, `google-genai: capability ${opts.capability} is not supported by any google-genai model`);
|
|
292
332
|
default: {
|
|
293
333
|
const exhaustive = opts.capability;
|
|
294
334
|
throw new Error(`google-genai: unhandled capability ${String(exhaustive)}`);
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function reconstructResponseFromSSE(bytes: Uint8Array): unknown;
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Gemini streams generateContent as Server-Sent Events: each `data: {json}`
|
|
2
|
+
// line is a full GenerateContentResponse chunk whose candidates[0].content.parts
|
|
3
|
+
// carry incremental deltas. To build a turn-2 multi-turn body for a streaming
|
|
4
|
+
// capability, the assistant content has to be reconstructed from those chunks.
|
|
5
|
+
//
|
|
6
|
+
// Reconstruction flattens every chunk's parts in order and coalesces
|
|
7
|
+
// consecutive text deltas that share the same shape — plain text (`{text}`)
|
|
8
|
+
// with plain text, and thought text (`{text, thought: true}`) with thought
|
|
9
|
+
// text — but never across shapes. Every non-text part (a functionCall, or any
|
|
10
|
+
// part carrying a thoughtSignature) is emitted as-is, in order, so the
|
|
11
|
+
// signature the API requires on an echoed thinking turn survives.
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
// The coalescing signature of a streamed text delta, or null when the part is
|
|
16
|
+
// not a plain text delta (a functionCall, a thoughtSignature-bearing part, …)
|
|
17
|
+
// and must therefore stand as its own part.
|
|
18
|
+
function textDeltaSignature(part) {
|
|
19
|
+
if (typeof part.text !== "string")
|
|
20
|
+
return null;
|
|
21
|
+
const keys = Object.keys(part);
|
|
22
|
+
if (keys.length === 1)
|
|
23
|
+
return "text";
|
|
24
|
+
if (keys.length === 2 && part.thought === true)
|
|
25
|
+
return "thought-text";
|
|
26
|
+
// A text delta carrying any further key (notably a thoughtSignature) is not
|
|
27
|
+
// coalescible: it stays its own part so the signature's exact placement in
|
|
28
|
+
// the thought stream survives verbatim into the echoed turn-2 content.
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function sseDataPayloads(bytes) {
|
|
32
|
+
const text = new TextDecoder().decode(bytes);
|
|
33
|
+
const payloads = [];
|
|
34
|
+
for (const line of text.split(/\r?\n/)) {
|
|
35
|
+
if (!line.startsWith("data:"))
|
|
36
|
+
continue;
|
|
37
|
+
const payload = line.slice("data:".length).trim();
|
|
38
|
+
if (payload.length > 0 && payload !== "[DONE]")
|
|
39
|
+
payloads.push(payload);
|
|
40
|
+
}
|
|
41
|
+
return payloads;
|
|
42
|
+
}
|
|
43
|
+
// Reconstructs the shape a non-streaming turn-1 response would have —
|
|
44
|
+
// `{ candidates: [{ content: { role, parts } }] }` — from a Gemini SSE stream,
|
|
45
|
+
// so the multi-turn turn-2 builder consumes it unchanged.
|
|
46
|
+
export function reconstructResponseFromSSE(bytes) {
|
|
47
|
+
const payloads = sseDataPayloads(bytes);
|
|
48
|
+
if (payloads.length === 0) {
|
|
49
|
+
throw new Error("google-genai SSE: stream carried no data payloads to reconstruct");
|
|
50
|
+
}
|
|
51
|
+
let role;
|
|
52
|
+
const parts = [];
|
|
53
|
+
let lastSignature = null;
|
|
54
|
+
for (const payload of payloads) {
|
|
55
|
+
const chunk = JSON.parse(payload);
|
|
56
|
+
if (!isRecord(chunk)) {
|
|
57
|
+
throw new Error("google-genai SSE: chunk is not a JSON object");
|
|
58
|
+
}
|
|
59
|
+
const candidates = chunk.candidates;
|
|
60
|
+
// Some trailing chunks carry only usageMetadata and no candidates.
|
|
61
|
+
if (candidates === undefined)
|
|
62
|
+
continue;
|
|
63
|
+
if (!Array.isArray(candidates) || candidates.length === 0) {
|
|
64
|
+
throw new Error("google-genai SSE: chunk.candidates is not a non-empty array");
|
|
65
|
+
}
|
|
66
|
+
const first = candidates[0];
|
|
67
|
+
if (!isRecord(first)) {
|
|
68
|
+
throw new Error("google-genai SSE: candidates[0] is not an object");
|
|
69
|
+
}
|
|
70
|
+
const content = first.content;
|
|
71
|
+
// A finishReason-only chunk closes the candidate without new content.
|
|
72
|
+
if (content === undefined)
|
|
73
|
+
continue;
|
|
74
|
+
if (!isRecord(content)) {
|
|
75
|
+
throw new Error("google-genai SSE: candidates[0].content is not an object");
|
|
76
|
+
}
|
|
77
|
+
if (typeof content.role === "string")
|
|
78
|
+
role = content.role;
|
|
79
|
+
const chunkParts = content.parts;
|
|
80
|
+
if (chunkParts === undefined)
|
|
81
|
+
continue;
|
|
82
|
+
if (!Array.isArray(chunkParts)) {
|
|
83
|
+
throw new Error("google-genai SSE: candidates[0].content.parts is not an array");
|
|
84
|
+
}
|
|
85
|
+
for (const part of chunkParts) {
|
|
86
|
+
if (!isRecord(part)) {
|
|
87
|
+
throw new Error("google-genai SSE: a content part is not an object");
|
|
88
|
+
}
|
|
89
|
+
const signature = textDeltaSignature(part);
|
|
90
|
+
const previous = parts[parts.length - 1];
|
|
91
|
+
if (signature !== null &&
|
|
92
|
+
signature === lastSignature &&
|
|
93
|
+
previous !== undefined) {
|
|
94
|
+
previous.text = `${String(previous.text)}${String(part.text)}`;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
parts.push({ ...part });
|
|
98
|
+
lastSignature = signature;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// The assistant role must come off the wire; defaulting it would fabricate a
|
|
103
|
+
// turn-1 shape the model never sent and mask a provider change the probe
|
|
104
|
+
// exists to catch. Gemini emits role on the first content-bearing chunk.
|
|
105
|
+
if (role === undefined) {
|
|
106
|
+
throw new Error("google-genai SSE: no candidate content carried a role; cannot reconstruct the assistant turn");
|
|
107
|
+
}
|
|
108
|
+
return { candidates: [{ content: { role, parts } }] };
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intx/inference-discovery-google-genai",
|
|
3
|
-
"
|
|
3
|
+
"description": "Google GenAI provider plug-in for the inference discovery rig",
|
|
4
|
+
"version": "0.4.0",
|
|
4
5
|
"license": "LGPL-2.1-only",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"exports": {
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
}
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
14
|
-
"@intx/inference-discovery": "0.
|
|
15
|
+
"@intx/inference-discovery": "0.4.0",
|
|
15
16
|
"arktype": "^2.1.29"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|