@effect-uai/google 0.4.0 → 0.5.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/dist/Gemini.d.mts +6 -5
- package/dist/Gemini.d.mts.map +1 -1
- package/dist/Gemini.mjs +41 -21
- package/dist/Gemini.mjs.map +1 -1
- package/dist/GeminiEmbedding.d.mts.map +1 -1
- package/dist/GeminiEmbedding.mjs +1 -1
- package/dist/GeminiEmbedding.mjs.map +1 -1
- package/dist/GeminiSynthesizer.d.mts +3 -13
- package/dist/GeminiSynthesizer.d.mts.map +1 -1
- package/dist/GeminiSynthesizer.mjs +1 -1
- package/dist/GeminiSynthesizer.mjs.map +1 -1
- package/dist/GeminiTranscriber.d.mts +2 -2
- package/dist/GeminiTranscriber.d.mts.map +1 -1
- package/dist/GeminiTranscriber.mjs +1 -1
- package/dist/GeminiTranscriber.mjs.map +1 -1
- package/dist/LyriaGenerator.d.mts.map +1 -1
- package/dist/LyriaGenerator.mjs +5 -5
- package/dist/LyriaGenerator.mjs.map +1 -1
- package/dist/codec.d.mts +51 -6
- package/dist/codec.d.mts.map +1 -1
- package/dist/codec.mjs +150 -26
- package/dist/codec.mjs.map +1 -1
- package/package.json +2 -2
- package/src/Gemini.ts +53 -18
- package/src/GeminiEmbedding.ts +23 -5
- package/src/GeminiSynthesizer.ts +1 -1
- package/src/GeminiTranscriber.ts +1 -1
- package/src/LyriaGenerator.ts +15 -19
- package/src/codec.ts +312 -36
package/src/codec.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Array as Arr, Encoding, Match, Option, Result, Schema, pipe } from "effect"
|
|
2
|
-
import type { ContentBlock, InputImage, Item, Message } from "@effect-uai/core/Items"
|
|
2
|
+
import type { ContentBlock, FunctionCall, InputImage, Item, Message } from "@effect-uai/core/Items"
|
|
3
|
+
import type { ToolDescriptor } from "@effect-uai/core/Tool"
|
|
3
4
|
import type { Turn } from "@effect-uai/core/Turn"
|
|
4
5
|
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
@@ -17,7 +18,22 @@ const TextPart = Schema.Struct({
|
|
|
17
18
|
thought: Schema.optional(Schema.Boolean),
|
|
18
19
|
})
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Gemini function-call part. Args are delivered as a whole JSON *object*
|
|
23
|
+
* in a single chunk (Gemini does not stream tool-call args). On Gemini 3
|
|
24
|
+
* the response also carries an `id` per call, which we echo back on the
|
|
25
|
+
* corresponding `functionResponse`.
|
|
26
|
+
*/
|
|
27
|
+
const FunctionCallPart = Schema.Struct({
|
|
28
|
+
functionCall: Schema.Struct({
|
|
29
|
+
id: Schema.optional(Schema.String),
|
|
30
|
+
name: Schema.String,
|
|
31
|
+
args: Schema.optional(Schema.Unknown),
|
|
32
|
+
}),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const Part = Schema.Union([TextPart, FunctionCallPart])
|
|
36
|
+
type WireFunctionCallPart = typeof FunctionCallPart.Type
|
|
21
37
|
|
|
22
38
|
const Content = Schema.Struct({
|
|
23
39
|
role: Schema.optional(Schema.String),
|
|
@@ -51,6 +67,13 @@ export type WireChunk = typeof WireChunk.Type
|
|
|
51
67
|
type RequestPart =
|
|
52
68
|
| { readonly text: string }
|
|
53
69
|
| { readonly inlineData: { readonly mimeType: string; readonly data: string } }
|
|
70
|
+
| { readonly functionCall: { readonly name: string; readonly args: unknown } }
|
|
71
|
+
| {
|
|
72
|
+
readonly functionResponse: {
|
|
73
|
+
readonly name: string
|
|
74
|
+
readonly response: Record<string, unknown>
|
|
75
|
+
}
|
|
76
|
+
}
|
|
54
77
|
|
|
55
78
|
type RequestContent = {
|
|
56
79
|
readonly role: "user" | "model"
|
|
@@ -61,6 +84,22 @@ type RequestSystemInstruction = {
|
|
|
61
84
|
readonly parts: ReadonlyArray<{ readonly text: string }>
|
|
62
85
|
}
|
|
63
86
|
|
|
87
|
+
/** Gemini's tool declaration. We translate one `ToolDescriptor` per entry. */
|
|
88
|
+
type RequestTool = {
|
|
89
|
+
readonly functionDeclarations: ReadonlyArray<{
|
|
90
|
+
readonly name: string
|
|
91
|
+
readonly description: string
|
|
92
|
+
readonly parameters: Record<string, unknown>
|
|
93
|
+
}>
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type RequestToolConfig = {
|
|
97
|
+
readonly functionCallingConfig: {
|
|
98
|
+
readonly mode: "AUTO" | "ANY" | "NONE"
|
|
99
|
+
readonly allowedFunctionNames?: ReadonlyArray<string>
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
64
103
|
export type ThinkingConfig = {
|
|
65
104
|
readonly thinkingBudget: number
|
|
66
105
|
}
|
|
@@ -80,6 +119,8 @@ export type RequestBody = {
|
|
|
80
119
|
readonly contents: ReadonlyArray<RequestContent>
|
|
81
120
|
readonly systemInstruction?: RequestSystemInstruction
|
|
82
121
|
readonly generationConfig?: GenerationConfig
|
|
122
|
+
readonly tools?: ReadonlyArray<RequestTool>
|
|
123
|
+
readonly toolConfig?: RequestToolConfig
|
|
83
124
|
}
|
|
84
125
|
|
|
85
126
|
const blockText = Match.type<ContentBlock>().pipe(
|
|
@@ -142,19 +183,165 @@ const systemMessageText = (message: Message): Result.Result<string, void> => {
|
|
|
142
183
|
return text.length === 0 ? Result.failVoid : Result.succeed(text)
|
|
143
184
|
}
|
|
144
185
|
|
|
145
|
-
const
|
|
186
|
+
const allMessages = (history: ReadonlyArray<Item>): ReadonlyArray<Message> =>
|
|
146
187
|
pipe(
|
|
147
188
|
history,
|
|
148
189
|
Arr.filterMap((item) => (item.type === "message" ? Result.succeed(item) : Result.failVoid)),
|
|
149
190
|
)
|
|
150
191
|
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Function-call round-trip
|
|
194
|
+
//
|
|
195
|
+
// `FunctionCall.arguments` and `FunctionCallOutput.output` are JSON-encoded
|
|
196
|
+
// strings; Gemini's `functionCall.args` and `functionResponse.response`
|
|
197
|
+
// expect parsed JSON *objects*. Decode via Schema:
|
|
198
|
+
// - object payload → use as-is
|
|
199
|
+
// - scalar / array / malformed → wrap as `{ output: <raw string> }` so the
|
|
200
|
+
// model still sees *some* response without crashing the request.
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
const JsonObject = Schema.Record(Schema.String, Schema.Unknown)
|
|
204
|
+
const decodeJsonObject = Schema.decodeResult(Schema.fromJsonString(JsonObject))
|
|
205
|
+
|
|
206
|
+
const parsedJsonObject =
|
|
207
|
+
(fallback: (raw: string) => Record<string, unknown>) =>
|
|
208
|
+
(encoded: string): Record<string, unknown> =>
|
|
209
|
+
pipe(
|
|
210
|
+
decodeJsonObject(encoded),
|
|
211
|
+
Result.match({
|
|
212
|
+
onSuccess: (v) => v,
|
|
213
|
+
onFailure: () => fallback(encoded),
|
|
214
|
+
}),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
const parsedArgs = parsedJsonObject(() => ({}))
|
|
218
|
+
const parsedResponse = parsedJsonObject((raw) => ({ output: raw }))
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* `FunctionCallOutput` only carries `call_id`; Gemini's `functionResponse`
|
|
222
|
+
* requires the declared function `name`. Resolve the name by scanning prior
|
|
223
|
+
* `function_call` items in the history for a matching `call_id`. If we
|
|
224
|
+
* cannot resolve, fall back to `call_id` as the name - imperfect but
|
|
225
|
+
* preserves stream shape so the model sees *some* response.
|
|
226
|
+
*/
|
|
227
|
+
const isFunctionCallItem = (item: Item): item is FunctionCall => item.type === "function_call"
|
|
228
|
+
|
|
229
|
+
const nameForCallId = (history: ReadonlyArray<Item>, call_id: string): Option.Option<string> =>
|
|
230
|
+
pipe(
|
|
231
|
+
history,
|
|
232
|
+
Arr.findFirst(
|
|
233
|
+
(item): item is FunctionCall => isFunctionCallItem(item) && item.call_id === call_id,
|
|
234
|
+
),
|
|
235
|
+
Option.map((f) => f.name),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Extract the Gemini-3 wire id we stashed in `providerData` on the way out.
|
|
240
|
+
* Schema-driven so the shape lives in one place; failure → `Option.none()`.
|
|
241
|
+
*/
|
|
242
|
+
const ProviderDataWithGeminiId = Schema.Struct({
|
|
243
|
+
gemini: Schema.Struct({ id: Schema.String }),
|
|
244
|
+
})
|
|
245
|
+
const decodeProviderId = Schema.decodeUnknownResult(ProviderDataWithGeminiId)
|
|
246
|
+
|
|
247
|
+
const providerIdFor = (item: FunctionCall): Option.Option<string> =>
|
|
248
|
+
pipe(
|
|
249
|
+
decodeProviderId(item.providerData),
|
|
250
|
+
Result.match({
|
|
251
|
+
onSuccess: (d) => Option.some(d.gemini.id),
|
|
252
|
+
onFailure: () => Option.none<string>(),
|
|
253
|
+
}),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
const itemToContent =
|
|
257
|
+
(history: ReadonlyArray<Item>) =>
|
|
258
|
+
(item: Item): Result.Result<RequestContent, void> =>
|
|
259
|
+
Match.value(item).pipe(
|
|
260
|
+
Match.discriminatorsExhaustive("type")({
|
|
261
|
+
message: messageToContent,
|
|
262
|
+
function_call: (f) =>
|
|
263
|
+
Result.succeed({
|
|
264
|
+
role: "model" as const,
|
|
265
|
+
parts: [
|
|
266
|
+
{
|
|
267
|
+
functionCall: {
|
|
268
|
+
...Option.match(providerIdFor(f), {
|
|
269
|
+
onSome: (id) => ({ id }),
|
|
270
|
+
onNone: () => ({}),
|
|
271
|
+
}),
|
|
272
|
+
name: f.name,
|
|
273
|
+
args: parsedArgs(f.arguments),
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
}),
|
|
278
|
+
function_call_output: (o) =>
|
|
279
|
+
Result.succeed({
|
|
280
|
+
role: "user" as const,
|
|
281
|
+
parts: [
|
|
282
|
+
{
|
|
283
|
+
functionResponse: {
|
|
284
|
+
name: Option.getOrElse(nameForCallId(history, o.call_id), () => o.call_id),
|
|
285
|
+
response: parsedResponse(o.output),
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
}),
|
|
290
|
+
reasoning: () => Result.failVoid,
|
|
291
|
+
}),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Tool descriptors → Gemini `functionDeclarations`. Gemini accepts only a
|
|
296
|
+
// strict OpenAPI 3.0 subset for `parameters`; strip JSON-Schema keys it
|
|
297
|
+
// rejects (`$schema`, `$ref`, `additionalProperties`, `oneOf`,
|
|
298
|
+
// `definitions`).
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
const UNSUPPORTED_SCHEMA_KEYS: ReadonlySet<string> = new Set([
|
|
302
|
+
"$schema",
|
|
303
|
+
"$ref",
|
|
304
|
+
"$defs",
|
|
305
|
+
"definitions",
|
|
306
|
+
"additionalProperties",
|
|
307
|
+
"oneOf",
|
|
308
|
+
])
|
|
309
|
+
|
|
310
|
+
const sanitizeSchema = (schema: unknown): unknown => {
|
|
311
|
+
if (Array.isArray(schema)) return schema.map(sanitizeSchema)
|
|
312
|
+
if (schema === null || typeof schema !== "object") return schema
|
|
313
|
+
return pipe(
|
|
314
|
+
Object.entries(schema as Record<string, unknown>),
|
|
315
|
+
Arr.filterMap(([k, v]) =>
|
|
316
|
+
UNSUPPORTED_SCHEMA_KEYS.has(k)
|
|
317
|
+
? Result.failVoid
|
|
318
|
+
: Result.succeed([k, sanitizeSchema(v)] as const),
|
|
319
|
+
),
|
|
320
|
+
Object.fromEntries,
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const toolDescriptorsToTools = (tools: ReadonlyArray<ToolDescriptor>): ReadonlyArray<RequestTool> =>
|
|
325
|
+
tools.length === 0
|
|
326
|
+
? []
|
|
327
|
+
: [
|
|
328
|
+
{
|
|
329
|
+
functionDeclarations: tools.map((t) => ({
|
|
330
|
+
name: t.name,
|
|
331
|
+
description: t.description,
|
|
332
|
+
parameters: sanitizeSchema(t.inputSchema) as Record<string, unknown>,
|
|
333
|
+
})),
|
|
334
|
+
},
|
|
335
|
+
]
|
|
336
|
+
|
|
151
337
|
export const buildRequestBody = (
|
|
152
338
|
history: ReadonlyArray<Item>,
|
|
153
339
|
generationConfig: Option.Option<GenerationConfig>,
|
|
340
|
+
tools: ReadonlyArray<ToolDescriptor> = [],
|
|
154
341
|
): RequestBody => {
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
const
|
|
342
|
+
const systemTexts = pipe(allMessages(history), Arr.filterMap(systemMessageText))
|
|
343
|
+
const contents = pipe(history, Arr.filterMap(itemToContent(history)))
|
|
344
|
+
const requestTools = toolDescriptorsToTools(tools)
|
|
158
345
|
return {
|
|
159
346
|
contents,
|
|
160
347
|
...(systemTexts.length > 0 && {
|
|
@@ -164,6 +351,10 @@ export const buildRequestBody = (
|
|
|
164
351
|
onNone: () => ({}),
|
|
165
352
|
onSome: (cfg) => ({ generationConfig: cfg }),
|
|
166
353
|
}),
|
|
354
|
+
...(requestTools.length > 0 && {
|
|
355
|
+
tools: requestTools,
|
|
356
|
+
toolConfig: { functionCallingConfig: { mode: "AUTO" as const } },
|
|
357
|
+
}),
|
|
167
358
|
}
|
|
168
359
|
}
|
|
169
360
|
|
|
@@ -180,9 +371,20 @@ const finishReasonToStop = (reason: Option.Option<string>): Turn["stop_reason"]
|
|
|
180
371
|
onSome: (r) => (r === "MAX_TOKENS" ? ("max_tokens" as const) : ("stop" as const)),
|
|
181
372
|
})
|
|
182
373
|
|
|
374
|
+
export type AccumulatedFunctionCall = {
|
|
375
|
+
/** Synthesized id-or-name we surface as `call_id` on the canonical item. */
|
|
376
|
+
readonly callId: string
|
|
377
|
+
readonly name: string
|
|
378
|
+
/** Wire id from Gemini 3, when present - echoed back on `functionResponse`. */
|
|
379
|
+
readonly providerId: Option.Option<string>
|
|
380
|
+
/** Args as JSON-encoded string, mirroring `Items.FunctionCall.arguments`. */
|
|
381
|
+
readonly arguments: string
|
|
382
|
+
}
|
|
383
|
+
|
|
183
384
|
export type Accumulator = {
|
|
184
385
|
readonly text: string
|
|
185
386
|
readonly reasoning: string
|
|
387
|
+
readonly functionCalls: ReadonlyArray<AccumulatedFunctionCall>
|
|
186
388
|
readonly finishReason: Option.Option<string>
|
|
187
389
|
readonly usage: {
|
|
188
390
|
readonly input_tokens?: number
|
|
@@ -196,18 +398,25 @@ export type Accumulator = {
|
|
|
196
398
|
export const emptyAccumulator: Accumulator = {
|
|
197
399
|
text: "",
|
|
198
400
|
reasoning: "",
|
|
401
|
+
functionCalls: [],
|
|
199
402
|
finishReason: Option.none(),
|
|
200
403
|
usage: {},
|
|
201
404
|
}
|
|
202
405
|
|
|
203
406
|
/**
|
|
204
|
-
* One part's worth of streamable text
|
|
205
|
-
*
|
|
206
|
-
*
|
|
407
|
+
* One part's worth of streamable output. `text` and `reasoning` are
|
|
408
|
+
* incremental string deltas; `function_call` arrives whole-in-one-chunk
|
|
409
|
+
* (Gemini does not stream tool-call args).
|
|
207
410
|
*/
|
|
208
411
|
export type ChunkPart =
|
|
209
412
|
| { readonly kind: "text"; readonly text: string }
|
|
210
413
|
| { readonly kind: "reasoning"; readonly text: string }
|
|
414
|
+
| {
|
|
415
|
+
readonly kind: "function_call"
|
|
416
|
+
readonly id: Option.Option<string>
|
|
417
|
+
readonly name: string
|
|
418
|
+
readonly args: unknown
|
|
419
|
+
}
|
|
211
420
|
|
|
212
421
|
export type ChunkResult = {
|
|
213
422
|
readonly accumulator: Accumulator
|
|
@@ -215,25 +424,40 @@ export type ChunkResult = {
|
|
|
215
424
|
readonly finished: boolean
|
|
216
425
|
}
|
|
217
426
|
|
|
427
|
+
const isTextPart = (p: typeof Part.Type): p is typeof TextPart.Type => "text" in p
|
|
428
|
+
|
|
429
|
+
const textToChunkParts = (p: typeof TextPart.Type): ReadonlyArray<ChunkPart> =>
|
|
430
|
+
p.text.length === 0 ? [] : [{ kind: p.thought === true ? "reasoning" : "text", text: p.text }]
|
|
431
|
+
|
|
432
|
+
const functionCallToChunkParts = (p: WireFunctionCallPart): ReadonlyArray<ChunkPart> => [
|
|
433
|
+
{
|
|
434
|
+
kind: "function_call",
|
|
435
|
+
id: Option.fromNullishOr(p.functionCall.id),
|
|
436
|
+
name: p.functionCall.name,
|
|
437
|
+
args: p.functionCall.args ?? {},
|
|
438
|
+
},
|
|
439
|
+
]
|
|
440
|
+
|
|
441
|
+
const partToChunkParts = (p: typeof Part.Type): ReadonlyArray<ChunkPart> =>
|
|
442
|
+
isTextPart(p) ? textToChunkParts(p) : functionCallToChunkParts(p)
|
|
443
|
+
|
|
218
444
|
const chunkParts = (chunk: WireChunk): ReadonlyArray<ChunkPart> =>
|
|
219
|
-
pipe(
|
|
220
|
-
chunk.candidates?.[0]?.content?.parts ?? [],
|
|
221
|
-
Arr.filterMap((p) =>
|
|
222
|
-
p.text.length === 0
|
|
223
|
-
? Result.failVoid
|
|
224
|
-
: Result.succeed({
|
|
225
|
-
kind: p.thought === true ? ("reasoning" as const) : ("text" as const),
|
|
226
|
-
text: p.text,
|
|
227
|
-
}),
|
|
228
|
-
),
|
|
229
|
-
)
|
|
445
|
+
pipe(chunk.candidates?.[0]?.content?.parts ?? [], Arr.flatMap(partToChunkParts))
|
|
230
446
|
|
|
231
|
-
const
|
|
447
|
+
const sumStrings = (parts: ReadonlyArray<ChunkPart>, kind: "text" | "reasoning"): string =>
|
|
232
448
|
pipe(
|
|
233
449
|
parts,
|
|
234
450
|
Arr.filterMap((p) => (p.kind === kind ? Result.succeed(p.text) : Result.failVoid)),
|
|
235
451
|
).join("")
|
|
236
452
|
|
|
453
|
+
const collectFunctionCalls = (
|
|
454
|
+
parts: ReadonlyArray<ChunkPart>,
|
|
455
|
+
): ReadonlyArray<Extract<ChunkPart, { kind: "function_call" }>> =>
|
|
456
|
+
pipe(
|
|
457
|
+
parts,
|
|
458
|
+
Arr.filterMap((p) => (p.kind === "function_call" ? Result.succeed(p) : Result.failVoid)),
|
|
459
|
+
)
|
|
460
|
+
|
|
237
461
|
const mergeUsage = (
|
|
238
462
|
prev: Accumulator["usage"],
|
|
239
463
|
next: WireChunk["usageMetadata"],
|
|
@@ -255,6 +479,41 @@ const mergeUsage = (
|
|
|
255
479
|
}),
|
|
256
480
|
}
|
|
257
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Synthesize a stable `call_id` for a function call. Gemini 3 provides one
|
|
484
|
+
* via `functionCall.id`; older models do not, so we fall back to
|
|
485
|
+
* `<name>_<index>` based on prior calls' position in the accumulator.
|
|
486
|
+
*/
|
|
487
|
+
const synthesizeCallId = (
|
|
488
|
+
call: Extract<ChunkPart, { kind: "function_call" }>,
|
|
489
|
+
priorCalls: ReadonlyArray<AccumulatedFunctionCall>,
|
|
490
|
+
): string =>
|
|
491
|
+
Option.match(call.id, {
|
|
492
|
+
onSome: (id) => id,
|
|
493
|
+
onNone: () => `${call.name}_${priorCalls.length}`,
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
const chunkCallToAccumulated = (
|
|
497
|
+
prior: ReadonlyArray<AccumulatedFunctionCall>,
|
|
498
|
+
call: Extract<ChunkPart, { kind: "function_call" }>,
|
|
499
|
+
): AccumulatedFunctionCall => ({
|
|
500
|
+
callId: synthesizeCallId(call, prior),
|
|
501
|
+
name: call.name,
|
|
502
|
+
providerId: call.id,
|
|
503
|
+
// `functionCallToChunkParts` already replaced null/undefined with `{}`,
|
|
504
|
+
// so `call.args` is always a JSON-encodable value here.
|
|
505
|
+
arguments: JSON.stringify(call.args),
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
const appendFunctionCalls = (
|
|
509
|
+
prior: ReadonlyArray<AccumulatedFunctionCall>,
|
|
510
|
+
fromChunk: ReadonlyArray<Extract<ChunkPart, { kind: "function_call" }>>,
|
|
511
|
+
): ReadonlyArray<AccumulatedFunctionCall> =>
|
|
512
|
+
fromChunk.reduce<ReadonlyArray<AccumulatedFunctionCall>>(
|
|
513
|
+
(acc, call) => [...acc, chunkCallToAccumulated(acc, call)],
|
|
514
|
+
prior,
|
|
515
|
+
)
|
|
516
|
+
|
|
258
517
|
export const ingestChunk = (acc: Accumulator, chunk: WireChunk): ChunkResult => {
|
|
259
518
|
const parts = chunkParts(chunk)
|
|
260
519
|
const finishReason = Option.fromNullishOr(chunk.candidates?.[0]?.finishReason)
|
|
@@ -262,27 +521,44 @@ export const ingestChunk = (acc: Accumulator, chunk: WireChunk): ChunkResult =>
|
|
|
262
521
|
parts,
|
|
263
522
|
finished: Option.isSome(finishReason),
|
|
264
523
|
accumulator: {
|
|
265
|
-
text: acc.text +
|
|
266
|
-
reasoning: acc.reasoning +
|
|
524
|
+
text: acc.text + sumStrings(parts, "text"),
|
|
525
|
+
reasoning: acc.reasoning + sumStrings(parts, "reasoning"),
|
|
526
|
+
functionCalls: appendFunctionCalls(acc.functionCalls, collectFunctionCalls(parts)),
|
|
267
527
|
finishReason: Option.orElse(finishReason, () => acc.finishReason),
|
|
268
528
|
usage: mergeUsage(acc.usage, chunk.usageMetadata),
|
|
269
529
|
},
|
|
270
530
|
}
|
|
271
531
|
}
|
|
272
532
|
|
|
533
|
+
const reasoningItems = (acc: Accumulator): ReadonlyArray<Item> =>
|
|
534
|
+
acc.reasoning.length > 0 ? [{ type: "reasoning", summary: acc.reasoning }] : []
|
|
535
|
+
|
|
536
|
+
const assistantMessageItems = (acc: Accumulator): ReadonlyArray<Item> =>
|
|
537
|
+
acc.text.length === 0
|
|
538
|
+
? []
|
|
539
|
+
: [
|
|
540
|
+
{
|
|
541
|
+
type: "message",
|
|
542
|
+
role: "assistant",
|
|
543
|
+
content: [{ type: "output_text", text: acc.text }],
|
|
544
|
+
},
|
|
545
|
+
]
|
|
546
|
+
|
|
547
|
+
const functionCallItems = (acc: Accumulator): ReadonlyArray<Item> =>
|
|
548
|
+
pipe(
|
|
549
|
+
acc.functionCalls,
|
|
550
|
+
Arr.map((c) => ({
|
|
551
|
+
type: "function_call" as const,
|
|
552
|
+
call_id: c.callId,
|
|
553
|
+
name: c.name,
|
|
554
|
+
arguments: c.arguments,
|
|
555
|
+
...(Option.isSome(c.providerId) && { providerData: { gemini: { id: c.providerId.value } } }),
|
|
556
|
+
})),
|
|
557
|
+
)
|
|
558
|
+
|
|
273
559
|
export const accumulatorToTurn = (acc: Accumulator): Turn => ({
|
|
274
|
-
stop_reason:
|
|
560
|
+
stop_reason:
|
|
561
|
+
acc.functionCalls.length > 0 ? ("tool_calls" as const) : finishReasonToStop(acc.finishReason),
|
|
275
562
|
usage: { ...acc.usage },
|
|
276
|
-
items: [
|
|
277
|
-
...(acc.reasoning.length > 0 ? [{ type: "reasoning" as const, summary: acc.reasoning }] : []),
|
|
278
|
-
...(acc.text.length === 0
|
|
279
|
-
? []
|
|
280
|
-
: [
|
|
281
|
-
{
|
|
282
|
-
type: "message" as const,
|
|
283
|
-
role: "assistant" as const,
|
|
284
|
-
content: [{ type: "output_text" as const, text: acc.text }],
|
|
285
|
-
},
|
|
286
|
-
]),
|
|
287
|
-
],
|
|
563
|
+
items: [...reasoningItems(acc), ...assistantMessageItems(acc), ...functionCallItems(acc)],
|
|
288
564
|
})
|