@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.
Files changed (70) hide show
  1. package/README.md +4 -4
  2. package/dist/Gemini.d.mts +1 -1
  3. package/dist/Gemini.mjs +1 -1
  4. package/dist/Gemini.mjs.map +1 -1
  5. package/dist/GeminiEmbedding.d.mts +1 -1
  6. package/dist/GeminiEmbedding.d.mts.map +1 -1
  7. package/dist/GeminiEmbedding.mjs +4 -3
  8. package/dist/GeminiEmbedding.mjs.map +1 -1
  9. package/dist/GeminiImageGenerator.d.mts +1 -1
  10. package/dist/GeminiImageGenerator.d.mts.map +1 -1
  11. package/dist/GeminiImageGenerator.mjs +4 -3
  12. package/dist/GeminiImageGenerator.mjs.map +1 -1
  13. package/dist/GeminiLiveSession.d.mts +1 -1
  14. package/dist/GeminiSynthesizer.d.mts +1 -1
  15. package/dist/GeminiSynthesizer.mjs +1 -1
  16. package/dist/GeminiSynthesizer.mjs.map +1 -1
  17. package/dist/GeminiSynthesizer.test.mjs +1 -1
  18. package/dist/GoogleDeepResearch.d.mts +1 -1
  19. package/dist/GoogleDeepResearch.mjs +1 -1
  20. package/dist/GoogleDeepResearch.mjs.map +1 -1
  21. package/dist/GoogleDeepResearch.test.mjs +1 -1
  22. package/dist/LyriaGenerator.d.mts +1 -1
  23. package/dist/LyriaGenerator.d.mts.map +1 -1
  24. package/dist/LyriaGenerator.mjs +4 -3
  25. package/dist/LyriaGenerator.mjs.map +1 -1
  26. package/dist/LyriaGenerator.test.mjs +4 -142
  27. package/dist/LyriaGenerator.test.mjs.map +1 -1
  28. package/dist/codec.d.mts +28 -7
  29. package/dist/codec.d.mts.map +1 -1
  30. package/dist/codec.mjs +55 -12
  31. package/dist/codec.mjs.map +1 -1
  32. package/dist/codec.test.d.mts +1 -0
  33. package/dist/codec.test.mjs +61 -0
  34. package/dist/codec.test.mjs.map +1 -0
  35. package/dist/dist-DHer1cql.mjs +1533 -0
  36. package/dist/dist-DHer1cql.mjs.map +1 -0
  37. package/dist/geminiSpeechCodec.d.mts.map +1 -1
  38. package/dist/geminiSpeechCodec.mjs +4 -3
  39. package/dist/geminiSpeechCodec.mjs.map +1 -1
  40. package/dist/geminiSpeechCodec.test.mjs +5 -4
  41. package/dist/geminiSpeechCodec.test.mjs.map +1 -1
  42. package/dist/geminiTools.test.mjs +2 -1
  43. package/dist/geminiTools.test.mjs.map +1 -1
  44. package/dist/{test.DNmyFkvJ-BPz3G1HS.mjs → index.C4tKZ0yW-DjxCdpW_.mjs} +9115 -5993
  45. package/dist/index.C4tKZ0yW-DjxCdpW_.mjs.map +1 -0
  46. package/dist/{realtimeSession-7HSIjWh4.d.mts → realtimeSession-BlBgLDdp.d.mts} +2 -2
  47. package/dist/{realtimeSession-7HSIjWh4.d.mts.map → realtimeSession-BlBgLDdp.d.mts.map} +1 -1
  48. package/dist/realtimeSession.d.mts +1 -1
  49. package/dist/realtimeSession.mjs +8 -7
  50. package/dist/realtimeSession.mjs.map +1 -1
  51. package/dist/realtimeSession.test.mjs +120 -98
  52. package/dist/realtimeSession.test.mjs.map +1 -1
  53. package/package.json +5 -5
  54. package/src/Gemini.ts +1 -1
  55. package/src/GeminiEmbedding.ts +4 -3
  56. package/src/GeminiImageGenerator.ts +4 -3
  57. package/src/GeminiSynthesizer.ts +1 -1
  58. package/src/GoogleDeepResearch.ts +1 -1
  59. package/src/LyriaGenerator.test.ts +1 -1
  60. package/src/LyriaGenerator.ts +4 -14
  61. package/src/codec.test.ts +54 -0
  62. package/src/codec.ts +93 -23
  63. package/src/geminiSpeechCodec.test.ts +4 -3
  64. package/src/geminiSpeechCodec.ts +4 -3
  65. package/src/geminiTools.test.ts +1 -0
  66. package/src/realtimeSession.test.ts +9 -8
  67. package/src/realtimeSession.ts +7 -7
  68. package/dist/magic-string.es-CUAnjKYC.mjs +0 -1014
  69. package/dist/magic-string.es-CUAnjKYC.mjs.map +0 -1
  70. package/dist/test.DNmyFkvJ-BPz3G1HS.mjs.map +0 -1
@@ -1,5 +1,6 @@
1
- import { Context, Effect, Encoding, Layer, Match, Redacted, Schema } from "effect"
2
- import { HttpClient, HttpClientRequest } from "effect/unstable/http"
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: Encoding.encodeBase64(s.bytes) },
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, Encoding, Layer, Match, Redacted, Schema, Stream } from "effect"
2
- import { HttpClient, HttpClientRequest } from "effect/unstable/http"
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: Encoding.encodeBase64(i.bytes) },
133
+ inlineData: { mimeType: i.mimeType, data: Base64.encode(i.bytes) },
133
134
  }),
134
135
  ),
135
136
  Match.tag("url", () => Effect.fail(urlRefNotSupported)),
@@ -1,5 +1,5 @@
1
1
  import { Context, Effect, Layer, Match, Redacted, Result, Schema, Stream } from "effect"
2
- import { HttpClient, HttpClientRequest } from "effect/unstable/http"
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/unstable/http"
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/unstable/http"
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"
@@ -1,16 +1,6 @@
1
- import {
2
- Context,
3
- Duration,
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(Encoding.decodeBase64(b64), {
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, Encoding, Match, Option, Result, Schema, pipe } from "effect"
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: Encoding.encodeBase64(s.bytes) } },
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` and `ToolCallOutput.output` are JSON-encoded
298
- // strings; Gemini's `functionCall.args` and `functionResponse.response`
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 parsedJsonObject =
309
- (fallback: (raw: string) => Record<string, unknown>) =>
310
- (encoded: string): Record<string, unknown> =>
311
- pipe(
312
- decodeJsonObject(encoded),
313
- Result.match({
314
- onSuccess: (v) => v,
315
- onFailure: () => fallback(encoded),
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
- const parsedArgs = parsedJsonObject(() => ({}))
320
- /** A `ToolCallOutput.output` string as the object `functionResponse` wants. */
321
- export const parsedResponse = parsedJsonObject((raw) => ({ output: raw }))
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
- response: parsedResponse(o.output),
495
+ ...functionResponseBody(o),
426
496
  },
427
497
  },
428
498
  ],
@@ -1,4 +1,5 @@
1
- import { Effect, Encoding } from "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(Encoding.encodeBase64(original))
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 = Encoding.encodeBase64(new Uint8Array([1, 2, 3]))
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")
@@ -1,4 +1,5 @@
1
- import { Effect, Encoding, Match, Result } from "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: Encoding.encodeBase64(a.bytes),
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(Encoding.decodeBase64(b64), {
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 })),
@@ -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, Encoding, Fiber, Logger, Redacted, Ref, Stream } from "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: Encoding.encodeBase64(new Uint8Array(bytes)),
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: '{"celsius":32}',
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: '{"now":"12:00"}',
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: Encoding.encodeBase64(new Uint8Array([1, 2])), mimeType: "image/jpeg" },
410
+ video: { data: Base64.encode(new Uint8Array([1, 2])), mimeType: "image/jpeg" },
410
411
  },
411
412
  },
412
413
  ])
@@ -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 * as Socket from "effect/unstable/socket/Socket"
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 { parsedResponse, toolDescriptorsToTools } from "./codec.js"
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(Encoding.decodeBase64(b64), {
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: Encoding.encodeBase64(s.bytes), mimeType: s.mimeType }),
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: Encoding.encodeBase64(i.bytes),
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
- response: parsedResponse(i.output.output),
638
+ ...functionResponseBody(i.output),
639
639
  scheduling: options.scheduling,
640
640
  },
641
641
  ],