@effect-uai/google 0.3.0 → 0.5.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 (62) hide show
  1. package/dist/Gemini.d.mts +6 -5
  2. package/dist/Gemini.d.mts.map +1 -1
  3. package/dist/Gemini.mjs +42 -22
  4. package/dist/Gemini.mjs.map +1 -1
  5. package/dist/GeminiEmbedding.d.mts.map +1 -1
  6. package/dist/GeminiEmbedding.mjs +2 -2
  7. package/dist/GeminiEmbedding.mjs.map +1 -1
  8. package/dist/GeminiSynthesizer.d.mts +38 -0
  9. package/dist/GeminiSynthesizer.d.mts.map +1 -0
  10. package/dist/GeminiSynthesizer.mjs +95 -0
  11. package/dist/GeminiSynthesizer.mjs.map +1 -0
  12. package/dist/GeminiSynthesizer.test.d.mts +1 -0
  13. package/dist/GeminiSynthesizer.test.mjs +84 -0
  14. package/dist/GeminiSynthesizer.test.mjs.map +1 -0
  15. package/dist/GeminiTranscriber.d.mts +42 -0
  16. package/dist/GeminiTranscriber.d.mts.map +1 -0
  17. package/dist/GeminiTranscriber.mjs +80 -0
  18. package/dist/GeminiTranscriber.mjs.map +1 -0
  19. package/dist/GeminiTranscriber.test.d.mts +1 -0
  20. package/dist/GeminiTranscriber.test.mjs +125 -0
  21. package/dist/GeminiTranscriber.test.mjs.map +1 -0
  22. package/dist/LyriaGenerator.d.mts +57 -0
  23. package/dist/LyriaGenerator.d.mts.map +1 -0
  24. package/dist/LyriaGenerator.mjs +202 -0
  25. package/dist/LyriaGenerator.mjs.map +1 -0
  26. package/dist/LyriaGenerator.test.d.mts +1 -0
  27. package/dist/LyriaGenerator.test.mjs +138 -0
  28. package/dist/LyriaGenerator.test.mjs.map +1 -0
  29. package/dist/{chunk-CfYAbeIz.mjs → chunk-uyGKjUfl.mjs} +2 -1
  30. package/dist/codec.d.mts +51 -6
  31. package/dist/codec.d.mts.map +1 -1
  32. package/dist/codec.mjs +151 -27
  33. package/dist/codec.mjs.map +1 -1
  34. package/dist/dist-DMsg8YLb.mjs +13782 -0
  35. package/dist/dist-DMsg8YLb.mjs.map +1 -0
  36. package/dist/geminiSpeechCodec.d.mts +37 -0
  37. package/dist/geminiSpeechCodec.d.mts.map +1 -0
  38. package/dist/geminiSpeechCodec.mjs +110 -0
  39. package/dist/geminiSpeechCodec.mjs.map +1 -0
  40. package/dist/geminiSpeechCodec.test.d.mts +1 -0
  41. package/dist/geminiSpeechCodec.test.mjs +78 -0
  42. package/dist/geminiSpeechCodec.test.mjs.map +1 -0
  43. package/dist/index.d.mts +5 -2
  44. package/dist/index.mjs +4 -1
  45. package/dist/magic-string.es-CaWgF-3p.mjs +1013 -0
  46. package/dist/magic-string.es-CaWgF-3p.mjs.map +1 -0
  47. package/dist/models.d.mts +35 -1
  48. package/dist/models.d.mts.map +1 -1
  49. package/package.json +14 -2
  50. package/src/Gemini.ts +53 -18
  51. package/src/GeminiEmbedding.ts +23 -5
  52. package/src/GeminiSynthesizer.test.ts +93 -0
  53. package/src/GeminiSynthesizer.ts +214 -0
  54. package/src/GeminiTranscriber.test.ts +136 -0
  55. package/src/GeminiTranscriber.ts +204 -0
  56. package/src/LyriaGenerator.test.ts +162 -0
  57. package/src/LyriaGenerator.ts +390 -0
  58. package/src/codec.ts +312 -36
  59. package/src/geminiSpeechCodec.test.ts +56 -0
  60. package/src/geminiSpeechCodec.ts +137 -0
  61. package/src/index.ts +3 -0
  62. package/src/models.ts +84 -0
package/dist/Gemini.d.mts CHANGED
@@ -23,6 +23,12 @@ declare const ProviderEvent: Schema.Struct<{
23
23
  readonly parts: Schema.optional<Schema.$Array<Schema.Union<readonly [Schema.Struct<{
24
24
  readonly text: Schema.String;
25
25
  readonly thought: Schema.optional<Schema.Boolean>;
26
+ }>, Schema.Struct<{
27
+ readonly functionCall: Schema.Struct<{
28
+ readonly id: Schema.optional<Schema.String>;
29
+ readonly name: Schema.String;
30
+ readonly args: Schema.optional<Schema.Unknown>;
31
+ }>;
26
32
  }>]>>>;
27
33
  }>>;
28
34
  readonly finishReason: Schema.optional<Schema.String>;
@@ -82,11 +88,6 @@ type Config = {
82
88
  readonly apiKey: Redacted.Redacted;
83
89
  readonly baseUrl?: string;
84
90
  };
85
- /**
86
- * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.
87
- * Threads a fresh `Accumulator` so chunk-level text/usage merging works
88
- * across the run.
89
- */
90
91
  declare const toCanonical: <E, R>(s: Stream.Stream<ProviderEvent, E, R>) => Stream.Stream<TurnEvent, E, R>;
91
92
  /**
92
93
  * Build a `GeminiService` value. For Layer-based setup, prefer `layer`.
@@ -1 +1 @@
1
- {"version":3,"file":"Gemini.d.mts","names":[],"sources":["../src/Gemini.ts"],"mappings":";;;;;;;;;;;;;;;;;;cA2Ba,aAAA,EAAa,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;KACd,aAAA,GAAgB,SAAA;AAAA,KAMhB,aAAA,GAAgB,IAAA,CAAK,aAAA;;;;;WAKtB,KAAA,EAAO,WAAA;;;;;;WAMP,cAAA;AAAA;AAAA,KAGC,aAAA;;;;;;;;WAQD,YAAA,GAAe,OAAA,EAAS,aAAA,KAAkB,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,OAAA,CAAQ,OAAA;EA7BhE;;;;EAAA,SAkCf,UAAA,GAAa,OAAA,EAAS,aAAA,KAAkB,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,OAAA,CAAQ,OAAA;;;;;;WAMzE,WAAA,SACP,CAAA,EAAG,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,CAAA,EAAG,CAAA,MAChC,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,CAAA,EAAG,CAAA;AAAA;AAAA,cAClC,WAAA;;;;;;cAOY,MAAA,SAAe,WAAA;AAAA,KAIhB,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;AAAA;;;;;;cAiHE,WAAA,SACX,CAAA,EAAG,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,CAAA,EAAG,CAAA,MAClC,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,CAAA,EAAG,CAAA;;;;cAgClB,IAAA,GAAQ,GAAA,EAAK,MAAA,KAAS,MAAA,CAAO,MAAA,CAAO,aAAA,SAAsB,UAAA,CAAW,UAAA;;;;;;;;cAkBrE,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,MAAA,GAAS,aAAA,SAAsB,UAAA,CAAW,UAAA"}
1
+ {"version":3,"file":"Gemini.d.mts","names":[],"sources":["../src/Gemini.ts"],"mappings":";;;;;;;;;;;;;;;;;;cA6Ba,aAAA,EAAa,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;KACd,aAAA,GAAgB,SAAA;AAAA,KAMhB,aAAA,GAAgB,IAAA,CAAK,aAAA;;;;;WAKtB,KAAA,EAAO,WAAA;;;;;;WAMP,cAAA;AAAA;AAAA,KAGC,aAAA;;;;;;;;WAQD,YAAA,GAAe,OAAA,EAAS,aAAA,KAAkB,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,OAAA,CAAQ,OAAA;EA7BhE;;;;EAAA,SAkCf,UAAA,GAAa,OAAA,EAAS,aAAA,KAAkB,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,OAAA,CAAQ,OAAA;;;;;;WAMzE,WAAA,SACP,CAAA,EAAG,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,CAAA,EAAG,CAAA,MAChC,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,CAAA,EAAG,CAAA;AAAA;AAAA,cAClC,WAAA;;;;;;cAOY,MAAA,SAAe,WAAA;AAAA,KAIhB,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;AAAA;AAAA,cA8IE,WAAA,SACX,CAAA,EAAG,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,CAAA,EAAG,CAAA,MAClC,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,CAAA,EAAG,CAAA;;;;cAoClB,IAAA,GAAQ,GAAA,EAAK,MAAA,KAAS,MAAA,CAAO,MAAA,CAAO,aAAA,SAAsB,UAAA,CAAW,UAAA;;;;;;;;cAkBrE,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,MAAA,GAAS,aAAA,SAAsB,UAAA,CAAW,UAAA"}
package/dist/Gemini.mjs CHANGED
@@ -1,11 +1,12 @@
1
- import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
1
+ import { n as __exportAll } from "./chunk-uyGKjUfl.mjs";
2
2
  import { WireChunk, accumulatorToTurn, buildRequestBody, emptyAccumulator, ingestChunk } from "./codec.mjs";
3
- import { Context, Effect, Layer, Option, Redacted, Schema, Stream } from "effect";
3
+ import { Context, Effect, Layer, Match, Option, Redacted, Schema, Stream } from "effect";
4
4
  import { HttpClient, HttpClientRequest } from "effect/unstable/http";
5
5
  import * as AiError from "@effect-uai/core/AiError";
6
6
  import { LanguageModel } from "@effect-uai/core/LanguageModel";
7
7
  import { JsonParseError } from "@effect-uai/core/JSONL";
8
8
  import * as SSE from "@effect-uai/core/SSE";
9
+ import { TurnEvent } from "@effect-uai/core/Turn";
9
10
  //#region src/Gemini.ts
10
11
  var Gemini_exports = /* @__PURE__ */ __exportAll({
11
12
  Gemini: () => Gemini,
@@ -48,13 +49,11 @@ const decodeChunk = Schema.decodeUnknownEffect(WireChunk);
48
49
  * an `_unknown` variant, so JSON parse / schema decode failures flow
49
50
  * through as transport errors rather than being silently dropped.
50
51
  */
51
- const sseEventToChunk = (ev) => Effect.try({
52
- try: () => JSON.parse(ev.data),
53
- catch: (cause) => new JsonParseError({
54
- line: ev.data,
55
- cause
56
- })
57
- }).pipe(Effect.flatMap(decodeChunk));
52
+ const parseJsonUnknown = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
53
+ const sseEventToChunk = (ev) => parseJsonUnknown(ev.data).pipe(Effect.mapError((cause) => new JsonParseError({
54
+ line: ev.data,
55
+ cause
56
+ })), Effect.flatMap(decodeChunk));
58
57
  const httpStatusError = (status, body) => {
59
58
  const provider = "gemini";
60
59
  const raw = body;
@@ -101,7 +100,7 @@ const buildNativeStream = (cfg) => {
101
100
  const client = yield* HttpClient.HttpClient;
102
101
  const url = `${baseUrl}/models/${request.model}:streamGenerateContent?alt=sse`;
103
102
  const generationConfig = buildGenerationConfig(request);
104
- const body = buildRequestBody(request.history, generationConfig);
103
+ const body = buildRequestBody(request.history, generationConfig, request.tools ?? []);
105
104
  const httpRequest = HttpClientRequest.post(url).pipe(HttpClientRequest.setHeader("x-goog-api-key", Redacted.value(cfg.apiKey)), HttpClientRequest.bodyJsonUnsafe(body), HttpClientRequest.accept("text/event-stream"));
106
105
  const response = yield* client.execute(httpRequest).pipe(Effect.mapError((cause) => new AiError.Unavailable({
107
106
  provider: "gemini",
@@ -125,20 +124,41 @@ const buildNativeStream = (cfg) => {
125
124
  * Threads a fresh `Accumulator` so chunk-level text/usage merging works
126
125
  * across the run.
127
126
  */
128
- const toCanonical = (s) => s.pipe(Stream.mapAccum(() => emptyAccumulator, (acc, chunk) => {
129
- const result = ingestChunk(acc, chunk);
130
- const partDeltas = result.parts.map((p) => p.kind === "text" ? {
131
- type: "text_delta",
132
- text: p.text
133
- } : {
134
- type: "reasoning_delta",
127
+ /**
128
+ * Map one `ChunkPart` to the `TurnEvent`(s) it produces. Function calls
129
+ * emit two events back-to-back since Gemini delivers args whole: a
130
+ * `tool_call_start` immediately followed by a `tool_call_args_delta` with
131
+ * the full JSON-encoded args. The `call_id` is the synthesized id from
132
+ * the accumulator (Gemini-3 wire id when present, `<name>_<index>` otherwise).
133
+ */
134
+ const partToTurnEvents = (part, callIdAt) => Match.value(part).pipe(Match.discriminatorsExhaustive("kind")({
135
+ text: (p) => [TurnEvent.TextDelta({ text: p.text })],
136
+ reasoning: (p) => [TurnEvent.ReasoningDelta({
135
137
  text: p.text,
136
138
  kind: "trace"
137
- });
138
- const deltas = result.finished ? [...partDeltas, {
139
- type: "turn_complete",
140
- turn: accumulatorToTurn(result.accumulator)
141
- }] : partDeltas;
139
+ })],
140
+ function_call: (p) => {
141
+ const call_id = callIdAt(p.name, 0);
142
+ return [TurnEvent.ToolCallStart({
143
+ call_id,
144
+ name: p.name
145
+ }), TurnEvent.ToolCallArgsDelta({
146
+ call_id,
147
+ delta: JSON.stringify(p.args ?? {})
148
+ })];
149
+ }
150
+ }));
151
+ const toCanonical = (s) => s.pipe(Stream.mapAccum(() => emptyAccumulator, (acc, chunk) => {
152
+ const result = ingestChunk(acc, chunk);
153
+ const newCalls = result.parts.filter((p) => p.kind === "function_call");
154
+ const tailCalls = result.accumulator.functionCalls.slice(result.accumulator.functionCalls.length - newCalls.length);
155
+ let consumedCalls = 0;
156
+ const partDeltas = result.parts.flatMap((p) => partToTurnEvents(p, () => {
157
+ const id = tailCalls[consumedCalls]?.callId ?? p.kind;
158
+ consumedCalls += 1;
159
+ return id;
160
+ }));
161
+ const deltas = result.finished ? [...partDeltas, TurnEvent.TurnComplete({ turn: accumulatorToTurn(result.accumulator) })] : partDeltas;
142
162
  return [result.accumulator, deltas];
143
163
  }));
144
164
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"Gemini.mjs","names":[],"sources":["../src/Gemini.ts"],"sourcesContent":["import { Context, Effect, Layer, Option, Redacted, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport {\n type CommonRequest,\n LanguageModel,\n type LanguageModelService,\n} from \"@effect-uai/core/LanguageModel\"\nimport { JsonParseError } from \"@effect-uai/core/JSONL\"\nimport * as SSE from \"@effect-uai/core/SSE\"\nimport type { TurnEvent } from \"@effect-uai/core/Turn\"\nimport {\n type GenerationConfig,\n WireChunk,\n accumulatorToTurn,\n buildRequestBody,\n emptyAccumulator,\n ingestChunk,\n} from \"./codec.js\"\nimport type { GoogleModel } from \"./models.js\"\n\n/**\n * Gemini's native event vocabulary. Aliased from `WireChunk` so the public\n * surface matches `@effect-uai/responses` and `@effect-uai/anthropic`. Each\n * `ProviderEvent` is a full `GenerateContentResponse` chunk (not a per-field\n * delta) - that's how Gemini's SSE works.\n */\nexport const ProviderEvent = WireChunk\nexport type ProviderEvent = WireChunk\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type GeminiRequest = Omit<CommonRequest, \"model\"> & {\n /**\n * Narrows `CommonRequest.model` (`string`) to the typed `GoogleModel`\n * literal union for autocomplete.\n */\n readonly model: GoogleModel\n /**\n * Gemini 2.5 thinking budget. `0` disables thinking entirely (lowest\n * latency); higher values let the model think longer. Forwarded as\n * `generationConfig.thinkingConfig.thinkingBudget`.\n */\n readonly thinkingBudget?: number\n}\n\nexport type GeminiService = {\n /**\n * Stream the provider's native event vocabulary (post-SSE-decode). For\n * Gemini, each event is a full `GenerateContentResponse` chunk. Use this\n * when you need access to fields the canonical view doesn't surface\n * (`thoughtSignature`, granular `safetyRatings`, etc.). For\n * provider-portable code, use `streamTurn`.\n */\n readonly streamNative: (request: GeminiRequest) => Stream.Stream<ProviderEvent, AiError.AiError>\n /**\n * Stream canonical `TurnEvent`s. Implemented as\n * `streamNative |> toCanonical`.\n */\n readonly streamTurn: (request: GeminiRequest) => Stream.Stream<TurnEvent, AiError.AiError>\n /**\n * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.\n * Threads a fresh `Accumulator` so chunk-level text/usage merging happens\n * per stream.\n */\n readonly toCanonical: <E, R>(\n s: Stream.Stream<ProviderEvent, E, R>,\n ) => Stream.Stream<TurnEvent, E, R>\n}\n\n/**\n * Provider-typed service tag. Yield this when you want Gemini-specific\n * options (`thinkingBudget`); yield the generic `LanguageModel` tag for\n * provider-portable code. Both are registered by `layer`.\n */\nexport class Gemini extends Context.Service<Gemini, GeminiService>()(\n \"@betalyra/effect-uai/providers/google/Gemini\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Request body\n// ---------------------------------------------------------------------------\n\nconst buildGenerationConfig = (request: GeminiRequest): Option.Option<GenerationConfig> => {\n const cfg: GenerationConfig = {\n ...(request.temperature !== undefined && { temperature: request.temperature }),\n ...(request.maxOutputTokens !== undefined && { maxOutputTokens: request.maxOutputTokens }),\n ...(request.topP !== undefined && { topP: request.topP }),\n ...(request.thinkingBudget !== undefined && {\n thinkingConfig: { thinkingBudget: request.thinkingBudget },\n }),\n ...(request.structured !== undefined && {\n responseMimeType: \"application/json\",\n responseJsonSchema: request.structured.schema[\"~standard\"].jsonSchema.input({\n target: \"draft-2020-12\",\n }),\n }),\n }\n return Object.keys(cfg).length === 0 ? Option.none() : Option.some(cfg)\n}\n\n// ---------------------------------------------------------------------------\n// SSE event → wire chunk\n// ---------------------------------------------------------------------------\n\nconst decodeChunk = Schema.decodeUnknownEffect(WireChunk)\n\n/**\n * Parse one SSE event's `data` payload into a `WireChunk`. Unlike the other\n * providers, Gemini's wire vocabulary is non-discriminated (a single\n * permissive struct, all fields optional) - there's no natural shape for\n * an `_unknown` variant, so JSON parse / schema decode failures flow\n * through as transport errors rather than being silently dropped.\n */\nconst sseEventToChunk = (ev: SSE.Event) =>\n Effect.try({\n try: () => JSON.parse(ev.data) as unknown,\n catch: (cause) => new JsonParseError({ line: ev.data, cause }),\n }).pipe(Effect.flatMap(decodeChunk))\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"gemini\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status === 413) return new AiError.ContextLengthExceeded({ provider, raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\nconst buildNativeStream = (cfg: Config) => {\n const baseUrl = cfg.baseUrl ?? \"https://generativelanguage.googleapis.com/v1beta\"\n return (\n request: GeminiRequest,\n ): Stream.Stream<ProviderEvent, AiError.AiError, HttpClient.HttpClient> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const url = `${baseUrl}/models/${request.model}:streamGenerateContent?alt=sse`\n const generationConfig = buildGenerationConfig(request)\n const body = buildRequestBody(request.history, generationConfig)\n const httpRequest = HttpClientRequest.post(url).pipe(\n HttpClientRequest.setHeader(\"x-goog-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n HttpClientRequest.accept(\"text/event-stream\"),\n )\n const response = yield* client\n .execute(httpRequest)\n .pipe(\n Effect.mapError(\n (cause): AiError.AiError =>\n new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n )\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return Stream.fail(httpStatusError(response.status, text))\n }\n\n return response.stream.pipe(\n Stream.mapError(\n (cause): AiError.AiError => new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n SSE.fromBytes,\n Stream.mapEffect((ev) =>\n sseEventToChunk(ev).pipe(\n Effect.mapError(\n (cause): AiError.AiError =>\n new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n ),\n ),\n )\n }),\n )\n}\n\n/**\n * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.\n * Threads a fresh `Accumulator` so chunk-level text/usage merging works\n * across the run.\n */\nexport const toCanonical = <E, R>(\n s: Stream.Stream<ProviderEvent, E, R>,\n): Stream.Stream<TurnEvent, E, R> =>\n s.pipe(\n Stream.mapAccum(\n () => emptyAccumulator,\n (acc, chunk) => {\n const result = ingestChunk(acc, chunk)\n const partDeltas = result.parts.map((p) =>\n p.kind === \"text\"\n ? { type: \"text_delta\" as const, text: p.text }\n : { type: \"reasoning_delta\" as const, text: p.text, kind: \"trace\" as const },\n )\n const deltas = result.finished\n ? [\n ...partDeltas,\n {\n type: \"turn_complete\" as const,\n turn: accumulatorToTurn(result.accumulator),\n },\n ]\n : partDeltas\n return [result.accumulator, deltas] as const\n },\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `GeminiService` value. For Layer-based setup, prefer `layer`.\n */\nexport const make = (cfg: Config): Effect.Effect<GeminiService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => {\n const streamNative: GeminiService[\"streamNative\"] = (request) =>\n buildNativeStream(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client))\n return {\n streamNative,\n streamTurn: (request) => toCanonical(streamNative(request)),\n toCanonical,\n }\n })\n\n/**\n * Layer that registers both the provider-specific `Gemini` tag and the\n * generic `LanguageModel` tag, sharing one underlying implementation.\n *\n * The generic tag accepts `CommonRequest`; the typed tag accepts the full\n * `GeminiRequest` surface.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<Gemini | LanguageModel, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(Gemini, make(cfg))\n const generic = Layer.effect(\n LanguageModel,\n Effect.map(\n make(cfg),\n (s): LanguageModelService => ({\n streamTurn: (request) => s.streamTurn(request as GeminiRequest),\n }),\n ),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Gemini.mjs","names":[],"sources":["../src/Gemini.ts"],"sourcesContent":["import { Context, Effect, Layer, Option, Redacted, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport {\n type CommonRequest,\n LanguageModel,\n type LanguageModelService,\n} from \"@effect-uai/core/LanguageModel\"\nimport { JsonParseError } from \"@effect-uai/core/JSONL\"\nimport * as SSE from \"@effect-uai/core/SSE\"\nimport { TurnEvent } from \"@effect-uai/core/Turn\"\nimport {\n type ChunkPart,\n type GenerationConfig,\n WireChunk,\n accumulatorToTurn,\n buildRequestBody,\n emptyAccumulator,\n ingestChunk,\n} from \"./codec.js\"\nimport { Match } from \"effect\"\nimport type { GoogleModel } from \"./models.js\"\n\n/**\n * Gemini's native event vocabulary. Aliased from `WireChunk` so the public\n * surface matches `@effect-uai/responses` and `@effect-uai/anthropic`. Each\n * `ProviderEvent` is a full `GenerateContentResponse` chunk (not a per-field\n * delta) - that's how Gemini's SSE works.\n */\nexport const ProviderEvent = WireChunk\nexport type ProviderEvent = WireChunk\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type GeminiRequest = Omit<CommonRequest, \"model\"> & {\n /**\n * Narrows `CommonRequest.model` (`string`) to the typed `GoogleModel`\n * literal union for autocomplete.\n */\n readonly model: GoogleModel\n /**\n * Gemini 2.5 thinking budget. `0` disables thinking entirely (lowest\n * latency); higher values let the model think longer. Forwarded as\n * `generationConfig.thinkingConfig.thinkingBudget`.\n */\n readonly thinkingBudget?: number\n}\n\nexport type GeminiService = {\n /**\n * Stream the provider's native event vocabulary (post-SSE-decode). For\n * Gemini, each event is a full `GenerateContentResponse` chunk. Use this\n * when you need access to fields the canonical view doesn't surface\n * (`thoughtSignature`, granular `safetyRatings`, etc.). For\n * provider-portable code, use `streamTurn`.\n */\n readonly streamNative: (request: GeminiRequest) => Stream.Stream<ProviderEvent, AiError.AiError>\n /**\n * Stream canonical `TurnEvent`s. Implemented as\n * `streamNative |> toCanonical`.\n */\n readonly streamTurn: (request: GeminiRequest) => Stream.Stream<TurnEvent, AiError.AiError>\n /**\n * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.\n * Threads a fresh `Accumulator` so chunk-level text/usage merging happens\n * per stream.\n */\n readonly toCanonical: <E, R>(\n s: Stream.Stream<ProviderEvent, E, R>,\n ) => Stream.Stream<TurnEvent, E, R>\n}\n\n/**\n * Provider-typed service tag. Yield this when you want Gemini-specific\n * options (`thinkingBudget`); yield the generic `LanguageModel` tag for\n * provider-portable code. Both are registered by `layer`.\n */\nexport class Gemini extends Context.Service<Gemini, GeminiService>()(\n \"@betalyra/effect-uai/providers/google/Gemini\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Request body\n// ---------------------------------------------------------------------------\n\nconst buildGenerationConfig = (request: GeminiRequest): Option.Option<GenerationConfig> => {\n const cfg: GenerationConfig = {\n ...(request.temperature !== undefined && { temperature: request.temperature }),\n ...(request.maxOutputTokens !== undefined && { maxOutputTokens: request.maxOutputTokens }),\n ...(request.topP !== undefined && { topP: request.topP }),\n ...(request.thinkingBudget !== undefined && {\n thinkingConfig: { thinkingBudget: request.thinkingBudget },\n }),\n ...(request.structured !== undefined && {\n responseMimeType: \"application/json\",\n responseJsonSchema: request.structured.schema[\"~standard\"].jsonSchema.input({\n target: \"draft-2020-12\",\n }),\n }),\n }\n return Object.keys(cfg).length === 0 ? Option.none() : Option.some(cfg)\n}\n\n// ---------------------------------------------------------------------------\n// SSE event → wire chunk\n// ---------------------------------------------------------------------------\n\nconst decodeChunk = Schema.decodeUnknownEffect(WireChunk)\n\n/**\n * Parse one SSE event's `data` payload into a `WireChunk`. Unlike the other\n * providers, Gemini's wire vocabulary is non-discriminated (a single\n * permissive struct, all fields optional) - there's no natural shape for\n * an `_unknown` variant, so JSON parse / schema decode failures flow\n * through as transport errors rather than being silently dropped.\n */\nconst parseJsonUnknown = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))\n\nconst sseEventToChunk = (ev: SSE.Event) =>\n parseJsonUnknown(ev.data).pipe(\n Effect.mapError((cause) => new JsonParseError({ line: ev.data, cause })),\n Effect.flatMap(decodeChunk),\n )\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"gemini\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status === 413) return new AiError.ContextLengthExceeded({ provider, raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\nconst buildNativeStream = (cfg: Config) => {\n const baseUrl = cfg.baseUrl ?? \"https://generativelanguage.googleapis.com/v1beta\"\n return (\n request: GeminiRequest,\n ): Stream.Stream<ProviderEvent, AiError.AiError, HttpClient.HttpClient> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const url = `${baseUrl}/models/${request.model}:streamGenerateContent?alt=sse`\n const generationConfig = buildGenerationConfig(request)\n const body = buildRequestBody(request.history, generationConfig, request.tools ?? [])\n const httpRequest = HttpClientRequest.post(url).pipe(\n HttpClientRequest.setHeader(\"x-goog-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n HttpClientRequest.accept(\"text/event-stream\"),\n )\n const response = yield* client\n .execute(httpRequest)\n .pipe(\n Effect.mapError(\n (cause): AiError.AiError =>\n new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n )\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return Stream.fail(httpStatusError(response.status, text))\n }\n\n return response.stream.pipe(\n Stream.mapError(\n (cause): AiError.AiError => new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n SSE.fromBytes,\n Stream.mapEffect((ev) =>\n sseEventToChunk(ev).pipe(\n Effect.mapError(\n (cause): AiError.AiError =>\n new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n ),\n ),\n )\n }),\n )\n}\n\n/**\n * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.\n * Threads a fresh `Accumulator` so chunk-level text/usage merging works\n * across the run.\n */\n/**\n * Map one `ChunkPart` to the `TurnEvent`(s) it produces. Function calls\n * emit two events back-to-back since Gemini delivers args whole: a\n * `tool_call_start` immediately followed by a `tool_call_args_delta` with\n * the full JSON-encoded args. The `call_id` is the synthesized id from\n * the accumulator (Gemini-3 wire id when present, `<name>_<index>` otherwise).\n */\nconst partToTurnEvents = (\n part: ChunkPart,\n callIdAt: (name: string, indexFromTail: number) => string,\n): ReadonlyArray<TurnEvent> =>\n Match.value(part).pipe(\n Match.discriminatorsExhaustive(\"kind\")({\n text: (p): ReadonlyArray<TurnEvent> => [TurnEvent.TextDelta({ text: p.text })],\n reasoning: (p): ReadonlyArray<TurnEvent> => [\n TurnEvent.ReasoningDelta({ text: p.text, kind: \"trace\" }),\n ],\n function_call: (p): ReadonlyArray<TurnEvent> => {\n const call_id = callIdAt(p.name, 0)\n return [\n TurnEvent.ToolCallStart({ call_id, name: p.name }),\n TurnEvent.ToolCallArgsDelta({ call_id, delta: JSON.stringify(p.args ?? {}) }),\n ]\n },\n }),\n )\n\nexport const toCanonical = <E, R>(\n s: Stream.Stream<ProviderEvent, E, R>,\n): Stream.Stream<TurnEvent, E, R> =>\n s.pipe(\n Stream.mapAccum(\n () => emptyAccumulator,\n (acc, chunk) => {\n const result = ingestChunk(acc, chunk)\n // The accumulator already contains the synthesized call_ids for any\n // function_call parts in *this* chunk - they are the tail. Walk back\n // by chunk-local function-call index to recover them.\n const newCalls = result.parts.filter((p) => p.kind === \"function_call\")\n const tailCalls = result.accumulator.functionCalls.slice(\n result.accumulator.functionCalls.length - newCalls.length,\n )\n let consumedCalls = 0\n const partDeltas: ReadonlyArray<TurnEvent> = result.parts.flatMap((p) =>\n partToTurnEvents(p, () => {\n const id = tailCalls[consumedCalls]?.callId ?? p.kind\n consumedCalls += 1\n return id\n }),\n )\n const deltas: ReadonlyArray<TurnEvent> = result.finished\n ? [...partDeltas, TurnEvent.TurnComplete({ turn: accumulatorToTurn(result.accumulator) })]\n : partDeltas\n return [result.accumulator, deltas] as const\n },\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `GeminiService` value. For Layer-based setup, prefer `layer`.\n */\nexport const make = (cfg: Config): Effect.Effect<GeminiService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => {\n const streamNative: GeminiService[\"streamNative\"] = (request) =>\n buildNativeStream(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client))\n return {\n streamNative,\n streamTurn: (request) => toCanonical(streamNative(request)),\n toCanonical,\n }\n })\n\n/**\n * Layer that registers both the provider-specific `Gemini` tag and the\n * generic `LanguageModel` tag, sharing one underlying implementation.\n *\n * The generic tag accepts `CommonRequest`; the typed tag accepts the full\n * `GeminiRequest` surface.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<Gemini | LanguageModel, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(Gemini, make(cfg))\n const generic = Layer.effect(\n LanguageModel,\n Effect.map(\n make(cfg),\n (s): LanguageModelService => ({\n streamTurn: (request) => s.streamTurn(request as GeminiRequest),\n }),\n ),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"GeminiEmbedding.d.mts","names":[],"sources":["../src/GeminiEmbedding.ts"],"mappings":";;;;;;;;;;;;;;;;;KAyBY,mBAAA;AAAA,KAUA,kBAAA,GAAqB,IAAA,CAAK,kBAAA;+EAE3B,KAAA,EAAO,oBAAA;;;;AAZlB;;WAkBW,IAAA,GAAO,mBAAA;EAlBa;;AAU/B;;EAV+B,SAuBpB,KAAA;AAAA;AAAA,KAGC,sBAAA,GAAyB,IAAA,CAAK,kBAAA;EAAA,SAC/B,MAAA,EAAQ,aAAA,CAAc,UAAA;AAAA;AAAA,KAGrB,sBAAA;EAAA,SACD,KAAA,GAAQ,OAAA,EAAS,kBAAA,KAAuB,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,OAAA,CAAQ,OAAA;EAAA,SAC7E,SAAA,GACP,OAAA,EAAS,sBAAA,KACN,MAAA,CAAO,MAAA,CAAO,iBAAA,EAAmB,OAAA,CAAQ,OAAA;AAAA;AAAA,cAC/C,oBAAA;;;;;;cAOY,eAAA,SAAwB,oBAAA;AAAA,KAIzB,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;AAAA;;;;;cAuPE,IAAA,GACX,GAAA,EAAK,MAAA,KACJ,MAAA,CAAO,MAAA,CAAO,sBAAA,SAA+B,UAAA,CAAW,UAAA;;;;;;cAa9C,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,eAAA,GAAkB,cAAA,SAAuB,UAAA,CAAW,UAAA"}
1
+ {"version":3,"file":"GeminiEmbedding.d.mts","names":[],"sources":["../src/GeminiEmbedding.ts"],"mappings":";;;;;;;;;;;;;;;;;KA+BY,mBAAA;AAAA,KAUA,kBAAA,GAAqB,IAAA,CAAK,kBAAA;+EAE3B,KAAA,EAAO,oBAAA;;;;AAZlB;;WAkBW,IAAA,GAAO,mBAAA;EAlBa;;AAU/B;;EAV+B,SAuBpB,KAAA;AAAA;AAAA,KAGC,sBAAA,GAAyB,IAAA,CAAK,kBAAA;EAAA,SAC/B,MAAA,EAAQ,aAAA,CAAc,UAAA;AAAA;AAAA,KAGrB,sBAAA;EAAA,SACD,KAAA,GAAQ,OAAA,EAAS,kBAAA,KAAuB,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,OAAA,CAAQ,OAAA;EAAA,SAC7E,SAAA,GACP,OAAA,EAAS,sBAAA,KACN,MAAA,CAAO,MAAA,CAAO,iBAAA,EAAmB,OAAA,CAAQ,OAAA;AAAA;AAAA,cAC/C,oBAAA;;;;;;cAOY,eAAA,SAAwB,oBAAA;AAAA,KAIzB,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;AAAA;;;;;cAuPE,IAAA,GACX,GAAA,EAAK,MAAA,KACJ,MAAA,CAAO,MAAA,CAAO,sBAAA,SAA+B,UAAA,CAAW,UAAA;;;;;;cAa9C,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,eAAA,GAAkB,cAAA,SAAuB,UAAA,CAAW,UAAA"}
@@ -1,4 +1,4 @@
1
- import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
1
+ import { n as __exportAll } from "./chunk-uyGKjUfl.mjs";
2
2
  import { Context, Effect, Encoding, Layer, Match, Redacted, Schema } from "effect";
3
3
  import { HttpClient, HttpClientRequest } from "effect/unstable/http";
4
4
  import * as AiError from "@effect-uai/core/AiError";
@@ -125,7 +125,7 @@ const postJson = (cfg, url, body) => Effect.gen(function* () {
125
125
  const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
126
126
  if (response.status >= 400) {
127
127
  const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
128
- return yield* Effect.fail(httpStatusError(response.status, text));
128
+ return yield* httpStatusError(response.status, text);
129
129
  }
130
130
  return yield* response.json.pipe(Effect.mapError(transportFailure));
131
131
  });
@@ -1 +1 @@
1
- {"version":3,"file":"GeminiEmbedding.mjs","names":[],"sources":["../src/GeminiEmbedding.ts"],"sourcesContent":["import { Context, Effect, Encoding, Layer, Match, Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { Embedding, EmbedContentPart, EmbedInput, Usage } from \"@effect-uai/core/Embedding\"\nimport {\n type CommonEmbedManyRequest,\n type CommonEmbedRequest,\n EmbeddingModel,\n type EmbeddingModelService,\n type EmbedManyResponse,\n type EmbedResponse,\n} from \"@effect-uai/core/EmbeddingModel\"\nimport type { ImageSource } from \"@effect-uai/core/Image\"\nimport type { GoogleEmbeddingModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Gemini's task-type enum, exposed under our friendly names. Mapped to the\n * wire `taskType` values inside the codec. Honoured by\n * `gemini-embedding-001`; ignored by `gemini-embedding-2` (which expects\n * task instructions in the prompt text instead).\n */\nexport type GoogleEmbeddingTask =\n | \"query\"\n | \"document\"\n | \"similarity\"\n | \"classification\"\n | \"clustering\"\n | \"qa\"\n | \"fact_verification\"\n | \"code_query\"\n\nexport type GeminiEmbedRequest = Omit<CommonEmbedRequest, \"model\" | \"task\" | \"encoding\"> & {\n /** Narrows `CommonEmbedRequest.model` to the typed Google union. */\n readonly model: GoogleEmbeddingModel\n /**\n * Widens the cross-provider `\"query\" | \"document\"` to the full Google\n * task enum. Optional. Honoured by `gemini-embedding-001`; ignored by\n * `gemini-embedding-2`.\n */\n readonly task?: GoogleEmbeddingTask\n /**\n * Optional document title. Only meaningful for `RETRIEVAL_DOCUMENT`\n * tasks on `gemini-embedding-001`.\n */\n readonly title?: string\n}\n\nexport type GeminiEmbedManyRequest = Omit<GeminiEmbedRequest, \"input\"> & {\n readonly inputs: ReadonlyArray<EmbedInput>\n}\n\nexport type GeminiEmbeddingService = {\n readonly embed: (request: GeminiEmbedRequest) => Effect.Effect<EmbedResponse, AiError.AiError>\n readonly embedMany: (\n request: GeminiEmbedManyRequest,\n ) => Effect.Effect<EmbedManyResponse, AiError.AiError>\n}\n\n/**\n * Provider-typed service tag. Yield this when you want Gemini-specific\n * options (full `task` enum, `title`); yield the generic `EmbeddingModel`\n * tag for provider-portable code. Both are registered by `layer`.\n */\nexport class GeminiEmbedding extends Context.Service<GeminiEmbedding, GeminiEmbeddingService>()(\n \"@betalyra/effect-uai/providers/google/GeminiEmbedding\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec - request body\n// ---------------------------------------------------------------------------\n\nconst taskToWire: Record<GoogleEmbeddingTask, string> = {\n query: \"RETRIEVAL_QUERY\",\n document: \"RETRIEVAL_DOCUMENT\",\n similarity: \"SEMANTIC_SIMILARITY\",\n classification: \"CLASSIFICATION\",\n clustering: \"CLUSTERING\",\n qa: \"QUESTION_ANSWERING\",\n fact_verification: \"FACT_VERIFICATION\",\n code_query: \"CODE_RETRIEVAL_QUERY\",\n}\n\ntype WireTextPart = {\n readonly text: string\n}\ntype WireInlineDataPart = {\n readonly inlineData: { readonly mimeType: string; readonly data: string }\n}\ntype WirePart = WireTextPart | WireInlineDataPart\ntype WireContent = {\n readonly parts: ReadonlyArray<WirePart>\n}\n\nconst urlRejected = (param: string): AiError.AiError =>\n new AiError.InvalidRequest({\n provider: \"gemini\",\n param,\n raw: \"URL-form image inputs aren't supported; pass base64 or bytes\",\n })\n\n/**\n * Pre-uploading via Google's Files API isn't free, so we reject `url`\n * sources up front rather than silently dropping them. Users with HTTPS\n * URLs should fetch + pass `bytes` (or `base64`).\n */\nconst imageSourceToPart = (param: string) =>\n Match.type<ImageSource>().pipe(\n Match.tag(\n \"base64\",\n (s): Effect.Effect<WirePart, AiError.AiError> =>\n Effect.succeed({ inlineData: { mimeType: s.mimeType, data: s.base64 } }),\n ),\n Match.tag(\n \"bytes\",\n (s): Effect.Effect<WirePart, AiError.AiError> =>\n Effect.succeed({\n inlineData: { mimeType: s.mimeType, data: Encoding.encodeBase64(s.bytes) },\n }),\n ),\n Match.tag(\n \"url\",\n (): Effect.Effect<WirePart, AiError.AiError> => Effect.fail(urlRejected(param)),\n ),\n Match.exhaustive,\n )\n\nconst contentPartToPart = (\n param: string,\n part: EmbedContentPart,\n): Effect.Effect<WirePart, AiError.AiError> =>\n \"text\" in part ? Effect.succeed({ text: part.text }) : imageSourceToPart(param)(part.image)\n\nconst inputToContent = (input: EmbedInput): Effect.Effect<WireContent, AiError.AiError> => {\n if (typeof input === \"string\") return Effect.succeed({ parts: [{ text: input }] })\n if (\"text\" in input) return Effect.succeed({ parts: [{ text: input.text }] })\n if (\"image\" in input) {\n return imageSourceToPart(\"input.image\")(input.image).pipe(\n Effect.map((part) => ({ parts: [part] })),\n )\n }\n return Effect.forEach(input.content, (p) => contentPartToPart(\"input.content[].image\", p)).pipe(\n Effect.map((parts) => ({ parts })),\n )\n}\n\ntype WireSingleBody = {\n readonly content: WireContent\n readonly taskType?: string\n readonly outputDimensionality?: number\n readonly title?: string\n}\n\nconst wireOptions = (\n request: Pick<GeminiEmbedRequest, \"task\" | \"dimensions\" | \"title\">,\n): Omit<WireSingleBody, \"content\"> => ({\n ...(request.task !== undefined && { taskType: taskToWire[request.task] }),\n ...(request.dimensions !== undefined && { outputDimensionality: request.dimensions }),\n ...(request.title !== undefined && { title: request.title }),\n})\n\nconst buildSingleBody = (\n request: GeminiEmbedRequest,\n): Effect.Effect<WireSingleBody, AiError.AiError> =>\n inputToContent(request.input).pipe(\n Effect.map((content) => ({ content, ...wireOptions(request) })),\n )\n\ntype WireBatchEntry = WireSingleBody & {\n readonly model: string\n}\ntype WireBatchBody = {\n readonly requests: ReadonlyArray<WireBatchEntry>\n}\n\nconst buildBatchBody = (\n request: GeminiEmbedManyRequest,\n): Effect.Effect<WireBatchBody, AiError.AiError> => {\n const modelPath = `models/${request.model}`\n const options = wireOptions(request)\n return Effect.forEach(request.inputs, (input) =>\n inputToContent(input).pipe(\n Effect.map((content): WireBatchEntry => ({ model: modelPath, content, ...options })),\n ),\n ).pipe(Effect.map((requests) => ({ requests })))\n}\n\n// ---------------------------------------------------------------------------\n// Codec - response\n// ---------------------------------------------------------------------------\n\nconst WireEmbedding = Schema.Struct({\n values: Schema.Array(Schema.Number),\n})\n\nconst WireSingleResponse = Schema.Struct({\n embedding: WireEmbedding,\n})\n\nconst WireBatchResponse = Schema.Struct({\n embeddings: Schema.Array(WireEmbedding),\n})\n\nconst valuesToEmbedding = (values: ReadonlyArray<number>): Embedding => ({\n _tag: \"float32\",\n vector: Float32Array.from(values),\n})\n\n// Gemini's embed endpoints don't return token-count metadata. Estimating\n// client-side would be provider-specific cruft; honest `undefined` is better.\nconst emptyUsage: Usage = {}\n\n// ---------------------------------------------------------------------------\n// HTTP errors\n// ---------------------------------------------------------------------------\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"gemini\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status === 413) return new AiError.ContextLengthExceeded({ provider, raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\nconst transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"gemini\", raw: cause })\n\n// ---------------------------------------------------------------------------\n// HTTP plumbing - send a JSON body, error-map status, return parsed JSON.\n// ---------------------------------------------------------------------------\n\nconst postJson = (\n cfg: Config,\n url: string,\n body: unknown,\n): Effect.Effect<unknown, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(url).pipe(\n HttpClientRequest.setHeader(\"x-goog-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* Effect.fail(httpStatusError(response.status, text))\n }\n return yield* response.json.pipe(Effect.mapError(transportFailure))\n })\n\nconst decodeSingle = (json: unknown) =>\n Schema.decodeUnknownEffect(WireSingleResponse)(json).pipe(Effect.mapError(transportFailure))\n\nconst decodeBatch = (json: unknown) =>\n Schema.decodeUnknownEffect(WireBatchResponse)(json).pipe(Effect.mapError(transportFailure))\n\nconst baseUrl = (cfg: Config): string =>\n cfg.baseUrl ?? \"https://generativelanguage.googleapis.com/v1beta\"\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst embedImpl =\n (cfg: Config) =>\n (\n request: GeminiEmbedRequest,\n ): Effect.Effect<EmbedResponse, AiError.AiError, HttpClient.HttpClient> =>\n buildSingleBody(request).pipe(\n Effect.flatMap((body) =>\n postJson(cfg, `${baseUrl(cfg)}/models/${request.model}:embedContent`, body),\n ),\n Effect.flatMap(decodeSingle),\n Effect.map(\n (decoded): EmbedResponse => ({\n embedding: valuesToEmbedding(decoded.embedding.values),\n usage: emptyUsage,\n }),\n ),\n )\n\nconst embedManyImpl =\n (cfg: Config) =>\n (\n request: GeminiEmbedManyRequest,\n ): Effect.Effect<EmbedManyResponse, AiError.AiError, HttpClient.HttpClient> =>\n buildBatchBody(request).pipe(\n Effect.flatMap((body) =>\n postJson(cfg, `${baseUrl(cfg)}/models/${request.model}:batchEmbedContents`, body),\n ),\n Effect.flatMap(decodeBatch),\n Effect.map(\n (decoded): EmbedManyResponse => ({\n embeddings: decoded.embeddings.map((e) => valuesToEmbedding(e.values)),\n usage: emptyUsage,\n }),\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `GeminiEmbeddingService` value. For Layer-based setup, prefer\n * `layer`.\n */\nexport const make = (\n cfg: Config,\n): Effect.Effect<GeminiEmbeddingService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n embed: (request) =>\n embedImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n embedMany: (request) =>\n embedManyImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n }))\n\n/**\n * Layer that registers both the provider-specific `GeminiEmbedding` tag\n * and the generic `EmbeddingModel` tag, sharing one underlying\n * implementation.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<GeminiEmbedding | EmbeddingModel, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(GeminiEmbedding, make(cfg))\n const generic = Layer.effect(\n EmbeddingModel,\n Effect.map(\n make(cfg),\n (s): EmbeddingModelService => ({\n embed: (req: CommonEmbedRequest) => s.embed(req as GeminiEmbedRequest),\n embedMany: (req: CommonEmbedManyRequest) => s.embedMany(req as GeminiEmbedManyRequest),\n }),\n ),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"GeminiEmbedding.mjs","names":[],"sources":["../src/GeminiEmbedding.ts"],"sourcesContent":["import { Context, Effect, Encoding, Layer, Match, Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type {\n EmbedContentPart,\n EmbedInput,\n Float32Embedding,\n Usage,\n} from \"@effect-uai/core/Embedding\"\nimport {\n type CommonEmbedManyRequest,\n type CommonEmbedRequest,\n type EmbedEncoding,\n EmbeddingModel,\n type EmbeddingModelService,\n type EmbedManyResponse,\n type EmbedResponse,\n} from \"@effect-uai/core/EmbeddingModel\"\nimport type { ImageSource } from \"@effect-uai/core/Image\"\nimport type { GoogleEmbeddingModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Gemini's task-type enum, exposed under our friendly names. Mapped to the\n * wire `taskType` values inside the codec. Honoured by\n * `gemini-embedding-001`; ignored by `gemini-embedding-2` (which expects\n * task instructions in the prompt text instead).\n */\nexport type GoogleEmbeddingTask =\n | \"query\"\n | \"document\"\n | \"similarity\"\n | \"classification\"\n | \"clustering\"\n | \"qa\"\n | \"fact_verification\"\n | \"code_query\"\n\nexport type GeminiEmbedRequest = Omit<CommonEmbedRequest, \"model\" | \"task\" | \"encoding\"> & {\n /** Narrows `CommonEmbedRequest.model` to the typed Google union. */\n readonly model: GoogleEmbeddingModel\n /**\n * Widens the cross-provider `\"query\" | \"document\"` to the full Google\n * task enum. Optional. Honoured by `gemini-embedding-001`; ignored by\n * `gemini-embedding-2`.\n */\n readonly task?: GoogleEmbeddingTask\n /**\n * Optional document title. Only meaningful for `RETRIEVAL_DOCUMENT`\n * tasks on `gemini-embedding-001`.\n */\n readonly title?: string\n}\n\nexport type GeminiEmbedManyRequest = Omit<GeminiEmbedRequest, \"input\"> & {\n readonly inputs: ReadonlyArray<EmbedInput>\n}\n\nexport type GeminiEmbeddingService = {\n readonly embed: (request: GeminiEmbedRequest) => Effect.Effect<EmbedResponse, AiError.AiError>\n readonly embedMany: (\n request: GeminiEmbedManyRequest,\n ) => Effect.Effect<EmbedManyResponse, AiError.AiError>\n}\n\n/**\n * Provider-typed service tag. Yield this when you want Gemini-specific\n * options (full `task` enum, `title`); yield the generic `EmbeddingModel`\n * tag for provider-portable code. Both are registered by `layer`.\n */\nexport class GeminiEmbedding extends Context.Service<GeminiEmbedding, GeminiEmbeddingService>()(\n \"@betalyra/effect-uai/providers/google/GeminiEmbedding\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec - request body\n// ---------------------------------------------------------------------------\n\nconst taskToWire: Record<GoogleEmbeddingTask, string> = {\n query: \"RETRIEVAL_QUERY\",\n document: \"RETRIEVAL_DOCUMENT\",\n similarity: \"SEMANTIC_SIMILARITY\",\n classification: \"CLASSIFICATION\",\n clustering: \"CLUSTERING\",\n qa: \"QUESTION_ANSWERING\",\n fact_verification: \"FACT_VERIFICATION\",\n code_query: \"CODE_RETRIEVAL_QUERY\",\n}\n\ntype WireTextPart = {\n readonly text: string\n}\ntype WireInlineDataPart = {\n readonly inlineData: { readonly mimeType: string; readonly data: string }\n}\ntype WirePart = WireTextPart | WireInlineDataPart\ntype WireContent = {\n readonly parts: ReadonlyArray<WirePart>\n}\n\nconst urlRejected = (param: string): AiError.AiError =>\n new AiError.InvalidRequest({\n provider: \"gemini\",\n param,\n raw: \"URL-form image inputs aren't supported; pass base64 or bytes\",\n })\n\n/**\n * Pre-uploading via Google's Files API isn't free, so we reject `url`\n * sources up front rather than silently dropping them. Users with HTTPS\n * URLs should fetch + pass `bytes` (or `base64`).\n */\nconst imageSourceToPart = (param: string) =>\n Match.type<ImageSource>().pipe(\n Match.tag(\n \"base64\",\n (s): Effect.Effect<WirePart, AiError.AiError> =>\n Effect.succeed({ inlineData: { mimeType: s.mimeType, data: s.base64 } }),\n ),\n Match.tag(\n \"bytes\",\n (s): Effect.Effect<WirePart, AiError.AiError> =>\n Effect.succeed({\n inlineData: { mimeType: s.mimeType, data: Encoding.encodeBase64(s.bytes) },\n }),\n ),\n Match.tag(\n \"url\",\n (): Effect.Effect<WirePart, AiError.AiError> => Effect.fail(urlRejected(param)),\n ),\n Match.exhaustive,\n )\n\nconst contentPartToPart = (\n param: string,\n part: EmbedContentPart,\n): Effect.Effect<WirePart, AiError.AiError> =>\n \"text\" in part ? Effect.succeed({ text: part.text }) : imageSourceToPart(param)(part.image)\n\nconst inputToContent = (input: EmbedInput): Effect.Effect<WireContent, AiError.AiError> => {\n if (typeof input === \"string\") return Effect.succeed({ parts: [{ text: input }] })\n if (\"text\" in input) return Effect.succeed({ parts: [{ text: input.text }] })\n if (\"image\" in input) {\n return imageSourceToPart(\"input.image\")(input.image).pipe(\n Effect.map((part) => ({ parts: [part] })),\n )\n }\n return Effect.forEach(input.content, (p) => contentPartToPart(\"input.content[].image\", p)).pipe(\n Effect.map((parts) => ({ parts })),\n )\n}\n\ntype WireSingleBody = {\n readonly content: WireContent\n readonly taskType?: string\n readonly outputDimensionality?: number\n readonly title?: string\n}\n\nconst wireOptions = (\n request: Pick<GeminiEmbedRequest, \"task\" | \"dimensions\" | \"title\">,\n): Omit<WireSingleBody, \"content\"> => ({\n ...(request.task !== undefined && { taskType: taskToWire[request.task] }),\n ...(request.dimensions !== undefined && { outputDimensionality: request.dimensions }),\n ...(request.title !== undefined && { title: request.title }),\n})\n\nconst buildSingleBody = (\n request: GeminiEmbedRequest,\n): Effect.Effect<WireSingleBody, AiError.AiError> =>\n inputToContent(request.input).pipe(\n Effect.map((content) => ({ content, ...wireOptions(request) })),\n )\n\ntype WireBatchEntry = WireSingleBody & {\n readonly model: string\n}\ntype WireBatchBody = {\n readonly requests: ReadonlyArray<WireBatchEntry>\n}\n\nconst buildBatchBody = (\n request: GeminiEmbedManyRequest,\n): Effect.Effect<WireBatchBody, AiError.AiError> => {\n const modelPath = `models/${request.model}`\n const options = wireOptions(request)\n return Effect.forEach(request.inputs, (input) =>\n inputToContent(input).pipe(\n Effect.map((content): WireBatchEntry => ({ model: modelPath, content, ...options })),\n ),\n ).pipe(Effect.map((requests) => ({ requests })))\n}\n\n// ---------------------------------------------------------------------------\n// Codec - response\n// ---------------------------------------------------------------------------\n\nconst WireEmbedding = Schema.Struct({\n values: Schema.Array(Schema.Number),\n})\n\nconst WireSingleResponse = Schema.Struct({\n embedding: WireEmbedding,\n})\n\nconst WireBatchResponse = Schema.Struct({\n embeddings: Schema.Array(WireEmbedding),\n})\n\nconst valuesToEmbedding = (values: ReadonlyArray<number>): Float32Embedding => ({\n _tag: \"float32\",\n vector: Float32Array.from(values),\n})\n\n// Gemini's embed endpoints don't return token-count metadata. Estimating\n// client-side would be provider-specific cruft; honest `undefined` is better.\nconst emptyUsage: Usage = {}\n\n// ---------------------------------------------------------------------------\n// HTTP errors\n// ---------------------------------------------------------------------------\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"gemini\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status === 413) return new AiError.ContextLengthExceeded({ provider, raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\nconst transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"gemini\", raw: cause })\n\n// ---------------------------------------------------------------------------\n// HTTP plumbing - send a JSON body, error-map status, return parsed JSON.\n// ---------------------------------------------------------------------------\n\nconst postJson = (\n cfg: Config,\n url: string,\n body: unknown,\n): Effect.Effect<unknown, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(url).pipe(\n HttpClientRequest.setHeader(\"x-goog-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n return yield* response.json.pipe(Effect.mapError(transportFailure))\n })\n\nconst decodeSingle = (json: unknown) =>\n Schema.decodeUnknownEffect(WireSingleResponse)(json).pipe(Effect.mapError(transportFailure))\n\nconst decodeBatch = (json: unknown) =>\n Schema.decodeUnknownEffect(WireBatchResponse)(json).pipe(Effect.mapError(transportFailure))\n\nconst baseUrl = (cfg: Config): string =>\n cfg.baseUrl ?? \"https://generativelanguage.googleapis.com/v1beta\"\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst embedImpl =\n (cfg: Config) =>\n (\n request: GeminiEmbedRequest,\n ): Effect.Effect<EmbedResponse, AiError.AiError, HttpClient.HttpClient> =>\n buildSingleBody(request).pipe(\n Effect.flatMap((body) =>\n postJson(cfg, `${baseUrl(cfg)}/models/${request.model}:embedContent`, body),\n ),\n Effect.flatMap(decodeSingle),\n Effect.map(\n (decoded): EmbedResponse => ({\n embedding: valuesToEmbedding(decoded.embedding.values),\n usage: emptyUsage,\n }),\n ),\n )\n\nconst embedManyImpl =\n (cfg: Config) =>\n (\n request: GeminiEmbedManyRequest,\n ): Effect.Effect<EmbedManyResponse, AiError.AiError, HttpClient.HttpClient> =>\n buildBatchBody(request).pipe(\n Effect.flatMap((body) =>\n postJson(cfg, `${baseUrl(cfg)}/models/${request.model}:batchEmbedContents`, body),\n ),\n Effect.flatMap(decodeBatch),\n Effect.map(\n (decoded): EmbedManyResponse => ({\n embeddings: decoded.embeddings.map((e) => valuesToEmbedding(e.values)),\n usage: emptyUsage,\n }),\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `GeminiEmbeddingService` value. For Layer-based setup, prefer\n * `layer`.\n */\nexport const make = (\n cfg: Config,\n): Effect.Effect<GeminiEmbeddingService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n embed: (request) =>\n embedImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n embedMany: (request) =>\n embedManyImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n }))\n\n/**\n * Layer that registers both the provider-specific `GeminiEmbedding` tag\n * and the generic `EmbeddingModel` tag, sharing one underlying\n * implementation.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<GeminiEmbedding | EmbeddingModel, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(GeminiEmbedding, make(cfg))\n const generic = Layer.effect(\n EmbeddingModel,\n Effect.map(\n make(cfg),\n (s): EmbeddingModelService => ({\n // Gemini's endpoints only emit float32; the cast is sound for the\n // 99% case. A caller who asks for `encoding: \"int8\"` via the\n // generic tag gets the type they requested but the runtime returns\n // float32 (Gemini ignores the field).\n embed: <E extends EmbedEncoding | undefined = undefined>(\n req: Omit<CommonEmbedRequest, \"encoding\"> & { readonly encoding?: E },\n ) => s.embed(req as GeminiEmbedRequest) as Effect.Effect<EmbedResponse<E>, AiError.AiError>,\n embedMany: <E extends EmbedEncoding | undefined = undefined>(\n req: Omit<CommonEmbedManyRequest, \"encoding\"> & { readonly encoding?: E },\n ) =>\n s.embedMany(req as GeminiEmbedManyRequest) as Effect.Effect<\n EmbedManyResponse<E>,\n AiError.AiError\n >,\n }),\n ),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import { GeminiTtsModel, GeminiVoiceName } from "./models.mjs";
2
+ import { Context, Effect, Layer, Redacted, Stream } from "effect";
3
+ import { HttpClient } from "effect/unstable/http";
4
+ import * as AiError from "@effect-uai/core/AiError";
5
+ import { CommonSynthesizeRequest, SpeechSynthesizerService } from "@effect-uai/core/SpeechSynthesizer";
6
+ import { AudioBlob, AudioChunk } from "@effect-uai/core/Audio";
7
+
8
+ //#region src/GeminiSynthesizer.d.ts
9
+ declare namespace GeminiSynthesizer_d_exports {
10
+ export { Config, GeminiSynthesizeRequest, GeminiSynthesizer, GeminiSynthesizerService, layer, make, realizeOutput };
11
+ }
12
+ type GeminiSynthesizeRequest = Omit<CommonSynthesizeRequest, "model" | "voiceId"> & {
13
+ readonly model: GeminiTtsModel;
14
+ readonly voiceId: GeminiVoiceName;
15
+ };
16
+ type GeminiSynthesizerService = {
17
+ readonly synthesize: (r: GeminiSynthesizeRequest) => Effect.Effect<AudioBlob, AiError.AiError>;
18
+ readonly streamSynthesis: (r: GeminiSynthesizeRequest) => Stream.Stream<AudioChunk, AiError.AiError>;
19
+ readonly streamSynthesisFrom: SpeechSynthesizerService["streamSynthesisFrom"];
20
+ };
21
+ declare const GeminiSynthesizer_base: Context.ServiceClass<GeminiSynthesizer, "@betalyra/effect-uai/providers/google/GeminiSynthesizer", GeminiSynthesizerService>;
22
+ declare class GeminiSynthesizer extends GeminiSynthesizer_base {}
23
+ type Config = {
24
+ readonly apiKey: Redacted.Redacted;
25
+ readonly baseUrl?: string;
26
+ };
27
+ declare const realizeOutput: never;
28
+ declare const make: (cfg: Config) => Effect.Effect<GeminiSynthesizerService, never, HttpClient.HttpClient>;
29
+ /**
30
+ * Layer registers both `GeminiSynthesizer` and the generic
31
+ * `SpeechSynthesizer`. Does NOT register `TtsIncrementalText` — Gemini
32
+ * TTS is sync-only at the wire level, so `streamSynthesisFrom` against
33
+ * this Layer alone is a compile-time error.
34
+ */
35
+ declare const layer: (cfg: Config) => Layer.Layer<unknown, never, HttpClient.HttpClient>;
36
+ //#endregion
37
+ export { Config, GeminiSynthesizeRequest, GeminiSynthesizer, GeminiSynthesizerService, layer, make, realizeOutput, GeminiSynthesizer_d_exports as t };
38
+ //# sourceMappingURL=GeminiSynthesizer.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeminiSynthesizer.d.mts","names":[],"sources":["../src/GeminiSynthesizer.ts"],"mappings":";;;;;;;;;;;KAsBY,uBAAA,GAA0B,IAAA,CAAK,uBAAA;EAAA,SAChC,KAAA,EAAO,cAAA;EAAA,SACP,OAAA,EAAS,eAAA;AAAA;AAAA,KAGR,wBAAA;EAAA,SACD,UAAA,GAAa,CAAA,EAAG,uBAAA,KAA4B,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,OAAA,CAAQ,OAAA;EAAA,SAC7E,eAAA,GACP,CAAA,EAAG,uBAAA,KACA,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,CAAQ,OAAA;EAAA,SAC9B,mBAAA,EAAqB,wBAAA;AAAA;AAAA,cAC/B,sBAAA;cAEY,iBAAA,SAA0B,sBAAA;AAAA,KAK3B,MAAA;EAAA,SAAoB,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SAAmB,OAAA;AAAA;AAAA,cAUvD,aAAA;AAAA,cA8HA,IAAA,GAAQ,GAAA,EAAK,MAAA,KAAM,MAAA,CAAA,MAAA,CAAA,wBAAA,SAAA,UAAA,CAAA,UAAA;;;;;;;cAqBnB,KAAA,GAAS,GAAA,EAAK,MAAA,KAAM,KAAA,CAAA,KAAA,iBAAA,UAAA,CAAA,UAAA"}
@@ -0,0 +1,95 @@
1
+ import { n as __exportAll } from "./chunk-uyGKjUfl.mjs";
2
+ import { decodeBase64Audio, httpStatusError, transportFailure, wrapPcmAsWav } from "./geminiSpeechCodec.mjs";
3
+ import { Context, Effect, Layer, Match, Redacted, Schema, Stream } from "effect";
4
+ import { HttpClient, HttpClientRequest } from "effect/unstable/http";
5
+ import * as AiError from "@effect-uai/core/AiError";
6
+ import { SpeechSynthesizer } from "@effect-uai/core/SpeechSynthesizer";
7
+ //#region src/GeminiSynthesizer.ts
8
+ var GeminiSynthesizer_exports = /* @__PURE__ */ __exportAll({
9
+ GeminiSynthesizer: () => GeminiSynthesizer,
10
+ layer: () => layer,
11
+ make: () => make,
12
+ realizeOutput: () => realizeOutput
13
+ });
14
+ var GeminiSynthesizer = class extends Context.Service()("@betalyra/effect-uai/providers/google/GeminiSynthesizer") {};
15
+ const PCM_RATE = 24e3;
16
+ const PCM_CHANNELS = 1;
17
+ const realizeOutput = Match.type().pipe(Match.when("raw", () => Effect.succeed([{
18
+ container: "raw",
19
+ encoding: "pcm_s16le",
20
+ sampleRate: PCM_RATE,
21
+ channels: PCM_CHANNELS
22
+ }, (b) => b])), Match.when("wav", () => Effect.succeed([{
23
+ container: "wav",
24
+ encoding: "pcm_s16le",
25
+ sampleRate: PCM_RATE,
26
+ channels: PCM_CHANNELS
27
+ }, (b) => wrapPcmAsWav(b, PCM_RATE, PCM_CHANNELS)])), Match.whenOr("mp3", "opus", "aac", "flac", "ogg", "webm", (c) => Effect.fail(new AiError.Unsupported({
28
+ provider: "gemini",
29
+ capability: "outputFormat",
30
+ reason: `Gemini TTS emits raw 16-bit PCM at 24 kHz. Request "raw" or "wav"; ${c} is unavailable.`
31
+ }))), Match.exhaustive);
32
+ const Wire = Schema.Struct({ candidates: Schema.optional(Schema.Array(Schema.Struct({ content: Schema.optional(Schema.Struct({ parts: Schema.optional(Schema.Array(Schema.Struct({ inlineData: Schema.optional(Schema.Struct({
33
+ mimeType: Schema.optional(Schema.String),
34
+ data: Schema.String
35
+ })) }))) })) }))) });
36
+ const decodeWire = Schema.decodeUnknownEffect(Wire);
37
+ const findInline = (wire) => (wire.candidates ?? []).flatMap((c) => c.content?.parts ?? []).find((p) => p.inlineData !== void 0)?.inlineData;
38
+ const ttsBody = (r) => ({
39
+ contents: [{ parts: [{ text: r.text }] }],
40
+ generationConfig: {
41
+ responseModalities: ["AUDIO"],
42
+ speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: r.voiceId } } }
43
+ }
44
+ });
45
+ const baseUrl = (cfg) => cfg.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
46
+ const synthesizeImpl = (cfg) => (request) => Effect.gen(function* () {
47
+ const client = yield* HttpClient.HttpClient;
48
+ const [format, wrap] = yield* realizeOutput(request.outputFormat?.container ?? "raw");
49
+ const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/models/${request.model}:generateContent`).pipe(HttpClientRequest.setHeader("x-goog-api-key", Redacted.value(cfg.apiKey)), HttpClientRequest.bodyJsonUnsafe(ttsBody(request)));
50
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
51
+ if (response.status >= 400) {
52
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
53
+ return yield* httpStatusError(response.status, text);
54
+ }
55
+ const json = yield* response.json.pipe(Effect.mapError(transportFailure));
56
+ return {
57
+ format,
58
+ bytes: wrap(yield* decodeBase64Audio((yield* decodeWire(json).pipe(Effect.map(findInline), Effect.mapError((cause) => new AiError.GenerationFailed({
59
+ provider: "gemini",
60
+ raw: cause
61
+ })), Effect.flatMap((found) => found === void 0 ? Effect.fail(new AiError.GenerationFailed({
62
+ provider: "gemini",
63
+ raw: {
64
+ message: "Gemini TTS response had no inlineData audio part.",
65
+ hint: "Likely a prompt-safety rejection or the model didn't honour responseModalities.",
66
+ response: json
67
+ }
68
+ })) : Effect.succeed(found)))).data))
69
+ };
70
+ });
71
+ const unsupportedStreamFrom = () => Stream.fail(new AiError.Unsupported({
72
+ provider: "gemini",
73
+ capability: "streamSynthesisFrom",
74
+ reason: "Gemini TTS via generateContent is sync-only. Use Cloud Text-to-Speech (Chirp 3 HD) for incremental text-in."
75
+ }));
76
+ const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
77
+ synthesize: (r) => synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),
78
+ streamSynthesis: (r) => Stream.fromEffect(synthesizeImpl(cfg)(r)).pipe(Stream.map((b) => ({ bytes: b.bytes })), Stream.provideService(HttpClient.HttpClient, client)),
79
+ streamSynthesisFrom: unsupportedStreamFrom
80
+ }));
81
+ /**
82
+ * Layer registers both `GeminiSynthesizer` and the generic
83
+ * `SpeechSynthesizer`. Does NOT register `TtsIncrementalText` — Gemini
84
+ * TTS is sync-only at the wire level, so `streamSynthesisFrom` against
85
+ * this Layer alone is a compile-time error.
86
+ */
87
+ const layer = (cfg) => Layer.merge(Layer.effect(GeminiSynthesizer, make(cfg)), Layer.effect(SpeechSynthesizer, Effect.map(make(cfg), (s) => ({
88
+ synthesize: (req) => s.synthesize(req),
89
+ streamSynthesis: (req) => s.streamSynthesis(req),
90
+ streamSynthesisFrom: s.streamSynthesisFrom
91
+ }))));
92
+ //#endregion
93
+ export { GeminiSynthesizer, layer, make, realizeOutput, GeminiSynthesizer_exports as t };
94
+
95
+ //# sourceMappingURL=GeminiSynthesizer.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeminiSynthesizer.mjs","names":[],"sources":["../src/GeminiSynthesizer.ts"],"sourcesContent":["import { Context, Effect, Layer, Match, Redacted, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioBlob, AudioChunk, AudioFormat } from \"@effect-uai/core/Audio\"\nimport {\n type CommonStreamSynthesizeRequest,\n type CommonSynthesizeRequest,\n SpeechSynthesizer,\n type SpeechSynthesizerService,\n} from \"@effect-uai/core/SpeechSynthesizer\"\nimport {\n decodeBase64Audio,\n httpStatusError,\n transportFailure,\n wrapPcmAsWav,\n} from \"./geminiSpeechCodec.js\"\nimport type { GeminiTtsModel, GeminiVoiceName } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type GeminiSynthesizeRequest = Omit<CommonSynthesizeRequest, \"model\" | \"voiceId\"> & {\n readonly model: GeminiTtsModel\n readonly voiceId: GeminiVoiceName\n}\n\nexport type GeminiSynthesizerService = {\n readonly synthesize: (r: GeminiSynthesizeRequest) => Effect.Effect<AudioBlob, AiError.AiError>\n readonly streamSynthesis: (\n r: GeminiSynthesizeRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError>\n readonly streamSynthesisFrom: SpeechSynthesizerService[\"streamSynthesisFrom\"]\n}\n\nexport class GeminiSynthesizer extends Context.Service<\n GeminiSynthesizer,\n GeminiSynthesizerService\n>()(\"@betalyra/effect-uai/providers/google/GeminiSynthesizer\") {}\n\nexport type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }\n\n// ---------------------------------------------------------------------------\n// Codec — Gemini TTS always emits 24 kHz mono signed-16 PCM. Caller picks\n// raw bytes or WAV-wrapped.\n// ---------------------------------------------------------------------------\n\nconst PCM_RATE = 24000\nconst PCM_CHANNELS = 1\n\nexport const realizeOutput = Match.type<AudioFormat[\"container\"]>().pipe(\n Match.when(\"raw\", () =>\n Effect.succeed([\n { container: \"raw\", encoding: \"pcm_s16le\", sampleRate: PCM_RATE, channels: PCM_CHANNELS },\n (b: Uint8Array) => b,\n ] as const),\n ),\n Match.when(\"wav\", () =>\n Effect.succeed([\n { container: \"wav\", encoding: \"pcm_s16le\", sampleRate: PCM_RATE, channels: PCM_CHANNELS },\n (b: Uint8Array) => wrapPcmAsWav(b, PCM_RATE, PCM_CHANNELS),\n ] as const),\n ),\n Match.whenOr(\"mp3\", \"opus\", \"aac\", \"flac\", \"ogg\", \"webm\", (c) =>\n Effect.fail(\n new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"outputFormat\",\n reason: `Gemini TTS emits raw 16-bit PCM at 24 kHz. Request \"raw\" or \"wav\"; ${c} is unavailable.`,\n }),\n ),\n ),\n Match.exhaustive,\n)\n\n// ---------------------------------------------------------------------------\n// Wire\n// ---------------------------------------------------------------------------\n\nconst Wire = Schema.Struct({\n candidates: Schema.optional(\n Schema.Array(\n Schema.Struct({\n content: Schema.optional(\n Schema.Struct({\n parts: Schema.optional(\n Schema.Array(\n Schema.Struct({\n inlineData: Schema.optional(\n Schema.Struct({\n mimeType: Schema.optional(Schema.String),\n data: Schema.String,\n }),\n ),\n }),\n ),\n ),\n }),\n ),\n }),\n ),\n ),\n})\nconst decodeWire = Schema.decodeUnknownEffect(Wire)\n\nconst findInline = (wire: typeof Wire.Type) =>\n (wire.candidates ?? [])\n .flatMap((c) => c.content?.parts ?? [])\n .find((p) => p.inlineData !== undefined)?.inlineData\n\nconst ttsBody = (r: GeminiSynthesizeRequest) => ({\n contents: [{ parts: [{ text: r.text }] }],\n generationConfig: {\n responseModalities: [\"AUDIO\"],\n speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: r.voiceId } } },\n },\n})\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config) => cfg.baseUrl ?? \"https://generativelanguage.googleapis.com/v1beta\"\n\nconst synthesizeImpl = (cfg: Config) => (request: GeminiSynthesizeRequest) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const [format, wrap] = yield* realizeOutput(request.outputFormat?.container ?? \"raw\")\n const httpRequest = HttpClientRequest.post(\n `${baseUrl(cfg)}/models/${request.model}:generateContent`,\n ).pipe(\n HttpClientRequest.setHeader(\"x-goog-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(ttsBody(request)),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const inline = yield* decodeWire(json).pipe(\n Effect.map(findInline),\n Effect.mapError((cause) => new AiError.GenerationFailed({ provider: \"gemini\", raw: cause })),\n Effect.flatMap((found) =>\n found === undefined\n ? Effect.fail(\n new AiError.GenerationFailed({\n provider: \"gemini\",\n raw: {\n message: \"Gemini TTS response had no inlineData audio part.\",\n hint: \"Likely a prompt-safety rejection or the model didn't honour responseModalities.\",\n response: json,\n },\n }),\n )\n : Effect.succeed(found),\n ),\n )\n const pcm = yield* decodeBase64Audio(inline.data)\n return { format, bytes: wrap(pcm) } satisfies AudioBlob\n })\n\nconst unsupportedStreamFrom: SpeechSynthesizerService[\"streamSynthesisFrom\"] = () =>\n Stream.fail(\n new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"streamSynthesisFrom\",\n reason:\n \"Gemini TTS via generateContent is sync-only. Use Cloud Text-to-Speech (Chirp 3 HD) for incremental text-in.\",\n }),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (cfg: Config) =>\n Effect.map(\n HttpClient.HttpClient.asEffect(),\n (client): GeminiSynthesizerService => ({\n synthesize: (r) =>\n synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n streamSynthesis: (r) =>\n Stream.fromEffect(synthesizeImpl(cfg)(r)).pipe(\n Stream.map((b): AudioChunk => ({ bytes: b.bytes })),\n Stream.provideService(HttpClient.HttpClient, client),\n ),\n streamSynthesisFrom: unsupportedStreamFrom,\n }),\n )\n\n/**\n * Layer registers both `GeminiSynthesizer` and the generic\n * `SpeechSynthesizer`. Does NOT register `TtsIncrementalText` — Gemini\n * TTS is sync-only at the wire level, so `streamSynthesisFrom` against\n * this Layer alone is a compile-time error.\n */\nexport const layer = (cfg: Config) =>\n Layer.merge(\n Layer.effect(GeminiSynthesizer, make(cfg)),\n Layer.effect(\n SpeechSynthesizer,\n Effect.map(\n make(cfg),\n (s): SpeechSynthesizerService => ({\n synthesize: (req: CommonSynthesizeRequest) =>\n s.synthesize(req as GeminiSynthesizeRequest),\n streamSynthesis: (req: CommonSynthesizeRequest) =>\n s.streamSynthesis(req as GeminiSynthesizeRequest),\n streamSynthesisFrom: s.streamSynthesisFrom,\n }),\n ),\n ),\n )\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,84 @@
1
+ import { i as it, n as globalExpect, r as describe, t as import_dist } from "./dist-DMsg8YLb.mjs";
2
+ import { GeminiSynthesizer, layer, realizeOutput } from "./GeminiSynthesizer.mjs";
3
+ import { Effect, Layer, Redacted, Stream } from "effect";
4
+ import { FetchHttpClient } from "effect/unstable/http";
5
+ import * as SpeechSynthesizer$1 from "@effect-uai/core/SpeechSynthesizer";
6
+ //#region src/GeminiSynthesizer.test.ts
7
+ const cfg = { apiKey: Redacted.make("test-key") };
8
+ const live = Layer.provide(layer(cfg), FetchHttpClient.layer);
9
+ describe("GeminiSynthesizer.realizeOutput", () => {
10
+ it("maps raw to native PCM 24 kHz mono with an identity wrapper", async () => {
11
+ const [format, wrap] = await Effect.runPromise(realizeOutput("raw"));
12
+ globalExpect(format).toEqual({
13
+ container: "raw",
14
+ encoding: "pcm_s16le",
15
+ sampleRate: 24e3,
16
+ channels: 1
17
+ });
18
+ const pcm = new Uint8Array([
19
+ 1,
20
+ 2,
21
+ 3,
22
+ 4
23
+ ]);
24
+ globalExpect(wrap(pcm)).toBe(pcm);
25
+ });
26
+ it("maps wav to a RIFF/WAVE wrapper that prepends a 44-byte header", async () => {
27
+ const [format, wrap] = await Effect.runPromise(realizeOutput("wav"));
28
+ globalExpect(format.container).toBe("wav");
29
+ const out = wrap(new Uint8Array([
30
+ 0,
31
+ 1,
32
+ 2,
33
+ 3
34
+ ]));
35
+ globalExpect(out.length).toBe(48);
36
+ const ascii = (start, len) => String.fromCharCode(...Array.from(out.slice(start, start + len)));
37
+ globalExpect(ascii(0, 4)).toBe("RIFF");
38
+ globalExpect(ascii(8, 4)).toBe("WAVE");
39
+ globalExpect(ascii(12, 4)).toBe("fmt ");
40
+ globalExpect(ascii(36, 4)).toBe("data");
41
+ });
42
+ it("fails Unsupported for mp3 / opus / aac / flac / ogg / webm", async () => {
43
+ const exit = await Effect.runPromiseExit(realizeOutput("mp3"));
44
+ globalExpect(exit._tag).toBe("Failure");
45
+ if (exit._tag === "Failure") {
46
+ globalExpect(JSON.stringify(exit.cause)).toContain("Unsupported");
47
+ globalExpect(JSON.stringify(exit.cause)).toContain("outputFormat");
48
+ }
49
+ });
50
+ });
51
+ describe("GeminiSynthesizer capability guards (runtime)", () => {
52
+ it("streamSynthesisFrom returns an Unsupported stream", async () => {
53
+ const program = GeminiSynthesizer.use((s) => Stream.runDrain(s.streamSynthesisFrom(Stream.fromIterable(["hi"]), {
54
+ model: "gemini-2.5-flash-preview-tts",
55
+ voiceId: "Kore"
56
+ })));
57
+ const exit = await Effect.runPromiseExit(program.pipe(Effect.provide(live)));
58
+ globalExpect(exit._tag).toBe("Failure");
59
+ if (exit._tag === "Failure") {
60
+ globalExpect(JSON.stringify(exit.cause)).toContain("Unsupported");
61
+ globalExpect(JSON.stringify(exit.cause)).toContain("streamSynthesisFrom");
62
+ }
63
+ });
64
+ });
65
+ describe("GeminiSynthesizer Layer (compile-time)", () => {
66
+ it("leaves `TtsIncrementalText` unsatisfied when using `streamSynthesisFrom` against this Layer", () => {
67
+ const audio = Stream.fromIterable(["a"]).pipe(SpeechSynthesizer$1.streamSynthesisFrom({
68
+ model: "gemini-2.5-flash-preview-tts",
69
+ voiceId: "Kore"
70
+ }));
71
+ (0, import_dist.expectTypeOf)(Stream.runDrain(audio).pipe(Effect.provide(live))).toEqualTypeOf();
72
+ });
73
+ it("sync `synthesize` requires no marker", () => {
74
+ (0, import_dist.expectTypeOf)(SpeechSynthesizer$1.synthesize({
75
+ text: "hi",
76
+ model: "gemini-2.5-flash-preview-tts",
77
+ voiceId: "Kore"
78
+ }).pipe(Effect.provide(live))).toEqualTypeOf();
79
+ });
80
+ });
81
+ //#endregion
82
+ export {};
83
+
84
+ //# sourceMappingURL=GeminiSynthesizer.test.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeminiSynthesizer.test.mjs","names":["GeminiSynthesizer.layer","GeminiSynthesizer.realizeOutput","SpeechSynthesizer"],"sources":["../src/GeminiSynthesizer.test.ts"],"sourcesContent":["import { Effect, Layer, Redacted, Stream } from \"effect\"\nimport { FetchHttpClient } from \"effect/unstable/http\"\nimport { describe, expect, expectTypeOf, it } from \"vitest\"\nimport type * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioBlob } from \"@effect-uai/core/Audio\"\nimport * as SpeechSynthesizer from \"@effect-uai/core/SpeechSynthesizer\"\nimport * as GeminiSynthesizer from \"./GeminiSynthesizer.js\"\n\nconst cfg: GeminiSynthesizer.Config = { apiKey: Redacted.make(\"test-key\") }\n// FetchHttpClient is required for `make`, but these tests only exercise\n// the codec and the compile-time / runtime Unsupported branches — no\n// real HTTP call is made.\nconst live = Layer.provide(GeminiSynthesizer.layer(cfg), FetchHttpClient.layer)\n\ndescribe(\"GeminiSynthesizer.realizeOutput\", () => {\n it(\"maps raw to native PCM 24 kHz mono with an identity wrapper\", async () => {\n const [format, wrap] = await Effect.runPromise(GeminiSynthesizer.realizeOutput(\"raw\"))\n expect(format).toEqual({\n container: \"raw\",\n encoding: \"pcm_s16le\",\n sampleRate: 24000,\n channels: 1,\n })\n const pcm = new Uint8Array([1, 2, 3, 4])\n expect(wrap(pcm)).toBe(pcm)\n })\n\n it(\"maps wav to a RIFF/WAVE wrapper that prepends a 44-byte header\", async () => {\n const [format, wrap] = await Effect.runPromise(GeminiSynthesizer.realizeOutput(\"wav\"))\n expect(format.container).toBe(\"wav\")\n const out = wrap(new Uint8Array([0, 1, 2, 3]))\n expect(out.length).toBe(44 + 4)\n const ascii = (start: number, len: number) =>\n String.fromCharCode(...Array.from(out.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 })\n\n it(\"fails Unsupported for mp3 / opus / aac / flac / ogg / webm\", async () => {\n const exit = await Effect.runPromiseExit(GeminiSynthesizer.realizeOutput(\"mp3\"))\n expect(exit._tag).toBe(\"Failure\")\n if (exit._tag === \"Failure\") {\n expect(JSON.stringify(exit.cause)).toContain(\"Unsupported\")\n expect(JSON.stringify(exit.cause)).toContain(\"outputFormat\")\n }\n })\n})\n\ndescribe(\"GeminiSynthesizer capability guards (runtime)\", () => {\n it(\"streamSynthesisFrom returns an Unsupported stream\", async () => {\n const program = GeminiSynthesizer.GeminiSynthesizer.use((s) =>\n Stream.runDrain(\n s.streamSynthesisFrom(Stream.fromIterable([\"hi\"]), {\n model: \"gemini-2.5-flash-preview-tts\",\n voiceId: \"Kore\",\n }),\n ),\n )\n const exit = await Effect.runPromiseExit(program.pipe(Effect.provide(live)))\n expect(exit._tag).toBe(\"Failure\")\n if (exit._tag === \"Failure\") {\n expect(JSON.stringify(exit.cause)).toContain(\"Unsupported\")\n expect(JSON.stringify(exit.cause)).toContain(\"streamSynthesisFrom\")\n }\n })\n})\n\ndescribe(\"GeminiSynthesizer Layer (compile-time)\", () => {\n it(\"leaves `TtsIncrementalText` unsatisfied when using `streamSynthesisFrom` against this Layer\", () => {\n const tokens: Stream.Stream<string> = Stream.fromIterable([\"a\"])\n const audio = tokens.pipe(\n SpeechSynthesizer.streamSynthesisFrom({\n model: \"gemini-2.5-flash-preview-tts\",\n voiceId: \"Kore\",\n }),\n )\n const provided = Stream.runDrain(audio).pipe(Effect.provide(live))\n expectTypeOf(provided).toEqualTypeOf<\n Effect.Effect<void, AiError.AiError, SpeechSynthesizer.TtsIncrementalText>\n >()\n })\n\n it(\"sync `synthesize` requires no marker\", () => {\n const synth = SpeechSynthesizer.synthesize({\n text: \"hi\",\n model: \"gemini-2.5-flash-preview-tts\",\n voiceId: \"Kore\",\n }).pipe(Effect.provide(live))\n expectTypeOf(synth).toEqualTypeOf<Effect.Effect<AudioBlob, AiError.AiError, never>>()\n })\n})\n"],"mappings":";;;;;;AAQA,MAAM,MAAgC,EAAE,QAAQ,SAAS,KAAK,WAAW,EAAE;AAI3E,MAAM,OAAO,MAAM,QAAQA,MAAwB,IAAI,EAAE,gBAAgB,MAAM;AAE/E,SAAS,yCAAyC;AAChD,IAAG,+DAA+D,YAAY;EAC5E,MAAM,CAAC,QAAQ,QAAQ,MAAM,OAAO,WAAWC,cAAgC,MAAM,CAAC;AACtF,eAAO,OAAO,CAAC,QAAQ;GACrB,WAAW;GACX,UAAU;GACV,YAAY;GACZ,UAAU;GACX,CAAC;EACF,MAAM,MAAM,IAAI,WAAW;GAAC;GAAG;GAAG;GAAG;GAAE,CAAC;AACxC,eAAO,KAAK,IAAI,CAAC,CAAC,KAAK,IAAI;GAC3B;AAEF,IAAG,kEAAkE,YAAY;EAC/E,MAAM,CAAC,QAAQ,QAAQ,MAAM,OAAO,WAAWA,cAAgC,MAAM,CAAC;AACtF,eAAO,OAAO,UAAU,CAAC,KAAK,MAAM;EACpC,MAAM,MAAM,KAAK,IAAI,WAAW;GAAC;GAAG;GAAG;GAAG;GAAE,CAAC,CAAC;AAC9C,eAAO,IAAI,OAAO,CAAC,KAAK,GAAO;EAC/B,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;GACjC;AAEF,IAAG,8DAA8D,YAAY;EAC3E,MAAM,OAAO,MAAM,OAAO,eAAeA,cAAgC,MAAM,CAAC;AAChF,eAAO,KAAK,KAAK,CAAC,KAAK,UAAU;AACjC,MAAI,KAAK,SAAS,WAAW;AAC3B,gBAAO,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC,UAAU,cAAc;AAC3D,gBAAO,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC,UAAU,eAAe;;GAE9D;EACF;AAEF,SAAS,uDAAuD;AAC9D,IAAG,qDAAqD,YAAY;EAClE,MAAM,UAAA,kBAA8C,KAAK,MACvD,OAAO,SACL,EAAE,oBAAoB,OAAO,aAAa,CAAC,KAAK,CAAC,EAAE;GACjD,OAAO;GACP,SAAS;GACV,CAAC,CACH,CACF;EACD,MAAM,OAAO,MAAM,OAAO,eAAe,QAAQ,KAAK,OAAO,QAAQ,KAAK,CAAC,CAAC;AAC5E,eAAO,KAAK,KAAK,CAAC,KAAK,UAAU;AACjC,MAAI,KAAK,SAAS,WAAW;AAC3B,gBAAO,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC,UAAU,cAAc;AAC3D,gBAAO,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC,UAAU,sBAAsB;;GAErE;EACF;AAEF,SAAS,gDAAgD;AACvD,IAAG,qGAAqG;EAEtG,MAAM,QADgC,OAAO,aAAa,CAAC,IAAI,CAC3C,CAAC,KACnBC,oBAAkB,oBAAoB;GACpC,OAAO;GACP,SAAS;GACV,CAAC,CACH;AAED,GAAA,GAAA,YAAA,cADiB,OAAO,SAAS,MAAM,CAAC,KAAK,OAAO,QAAQ,KAAK,CAC5C,CAAC,CAAC,eAEpB;GACH;AAEF,IAAG,8CAA8C;AAM/C,GAAA,GAAA,YAAA,cALcA,oBAAkB,WAAW;GACzC,MAAM;GACN,OAAO;GACP,SAAS;GACV,CAAC,CAAC,KAAK,OAAO,QAAQ,KAAK,CACV,CAAC,CAAC,eAAiE;GACrF;EACF"}