@effect-uai/google 0.2.0 → 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.
Files changed (66) hide show
  1. package/dist/Gemini.d.mts +105 -0
  2. package/dist/Gemini.d.mts.map +1 -0
  3. package/dist/Gemini.mjs +170 -0
  4. package/dist/Gemini.mjs.map +1 -0
  5. package/dist/GeminiEmbedding.d.mts +64 -0
  6. package/dist/GeminiEmbedding.d.mts.map +1 -0
  7. package/dist/GeminiEmbedding.mjs +167 -0
  8. package/dist/GeminiEmbedding.mjs.map +1 -0
  9. package/dist/GeminiSynthesizer.d.mts +48 -0
  10. package/dist/GeminiSynthesizer.d.mts.map +1 -0
  11. package/dist/GeminiSynthesizer.mjs +95 -0
  12. package/dist/GeminiSynthesizer.mjs.map +1 -0
  13. package/dist/GeminiSynthesizer.test.d.mts +1 -0
  14. package/dist/GeminiSynthesizer.test.mjs +84 -0
  15. package/dist/GeminiSynthesizer.test.mjs.map +1 -0
  16. package/dist/GeminiTranscriber.d.mts +42 -0
  17. package/dist/GeminiTranscriber.d.mts.map +1 -0
  18. package/dist/GeminiTranscriber.mjs +80 -0
  19. package/dist/GeminiTranscriber.mjs.map +1 -0
  20. package/dist/GeminiTranscriber.test.d.mts +1 -0
  21. package/dist/GeminiTranscriber.test.mjs +125 -0
  22. package/dist/GeminiTranscriber.test.mjs.map +1 -0
  23. package/dist/LyriaGenerator.d.mts +57 -0
  24. package/dist/LyriaGenerator.d.mts.map +1 -0
  25. package/dist/LyriaGenerator.mjs +202 -0
  26. package/dist/LyriaGenerator.mjs.map +1 -0
  27. package/dist/LyriaGenerator.test.d.mts +1 -0
  28. package/dist/LyriaGenerator.test.mjs +138 -0
  29. package/dist/LyriaGenerator.test.mjs.map +1 -0
  30. package/dist/{chunk-CfYAbeIz.mjs → chunk-uyGKjUfl.mjs} +2 -1
  31. package/dist/codec.d.mts +107 -0
  32. package/dist/codec.d.mts.map +1 -0
  33. package/dist/codec.mjs +151 -0
  34. package/dist/codec.mjs.map +1 -0
  35. package/dist/dist-DMsg8YLb.mjs +13782 -0
  36. package/dist/dist-DMsg8YLb.mjs.map +1 -0
  37. package/dist/geminiSpeechCodec.d.mts +37 -0
  38. package/dist/geminiSpeechCodec.d.mts.map +1 -0
  39. package/dist/geminiSpeechCodec.mjs +110 -0
  40. package/dist/geminiSpeechCodec.mjs.map +1 -0
  41. package/dist/geminiSpeechCodec.test.d.mts +1 -0
  42. package/dist/geminiSpeechCodec.test.mjs +78 -0
  43. package/dist/geminiSpeechCodec.test.mjs.map +1 -0
  44. package/dist/index.d.mts +8 -214
  45. package/dist/index.mjs +8 -289
  46. package/dist/magic-string.es-CaWgF-3p.mjs +1013 -0
  47. package/dist/magic-string.es-CaWgF-3p.mjs.map +1 -0
  48. package/dist/models.d.mts +61 -0
  49. package/dist/models.d.mts.map +1 -0
  50. package/dist/models.mjs +1 -0
  51. package/package.json +32 -12
  52. package/src/Gemini.ts +3 -3
  53. package/src/GeminiEmbedding.ts +351 -0
  54. package/src/GeminiSynthesizer.test.ts +93 -0
  55. package/src/GeminiSynthesizer.ts +214 -0
  56. package/src/GeminiTranscriber.test.ts +136 -0
  57. package/src/GeminiTranscriber.ts +204 -0
  58. package/src/LyriaGenerator.test.ts +162 -0
  59. package/src/LyriaGenerator.ts +394 -0
  60. package/src/codec.ts +42 -35
  61. package/src/geminiSpeechCodec.test.ts +56 -0
  62. package/src/geminiSpeechCodec.ts +137 -0
  63. package/src/index.ts +6 -2
  64. package/src/models.ts +104 -0
  65. package/dist/index.d.mts.map +0 -1
  66. package/dist/index.mjs.map +0 -1
@@ -0,0 +1,37 @@
1
+ import { Effect } from "effect";
2
+ import * as AiError from "@effect-uai/core/AiError";
3
+ import { AudioSource } from "@effect-uai/core/Audio";
4
+
5
+ //#region src/geminiSpeechCodec.d.ts
6
+ /** Wire shape for an inline-audio part on `generateContent`. */
7
+ type InlineAudioData = {
8
+ readonly mimeType: string;
9
+ readonly data: string;
10
+ };
11
+ /**
12
+ * Encode any `AudioSource` into the `inlineData` shape expected by
13
+ * Gemini's `generateContent`. URL variants are rejected with
14
+ * `InvalidRequest` — Files API upload is a separate flow.
15
+ *
16
+ * Caller is responsible for keeping the total request body under
17
+ * 20 MB. Beyond that, Gemini's Files API is the only path.
18
+ */
19
+ declare const audioSourceToInlineData: (audio: AudioSource) => Effect.Effect<InlineAudioData, AiError.AiError>;
20
+ /** Decode a base64-encoded audio payload into bytes. */
21
+ declare const decodeBase64Audio: (b64: string) => Effect.Effect<Uint8Array, AiError.AiError>;
22
+ /**
23
+ * Wrap a raw 16-bit signed little-endian PCM buffer in a standard 44-byte
24
+ * RIFF/WAVE header. Gemini TTS returns PCM-only — for playback or for
25
+ * round-tripping into Gemini transcription (which accepts wav, not raw
26
+ * PCM), the bytes need a container.
27
+ */
28
+ declare const wrapPcmAsWav: (pcm: Uint8Array, sampleRate: number, channels: number, bitsPerSample?: number) => Uint8Array;
29
+ /**
30
+ * HTTP status → typed `AiError`. Shared between synthesizer and
31
+ * transcriber.
32
+ */
33
+ declare const httpStatusError: (status: number, body: string) => AiError.AiError;
34
+ declare const transportFailure: (cause: unknown) => AiError.AiError;
35
+ //#endregion
36
+ export { InlineAudioData, audioSourceToInlineData, decodeBase64Audio, httpStatusError, transportFailure, wrapPcmAsWav };
37
+ //# sourceMappingURL=geminiSpeechCodec.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geminiSpeechCodec.d.mts","names":[],"sources":["../src/geminiSpeechCodec.ts"],"mappings":";;;;;;KAiBY,eAAA;EAAA,SACD,QAAA;EAAA,SACA,IAAA;AAAA;;;AAWX;;;;;;cAAa,uBAAA,GACX,KAAA,EAAO,WAAA,KACJ,MAAA,CAAO,MAAA,CAAO,eAAA,EAAiB,OAAA,CAAQ,OAAA;;cAe/B,iBAAA,GAAqB,GAAA,aAAc,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,CAAQ,OAAA;;;;;;;cAarE,YAAA,GACX,GAAA,EAAK,UAAA,EACL,UAAA,UACA,QAAA,UACA,aAAA,cACC,UAAA;;;AAlBH;;cAiDa,eAAA,GAAkB,MAAA,UAAgB,IAAA,aAAiB,OAAA,CAAQ,OAAA;AAAA,cAuC3D,gBAAA,GAAoB,KAAA,cAAiB,OAAA,CAAQ,OAAA"}
@@ -0,0 +1,110 @@
1
+ import { Effect, Encoding, Match, Result } from "effect";
2
+ import * as AiError from "@effect-uai/core/AiError";
3
+ //#region src/geminiSpeechCodec.ts
4
+ /**
5
+ * Shared codec helpers for Gemini speech endpoints. The TTS and STT
6
+ * variants both ride on `:generateContent`, so the audio-in / audio-out
7
+ * shapes are siblings.
8
+ */
9
+ const urlNotSupported = new AiError.InvalidRequest({
10
+ provider: "gemini",
11
+ param: "audio",
12
+ raw: "Gemini inline audio does not accept URL sources. Either fetch the URL yourself and pass `{ _tag: \"bytes\", bytes, mimeType }`, or upload via the Files API and pass a `fileData` URI (not yet wired here)."
13
+ });
14
+ /**
15
+ * Encode any `AudioSource` into the `inlineData` shape expected by
16
+ * Gemini's `generateContent`. URL variants are rejected with
17
+ * `InvalidRequest` — Files API upload is a separate flow.
18
+ *
19
+ * Caller is responsible for keeping the total request body under
20
+ * 20 MB. Beyond that, Gemini's Files API is the only path.
21
+ */
22
+ const audioSourceToInlineData = Match.type().pipe(Match.tag("bytes", (a) => Effect.succeed({
23
+ mimeType: a.mimeType,
24
+ data: Encoding.encodeBase64(a.bytes)
25
+ })), Match.tag("base64", (a) => Effect.succeed({
26
+ mimeType: a.mimeType,
27
+ data: a.base64
28
+ })), Match.tag("url", () => Effect.fail(urlNotSupported)), Match.exhaustive);
29
+ /** Decode a base64-encoded audio payload into bytes. */
30
+ const decodeBase64Audio = (b64) => Result.match(Encoding.decodeBase64(b64), {
31
+ onSuccess: Effect.succeed,
32
+ onFailure: (cause) => Effect.fail(new AiError.InvalidRequest({
33
+ provider: "gemini",
34
+ param: "audio",
35
+ raw: cause
36
+ }))
37
+ });
38
+ /**
39
+ * Wrap a raw 16-bit signed little-endian PCM buffer in a standard 44-byte
40
+ * RIFF/WAVE header. Gemini TTS returns PCM-only — for playback or for
41
+ * round-tripping into Gemini transcription (which accepts wav, not raw
42
+ * PCM), the bytes need a container.
43
+ */
44
+ const wrapPcmAsWav = (pcm, sampleRate, channels, bitsPerSample = 16) => {
45
+ const dataSize = pcm.length;
46
+ const blockAlign = channels * bitsPerSample / 8;
47
+ const byteRate = sampleRate * blockAlign;
48
+ const totalSize = 44 + dataSize;
49
+ const buffer = new ArrayBuffer(totalSize);
50
+ const view = new DataView(buffer);
51
+ const writeAscii = (offset, s) => {
52
+ for (let i = 0; i < s.length; i++) view.setUint8(offset + i, s.charCodeAt(i));
53
+ };
54
+ writeAscii(0, "RIFF");
55
+ view.setUint32(4, totalSize - 8, true);
56
+ writeAscii(8, "WAVE");
57
+ writeAscii(12, "fmt ");
58
+ view.setUint32(16, 16, true);
59
+ view.setUint16(20, 1, true);
60
+ view.setUint16(22, channels, true);
61
+ view.setUint32(24, sampleRate, true);
62
+ view.setUint32(28, byteRate, true);
63
+ view.setUint16(32, blockAlign, true);
64
+ view.setUint16(34, bitsPerSample, true);
65
+ writeAscii(36, "data");
66
+ view.setUint32(40, dataSize, true);
67
+ new Uint8Array(buffer, 44, dataSize).set(pcm);
68
+ return new Uint8Array(buffer);
69
+ };
70
+ /**
71
+ * HTTP status → typed `AiError`. Shared between synthesizer and
72
+ * transcriber.
73
+ */
74
+ const httpStatusError = (status, body) => Match.value(status).pipe(Match.when(429, () => new AiError.RateLimited({
75
+ provider: "gemini",
76
+ raw: body
77
+ })), Match.whenOr(408, 504, () => new AiError.Timeout({
78
+ provider: "gemini",
79
+ raw: body
80
+ })), Match.when(401, () => new AiError.AuthFailed({
81
+ provider: "gemini",
82
+ subtype: "auth",
83
+ raw: body
84
+ })), Match.when(403, () => new AiError.AuthFailed({
85
+ provider: "gemini",
86
+ subtype: "permission",
87
+ raw: body
88
+ })), Match.when(402, () => new AiError.AuthFailed({
89
+ provider: "gemini",
90
+ subtype: "billing",
91
+ raw: body
92
+ })), Match.when(413, () => new AiError.ContextLengthExceeded({
93
+ provider: "gemini",
94
+ raw: body
95
+ })), Match.when((n) => n >= 500, (n) => new AiError.Unavailable({
96
+ provider: "gemini",
97
+ status: n,
98
+ raw: body
99
+ })), Match.orElse(() => new AiError.InvalidRequest({
100
+ provider: "gemini",
101
+ raw: body
102
+ })));
103
+ const transportFailure = (cause) => new AiError.Unavailable({
104
+ provider: "gemini",
105
+ raw: cause
106
+ });
107
+ //#endregion
108
+ export { audioSourceToInlineData, decodeBase64Audio, httpStatusError, transportFailure, wrapPcmAsWav };
109
+
110
+ //# sourceMappingURL=geminiSpeechCodec.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geminiSpeechCodec.mjs","names":[],"sources":["../src/geminiSpeechCodec.ts"],"sourcesContent":["import { Effect, Encoding, Match, Result } from \"effect\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioSource } from \"@effect-uai/core/Audio\"\n\n/**\n * Shared codec helpers for Gemini speech endpoints. The TTS and STT\n * variants both ride on `:generateContent`, so the audio-in / audio-out\n * shapes are siblings.\n */\n\nconst urlNotSupported: AiError.AiError = new AiError.InvalidRequest({\n provider: \"gemini\",\n param: \"audio\",\n raw: 'Gemini inline audio does not accept URL sources. Either fetch the URL yourself and pass `{ _tag: \"bytes\", bytes, mimeType }`, or upload via the Files API and pass a `fileData` URI (not yet wired here).',\n})\n\n/** Wire shape for an inline-audio part on `generateContent`. */\nexport type InlineAudioData = {\n readonly mimeType: string\n readonly data: string\n}\n\n/**\n * Encode any `AudioSource` into the `inlineData` shape expected by\n * Gemini's `generateContent`. URL variants are rejected with\n * `InvalidRequest` — Files API upload is a separate flow.\n *\n * Caller is responsible for keeping the total request body under\n * 20 MB. Beyond that, Gemini's Files API is the only path.\n */\nexport const audioSourceToInlineData: (\n audio: AudioSource,\n) => Effect.Effect<InlineAudioData, AiError.AiError> = Match.type<AudioSource>().pipe(\n Match.tag(\"bytes\", (a) =>\n Effect.succeed<InlineAudioData>({\n mimeType: a.mimeType,\n data: Encoding.encodeBase64(a.bytes),\n }),\n ),\n Match.tag(\"base64\", (a) =>\n Effect.succeed<InlineAudioData>({ mimeType: a.mimeType, data: a.base64 }),\n ),\n Match.tag(\"url\", () => Effect.fail(urlNotSupported)),\n Match.exhaustive,\n)\n\n/** Decode a base64-encoded audio payload into bytes. */\nexport const decodeBase64Audio = (b64: string): Effect.Effect<Uint8Array, AiError.AiError> =>\n Result.match(Encoding.decodeBase64(b64), {\n onSuccess: Effect.succeed,\n onFailure: (cause) =>\n Effect.fail(new AiError.InvalidRequest({ provider: \"gemini\", param: \"audio\", raw: cause })),\n })\n\n/**\n * Wrap a raw 16-bit signed little-endian PCM buffer in a standard 44-byte\n * RIFF/WAVE header. Gemini TTS returns PCM-only — for playback or for\n * round-tripping into Gemini transcription (which accepts wav, not raw\n * PCM), the bytes need a container.\n */\nexport const wrapPcmAsWav = (\n pcm: Uint8Array,\n sampleRate: number,\n channels: number,\n bitsPerSample = 16,\n): Uint8Array => {\n const dataSize = pcm.length\n const blockAlign = (channels * bitsPerSample) / 8\n const byteRate = sampleRate * blockAlign\n const totalSize = 44 + dataSize\n const buffer = new ArrayBuffer(totalSize)\n const view = new DataView(buffer)\n const writeAscii = (offset: number, s: string): void => {\n for (let i = 0; i < s.length; i++) view.setUint8(offset + i, s.charCodeAt(i))\n }\n writeAscii(0, \"RIFF\")\n view.setUint32(4, totalSize - 8, true)\n writeAscii(8, \"WAVE\")\n writeAscii(12, \"fmt \")\n view.setUint32(16, 16, true)\n view.setUint16(20, 1, true)\n view.setUint16(22, channels, true)\n view.setUint32(24, sampleRate, true)\n view.setUint32(28, byteRate, true)\n view.setUint16(32, blockAlign, true)\n view.setUint16(34, bitsPerSample, true)\n writeAscii(36, \"data\")\n view.setUint32(40, dataSize, true)\n new Uint8Array(buffer, 44, dataSize).set(pcm)\n return new Uint8Array(buffer)\n}\n\n/**\n * HTTP status → typed `AiError`. Shared between synthesizer and\n * transcriber.\n */\nexport const httpStatusError: (status: number, body: string) => AiError.AiError = (status, body) =>\n Match.value(status).pipe(\n Match.when(\n 429,\n (): AiError.AiError => new AiError.RateLimited({ provider: \"gemini\", raw: body }),\n ),\n Match.whenOr(\n 408,\n 504,\n (): AiError.AiError => new AiError.Timeout({ provider: \"gemini\", raw: body }),\n ),\n Match.when(\n 401,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"gemini\", subtype: \"auth\", raw: body }),\n ),\n Match.when(\n 403,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"gemini\", subtype: \"permission\", raw: body }),\n ),\n Match.when(\n 402,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"gemini\", subtype: \"billing\", raw: body }),\n ),\n Match.when(\n 413,\n (): AiError.AiError => new AiError.ContextLengthExceeded({ provider: \"gemini\", raw: body }),\n ),\n Match.when(\n (n) => n >= 500,\n (n): AiError.AiError => new AiError.Unavailable({ provider: \"gemini\", status: n, raw: body }),\n ),\n Match.orElse(\n (): AiError.AiError => new AiError.InvalidRequest({ provider: \"gemini\", raw: body }),\n ),\n )\n\nexport const transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"gemini\", raw: cause })\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,78 @@
1
+ import { i as it, n as globalExpect, r as describe } from "./dist-DMsg8YLb.mjs";
2
+ import { audioSourceToInlineData, wrapPcmAsWav } from "./geminiSpeechCodec.mjs";
3
+ import { Effect, Encoding } from "effect";
4
+ //#region src/geminiSpeechCodec.test.ts
5
+ describe("audioSourceToInlineData", () => {
6
+ it("encodes a `bytes` source to base64 with its MIME type", async () => {
7
+ const original = new Uint8Array([
8
+ 222,
9
+ 173,
10
+ 190,
11
+ 239
12
+ ]);
13
+ const source = {
14
+ _tag: "bytes",
15
+ bytes: original,
16
+ mimeType: "audio/wav"
17
+ };
18
+ const inline = await Effect.runPromise(audioSourceToInlineData(source));
19
+ globalExpect(inline.mimeType).toBe("audio/wav");
20
+ globalExpect(inline.data).toBe(Encoding.encodeBase64(original));
21
+ });
22
+ it("passes a `base64` source through unchanged", async () => {
23
+ const b64 = Encoding.encodeBase64(new Uint8Array([
24
+ 1,
25
+ 2,
26
+ 3
27
+ ]));
28
+ const source = {
29
+ _tag: "base64",
30
+ base64: b64,
31
+ mimeType: "audio/mp3"
32
+ };
33
+ const inline = await Effect.runPromise(audioSourceToInlineData(source));
34
+ globalExpect(inline.mimeType).toBe("audio/mp3");
35
+ globalExpect(inline.data).toBe(b64);
36
+ });
37
+ it("fails InvalidRequest for a URL source", async () => {
38
+ const exit = await Effect.runPromiseExit(audioSourceToInlineData({
39
+ _tag: "url",
40
+ url: "https://x/y.mp3",
41
+ mimeType: "audio/mp3"
42
+ }));
43
+ globalExpect(exit._tag).toBe("Failure");
44
+ if (exit._tag === "Failure") {
45
+ globalExpect(JSON.stringify(exit.cause)).toContain("InvalidRequest");
46
+ globalExpect(JSON.stringify(exit.cause)).toContain("Files API");
47
+ }
48
+ });
49
+ });
50
+ describe("wrapPcmAsWav", () => {
51
+ it("produces a 44-byte RIFF/WAVE header for 16-bit mono PCM at 24 kHz", () => {
52
+ const pcm = new Uint8Array([
53
+ 0,
54
+ 1,
55
+ 0,
56
+ 2
57
+ ]);
58
+ const wav = wrapPcmAsWav(pcm, 24e3, 1, 16);
59
+ globalExpect(wav.length).toBe(44 + pcm.length);
60
+ const ascii = (start, len) => String.fromCharCode(...Array.from(wav.slice(start, start + len)));
61
+ globalExpect(ascii(0, 4)).toBe("RIFF");
62
+ globalExpect(ascii(8, 4)).toBe("WAVE");
63
+ globalExpect(ascii(12, 4)).toBe("fmt ");
64
+ globalExpect(ascii(36, 4)).toBe("data");
65
+ const view = new DataView(wav.buffer);
66
+ globalExpect(view.getUint32(4, true)).toBe(wav.length - 8);
67
+ globalExpect(view.getUint16(20, true)).toBe(1);
68
+ globalExpect(view.getUint16(22, true)).toBe(1);
69
+ globalExpect(view.getUint32(24, true)).toBe(24e3);
70
+ globalExpect(view.getUint16(34, true)).toBe(16);
71
+ globalExpect(view.getUint32(40, true)).toBe(pcm.length);
72
+ globalExpect(Array.from(wav.slice(44))).toEqual(Array.from(pcm));
73
+ });
74
+ });
75
+ //#endregion
76
+ export {};
77
+
78
+ //# sourceMappingURL=geminiSpeechCodec.test.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geminiSpeechCodec.test.mjs","names":[],"sources":["../src/geminiSpeechCodec.test.ts"],"sourcesContent":["import { Effect, Encoding } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport type { AudioSource } from \"@effect-uai/core/Audio\"\nimport { audioSourceToInlineData, wrapPcmAsWav } from \"./geminiSpeechCodec.js\"\n\ndescribe(\"audioSourceToInlineData\", () => {\n it(\"encodes a `bytes` source to base64 with its MIME type\", async () => {\n const original = new Uint8Array([0xde, 0xad, 0xbe, 0xef])\n const source: AudioSource = { _tag: \"bytes\", bytes: original, mimeType: \"audio/wav\" }\n const inline = await Effect.runPromise(audioSourceToInlineData(source))\n expect(inline.mimeType).toBe(\"audio/wav\")\n expect(inline.data).toBe(Encoding.encodeBase64(original))\n })\n\n it(\"passes a `base64` source through unchanged\", async () => {\n const b64 = Encoding.encodeBase64(new Uint8Array([1, 2, 3]))\n const source: AudioSource = { _tag: \"base64\", base64: b64, mimeType: \"audio/mp3\" }\n const inline = await Effect.runPromise(audioSourceToInlineData(source))\n expect(inline.mimeType).toBe(\"audio/mp3\")\n expect(inline.data).toBe(b64)\n })\n\n it(\"fails InvalidRequest for a URL source\", async () => {\n const source: AudioSource = { _tag: \"url\", url: \"https://x/y.mp3\", mimeType: \"audio/mp3\" }\n const exit = await Effect.runPromiseExit(audioSourceToInlineData(source))\n expect(exit._tag).toBe(\"Failure\")\n if (exit._tag === \"Failure\") {\n expect(JSON.stringify(exit.cause)).toContain(\"InvalidRequest\")\n expect(JSON.stringify(exit.cause)).toContain(\"Files API\")\n }\n })\n})\n\ndescribe(\"wrapPcmAsWav\", () => {\n it(\"produces a 44-byte RIFF/WAVE header for 16-bit mono PCM at 24 kHz\", () => {\n const pcm = new Uint8Array([0x00, 0x01, 0x00, 0x02])\n const wav = wrapPcmAsWav(pcm, 24000, 1, 16)\n expect(wav.length).toBe(44 + pcm.length)\n const ascii = (start: number, len: number) =>\n String.fromCharCode(...Array.from(wav.slice(start, start + len)))\n expect(ascii(0, 4)).toBe(\"RIFF\")\n expect(ascii(8, 4)).toBe(\"WAVE\")\n expect(ascii(12, 4)).toBe(\"fmt \")\n expect(ascii(36, 4)).toBe(\"data\")\n // RIFF chunk size = total - 8\n const view = new DataView(wav.buffer)\n expect(view.getUint32(4, true)).toBe(wav.length - 8)\n expect(view.getUint16(20, true)).toBe(1) // PCM\n expect(view.getUint16(22, true)).toBe(1) // mono\n expect(view.getUint32(24, true)).toBe(24000) // sample rate\n expect(view.getUint16(34, true)).toBe(16) // bits per sample\n expect(view.getUint32(40, true)).toBe(pcm.length) // data size\n // Payload preserved\n expect(Array.from(wav.slice(44))).toEqual(Array.from(pcm))\n })\n})\n"],"mappings":";;;;AAKA,SAAS,iCAAiC;AACxC,IAAG,yDAAyD,YAAY;EACtE,MAAM,WAAW,IAAI,WAAW;GAAC;GAAM;GAAM;GAAM;GAAK,CAAC;EACzD,MAAM,SAAsB;GAAE,MAAM;GAAS,OAAO;GAAU,UAAU;GAAa;EACrF,MAAM,SAAS,MAAM,OAAO,WAAW,wBAAwB,OAAO,CAAC;AACvE,eAAO,OAAO,SAAS,CAAC,KAAK,YAAY;AACzC,eAAO,OAAO,KAAK,CAAC,KAAK,SAAS,aAAa,SAAS,CAAC;GACzD;AAEF,IAAG,8CAA8C,YAAY;EAC3D,MAAM,MAAM,SAAS,aAAa,IAAI,WAAW;GAAC;GAAG;GAAG;GAAE,CAAC,CAAC;EAC5D,MAAM,SAAsB;GAAE,MAAM;GAAU,QAAQ;GAAK,UAAU;GAAa;EAClF,MAAM,SAAS,MAAM,OAAO,WAAW,wBAAwB,OAAO,CAAC;AACvE,eAAO,OAAO,SAAS,CAAC,KAAK,YAAY;AACzC,eAAO,OAAO,KAAK,CAAC,KAAK,IAAI;GAC7B;AAEF,IAAG,yCAAyC,YAAY;EAEtD,MAAM,OAAO,MAAM,OAAO,eAAe,wBAAwB;GADnC,MAAM;GAAO,KAAK;GAAmB,UAAU;GACN,CAAC,CAAC;AACzE,eAAO,KAAK,KAAK,CAAC,KAAK,UAAU;AACjC,MAAI,KAAK,SAAS,WAAW;AAC3B,gBAAO,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC,UAAU,iBAAiB;AAC9D,gBAAO,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC,UAAU,YAAY;;GAE3D;EACF;AAEF,SAAS,sBAAsB;AAC7B,IAAG,2EAA2E;EAC5E,MAAM,MAAM,IAAI,WAAW;GAAC;GAAM;GAAM;GAAM;GAAK,CAAC;EACpD,MAAM,MAAM,aAAa,KAAK,MAAO,GAAG,GAAG;AAC3C,eAAO,IAAI,OAAO,CAAC,KAAK,KAAK,IAAI,OAAO;EACxC,MAAM,SAAS,OAAe,QAC5B,OAAO,aAAa,GAAG,MAAM,KAAK,IAAI,MAAM,OAAO,QAAQ,IAAI,CAAC,CAAC;AACnE,eAAO,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK,OAAO;AAChC,eAAO,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK,OAAO;AAChC,eAAO,MAAM,IAAI,EAAE,CAAC,CAAC,KAAK,OAAO;AACjC,eAAO,MAAM,IAAI,EAAE,CAAC,CAAC,KAAK,OAAO;EAEjC,MAAM,OAAO,IAAI,SAAS,IAAI,OAAO;AACrC,eAAO,KAAK,UAAU,GAAG,KAAK,CAAC,CAAC,KAAK,IAAI,SAAS,EAAE;AACpD,eAAO,KAAK,UAAU,IAAI,KAAK,CAAC,CAAC,KAAK,EAAE;AACxC,eAAO,KAAK,UAAU,IAAI,KAAK,CAAC,CAAC,KAAK,EAAE;AACxC,eAAO,KAAK,UAAU,IAAI,KAAK,CAAC,CAAC,KAAK,KAAM;AAC5C,eAAO,KAAK,UAAU,IAAI,KAAK,CAAC,CAAC,KAAK,GAAG;AACzC,eAAO,KAAK,UAAU,IAAI,KAAK,CAAC,CAAC,KAAK,IAAI,OAAO;AAEjD,eAAO,MAAM,KAAK,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,QAAQ,MAAM,KAAK,IAAI,CAAC;GAC1D;EACF"}
package/dist/index.d.mts CHANGED
@@ -1,214 +1,8 @@
1
- import { Context, Effect, Layer, Option, Redacted, Schema, Stream } from "effect";
2
- import { HttpClient } from "effect/unstable/http";
3
- import * as AiError from "@effect-uai/core/AiError";
4
- import { CommonRequest, LanguageModel } from "@effect-uai/core/LanguageModel";
5
- import { Turn, TurnEvent } from "@effect-uai/core/Turn";
6
- import { Item } from "@effect-uai/core/Items";
7
-
8
- //#region src/codec.d.ts
9
- declare namespace codec_d_exports {
10
- export { Accumulator, ChunkPart, ChunkResult, GenerationConfig, RequestBody, ThinkingConfig, WireChunk, accumulatorToTurn, buildRequestBody, emptyAccumulator, ingestChunk };
11
- }
12
- declare const WireChunk: Schema.Struct<{
13
- readonly candidates: Schema.optional<Schema.$Array<Schema.Struct<{
14
- readonly content: Schema.optional<Schema.Struct<{
15
- readonly role: Schema.optional<Schema.String>;
16
- readonly parts: Schema.optional<Schema.$Array<Schema.Union<readonly [Schema.Struct<{
17
- readonly text: Schema.String;
18
- /**
19
- * Gemini's flag for chain-of-thought parts. When `true`, the part is
20
- * reasoning trace, not the model's user-facing answer. Maps onto
21
- * `reasoning_delta { kind: "trace" }` in the canonical view.
22
- */
23
- readonly thought: Schema.optional<Schema.Boolean>;
24
- }>]>>>;
25
- }>>;
26
- readonly finishReason: Schema.optional<Schema.String>;
27
- readonly index: Schema.optional<Schema.Number>;
28
- }>>>;
29
- readonly usageMetadata: Schema.optional<Schema.Struct<{
30
- readonly promptTokenCount: Schema.optional<Schema.Number>;
31
- readonly candidatesTokenCount: Schema.optional<Schema.Number>;
32
- readonly totalTokenCount: Schema.optional<Schema.Number>;
33
- readonly cachedContentTokenCount: Schema.optional<Schema.Number>;
34
- readonly thoughtsTokenCount: Schema.optional<Schema.Number>;
35
- }>>;
36
- }>;
37
- type WireChunk = typeof WireChunk.Type;
38
- type RequestPart = {
39
- readonly text: string;
40
- } | {
41
- readonly inlineData: {
42
- readonly mimeType: string;
43
- readonly data: string;
44
- };
45
- };
46
- interface RequestContent {
47
- readonly role: "user" | "model";
48
- readonly parts: ReadonlyArray<RequestPart>;
49
- }
50
- interface RequestSystemInstruction {
51
- readonly parts: ReadonlyArray<{
52
- readonly text: string;
53
- }>;
54
- }
55
- interface ThinkingConfig {
56
- readonly thinkingBudget: number;
57
- }
58
- interface GenerationConfig {
59
- readonly temperature?: number;
60
- readonly maxOutputTokens?: number;
61
- readonly topP?: number;
62
- readonly thinkingConfig?: ThinkingConfig;
63
- /** Set together with `responseJsonSchema` to constrain output to JSON. */
64
- readonly responseMimeType?: string;
65
- /** JSON Schema constraint on the output. */
66
- readonly responseJsonSchema?: Record<string, unknown>;
67
- }
68
- interface RequestBody {
69
- readonly contents: ReadonlyArray<RequestContent>;
70
- readonly systemInstruction?: RequestSystemInstruction;
71
- readonly generationConfig?: GenerationConfig;
72
- }
73
- declare const buildRequestBody: (history: ReadonlyArray<Item>, generationConfig: Option.Option<GenerationConfig>) => RequestBody;
74
- interface Accumulator {
75
- readonly text: string;
76
- readonly reasoning: string;
77
- readonly finishReason: Option.Option<string>;
78
- readonly usage: {
79
- readonly input_tokens?: number;
80
- readonly output_tokens?: number;
81
- readonly total_tokens?: number;
82
- readonly input_tokens_details?: {
83
- readonly cached_tokens?: number;
84
- };
85
- readonly output_tokens_details?: {
86
- readonly reasoning_tokens?: number;
87
- };
88
- };
89
- }
90
- declare const emptyAccumulator: Accumulator;
91
- /**
92
- * One part's worth of streamable text: either the model's answer
93
- * (`kind: "text"`) or chain-of-thought (`kind: "reasoning"`). Emitted
94
- * in wire order so consumers can interleave faithfully.
95
- */
96
- type ChunkPart = {
97
- readonly kind: "text";
98
- readonly text: string;
99
- } | {
100
- readonly kind: "reasoning";
101
- readonly text: string;
102
- };
103
- interface ChunkResult {
104
- readonly accumulator: Accumulator;
105
- readonly parts: ReadonlyArray<ChunkPart>;
106
- readonly finished: boolean;
107
- }
108
- declare const ingestChunk: (acc: Accumulator, chunk: WireChunk) => ChunkResult;
109
- declare const accumulatorToTurn: (acc: Accumulator) => Turn;
110
- //#endregion
111
- //#region src/models.d.ts
112
- /**
113
- * Known Gemini model identifiers (as of April 2026). The `(string & {})`
114
- * tail keeps autocomplete on the literals while still accepting any string,
115
- * so newly-released models work without an SDK update.
116
- *
117
- * Reference: https://ai.google.dev/gemini-api/docs/models
118
- */
119
- type GoogleModel = "gemini-3.1-pro-preview" | "gemini-3-flash-preview" | "gemini-3.1-flash-lite-preview" | "gemini-3.1-flash-live-preview" | "gemini-3.1-flash-tts-preview" | "gemini-2.5-pro" | "gemini-2.5-flash" | "gemini-2.5-flash-lite" | (string & {});
120
- //#endregion
121
- //#region src/Gemini.d.ts
122
- /**
123
- * Gemini's native event vocabulary. Aliased from `WireChunk` so the public
124
- * surface matches `@effect-uai/responses` and `@effect-uai/anthropic`. Each
125
- * `ProviderEvent` is a full `GenerateContentResponse` chunk (not a per-field
126
- * delta) - that's how Gemini's SSE works.
127
- */
128
- declare const ProviderEvent: Schema.Struct<{
129
- readonly candidates: Schema.optional<Schema.$Array<Schema.Struct<{
130
- readonly content: Schema.optional<Schema.Struct<{
131
- readonly role: Schema.optional<Schema.String>;
132
- readonly parts: Schema.optional<Schema.$Array<Schema.Union<readonly [Schema.Struct<{
133
- readonly text: Schema.String;
134
- readonly thought: Schema.optional<Schema.Boolean>;
135
- }>]>>>;
136
- }>>;
137
- readonly finishReason: Schema.optional<Schema.String>;
138
- readonly index: Schema.optional<Schema.Number>;
139
- }>>>;
140
- readonly usageMetadata: Schema.optional<Schema.Struct<{
141
- readonly promptTokenCount: Schema.optional<Schema.Number>;
142
- readonly candidatesTokenCount: Schema.optional<Schema.Number>;
143
- readonly totalTokenCount: Schema.optional<Schema.Number>;
144
- readonly cachedContentTokenCount: Schema.optional<Schema.Number>;
145
- readonly thoughtsTokenCount: Schema.optional<Schema.Number>;
146
- }>>;
147
- }>;
148
- type ProviderEvent = WireChunk;
149
- interface GeminiRequest extends Omit<CommonRequest, "model"> {
150
- /**
151
- * Narrows `CommonRequest.model` (`string`) to the typed `GoogleModel`
152
- * literal union for autocomplete.
153
- */
154
- readonly model: GoogleModel;
155
- /**
156
- * Gemini 2.5 thinking budget. `0` disables thinking entirely (lowest
157
- * latency); higher values let the model think longer. Forwarded as
158
- * `generationConfig.thinkingConfig.thinkingBudget`.
159
- */
160
- readonly thinkingBudget?: number;
161
- }
162
- interface GeminiService {
163
- /**
164
- * Stream the provider's native event vocabulary (post-SSE-decode). For
165
- * Gemini, each event is a full `GenerateContentResponse` chunk. Use this
166
- * when you need access to fields the canonical view doesn't surface
167
- * (`thoughtSignature`, granular `safetyRatings`, etc.). For
168
- * provider-portable code, use `streamTurn`.
169
- */
170
- readonly streamNative: (request: GeminiRequest) => Stream.Stream<ProviderEvent, AiError.AiError>;
171
- /**
172
- * Stream canonical `TurnEvent`s. Implemented as
173
- * `streamNative |> toCanonical`.
174
- */
175
- readonly streamTurn: (request: GeminiRequest) => Stream.Stream<TurnEvent, AiError.AiError>;
176
- /**
177
- * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.
178
- * Threads a fresh `Accumulator` so chunk-level text/usage merging happens
179
- * per stream.
180
- */
181
- readonly toCanonical: <E, R>(s: Stream.Stream<ProviderEvent, E, R>) => Stream.Stream<TurnEvent, E, R>;
182
- }
183
- declare const Gemini_base: Context.ServiceClass<Gemini, "@betalyra/effect-uai/providers/google/Gemini", GeminiService>;
184
- /**
185
- * Provider-typed service tag. Yield this when you want Gemini-specific
186
- * options (`thinkingBudget`); yield the generic `LanguageModel` tag for
187
- * provider-portable code. Both are registered by `layer`.
188
- */
189
- declare class Gemini extends Gemini_base {}
190
- interface Config {
191
- readonly apiKey: Redacted.Redacted;
192
- readonly baseUrl?: string;
193
- }
194
- /**
195
- * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.
196
- * Threads a fresh `Accumulator` so chunk-level text/usage merging works
197
- * across the run.
198
- */
199
- declare const toCanonical: <E, R>(s: Stream.Stream<ProviderEvent, E, R>) => Stream.Stream<TurnEvent, E, R>;
200
- /**
201
- * Build a `GeminiService` value. For Layer-based setup, prefer `layer`.
202
- */
203
- declare const make: (cfg: Config) => Effect.Effect<GeminiService, never, HttpClient.HttpClient>;
204
- /**
205
- * Layer that registers both the provider-specific `Gemini` tag and the
206
- * generic `LanguageModel` tag, sharing one underlying implementation.
207
- *
208
- * The generic tag accepts `CommonRequest`; the typed tag accepts the full
209
- * `GeminiRequest` surface.
210
- */
211
- declare const layer: (cfg: Config) => Layer.Layer<Gemini | LanguageModel, never, HttpClient.HttpClient>;
212
- //#endregion
213
- export { Config, Gemini, GeminiRequest, GeminiService, GoogleModel, ProviderEvent, codec_d_exports as codec, layer, make, toCanonical };
214
- //# sourceMappingURL=index.d.mts.map
1
+ import { t as codec_d_exports } from "./codec.mjs";
2
+ import { GeminiSttModel, GeminiTtsModel, GeminiVoiceName, GoogleEmbeddingModel, GoogleModel, LyriaModel } from "./models.mjs";
3
+ import { t as Gemini_d_exports } from "./Gemini.mjs";
4
+ import { t as GeminiEmbedding_d_exports } from "./GeminiEmbedding.mjs";
5
+ import { t as GeminiSynthesizer_d_exports } from "./GeminiSynthesizer.mjs";
6
+ import { t as GeminiTranscriber_d_exports } from "./GeminiTranscriber.mjs";
7
+ import { t as LyriaGenerator_d_exports } from "./LyriaGenerator.mjs";
8
+ export { Gemini_d_exports as Gemini, GeminiEmbedding_d_exports as GeminiEmbedding, GeminiSttModel, GeminiSynthesizer_d_exports as GeminiSynthesizer, GeminiTranscriber_d_exports as GeminiTranscriber, GeminiTtsModel, GeminiVoiceName, GoogleEmbeddingModel, GoogleModel, LyriaGenerator_d_exports as LyriaGenerator, LyriaModel, codec_d_exports as codec };