@effect-uai/google 0.17.0 → 0.19.0-dev-20261109183959
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 +4 -4
- package/dist/Gemini.d.mts +1 -1
- package/dist/Gemini.mjs +1 -1
- package/dist/Gemini.mjs.map +1 -1
- package/dist/GeminiEmbedding.d.mts +1 -1
- package/dist/GeminiEmbedding.d.mts.map +1 -1
- package/dist/GeminiEmbedding.mjs +4 -3
- package/dist/GeminiEmbedding.mjs.map +1 -1
- package/dist/GeminiImageGenerator.d.mts +1 -1
- package/dist/GeminiImageGenerator.d.mts.map +1 -1
- package/dist/GeminiImageGenerator.mjs +4 -3
- package/dist/GeminiImageGenerator.mjs.map +1 -1
- package/dist/GeminiLiveSession.d.mts +1 -1
- package/dist/GeminiSynthesizer.d.mts +1 -1
- package/dist/GeminiSynthesizer.mjs +1 -1
- package/dist/GeminiSynthesizer.mjs.map +1 -1
- package/dist/GeminiSynthesizer.test.mjs +1 -1
- package/dist/GoogleDeepResearch.d.mts +1 -1
- package/dist/GoogleDeepResearch.mjs +1 -1
- package/dist/GoogleDeepResearch.mjs.map +1 -1
- package/dist/GoogleDeepResearch.test.mjs +1 -1
- package/dist/LyriaGenerator.d.mts +1 -1
- package/dist/LyriaGenerator.d.mts.map +1 -1
- package/dist/LyriaGenerator.mjs +4 -3
- package/dist/LyriaGenerator.mjs.map +1 -1
- package/dist/LyriaGenerator.test.mjs +4 -142
- package/dist/LyriaGenerator.test.mjs.map +1 -1
- package/dist/codec.d.mts +28 -7
- package/dist/codec.d.mts.map +1 -1
- package/dist/codec.mjs +55 -12
- package/dist/codec.mjs.map +1 -1
- package/dist/codec.test.d.mts +1 -0
- package/dist/codec.test.mjs +61 -0
- package/dist/codec.test.mjs.map +1 -0
- package/dist/dist-DHer1cql.mjs +1533 -0
- package/dist/dist-DHer1cql.mjs.map +1 -0
- package/dist/geminiSpeechCodec.d.mts.map +1 -1
- package/dist/geminiSpeechCodec.mjs +4 -3
- package/dist/geminiSpeechCodec.mjs.map +1 -1
- package/dist/geminiSpeechCodec.test.mjs +5 -4
- package/dist/geminiSpeechCodec.test.mjs.map +1 -1
- package/dist/geminiTools.test.mjs +2 -1
- package/dist/geminiTools.test.mjs.map +1 -1
- package/dist/{test.DNmyFkvJ-BPz3G1HS.mjs → index.C4tKZ0yW-DjxCdpW_.mjs} +9115 -5993
- package/dist/index.C4tKZ0yW-DjxCdpW_.mjs.map +1 -0
- package/dist/{realtimeSession-7HSIjWh4.d.mts → realtimeSession-BlBgLDdp.d.mts} +2 -2
- package/dist/{realtimeSession-7HSIjWh4.d.mts.map → realtimeSession-BlBgLDdp.d.mts.map} +1 -1
- package/dist/realtimeSession.d.mts +1 -1
- package/dist/realtimeSession.mjs +8 -7
- package/dist/realtimeSession.mjs.map +1 -1
- package/dist/realtimeSession.test.mjs +120 -98
- package/dist/realtimeSession.test.mjs.map +1 -1
- package/package.json +5 -5
- package/src/Gemini.ts +1 -1
- package/src/GeminiEmbedding.ts +4 -3
- package/src/GeminiImageGenerator.ts +4 -3
- package/src/GeminiSynthesizer.ts +1 -1
- package/src/GoogleDeepResearch.ts +1 -1
- package/src/LyriaGenerator.test.ts +1 -1
- package/src/LyriaGenerator.ts +4 -14
- package/src/codec.test.ts +54 -0
- package/src/codec.ts +93 -23
- package/src/geminiSpeechCodec.test.ts +4 -3
- package/src/geminiSpeechCodec.ts +4 -3
- package/src/geminiTools.test.ts +1 -0
- package/src/realtimeSession.test.ts +9 -8
- package/src/realtimeSession.ts +7 -7
- package/dist/magic-string.es-CUAnjKYC.mjs +0 -1014
- package/dist/magic-string.es-CUAnjKYC.mjs.map +0 -1
- package/dist/test.DNmyFkvJ-BPz3G1HS.mjs.map +0 -1
package/src/GeminiEmbedding.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Context, Effect,
|
|
2
|
-
import {
|
|
1
|
+
import { Context, Effect, Layer, Match, Redacted, Schema } from "effect"
|
|
2
|
+
import { Base64 } from "effect/encoding"
|
|
3
|
+
import { HttpClient, HttpClientRequest } from "effect/http"
|
|
3
4
|
import * as AiError from "@effect-uai/core/AiError"
|
|
4
5
|
import type {
|
|
5
6
|
EmbedContentPart,
|
|
@@ -126,7 +127,7 @@ const imageSourceToPart = (param: string) =>
|
|
|
126
127
|
),
|
|
127
128
|
Match.tag("bytes", (s): Effect.Effect<WirePart, AiError.AiError> =>
|
|
128
129
|
Effect.succeed({
|
|
129
|
-
inlineData: { mimeType: s.mimeType, data:
|
|
130
|
+
inlineData: { mimeType: s.mimeType, data: Base64.encode(s.bytes) },
|
|
130
131
|
}),
|
|
131
132
|
),
|
|
132
133
|
Match.tag("url", (): Effect.Effect<WirePart, AiError.AiError> =>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Context, Effect,
|
|
2
|
-
import {
|
|
1
|
+
import { Context, Effect, Layer, Match, Redacted, Schema, Stream } from "effect"
|
|
2
|
+
import { Base64 } from "effect/encoding"
|
|
3
|
+
import { HttpClient, HttpClientRequest } from "effect/http"
|
|
3
4
|
import * as AiError from "@effect-uai/core/AiError"
|
|
4
5
|
import * as Capabilities from "@effect-uai/core/Capabilities"
|
|
5
6
|
import type {
|
|
@@ -129,7 +130,7 @@ const referencePart: (image: ImageSource) => Effect.Effect<RequestPart, AiError.
|
|
|
129
130
|
),
|
|
130
131
|
Match.tag("bytes", (i) =>
|
|
131
132
|
Effect.succeed<RequestPart>({
|
|
132
|
-
inlineData: { mimeType: i.mimeType, data:
|
|
133
|
+
inlineData: { mimeType: i.mimeType, data: Base64.encode(i.bytes) },
|
|
133
134
|
}),
|
|
134
135
|
),
|
|
135
136
|
Match.tag("url", () => Effect.fail(urlRefNotSupported)),
|
package/src/GeminiSynthesizer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Context, Effect, Layer, Match, Redacted, Result, Schema, Stream } from "effect"
|
|
2
|
-
import { HttpClient, HttpClientRequest } from "effect/
|
|
2
|
+
import { HttpClient, HttpClientRequest } from "effect/http"
|
|
3
3
|
import * as AiError from "@effect-uai/core/AiError"
|
|
4
4
|
import type { AudioBlob, AudioChunk, AudioFormat } from "@effect-uai/core/Audio"
|
|
5
5
|
import * as Capabilities from "@effect-uai/core/Capabilities"
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
Stream,
|
|
13
13
|
pipe,
|
|
14
14
|
} from "effect"
|
|
15
|
-
import { HttpClient, HttpClientRequest } from "effect/
|
|
15
|
+
import { HttpClient, HttpClientRequest } from "effect/http"
|
|
16
16
|
import * as AiError from "@effect-uai/core/AiError"
|
|
17
17
|
import type { Source } from "@effect-uai/core/Citation"
|
|
18
18
|
import {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Duration, Effect, Layer, Redacted, Stream } from "effect"
|
|
2
|
-
import { FetchHttpClient } from "effect/
|
|
2
|
+
import { FetchHttpClient } from "effect/http"
|
|
3
3
|
import { describe, expect, expectTypeOf, it } from "vitest"
|
|
4
4
|
import type * as AiError from "@effect-uai/core/AiError"
|
|
5
5
|
import type { GenerateResult } from "@effect-uai/core/Music"
|
package/src/LyriaGenerator.ts
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
Effect,
|
|
5
|
-
Encoding,
|
|
6
|
-
Layer,
|
|
7
|
-
Match,
|
|
8
|
-
Redacted,
|
|
9
|
-
Result,
|
|
10
|
-
Schema,
|
|
11
|
-
Stream,
|
|
12
|
-
} from "effect"
|
|
13
|
-
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
|
1
|
+
import { Context, Duration, Effect, Layer, Match, Redacted, Result, Schema, Stream } from "effect"
|
|
2
|
+
import { Base64 } from "effect/encoding"
|
|
3
|
+
import { HttpClient, HttpClientRequest } from "effect/http"
|
|
14
4
|
import * as AiError from "@effect-uai/core/AiError"
|
|
15
5
|
import type { AudioChunk, AudioFormat } from "@effect-uai/core/Audio"
|
|
16
6
|
import { warnDroppedWhen } from "@effect-uai/core/Capabilities"
|
|
@@ -207,7 +197,7 @@ const transportFailure = sharedTransportFailure("lyria")
|
|
|
207
197
|
const httpStatusError = sharedHttpStatusError("lyria")
|
|
208
198
|
|
|
209
199
|
const decodeBase64ToBytes = (b64: string): Effect.Effect<Uint8Array, AiError.AiError> =>
|
|
210
|
-
Result.match(
|
|
200
|
+
Result.match(Base64.decode(b64), {
|
|
211
201
|
onSuccess: Effect.succeed,
|
|
212
202
|
onFailure: (cause) =>
|
|
213
203
|
Effect.fail(new AiError.InvalidRequest({ provider: "lyria", param: "audio", raw: cause })),
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { fileUrl } from "@effect-uai/core/File"
|
|
2
|
+
import { imageBytes } from "@effect-uai/core/Image"
|
|
3
|
+
import * as Items from "@effect-uai/core/Items"
|
|
4
|
+
import { describe, expect, it } from "vitest"
|
|
5
|
+
import { functionResponseBody } from "./codec.js"
|
|
6
|
+
|
|
7
|
+
const body = (output: Items.ToolCallOutput["output"], isError?: boolean) =>
|
|
8
|
+
functionResponseBody({
|
|
9
|
+
type: "function_call_output",
|
|
10
|
+
call_id: "c1",
|
|
11
|
+
output,
|
|
12
|
+
...(isError !== undefined && { isError }),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe("functionResponseBody", () => {
|
|
16
|
+
it("puts one JSON value under output as an object, never a string", () => {
|
|
17
|
+
expect(body([Items.json({ city: "Lisbon", temp: 18 })])).toEqual({
|
|
18
|
+
response: { output: { city: "Lisbon", temp: 18 } },
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("puts a failure under error", () => {
|
|
23
|
+
expect(body([Items.json({ _tag: "CityNotFound", city: "Atlantis" })], true)).toEqual({
|
|
24
|
+
response: { error: { _tag: "CityNotFound", city: "Atlantis" } },
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("keeps several values as an array and moves media to parts", () => {
|
|
29
|
+
expect(
|
|
30
|
+
body([
|
|
31
|
+
Items.text("Home"),
|
|
32
|
+
Items.json({ hits: 2 }),
|
|
33
|
+
Items.image(imageBytes(new Uint8Array([1, 2, 3]), "image/png")),
|
|
34
|
+
Items.file(fileUrl("gs://bucket/a.pdf", "application/pdf"), "a.pdf"),
|
|
35
|
+
]),
|
|
36
|
+
).toEqual({
|
|
37
|
+
response: { output: ["Home", { hits: 2 }] },
|
|
38
|
+
parts: [
|
|
39
|
+
{ inlineData: { mimeType: "image/png", data: "AQID" } },
|
|
40
|
+
{
|
|
41
|
+
fileData: {
|
|
42
|
+
fileUri: "gs://bucket/a.pdf",
|
|
43
|
+
mimeType: "application/pdf",
|
|
44
|
+
displayName: "a.pdf",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("sends an empty response for media-only output", () => {
|
|
52
|
+
expect(body([Items.image(imageBytes(new Uint8Array([1]), "image/png"))]).response).toEqual({})
|
|
53
|
+
})
|
|
54
|
+
})
|
package/src/codec.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { Array as Arr,
|
|
1
|
+
import { Array as Arr, Match, Option, Result, Schema, pipe } from "effect"
|
|
2
|
+
import { Base64 } from "effect/encoding"
|
|
2
3
|
import * as AiError from "@effect-uai/core/AiError"
|
|
3
4
|
import type { ImageSource } from "@effect-uai/core/Image"
|
|
4
5
|
import { imageBase64 } from "@effect-uai/core/Image"
|
|
6
|
+
import type { FileSource } from "@effect-uai/core/File"
|
|
5
7
|
import type {
|
|
6
8
|
ContentBlock,
|
|
7
9
|
ToolCall,
|
|
10
|
+
ToolCallOutput,
|
|
11
|
+
ToolOutputContent,
|
|
8
12
|
InputImage,
|
|
9
13
|
HistoryItem,
|
|
10
14
|
Message,
|
|
@@ -152,10 +156,9 @@ type RequestPart =
|
|
|
152
156
|
readonly thoughtSignature?: string
|
|
153
157
|
}
|
|
154
158
|
| {
|
|
155
|
-
readonly functionResponse: {
|
|
159
|
+
readonly functionResponse: FunctionResponseBody & {
|
|
156
160
|
readonly id?: string
|
|
157
161
|
readonly name: string
|
|
158
|
-
readonly response: Record<string, unknown>
|
|
159
162
|
}
|
|
160
163
|
}
|
|
161
164
|
|
|
@@ -249,7 +252,7 @@ const imageSourceToParts = Match.type<InputImage["source"]>().pipe(
|
|
|
249
252
|
{ inlineData: { mimeType: s.mimeType, data: s.base64 } },
|
|
250
253
|
]),
|
|
251
254
|
Match.tag("bytes", (s): ReadonlyArray<RequestPart> => [
|
|
252
|
-
{ inlineData: { mimeType: s.mimeType, data:
|
|
255
|
+
{ inlineData: { mimeType: s.mimeType, data: Base64.encode(s.bytes) } },
|
|
253
256
|
]),
|
|
254
257
|
Match.exhaustive,
|
|
255
258
|
)
|
|
@@ -294,31 +297,98 @@ const allMessages = (history: ReadonlyArray<HistoryItem>): ReadonlyArray<Message
|
|
|
294
297
|
// ---------------------------------------------------------------------------
|
|
295
298
|
// Function-call round-trip
|
|
296
299
|
//
|
|
297
|
-
// `ToolCall.arguments`
|
|
298
|
-
//
|
|
299
|
-
// expect parsed JSON *objects*. Decode via Schema:
|
|
300
|
-
// - object payload → use as-is
|
|
301
|
-
// - scalar / array / malformed → wrap as `{ output: <raw string> }` so the
|
|
302
|
-
// model still sees *some* response without crashing the request.
|
|
300
|
+
// `ToolCall.arguments` is a JSON string; Gemini's `functionCall.args` wants
|
|
301
|
+
// an object (malformed or non-object arguments become `{}`).
|
|
303
302
|
// ---------------------------------------------------------------------------
|
|
304
303
|
|
|
305
304
|
const JsonObject = Schema.Record(Schema.String, Schema.Unknown)
|
|
306
305
|
const decodeJsonObject = Schema.decodeResult(Schema.fromJsonString(JsonObject))
|
|
307
306
|
|
|
308
|
-
const
|
|
309
|
-
(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
307
|
+
const parsedArgs = (encoded: string): Record<string, unknown> =>
|
|
308
|
+
pipe(
|
|
309
|
+
decodeJsonObject(encoded),
|
|
310
|
+
Result.getOrElse(() => ({})),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
type MediaPart =
|
|
314
|
+
| {
|
|
315
|
+
readonly inlineData: {
|
|
316
|
+
readonly mimeType: string
|
|
317
|
+
readonly data: string
|
|
318
|
+
readonly displayName?: string
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
| {
|
|
322
|
+
readonly fileData: {
|
|
323
|
+
readonly fileUri: string
|
|
324
|
+
readonly mimeType?: string
|
|
325
|
+
readonly displayName?: string
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const mediaPart = (
|
|
330
|
+
source: ImageSource | FileSource,
|
|
331
|
+
displayName: string | undefined,
|
|
332
|
+
): MediaPart => {
|
|
333
|
+
const named = displayName === undefined ? {} : { displayName }
|
|
334
|
+
return Match.value(source).pipe(
|
|
335
|
+
Match.tagsExhaustive({
|
|
336
|
+
url: (s) => ({
|
|
337
|
+
fileData: {
|
|
338
|
+
fileUri: s.url,
|
|
339
|
+
...(s.mimeType !== undefined && { mimeType: s.mimeType }),
|
|
340
|
+
...named,
|
|
341
|
+
},
|
|
316
342
|
}),
|
|
317
|
-
|
|
343
|
+
base64: (s) => ({ inlineData: { mimeType: s.mimeType, data: s.base64, ...named } }),
|
|
344
|
+
bytes: (s) => ({
|
|
345
|
+
inlineData: { mimeType: s.mimeType, data: Base64.encode(s.bytes), ...named },
|
|
346
|
+
}),
|
|
347
|
+
}),
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Gemini's `functionResponse` body for a tool output, without `id` / `name`. */
|
|
352
|
+
export type FunctionResponseBody = {
|
|
353
|
+
readonly response: Record<string, unknown>
|
|
354
|
+
readonly parts?: ReadonlyArray<MediaPart>
|
|
355
|
+
}
|
|
318
356
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
357
|
+
/**
|
|
358
|
+
* Text and JSON go into `response.output` (or `response.error` when the call
|
|
359
|
+
* failed) as values, never stringified: one value as itself, several as an
|
|
360
|
+
* array. Media goes into `parts`.
|
|
361
|
+
*/
|
|
362
|
+
export const functionResponseBody = (output: ToolCallOutput): FunctionResponseBody => {
|
|
363
|
+
const values = Arr.filterMap(
|
|
364
|
+
output.output,
|
|
365
|
+
Match.type<ToolOutputContent>().pipe(
|
|
366
|
+
Match.discriminatorsExhaustive("type")({
|
|
367
|
+
input_text: (p) => Result.succeed<unknown>(p.text),
|
|
368
|
+
input_json: (p) => Result.succeed<unknown>(p.value),
|
|
369
|
+
input_image: () => Result.failVoid,
|
|
370
|
+
input_file: () => Result.failVoid,
|
|
371
|
+
}),
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
const media = Arr.filterMap(
|
|
375
|
+
output.output,
|
|
376
|
+
Match.type<ToolOutputContent>().pipe(
|
|
377
|
+
Match.discriminatorsExhaustive("type")({
|
|
378
|
+
input_text: () => Result.failVoid,
|
|
379
|
+
input_json: () => Result.failVoid,
|
|
380
|
+
input_image: (p) => Result.succeed(mediaPart(p.source, undefined)),
|
|
381
|
+
input_file: (p) => Result.succeed(mediaPart(p.source, p.filename)),
|
|
382
|
+
}),
|
|
383
|
+
),
|
|
384
|
+
)
|
|
385
|
+
const key = output.isError === true ? "error" : "output"
|
|
386
|
+
const response = Arr.match(values, {
|
|
387
|
+
onEmpty: () => ({}),
|
|
388
|
+
onNonEmpty: (all) => ({ [key]: all.length === 1 ? Arr.headNonEmpty(all) : all }),
|
|
389
|
+
})
|
|
390
|
+
return media.length === 0 ? { response } : { response, parts: media }
|
|
391
|
+
}
|
|
322
392
|
|
|
323
393
|
/**
|
|
324
394
|
* `ToolCallOutput` only carries `call_id`; Gemini's `functionResponse`
|
|
@@ -422,7 +492,7 @@ const itemToContent =
|
|
|
422
492
|
onNone: () => ({}),
|
|
423
493
|
}),
|
|
424
494
|
name: Option.getOrElse(nameForCallId(history, o.call_id), () => o.call_id),
|
|
425
|
-
|
|
495
|
+
...functionResponseBody(o),
|
|
426
496
|
},
|
|
427
497
|
},
|
|
428
498
|
],
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Effect
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { Base64 } from "effect/encoding"
|
|
2
3
|
import { describe, expect, it } from "vitest"
|
|
3
4
|
import type { AudioSource } from "@effect-uai/core/Audio"
|
|
4
5
|
import { audioSourceToInlineData, wrapPcmAsWav } from "./geminiSpeechCodec.js"
|
|
@@ -9,11 +10,11 @@ describe("audioSourceToInlineData", () => {
|
|
|
9
10
|
const source: AudioSource = { _tag: "bytes", bytes: original, mimeType: "audio/wav" }
|
|
10
11
|
const inline = await Effect.runPromise(audioSourceToInlineData(source))
|
|
11
12
|
expect(inline.mimeType).toBe("audio/wav")
|
|
12
|
-
expect(inline.data).toBe(
|
|
13
|
+
expect(inline.data).toBe(Base64.encode(original))
|
|
13
14
|
})
|
|
14
15
|
|
|
15
16
|
it("passes a `base64` source through unchanged", async () => {
|
|
16
|
-
const b64 =
|
|
17
|
+
const b64 = Base64.encode(new Uint8Array([1, 2, 3]))
|
|
17
18
|
const source: AudioSource = { _tag: "base64", base64: b64, mimeType: "audio/mp3" }
|
|
18
19
|
const inline = await Effect.runPromise(audioSourceToInlineData(source))
|
|
19
20
|
expect(inline.mimeType).toBe("audio/mp3")
|
package/src/geminiSpeechCodec.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Effect,
|
|
1
|
+
import { Effect, Match, Result } from "effect"
|
|
2
|
+
import { Base64 } from "effect/encoding"
|
|
2
3
|
import * as AiError from "@effect-uai/core/AiError"
|
|
3
4
|
import type { AudioSource } from "@effect-uai/core/Audio"
|
|
4
5
|
|
|
@@ -34,7 +35,7 @@ export const audioSourceToInlineData: (
|
|
|
34
35
|
Match.tag("bytes", (a) =>
|
|
35
36
|
Effect.succeed<InlineAudioData>({
|
|
36
37
|
mimeType: a.mimeType,
|
|
37
|
-
data:
|
|
38
|
+
data: Base64.encode(a.bytes),
|
|
38
39
|
}),
|
|
39
40
|
),
|
|
40
41
|
Match.tag("base64", (a) =>
|
|
@@ -46,7 +47,7 @@ export const audioSourceToInlineData: (
|
|
|
46
47
|
|
|
47
48
|
/** Decode a base64-encoded audio payload into bytes. */
|
|
48
49
|
export const decodeBase64Audio = (b64: string): Effect.Effect<Uint8Array, AiError.AiError> =>
|
|
49
|
-
Result.match(
|
|
50
|
+
Result.match(Base64.decode(b64), {
|
|
50
51
|
onSuccess: Effect.succeed,
|
|
51
52
|
onFailure: (cause) =>
|
|
52
53
|
Effect.fail(new AiError.InvalidRequest({ provider: "gemini", param: "audio", raw: cause })),
|
package/src/geminiTools.test.ts
CHANGED
|
@@ -16,6 +16,7 @@ const localTool = Tool.make({
|
|
|
16
16
|
name: "get_weather",
|
|
17
17
|
description: "Look up the weather.",
|
|
18
18
|
inputSchema: Tool.fromEffectSchema(Schema.Struct({ city: Schema.String })),
|
|
19
|
+
outputSchema: Schema.String,
|
|
19
20
|
run: ({ city }) => Effect.succeed(`sunny in ${city}`),
|
|
20
21
|
})
|
|
21
22
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it } from "@effect/vitest"
|
|
2
|
-
import { Duration, Effect,
|
|
2
|
+
import { Duration, Effect, Fiber, Logger, Redacted, Ref, Stream } from "effect"
|
|
3
|
+
import { Base64 } from "effect/encoding"
|
|
3
4
|
import { expect } from "vitest"
|
|
4
5
|
import type { RealtimeEvent } from "@effect-uai/core/Realtime"
|
|
5
6
|
import { RealtimeInput } from "@effect-uai/core/Realtime"
|
|
@@ -55,7 +56,7 @@ const tags = (events: ReadonlyArray<RealtimeEvent>) => events.map((e) => e._tag)
|
|
|
55
56
|
const audioPart = (bytes: ReadonlyArray<number>) => ({
|
|
56
57
|
inlineData: {
|
|
57
58
|
mimeType: "audio/pcm;rate=24000",
|
|
58
|
-
data:
|
|
59
|
+
data: Base64.encode(new Uint8Array(bytes)),
|
|
59
60
|
},
|
|
60
61
|
})
|
|
61
62
|
|
|
@@ -195,7 +196,7 @@ describe("Gemini live session tools", () => {
|
|
|
195
196
|
output: {
|
|
196
197
|
type: "function_call_output",
|
|
197
198
|
call_id: "fc_1",
|
|
198
|
-
output:
|
|
199
|
+
output: [{ type: "input_json", value: { celsius: 32 } }],
|
|
199
200
|
providerData: undefined,
|
|
200
201
|
},
|
|
201
202
|
}),
|
|
@@ -218,7 +219,7 @@ describe("Gemini live session tools", () => {
|
|
|
218
219
|
{
|
|
219
220
|
id: "fc_1",
|
|
220
221
|
name: "get_weather",
|
|
221
|
-
response: { celsius: 32 },
|
|
222
|
+
response: { output: { celsius: 32 } },
|
|
222
223
|
scheduling: "WHEN_IDLE",
|
|
223
224
|
},
|
|
224
225
|
])
|
|
@@ -241,7 +242,7 @@ describe("Gemini live session tools", () => {
|
|
|
241
242
|
output: {
|
|
242
243
|
type: "function_call_output",
|
|
243
244
|
call_id: "fc_1",
|
|
244
|
-
output: "{}
|
|
245
|
+
output: [{ type: "input_json", value: {} }],
|
|
245
246
|
providerData: undefined,
|
|
246
247
|
},
|
|
247
248
|
}),
|
|
@@ -266,7 +267,7 @@ describe("Gemini live session tools", () => {
|
|
|
266
267
|
output: {
|
|
267
268
|
type: "function_call_output",
|
|
268
269
|
call_id: "fc_unknown",
|
|
269
|
-
output: "{}
|
|
270
|
+
output: [{ type: "input_json", value: {} }],
|
|
270
271
|
providerData: undefined,
|
|
271
272
|
},
|
|
272
273
|
}),
|
|
@@ -359,7 +360,7 @@ describe("Gemini live session history", () => {
|
|
|
359
360
|
{
|
|
360
361
|
type: "function_call_output",
|
|
361
362
|
call_id: "fc_1",
|
|
362
|
-
output:
|
|
363
|
+
output: [{ type: "input_json", value: { now: "12:00" } }],
|
|
363
364
|
providerData: undefined,
|
|
364
365
|
},
|
|
365
366
|
],
|
|
@@ -406,7 +407,7 @@ describe("Gemini live session video", () => {
|
|
|
406
407
|
expect(sent.filter((f: any) => f.realtimeInput?.video !== undefined)).toEqual([
|
|
407
408
|
{
|
|
408
409
|
realtimeInput: {
|
|
409
|
-
video: { data:
|
|
410
|
+
video: { data: Base64.encode(new Uint8Array([1, 2])), mimeType: "image/jpeg" },
|
|
410
411
|
},
|
|
411
412
|
},
|
|
412
413
|
])
|
package/src/realtimeSession.ts
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
Cause,
|
|
14
14
|
Duration,
|
|
15
15
|
Effect,
|
|
16
|
-
Encoding,
|
|
17
16
|
Match,
|
|
18
17
|
Pull,
|
|
19
18
|
Queue,
|
|
@@ -24,7 +23,8 @@ import {
|
|
|
24
23
|
type Scope,
|
|
25
24
|
Stream,
|
|
26
25
|
} from "effect"
|
|
27
|
-
import
|
|
26
|
+
import { Base64 } from "effect/encoding"
|
|
27
|
+
import * as Socket from "effect/socket/Socket"
|
|
28
28
|
import * as AiError from "@effect-uai/core/AiError"
|
|
29
29
|
import type { AudioFormat } from "@effect-uai/core/Audio"
|
|
30
30
|
import * as Capabilities from "@effect-uai/core/Capabilities"
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
} from "@effect-uai/core/Realtime"
|
|
37
37
|
import type { RealtimeSessionHandle } from "@effect-uai/core/RealtimeSession"
|
|
38
38
|
import * as WebSocketSession from "@effect-uai/core/WebSocketSession"
|
|
39
|
-
import {
|
|
39
|
+
import { functionResponseBody, toolDescriptorsToTools } from "./codec.js"
|
|
40
40
|
import type { GeminiLiveModel } from "./models.js"
|
|
41
41
|
|
|
42
42
|
const PROVIDER = "gemini-live"
|
|
@@ -383,7 +383,7 @@ const usageOf = (metadata: typeof UsageMetadata.Type): Usage => ({
|
|
|
383
383
|
})
|
|
384
384
|
|
|
385
385
|
const decodeAudio = (b64: string): Effect.Effect<Uint8Array, AiError.AiError> =>
|
|
386
|
-
Result.match(
|
|
386
|
+
Result.match(Base64.decode(b64), {
|
|
387
387
|
onSuccess: Effect.succeed,
|
|
388
388
|
onFailure: (cause) =>
|
|
389
389
|
Effect.fail(
|
|
@@ -555,7 +555,7 @@ const videoPart = (
|
|
|
555
555
|
Match.value(frame).pipe(
|
|
556
556
|
Match.tag("base64", (s) => Effect.succeed({ data: s.base64, mimeType: s.mimeType })),
|
|
557
557
|
Match.tag("bytes", (s) =>
|
|
558
|
-
Effect.succeed({ data:
|
|
558
|
+
Effect.succeed({ data: Base64.encode(s.bytes), mimeType: s.mimeType }),
|
|
559
559
|
),
|
|
560
560
|
Match.tag("url", () =>
|
|
561
561
|
Effect.fail(
|
|
@@ -596,7 +596,7 @@ const handleInput = (
|
|
|
596
596
|
JSON.stringify({
|
|
597
597
|
realtimeInput: {
|
|
598
598
|
audio: {
|
|
599
|
-
data:
|
|
599
|
+
data: Base64.encode(i.bytes),
|
|
600
600
|
mimeType: `audio/pcm;rate=${options.rate}`,
|
|
601
601
|
},
|
|
602
602
|
},
|
|
@@ -635,7 +635,7 @@ const handleInput = (
|
|
|
635
635
|
{
|
|
636
636
|
id: i.output.call_id,
|
|
637
637
|
name,
|
|
638
|
-
|
|
638
|
+
...functionResponseBody(i.output),
|
|
639
639
|
scheduling: options.scheduling,
|
|
640
640
|
},
|
|
641
641
|
],
|