@effect-uai/google 0.5.2 → 0.7.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 (45) hide show
  1. package/dist/Gemini.d.mts.map +1 -1
  2. package/dist/Gemini.mjs +7 -2
  3. package/dist/Gemini.mjs.map +1 -1
  4. package/dist/GeminiEmbedding.d.mts.map +1 -1
  5. package/dist/GeminiEmbedding.mjs +3 -3
  6. package/dist/GeminiEmbedding.mjs.map +1 -1
  7. package/dist/GeminiSynthesizer.d.mts +2 -0
  8. package/dist/GeminiSynthesizer.d.mts.map +1 -1
  9. package/dist/GeminiSynthesizer.mjs +38 -6
  10. package/dist/GeminiSynthesizer.mjs.map +1 -1
  11. package/dist/GeminiSynthesizer.test.mjs +14 -48
  12. package/dist/GeminiSynthesizer.test.mjs.map +1 -1
  13. package/dist/LyriaGenerator.d.mts +18 -18
  14. package/dist/LyriaGenerator.d.mts.map +1 -1
  15. package/dist/LyriaGenerator.mjs +59 -42
  16. package/dist/LyriaGenerator.mjs.map +1 -1
  17. package/dist/LyriaGenerator.test.mjs +26 -72
  18. package/dist/LyriaGenerator.test.mjs.map +1 -1
  19. package/dist/codec.d.mts +13 -5
  20. package/dist/codec.d.mts.map +1 -1
  21. package/dist/codec.mjs +21 -4
  22. package/dist/codec.mjs.map +1 -1
  23. package/dist/index.d.mts +2 -3
  24. package/dist/index.mjs +1 -2
  25. package/dist/models.d.mts +1 -10
  26. package/dist/models.d.mts.map +1 -1
  27. package/package.json +2 -6
  28. package/src/Gemini.ts +15 -1
  29. package/src/GeminiEmbedding.ts +14 -9
  30. package/src/GeminiSynthesizer.test.ts +13 -67
  31. package/src/GeminiSynthesizer.ts +56 -5
  32. package/src/LyriaGenerator.test.ts +34 -78
  33. package/src/LyriaGenerator.ts +92 -73
  34. package/src/codec.ts +55 -18
  35. package/src/index.ts +0 -1
  36. package/src/models.ts +0 -17
  37. package/dist/GeminiTranscriber.d.mts +0 -42
  38. package/dist/GeminiTranscriber.d.mts.map +0 -1
  39. package/dist/GeminiTranscriber.mjs +0 -80
  40. package/dist/GeminiTranscriber.mjs.map +0 -1
  41. package/dist/GeminiTranscriber.test.d.mts +0 -1
  42. package/dist/GeminiTranscriber.test.mjs +0 -125
  43. package/dist/GeminiTranscriber.test.mjs.map +0 -1
  44. package/src/GeminiTranscriber.test.ts +0 -136
  45. package/src/GeminiTranscriber.ts +0 -204
@@ -1,12 +1,13 @@
1
1
  import { n as __exportAll } from "./chunk-uyGKjUfl.mjs";
2
- import { Array, Context, Effect, Encoding, Layer, Match, Option, Redacted, Result, Schema, Stream } from "effect";
2
+ import { Context, Duration, Effect, Encoding, Layer, Match, Redacted, Result, Schema, Stream } from "effect";
3
3
  import { HttpClient, HttpClientRequest } from "effect/unstable/http";
4
4
  import * as AiError from "@effect-uai/core/AiError";
5
+ import { warnDroppedWhen } from "@effect-uai/core/Capabilities";
6
+ import { singleVariant } from "@effect-uai/core/Music";
5
7
  import { MusicGenerator } from "@effect-uai/core/MusicGenerator";
6
8
  //#region src/LyriaGenerator.ts
7
9
  var LyriaGenerator_exports = /* @__PURE__ */ __exportAll({
8
10
  LyriaGenerator: () => LyriaGenerator,
9
- buildPrompt: () => buildPrompt,
10
11
  containerToMimeType: () => containerToMimeType,
11
12
  layer: () => layer,
12
13
  make: () => make,
@@ -19,10 +20,10 @@ const containerToMimeType = Match.type().pipe(Match.when("mp3", () => Effect.suc
19
20
  reason: `Lyria 3 only produces audio/mp3 or audio/wav; ${c} is not available. Use container: "mp3" or "wav".`
20
21
  }))), Match.exhaustive);
21
22
  /**
22
- * Is this a Lyria 3 Clip model variant? Clip is fixed at 30 s MP3 — it
23
+ * Is this a Lyria 3 Clip model variant? Clip is fixed at 30 s MP3, it
23
24
  * does not accept a `responseFormat` on `generationConfig` (the live
24
- * API rejects unknown fields, despite what the docs imply). Pro
25
- * accepts the documented `responseFormat` knob.
25
+ * API rejects unknown fields). Pro accepts the documented
26
+ * `responseFormat` knob.
26
27
  */
27
28
  const isClipModel = (model) => model.includes("clip");
28
29
  const realizedFormat = Match.type().pipe(Match.when("audio/mp3", () => ({
@@ -37,22 +38,33 @@ const realizedFormat = Match.type().pipe(Match.when("audio/mp3", () => ({
37
38
  channels: 2
38
39
  })), Match.exhaustive);
39
40
  /**
40
- * Collapse `prompts | WeightedPrompt[]` into a single prompt string and
41
- * splice in non-structural hints (`lyrics`, `bpm`, `scale`,
42
- * `instrumental`, `durationSeconds`). The Lyria 3 sync API only takes
43
- * one text part — every condition lives inline.
41
+ * Warn (don't fail) when the caller passed Common fields Lyria 3 has
42
+ * no wire field for. Bucket-2 per capabilities policy: provider has
43
+ * no structured interpretation; the output is still valid music, just
44
+ * less aligned with the caller's hint. We never embed these into the
45
+ * prompt on the caller's behalf, prompt construction is the
46
+ * developer's job.
44
47
  */
45
- const buildPrompt = (request) => {
46
- const promptText = Match.value(request.prompts).pipe(Match.when(Match.string, (s) => s), Match.orElse((ws) => Array.map(ws, (w) => w.weight !== void 0 && w.weight !== 1 ? `${w.text} (weight ${w.weight})` : w.text).join(". ")));
47
- const hints = Array.getSomes([
48
- request.instrumental === true ? Option.some("Instrumental only — no vocals.") : Option.none(),
49
- request.bpm !== void 0 ? Option.some(`BPM: ${request.bpm}.`) : Option.none(),
50
- request.scale !== void 0 ? Option.some(`Key/scale: ${request.scale}.`) : Option.none(),
51
- request.durationSeconds !== void 0 ? Option.some(`Target duration: ${request.durationSeconds}s.`) : Option.none()
52
- ]);
53
- const lyricsBlock = request.lyrics !== void 0 && request.instrumental !== true ? `\n\nLyrics:\n${request.lyrics}` : "";
54
- return [promptText, ...hints].join(" ") + lyricsBlock;
55
- };
48
+ const warnDroppedHints = (request) => Effect.all([
49
+ warnDroppedWhen(request.lyrics, {
50
+ provider: "lyria",
51
+ capability: "lyrics",
52
+ field: "lyrics",
53
+ reason: "Lyria 3 sync has no `lyrics` wire field. If you want vocals to follow specific lyrics, embed them in your prompt yourself (e.g. with `[Verse]` / `[Chorus]` tags)."
54
+ }),
55
+ warnDroppedWhen(request.duration !== void 0 ? Duration.toMillis(request.duration) : void 0, {
56
+ provider: "lyria",
57
+ capability: "duration",
58
+ field: "duration",
59
+ reason: "Lyria 3 clip is fixed at 30 s; pro derives duration from prompt content. There is no wire field for duration. If you want to steer length, mention it in your prompt."
60
+ }),
61
+ warnDroppedWhen(request.seed, {
62
+ provider: "lyria",
63
+ capability: "seed",
64
+ field: "seed",
65
+ reason: "Lyria 3 (Gemini surface) does not expose a seed parameter. Output is non-deterministic across identical prompts. Use Lyria 2 (Vertex) for seeded generation."
66
+ })
67
+ ], { discard: true });
56
68
  const InlineData = Schema.Struct({
57
69
  mimeType: Schema.optional(Schema.String),
58
70
  data: Schema.String
@@ -120,15 +132,11 @@ const extractParts = (wire) => {
120
132
  };
121
133
  const generateImpl = (cfg) => (request) => Effect.gen(function* () {
122
134
  const client = yield* HttpClient.HttpClient;
123
- const mimeType = yield* containerToMimeType(request.outputFormat?.container ?? "mp3");
124
- if (isClipModel(request.model) && mimeType === "audio/wav") return yield* new AiError.Unsupported({
125
- provider: "lyria",
126
- capability: "outputFormat",
127
- reason: `lyria-3-clip-preview is fixed at audio/mp3. Use lyria-3-pro-preview for audio/wav output.`
128
- });
129
- const promptText = buildPrompt(request);
130
- const body = isClipModel(request.model) ? { contents: [{ parts: [{ text: promptText }] }] } : {
131
- contents: [{ parts: [{ text: promptText }] }],
135
+ const requestedMime = yield* containerToMimeType(request.outputFormat?.container ?? "mp3");
136
+ const mimeType = isClipModel(request.model) ? "audio/mp3" : requestedMime;
137
+ yield* warnDroppedHints(request);
138
+ const body = isClipModel(request.model) ? { contents: [{ parts: [{ text: request.prompt }] }] } : {
139
+ contents: [{ parts: [{ text: request.prompt }] }],
132
140
  generationConfig: {
133
141
  responseModalities: ["AUDIO", "TEXT"],
134
142
  responseFormat: { audio: { mimeType } }
@@ -155,18 +163,21 @@ const generateImpl = (cfg) => (request) => Effect.gen(function* () {
155
163
  }
156
164
  });
157
165
  const bytes = yield* decodeBase64ToBytes(inline.data);
158
- return {
159
- format: realizedFormat(mimeType),
160
- bytes,
166
+ return singleVariant({
167
+ audio: {
168
+ format: realizedFormat(mimeType),
169
+ bytes
170
+ },
171
+ provider: "lyria",
161
172
  ...text !== void 0 && { lyrics: text },
162
- watermark: { kind: "synthid" }
163
- };
173
+ watermark: "synthid"
174
+ });
164
175
  });
165
- const streamGenerationImpl = (cfg) => (request) => Stream.unwrap(Effect.map(generateImpl(cfg)(request), (result) => Stream.succeed({ bytes: result.bytes })));
176
+ const streamGenerationImpl = (cfg) => (request) => Stream.unwrap(Effect.map(generateImpl(cfg)(request), (result) => Stream.succeed({ bytes: result.primary.audio.bytes })));
166
177
  /**
167
- * Lyria 3 sync has no bidirectional session at the wire level — that's
168
- * Lyria RealTime (Phase 1b, separate Layer). The provider Layer here
169
- * also does NOT register `MusicInteractiveSession`, so callers using
178
+ * Lyria 3 sync has no bidirectional session at the wire level, that's
179
+ * Lyria RealTime (separate Layer). The provider Layer here also does
180
+ * NOT register `MusicInteractiveSession`, so callers using
170
181
  * `MusicGenerator.streamGenerationFrom` against only this Layer get a
171
182
  * compile-time error before this runtime fallback ever fires.
172
183
  */
@@ -186,17 +197,23 @@ const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => (
186
197
  * Layer that registers both the provider-specific `LyriaGenerator` tag
187
198
  * and the generic `MusicGenerator` tag.
188
199
  *
189
- * Does NOT register the `MusicInteractiveSession` capability marker —
200
+ * Does NOT register the `MusicInteractiveSession` capability marker,
190
201
  * Lyria 3 sync has no bidirectional session at the wire level. Code
191
202
  * calling `MusicGenerator.streamGenerationFrom` will fail to typecheck
192
203
  * against this Layer alone, which is the intended UX.
193
204
  */
194
205
  const layer = (cfg) => Layer.merge(Layer.effect(LyriaGenerator, make(cfg)), Layer.effect(MusicGenerator, Effect.map(make(cfg), (s) => ({
195
- generate: (req) => s.generate(req),
196
- streamGeneration: (req) => s.streamGeneration(req),
206
+ generate: (req) => s.generate({
207
+ ...req,
208
+ model: req.model
209
+ }),
210
+ streamGeneration: (req) => s.streamGeneration({
211
+ ...req,
212
+ model: req.model
213
+ }),
197
214
  streamGenerationFrom: s.streamGenerationFrom
198
215
  }))));
199
216
  //#endregion
200
- export { LyriaGenerator, buildPrompt, containerToMimeType, layer, make, realizedFormat, LyriaGenerator_exports as t };
217
+ export { LyriaGenerator, containerToMimeType, layer, make, realizedFormat, LyriaGenerator_exports as t };
201
218
 
202
219
  //# sourceMappingURL=LyriaGenerator.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"LyriaGenerator.mjs","names":["Arr"],"sources":["../src/LyriaGenerator.ts"],"sourcesContent":["import {\n Array as Arr,\n Context,\n Effect,\n Encoding,\n Layer,\n Match,\n Option,\n Redacted,\n Result,\n Schema,\n Stream,\n} from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioChunk, AudioFormat } from \"@effect-uai/core/Audio\"\nimport type {\n CommonGenerateMusicRequest,\n CommonStreamGenerateMusicRequest,\n MusicResult,\n MusicSessionInput,\n WeightedPrompt,\n} from \"@effect-uai/core/Music\"\nimport { MusicGenerator, type MusicGeneratorService } from \"@effect-uai/core/MusicGenerator\"\nimport type { LyriaModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Lyria-typed generate request. Narrows `model` to `LyriaModel`. The\n * Lyria sync API (`generateContent`) accepts only `audio/mp3` and\n * `audio/wav` output; `bpm`, `scale`, `instrumental` etc. on the\n * `CommonGenerateMusicRequest` are flattened into the prompt text\n * because the public API does not expose them as structured fields.\n */\nexport type LyriaGenerateRequest = Omit<CommonGenerateMusicRequest, \"model\"> & {\n readonly model: LyriaModel\n}\n\nexport type LyriaGeneratorService = {\n readonly generate: (request: LyriaGenerateRequest) => Effect.Effect<MusicResult, AiError.AiError>\n readonly streamGeneration: (\n request: LyriaGenerateRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError>\n readonly streamGenerationFrom: <E, R>(\n input: Stream.Stream<MusicSessionInput, E, R>,\n request: CommonStreamGenerateMusicRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError | E, R>\n}\n\nexport class LyriaGenerator extends Context.Service<LyriaGenerator, LyriaGeneratorService>()(\n \"@betalyra/effect-uai/providers/google/LyriaGenerator\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec — request → prompt text + response_format\n// ---------------------------------------------------------------------------\n\nexport type LyriaResponseMimeType = \"audio/mp3\" | \"audio/wav\"\n\nexport const containerToMimeType: (\n container: AudioFormat[\"container\"],\n) => Effect.Effect<LyriaResponseMimeType, AiError.AiError> = Match.type<\n AudioFormat[\"container\"]\n>().pipe(\n Match.when(\"mp3\", () => Effect.succeed<LyriaResponseMimeType>(\"audio/mp3\")),\n Match.when(\"wav\", () => Effect.succeed<LyriaResponseMimeType>(\"audio/wav\")),\n Match.whenOr(\"ogg\", \"opus\", \"flac\", \"aac\", \"webm\", \"raw\", (c) =>\n Effect.fail(\n new AiError.Unsupported({\n provider: \"lyria\",\n capability: \"outputFormat\",\n reason: `Lyria 3 only produces audio/mp3 or audio/wav; ${c} is not available. Use container: \"mp3\" or \"wav\".`,\n }),\n ),\n ),\n Match.exhaustive,\n)\n\n/**\n * Is this a Lyria 3 Clip model variant? Clip is fixed at 30 s MP3 — it\n * does not accept a `responseFormat` on `generationConfig` (the live\n * API rejects unknown fields, despite what the docs imply). Pro\n * accepts the documented `responseFormat` knob.\n */\nconst isClipModel = (model: string): boolean => model.includes(\"clip\")\n\nexport const realizedFormat: (mime: LyriaResponseMimeType) => AudioFormat =\n Match.type<LyriaResponseMimeType>().pipe(\n Match.when(\n \"audio/mp3\",\n (): AudioFormat => ({ container: \"mp3\", encoding: \"mp3\", sampleRate: 44100, channels: 2 }),\n ),\n Match.when(\n \"audio/wav\",\n (): AudioFormat => ({\n container: \"wav\",\n encoding: \"pcm_s16le\",\n sampleRate: 44100,\n channels: 2,\n }),\n ),\n Match.exhaustive,\n )\n\n/**\n * Collapse `prompts | WeightedPrompt[]` into a single prompt string and\n * splice in non-structural hints (`lyrics`, `bpm`, `scale`,\n * `instrumental`, `durationSeconds`). The Lyria 3 sync API only takes\n * one text part — every condition lives inline.\n */\nexport const buildPrompt = (request: CommonGenerateMusicRequest): string => {\n const promptText = Match.value(request.prompts).pipe(\n Match.when(Match.string, (s) => s),\n Match.orElse((ws: ReadonlyArray<WeightedPrompt>) =>\n Arr.map(ws, (w) =>\n w.weight !== undefined && w.weight !== 1 ? `${w.text} (weight ${w.weight})` : w.text,\n ).join(\". \"),\n ),\n )\n const hints = Arr.getSomes([\n request.instrumental === true ? Option.some(\"Instrumental only — no vocals.\") : Option.none(),\n request.bpm !== undefined ? Option.some(`BPM: ${request.bpm}.`) : Option.none(),\n request.scale !== undefined ? Option.some(`Key/scale: ${request.scale}.`) : Option.none(),\n request.durationSeconds !== undefined\n ? Option.some(`Target duration: ${request.durationSeconds}s.`)\n : Option.none(),\n ])\n const lyricsBlock =\n request.lyrics !== undefined && request.instrumental !== true\n ? `\\n\\nLyrics:\\n${request.lyrics}`\n : \"\"\n return [promptText, ...hints].join(\" \") + lyricsBlock\n}\n\n// ---------------------------------------------------------------------------\n// Wire response schema — just enough to find audio + optional lyrics text\n// ---------------------------------------------------------------------------\n\nconst InlineData = Schema.Struct({\n mimeType: Schema.optional(Schema.String),\n data: Schema.String,\n})\n\nconst Part = Schema.Struct({\n text: Schema.optional(Schema.String),\n inlineData: Schema.optional(InlineData),\n})\n\nconst Content = Schema.Struct({\n parts: Schema.optional(Schema.Array(Part)),\n})\n\nconst Candidate = Schema.Struct({\n content: Schema.optional(Content),\n})\n\nconst Wire = Schema.Struct({\n candidates: Schema.optional(Schema.Array(Candidate)),\n})\n\nconst decodeWire = Schema.decodeUnknownEffect(Wire)\n\n// ---------------------------------------------------------------------------\n// HTTP plumbing\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config): string =>\n cfg.baseUrl ?? \"https://generativelanguage.googleapis.com/v1beta\"\n\nconst transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"lyria\", raw: cause })\n\nconst 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: \"lyria\", raw: body }),\n ),\n Match.whenOr(\n 408,\n 504,\n (): AiError.AiError => new AiError.Timeout({ provider: \"lyria\", raw: body }),\n ),\n Match.when(\n 401,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"lyria\", subtype: \"auth\", raw: body }),\n ),\n Match.when(\n 403,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"lyria\", subtype: \"permission\", raw: body }),\n ),\n Match.when(\n 402,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"lyria\", subtype: \"billing\", raw: body }),\n ),\n Match.when(\n (n) => n >= 500,\n (n): AiError.AiError => new AiError.Unavailable({ provider: \"lyria\", status: n, raw: body }),\n ),\n Match.orElse(\n (): AiError.AiError => new AiError.InvalidRequest({ provider: \"lyria\", raw: body }),\n ),\n )\n\nconst decodeBase64ToBytes = (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: \"lyria\", param: \"audio\", raw: cause })),\n })\n\n/**\n * Find the first inline-data audio part and the concatenated text parts\n * from a Wire response.\n */\nconst extractParts = (\n wire: typeof Wire.Type,\n): { readonly inline?: typeof InlineData.Type; readonly text?: string } => {\n const allParts = (wire.candidates ?? []).flatMap((c) => c.content?.parts ?? [])\n const inline = allParts.find((p) => p.inlineData !== undefined)?.inlineData\n const textParts = allParts.flatMap((p) => (p.text !== undefined ? [p.text] : []))\n const text = textParts.length === 0 ? undefined : textParts.join(\"\\n\")\n return {\n ...(inline !== undefined && { inline }),\n ...(text !== undefined && { text }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst generateImpl =\n (cfg: Config) =>\n (\n request: LyriaGenerateRequest,\n ): Effect.Effect<MusicResult, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const requestedContainer = (request.outputFormat?.container ??\n \"mp3\") satisfies AudioFormat[\"container\"]\n const mimeType = yield* containerToMimeType(requestedContainer)\n // Clip is fixed 30 s mp3 — reject wav up front rather than sending\n // a `responseFormat` that the live API ignores or rejects.\n if (isClipModel(request.model) && mimeType === \"audio/wav\") {\n return yield* new AiError.Unsupported({\n provider: \"lyria\",\n capability: \"outputFormat\",\n reason: `lyria-3-clip-preview is fixed at audio/mp3. Use lyria-3-pro-preview for audio/wav output.`,\n })\n }\n const promptText = buildPrompt(request)\n const body = isClipModel(request.model)\n ? { contents: [{ parts: [{ text: promptText }] }] }\n : {\n contents: [{ parts: [{ text: promptText }] }],\n generationConfig: {\n responseModalities: [\"AUDIO\", \"TEXT\"],\n responseFormat: { audio: { mimeType } },\n },\n }\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(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 const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const wire = yield* decodeWire(json).pipe(\n Effect.mapError(\n (cause): AiError.AiError =>\n new AiError.GenerationFailed({ provider: \"lyria\", raw: cause }),\n ),\n )\n const { inline, text } = extractParts(wire)\n if (inline === undefined) {\n // No audio came back. Most common cause: Lyria filtered the prompt\n // (named-artist references, copyrighted lyrics, etc.). Surface the\n // full response body — text parts, finish reasons, prompt-feedback\n // blocks — so the caller can see why.\n return yield* new AiError.GenerationFailed({\n provider: \"lyria\",\n raw: {\n message: \"Lyria response had no inlineData audio part.\",\n hint: \"Likely a prompt-filter rejection. Lyria filters references to real artists / copyrighted song lyrics. Rephrase with descriptive style language instead of artist names.\",\n textParts: text,\n response: json,\n },\n })\n }\n const bytes = yield* decodeBase64ToBytes(inline.data)\n return {\n format: realizedFormat(mimeType),\n bytes,\n ...(text !== undefined && { lyrics: text }),\n watermark: { kind: \"synthid\" },\n } satisfies MusicResult\n })\n\nconst streamGenerationImpl =\n (cfg: Config) =>\n (\n request: LyriaGenerateRequest,\n ): Stream.Stream<AudioChunk, AiError.AiError, HttpClient.HttpClient> =>\n Stream.unwrap(\n Effect.map(generateImpl(cfg)(request), (result) =>\n Stream.succeed<AudioChunk>({ bytes: result.bytes }),\n ),\n )\n\n/**\n * Lyria 3 sync has no bidirectional session at the wire level — that's\n * Lyria RealTime (Phase 1b, separate Layer). The provider Layer here\n * also does NOT register `MusicInteractiveSession`, so callers using\n * `MusicGenerator.streamGenerationFrom` against only this Layer get a\n * compile-time error before this runtime fallback ever fires.\n */\nconst streamGenerationFromUnsupported = <E, R>(\n _input: Stream.Stream<MusicSessionInput, E, R>,\n _request: CommonStreamGenerateMusicRequest,\n): Stream.Stream<AudioChunk, AiError.AiError | E, R> => {\n const fail: Stream.Stream<AudioChunk, AiError.AiError | E, R> = Stream.fail(\n new AiError.Unsupported({\n provider: \"lyria\",\n capability: \"streamGenerationFrom\",\n reason:\n \"Lyria 3 sync does not support bidirectional sessions. Use the Lyria RealTime Layer (WebSocket BidiGenerateMusic) for interactive mid-session prompts.\",\n }),\n )\n return fail\n}\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (\n cfg: Config,\n): Effect.Effect<LyriaGeneratorService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n generate: (request) =>\n generateImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n streamGeneration: (request) =>\n streamGenerationImpl(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client)),\n streamGenerationFrom: streamGenerationFromUnsupported,\n }))\n\n/**\n * Layer that registers both the provider-specific `LyriaGenerator` tag\n * and the generic `MusicGenerator` tag.\n *\n * Does NOT register the `MusicInteractiveSession` capability marker —\n * Lyria 3 sync has no bidirectional session at the wire level. Code\n * calling `MusicGenerator.streamGenerationFrom` will fail to typecheck\n * against this Layer alone, which is the intended UX.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<LyriaGenerator | MusicGenerator, never, HttpClient.HttpClient> =>\n Layer.merge(\n Layer.effect(LyriaGenerator, make(cfg)),\n Layer.effect(\n MusicGenerator,\n Effect.map(\n make(cfg),\n (s): MusicGeneratorService => ({\n generate: (req: CommonGenerateMusicRequest) => s.generate(req as LyriaGenerateRequest),\n streamGeneration: (req: CommonStreamGenerateMusicRequest) =>\n s.streamGeneration(req as LyriaGenerateRequest),\n streamGenerationFrom: s.streamGenerationFrom,\n }),\n ),\n ),\n )\n"],"mappings":"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"}
1
+ {"version":3,"file":"LyriaGenerator.mjs","names":[],"sources":["../src/LyriaGenerator.ts"],"sourcesContent":["import {\n Context,\n Duration,\n Effect,\n Encoding,\n Layer,\n Match,\n Redacted,\n Result,\n Schema,\n Stream,\n} from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioChunk, AudioFormat } from \"@effect-uai/core/Audio\"\nimport { warnDroppedWhen } from \"@effect-uai/core/Capabilities\"\nimport type {\n CommonGenerateMusicRequest,\n CommonStreamGenerateMusicRequest,\n GenerateResult,\n MusicResult,\n MusicSessionInput,\n MusicStreamEvent,\n} from \"@effect-uai/core/Music\"\nimport { singleVariant } from \"@effect-uai/core/Music\"\nimport { MusicGenerator, type MusicGeneratorService } from \"@effect-uai/core/MusicGenerator\"\nimport type { LyriaModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Lyria-typed generate request. Narrows `model` to `LyriaModel`.\n *\n * Lyria 3 sync `generateContent` has no structured wire fields for\n * `lyrics` / `duration` / `seed` / `instrumental` / `bpm` / `scale` /\n * etc. Those fields on the Common request are logged as\n * dropped-bucket-2 hints; the adapter never rewrites or augments the\n * caller's prompt on their behalf. If you want lyrics or vocal\n * suppression, include them in your `prompt` text yourself.\n *\n * Output format: only `audio/mp3` and `audio/wav` are accepted; other\n * containers fail `Unsupported`. The Clip model variant is fixed at\n * `audio/mp3`.\n */\nexport type LyriaGenerateRequest = Omit<CommonGenerateMusicRequest, \"model\"> & {\n readonly model: LyriaModel\n}\n\nexport type LyriaGeneratorService = {\n readonly generate: (\n request: LyriaGenerateRequest,\n ) => Effect.Effect<GenerateResult, AiError.AiError>\n readonly streamGeneration: (\n request: LyriaGenerateRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError>\n readonly streamGenerationFrom: <E, R>(\n input: Stream.Stream<MusicSessionInput, E, R>,\n request: CommonStreamGenerateMusicRequest,\n ) => Stream.Stream<MusicStreamEvent, AiError.AiError | E, R>\n}\n\nexport class LyriaGenerator extends Context.Service<LyriaGenerator, LyriaGeneratorService>()(\n \"@betalyra/effect-uai/providers/google/LyriaGenerator\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec: AudioFormat → Lyria mimeType\n// ---------------------------------------------------------------------------\n\nexport type LyriaResponseMimeType = \"audio/mp3\" | \"audio/wav\"\n\nexport const containerToMimeType: (\n container: AudioFormat[\"container\"],\n) => Effect.Effect<LyriaResponseMimeType, AiError.AiError> = Match.type<\n AudioFormat[\"container\"]\n>().pipe(\n Match.when(\"mp3\", () => Effect.succeed<LyriaResponseMimeType>(\"audio/mp3\")),\n Match.when(\"wav\", () => Effect.succeed<LyriaResponseMimeType>(\"audio/wav\")),\n Match.whenOr(\"ogg\", \"opus\", \"flac\", \"aac\", \"webm\", \"raw\", (c) =>\n Effect.fail(\n new AiError.Unsupported({\n provider: \"lyria\",\n capability: \"outputFormat\",\n reason: `Lyria 3 only produces audio/mp3 or audio/wav; ${c} is not available. Use container: \"mp3\" or \"wav\".`,\n }),\n ),\n ),\n Match.exhaustive,\n)\n\n/**\n * Is this a Lyria 3 Clip model variant? Clip is fixed at 30 s MP3, it\n * does not accept a `responseFormat` on `generationConfig` (the live\n * API rejects unknown fields). Pro accepts the documented\n * `responseFormat` knob.\n */\nconst isClipModel = (model: string): boolean => model.includes(\"clip\")\n\nexport const realizedFormat: (mime: LyriaResponseMimeType) => AudioFormat =\n Match.type<LyriaResponseMimeType>().pipe(\n Match.when(\n \"audio/mp3\",\n (): AudioFormat => ({ container: \"mp3\", encoding: \"mp3\", sampleRate: 44100, channels: 2 }),\n ),\n Match.when(\n \"audio/wav\",\n (): AudioFormat => ({\n container: \"wav\",\n encoding: \"pcm_s16le\",\n sampleRate: 44100,\n channels: 2,\n }),\n ),\n Match.exhaustive,\n )\n\n// ---------------------------------------------------------------------------\n// Bucket-2 warn-and-drop for Common fields Lyria 3 can't honor\n// ---------------------------------------------------------------------------\n\n/**\n * Warn (don't fail) when the caller passed Common fields Lyria 3 has\n * no wire field for. Bucket-2 per capabilities policy: provider has\n * no structured interpretation; the output is still valid music, just\n * less aligned with the caller's hint. We never embed these into the\n * prompt on the caller's behalf, prompt construction is the\n * developer's job.\n */\nconst warnDroppedHints = (request: LyriaGenerateRequest): Effect.Effect<void> =>\n Effect.all(\n [\n warnDroppedWhen(request.lyrics, {\n provider: \"lyria\",\n capability: \"lyrics\",\n field: \"lyrics\",\n reason:\n \"Lyria 3 sync has no `lyrics` wire field. If you want vocals to follow specific lyrics, embed them in your prompt yourself (e.g. with `[Verse]` / `[Chorus]` tags).\",\n }),\n warnDroppedWhen(\n request.duration !== undefined ? Duration.toMillis(request.duration) : undefined,\n {\n provider: \"lyria\",\n capability: \"duration\",\n field: \"duration\",\n reason:\n \"Lyria 3 clip is fixed at 30 s; pro derives duration from prompt content. There is no wire field for duration. If you want to steer length, mention it in your prompt.\",\n },\n ),\n warnDroppedWhen(request.seed, {\n provider: \"lyria\",\n capability: \"seed\",\n field: \"seed\",\n reason:\n \"Lyria 3 (Gemini surface) does not expose a seed parameter. Output is non-deterministic across identical prompts. Use Lyria 2 (Vertex) for seeded generation.\",\n }),\n ],\n { discard: true },\n )\n\n// ---------------------------------------------------------------------------\n// Wire response schema\n// ---------------------------------------------------------------------------\n\nconst InlineData = Schema.Struct({\n mimeType: Schema.optional(Schema.String),\n data: Schema.String,\n})\n\nconst Part = Schema.Struct({\n text: Schema.optional(Schema.String),\n inlineData: Schema.optional(InlineData),\n})\n\nconst Content = Schema.Struct({\n parts: Schema.optional(Schema.Array(Part)),\n})\n\nconst Candidate = Schema.Struct({\n content: Schema.optional(Content),\n})\n\nconst Wire = Schema.Struct({\n candidates: Schema.optional(Schema.Array(Candidate)),\n})\n\nconst decodeWire = Schema.decodeUnknownEffect(Wire)\n\n// ---------------------------------------------------------------------------\n// HTTP plumbing\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config): string =>\n cfg.baseUrl ?? \"https://generativelanguage.googleapis.com/v1beta\"\n\nconst transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"lyria\", raw: cause })\n\nconst 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: \"lyria\", raw: body }),\n ),\n Match.whenOr(\n 408,\n 504,\n (): AiError.AiError => new AiError.Timeout({ provider: \"lyria\", raw: body }),\n ),\n Match.when(\n 401,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"lyria\", subtype: \"auth\", raw: body }),\n ),\n Match.when(\n 403,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"lyria\", subtype: \"permission\", raw: body }),\n ),\n Match.when(\n 402,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"lyria\", subtype: \"billing\", raw: body }),\n ),\n Match.when(\n (n) => n >= 500,\n (n): AiError.AiError => new AiError.Unavailable({ provider: \"lyria\", status: n, raw: body }),\n ),\n Match.orElse(\n (): AiError.AiError => new AiError.InvalidRequest({ provider: \"lyria\", raw: body }),\n ),\n )\n\nconst decodeBase64ToBytes = (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: \"lyria\", param: \"audio\", raw: cause })),\n })\n\n/**\n * Find the first inline-data audio part and the concatenated text parts\n * from a Wire response.\n */\nconst extractParts = (\n wire: typeof Wire.Type,\n): { readonly inline?: typeof InlineData.Type; readonly text?: string } => {\n const allParts = (wire.candidates ?? []).flatMap((c) => c.content?.parts ?? [])\n const inline = allParts.find((p) => p.inlineData !== undefined)?.inlineData\n const textParts = allParts.flatMap((p) => (p.text !== undefined ? [p.text] : []))\n const text = textParts.length === 0 ? undefined : textParts.join(\"\\n\")\n return {\n ...(inline !== undefined && { inline }),\n ...(text !== undefined && { text }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst generateImpl =\n (cfg: Config) =>\n (\n request: LyriaGenerateRequest,\n ): Effect.Effect<GenerateResult, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const requestedContainer = (request.outputFormat?.container ??\n \"mp3\") satisfies AudioFormat[\"container\"]\n const requestedMime = yield* containerToMimeType(requestedContainer)\n // lyria-3-clip-preview has no `responseFormat` wire field and always\n // emits mp3, so there is no format to send and no 400 to surface. Rather\n // than keep a per-model table that rejects `wav`, report the format the\n // model actually produces (mp3) so the result is never mislabeled.\n // lyria-3-pro honours the requested container.\n const mimeType = isClipModel(request.model) ? (\"audio/mp3\" as const) : requestedMime\n yield* warnDroppedHints(request)\n const body = isClipModel(request.model)\n ? { contents: [{ parts: [{ text: request.prompt }] }] }\n : {\n contents: [{ parts: [{ text: request.prompt }] }],\n generationConfig: {\n responseModalities: [\"AUDIO\", \"TEXT\"],\n responseFormat: { audio: { mimeType } },\n },\n }\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(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 const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const wire = yield* decodeWire(json).pipe(\n Effect.mapError(\n (cause): AiError.AiError =>\n new AiError.GenerationFailed({ provider: \"lyria\", raw: cause }),\n ),\n )\n const { inline, text } = extractParts(wire)\n if (inline === undefined) {\n return yield* new AiError.GenerationFailed({\n provider: \"lyria\",\n raw: {\n message: \"Lyria response had no inlineData audio part.\",\n hint: \"Likely a prompt-filter rejection. Lyria filters references to real artists / copyrighted song lyrics. Rephrase with descriptive style language instead of artist names.\",\n textParts: text,\n response: json,\n },\n })\n }\n const bytes = yield* decodeBase64ToBytes(inline.data)\n const result: MusicResult = {\n audio: { format: realizedFormat(mimeType), bytes },\n provider: \"lyria\",\n ...(text !== undefined && { lyrics: text }),\n watermark: \"synthid\",\n }\n return singleVariant(result)\n })\n\nconst streamGenerationImpl =\n (cfg: Config) =>\n (\n request: LyriaGenerateRequest,\n ): Stream.Stream<AudioChunk, AiError.AiError, HttpClient.HttpClient> =>\n Stream.unwrap(\n Effect.map(generateImpl(cfg)(request), (result) =>\n Stream.succeed<AudioChunk>({ bytes: result.primary.audio.bytes }),\n ),\n )\n\n/**\n * Lyria 3 sync has no bidirectional session at the wire level, that's\n * Lyria RealTime (separate Layer). The provider Layer here also does\n * NOT register `MusicInteractiveSession`, so callers using\n * `MusicGenerator.streamGenerationFrom` against only this Layer get a\n * compile-time error before this runtime fallback ever fires.\n */\nconst streamGenerationFromUnsupported = <E, R>(\n _input: Stream.Stream<MusicSessionInput, E, R>,\n _request: CommonStreamGenerateMusicRequest,\n): Stream.Stream<MusicStreamEvent, AiError.AiError | E, R> => {\n const fail: Stream.Stream<MusicStreamEvent, AiError.AiError | E, R> = Stream.fail(\n new AiError.Unsupported({\n provider: \"lyria\",\n capability: \"streamGenerationFrom\",\n reason:\n \"Lyria 3 sync does not support bidirectional sessions. Use the Lyria RealTime Layer (WebSocket BidiGenerateMusic) for interactive mid-session prompts.\",\n }),\n )\n return fail\n}\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (\n cfg: Config,\n): Effect.Effect<LyriaGeneratorService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n generate: (request) =>\n generateImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n streamGeneration: (request) =>\n streamGenerationImpl(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client)),\n streamGenerationFrom: streamGenerationFromUnsupported,\n }))\n\n/**\n * Layer that registers both the provider-specific `LyriaGenerator` tag\n * and the generic `MusicGenerator` tag.\n *\n * Does NOT register the `MusicInteractiveSession` capability marker,\n * Lyria 3 sync has no bidirectional session at the wire level. Code\n * calling `MusicGenerator.streamGenerationFrom` will fail to typecheck\n * against this Layer alone, which is the intended UX.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<LyriaGenerator | MusicGenerator, never, HttpClient.HttpClient> =>\n Layer.merge(\n Layer.effect(LyriaGenerator, make(cfg)),\n Layer.effect(\n MusicGenerator,\n Effect.map(\n make(cfg),\n (s): MusicGeneratorService => ({\n generate: (req: CommonGenerateMusicRequest) =>\n s.generate({ ...req, model: req.model as LyriaModel }),\n streamGeneration: (req: CommonStreamGenerateMusicRequest) =>\n s.streamGeneration({ ...req, model: req.model as LyriaModel }),\n streamGenerationFrom: s.streamGenerationFrom,\n }),\n ),\n ),\n )\n"],"mappings":"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"}
@@ -1,74 +1,12 @@
1
1
  import { i as it, n as globalExpect, r as describe, t as import_dist } from "./dist-DMsg8YLb.mjs";
2
- import { LyriaGenerator, buildPrompt, containerToMimeType, layer, realizedFormat } from "./LyriaGenerator.mjs";
3
- import { Effect, Layer, Redacted, Stream } from "effect";
2
+ import { LyriaGenerator, containerToMimeType, layer, realizedFormat } from "./LyriaGenerator.mjs";
3
+ import { Duration, Effect, Layer, Redacted, Stream } from "effect";
4
4
  import { FetchHttpClient } from "effect/unstable/http";
5
- import * as MusicGenerator$1 from "@effect-uai/core/MusicGenerator";
6
5
  import { promptsInput } from "@effect-uai/core/Music";
6
+ import * as MusicGenerator$1 from "@effect-uai/core/MusicGenerator";
7
7
  //#region src/LyriaGenerator.test.ts
8
8
  const cfg = { apiKey: Redacted.make("test-key") };
9
9
  const live = Layer.provide(layer(cfg), FetchHttpClient.layer);
10
- describe("buildPrompt", () => {
11
- it("returns a plain string when prompts is a string", () => {
12
- globalExpect(buildPrompt({
13
- model: "lyria-3-clip-preview",
14
- prompts: "upbeat indie pop"
15
- })).toBe("upbeat indie pop");
16
- });
17
- it("joins weighted prompts with weight annotations", () => {
18
- const out = buildPrompt({
19
- model: "lyria-3-clip-preview",
20
- prompts: [{
21
- text: "minimal techno",
22
- weight: 1
23
- }, {
24
- text: "1980s synthwave",
25
- weight: .3
26
- }]
27
- });
28
- globalExpect(out).toContain("minimal techno");
29
- globalExpect(out).toContain("1980s synthwave (weight 0.3)");
30
- });
31
- it("omits the weight annotation for weight === 1", () => {
32
- globalExpect(buildPrompt({
33
- model: "lyria-3-clip-preview",
34
- prompts: [{
35
- text: "ambient",
36
- weight: 1
37
- }]
38
- })).toBe("ambient");
39
- });
40
- it("splices in bpm, scale, duration, and instrumental hints", () => {
41
- const out = buildPrompt({
42
- model: "lyria-3-clip-preview",
43
- prompts: "house",
44
- bpm: 124,
45
- scale: "C_MAJOR",
46
- durationSeconds: 30,
47
- instrumental: true
48
- });
49
- globalExpect(out).toContain("Instrumental only");
50
- globalExpect(out).toContain("BPM: 124");
51
- globalExpect(out).toContain("Key/scale: C_MAJOR");
52
- globalExpect(out).toContain("Target duration: 30s");
53
- });
54
- it("appends a lyrics block when lyrics is set and instrumental is not true", () => {
55
- const out = buildPrompt({
56
- model: "lyria-3-clip-preview",
57
- prompts: "indie rock",
58
- lyrics: "[Verse]\nhello world"
59
- });
60
- globalExpect(out).toContain("Lyrics:");
61
- globalExpect(out).toContain("[Verse]");
62
- });
63
- it("drops the lyrics block when instrumental is true", () => {
64
- globalExpect(buildPrompt({
65
- model: "lyria-3-clip-preview",
66
- prompts: "indie rock",
67
- lyrics: "[Verse]\nhello world",
68
- instrumental: true
69
- })).not.toContain("Lyrics:");
70
- });
71
- });
72
10
  describe("containerToMimeType", () => {
73
11
  it("maps mp3 and wav", async () => {
74
12
  globalExpect(await Effect.runPromise(containerToMimeType("mp3"))).toBe("audio/mp3");
@@ -103,7 +41,7 @@ describe("LyriaGenerator capability guards (runtime)", () => {
103
41
  it("streamGenerationFrom returns an Unsupported stream", async () => {
104
42
  const program = LyriaGenerator.use((s) => Stream.runDrain(s.streamGenerationFrom(Stream.fromIterable([promptsInput([{ text: "x" }])]), {
105
43
  model: "lyria-3-clip-preview",
106
- prompts: ""
44
+ prompt: ""
107
45
  })));
108
46
  const exit = await Effect.runPromiseExit(program.pipe(Effect.provide(live)));
109
47
  globalExpect(exit._tag).toBe("Failure");
@@ -115,22 +53,38 @@ describe("LyriaGenerator capability guards (runtime)", () => {
115
53
  });
116
54
  describe("LyriaGenerator Layer (compile-time)", () => {
117
55
  it("leaves `MusicInteractiveSession` unsatisfied when using `streamGenerationFrom` against this Layer", () => {
118
- const audio = Stream.fromIterable([promptsInput([{ text: "techno" }])]).pipe(MusicGenerator$1.streamGenerationFrom({
56
+ const events = Stream.fromIterable([promptsInput([{ text: "techno" }])]).pipe(MusicGenerator$1.streamGenerationFrom({
119
57
  model: "lyria-3-clip-preview",
120
- prompts: ""
58
+ prompt: ""
121
59
  }));
122
- (0, import_dist.expectTypeOf)(Stream.runDrain(audio).pipe(Effect.provide(live))).toEqualTypeOf();
60
+ (0, import_dist.expectTypeOf)(Stream.runDrain(events).pipe(Effect.provide(live))).toEqualTypeOf();
123
61
  });
124
- it("sync `generate` and chunked `streamGeneration` require no marker", () => {
62
+ it("sync `generate` returns GenerateResult and requires no marker", () => {
125
63
  (0, import_dist.expectTypeOf)(MusicGenerator$1.generate({
126
64
  model: "lyria-3-clip-preview",
127
- prompts: "ambient"
65
+ prompt: "ambient"
128
66
  }).pipe(Effect.provide(live))).toEqualTypeOf();
67
+ });
68
+ it("chunked `streamGeneration` requires no marker", () => {
129
69
  globalExpect(Stream.runCollect(MusicGenerator$1.streamGeneration({
130
70
  model: "lyria-3-clip-preview",
131
- prompts: "ambient"
71
+ prompt: "ambient"
132
72
  })).pipe(Effect.provide(live))).toBeDefined();
133
73
  });
74
+ it("Lyria-typed request accepts no `instrumental` field", () => {
75
+ globalExpect({
76
+ model: "lyria-3-clip-preview",
77
+ prompt: "ambient",
78
+ instrumental: true
79
+ }).toBeDefined();
80
+ });
81
+ it("Common request supports `duration: Duration.Duration`", () => {
82
+ globalExpect({
83
+ model: "lyria-3-clip-preview",
84
+ prompt: "ambient",
85
+ duration: Duration.seconds(30)
86
+ }).toBeDefined();
87
+ });
134
88
  });
135
89
  //#endregion
136
90
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"LyriaGenerator.test.mjs","names":["LyriaGenerator.layer","LyriaGenerator.buildPrompt","LyriaGenerator.containerToMimeType","LyriaGenerator.realizedFormat","MusicGenerator"],"sources":["../src/LyriaGenerator.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 * as MusicGenerator from \"@effect-uai/core/MusicGenerator\"\nimport { promptsInput } from \"@effect-uai/core/Music\"\nimport * as LyriaGenerator from \"./LyriaGenerator.js\"\n\nconst cfg: LyriaGenerator.Config = { apiKey: Redacted.make(\"test-key\") }\n// FetchHttpClient is required for `make`, but these tests only exercise the\n// compile-time gating and runtime Unsupported branches — no real HTTP is made.\nconst live = Layer.provide(LyriaGenerator.layer(cfg), FetchHttpClient.layer)\n\ndescribe(\"buildPrompt\", () => {\n it(\"returns a plain string when prompts is a string\", () => {\n const out = LyriaGenerator.buildPrompt({\n model: \"lyria-3-clip-preview\",\n prompts: \"upbeat indie pop\",\n })\n expect(out).toBe(\"upbeat indie pop\")\n })\n\n it(\"joins weighted prompts with weight annotations\", () => {\n const out = LyriaGenerator.buildPrompt({\n model: \"lyria-3-clip-preview\",\n prompts: [\n { text: \"minimal techno\", weight: 1.0 },\n { text: \"1980s synthwave\", weight: 0.3 },\n ],\n })\n expect(out).toContain(\"minimal techno\")\n expect(out).toContain(\"1980s synthwave (weight 0.3)\")\n })\n\n it(\"omits the weight annotation for weight === 1\", () => {\n const out = LyriaGenerator.buildPrompt({\n model: \"lyria-3-clip-preview\",\n prompts: [{ text: \"ambient\", weight: 1 }],\n })\n expect(out).toBe(\"ambient\")\n })\n\n it(\"splices in bpm, scale, duration, and instrumental hints\", () => {\n const out = LyriaGenerator.buildPrompt({\n model: \"lyria-3-clip-preview\",\n prompts: \"house\",\n bpm: 124,\n scale: \"C_MAJOR\",\n durationSeconds: 30,\n instrumental: true,\n })\n expect(out).toContain(\"Instrumental only\")\n expect(out).toContain(\"BPM: 124\")\n expect(out).toContain(\"Key/scale: C_MAJOR\")\n expect(out).toContain(\"Target duration: 30s\")\n })\n\n it(\"appends a lyrics block when lyrics is set and instrumental is not true\", () => {\n const out = LyriaGenerator.buildPrompt({\n model: \"lyria-3-clip-preview\",\n prompts: \"indie rock\",\n lyrics: \"[Verse]\\nhello world\",\n })\n expect(out).toContain(\"Lyrics:\")\n expect(out).toContain(\"[Verse]\")\n })\n\n it(\"drops the lyrics block when instrumental is true\", () => {\n const out = LyriaGenerator.buildPrompt({\n model: \"lyria-3-clip-preview\",\n prompts: \"indie rock\",\n lyrics: \"[Verse]\\nhello world\",\n instrumental: true,\n })\n expect(out).not.toContain(\"Lyrics:\")\n })\n})\n\ndescribe(\"containerToMimeType\", () => {\n it(\"maps mp3 and wav\", async () => {\n expect(await Effect.runPromise(LyriaGenerator.containerToMimeType(\"mp3\"))).toBe(\"audio/mp3\")\n expect(await Effect.runPromise(LyriaGenerator.containerToMimeType(\"wav\"))).toBe(\"audio/wav\")\n })\n\n it(\"fails Unsupported for ogg / opus / flac / aac / webm / raw\", async () => {\n const exit = await Effect.runPromiseExit(LyriaGenerator.containerToMimeType(\"ogg\"))\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(\"realizedFormat\", () => {\n it(\"reports 44.1 kHz stereo for both mp3 and wav\", () => {\n expect(LyriaGenerator.realizedFormat(\"audio/mp3\")).toEqual({\n container: \"mp3\",\n encoding: \"mp3\",\n sampleRate: 44100,\n channels: 2,\n })\n expect(LyriaGenerator.realizedFormat(\"audio/wav\")).toEqual({\n container: \"wav\",\n encoding: \"pcm_s16le\",\n sampleRate: 44100,\n channels: 2,\n })\n })\n})\n\ndescribe(\"LyriaGenerator capability guards (runtime)\", () => {\n it(\"streamGenerationFrom returns an Unsupported stream\", async () => {\n const program = LyriaGenerator.LyriaGenerator.use((s) =>\n Stream.runDrain(\n s.streamGenerationFrom(Stream.fromIterable([promptsInput([{ text: \"x\" }])]), {\n model: \"lyria-3-clip-preview\",\n prompts: \"\",\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(\"streamGenerationFrom\")\n }\n })\n})\n\ndescribe(\"LyriaGenerator Layer (compile-time)\", () => {\n it(\"leaves `MusicInteractiveSession` unsatisfied when using `streamGenerationFrom` against this Layer\", () => {\n const inputs = Stream.fromIterable([promptsInput([{ text: \"techno\" }])])\n const audio = inputs.pipe(\n MusicGenerator.streamGenerationFrom({\n model: \"lyria-3-clip-preview\",\n prompts: \"\",\n }),\n )\n const provided = Stream.runDrain(audio).pipe(Effect.provide(live))\n expectTypeOf(provided).toEqualTypeOf<\n Effect.Effect<void, AiError.AiError, MusicGenerator.MusicInteractiveSession>\n >()\n })\n\n it(\"sync `generate` and chunked `streamGeneration` require no marker\", () => {\n const gen = MusicGenerator.generate({\n model: \"lyria-3-clip-preview\",\n prompts: \"ambient\",\n }).pipe(Effect.provide(live))\n expectTypeOf(gen).toEqualTypeOf<\n Effect.Effect<import(\"@effect-uai/core/Music\").MusicResult, AiError.AiError, never>\n >()\n const _stream = Stream.runCollect(\n MusicGenerator.streamGeneration({\n model: \"lyria-3-clip-preview\",\n prompts: \"ambient\",\n }),\n ).pipe(Effect.provide(live))\n expect(_stream).toBeDefined()\n })\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"LyriaGenerator.test.mjs","names":["LyriaGenerator.layer","LyriaGenerator.containerToMimeType","LyriaGenerator.realizedFormat","MusicGenerator"],"sources":["../src/LyriaGenerator.test.ts"],"sourcesContent":["import { Duration, 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 { GenerateResult } from \"@effect-uai/core/Music\"\nimport { promptsInput } from \"@effect-uai/core/Music\"\nimport * as MusicGenerator from \"@effect-uai/core/MusicGenerator\"\nimport * as LyriaGenerator from \"./LyriaGenerator.js\"\n\nconst cfg: LyriaGenerator.Config = { apiKey: Redacted.make(\"test-key\") }\n// FetchHttpClient is required for `make`, but these tests only exercise the\n// compile-time gating and runtime Unsupported branches, no real HTTP is made.\nconst live = Layer.provide(LyriaGenerator.layer(cfg), FetchHttpClient.layer)\n\ndescribe(\"containerToMimeType\", () => {\n it(\"maps mp3 and wav\", async () => {\n expect(await Effect.runPromise(LyriaGenerator.containerToMimeType(\"mp3\"))).toBe(\"audio/mp3\")\n expect(await Effect.runPromise(LyriaGenerator.containerToMimeType(\"wav\"))).toBe(\"audio/wav\")\n })\n\n it(\"fails Unsupported for ogg / opus / flac / aac / webm / raw\", async () => {\n const exit = await Effect.runPromiseExit(LyriaGenerator.containerToMimeType(\"ogg\"))\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(\"realizedFormat\", () => {\n it(\"reports 44.1 kHz stereo for both mp3 and wav\", () => {\n expect(LyriaGenerator.realizedFormat(\"audio/mp3\")).toEqual({\n container: \"mp3\",\n encoding: \"mp3\",\n sampleRate: 44100,\n channels: 2,\n })\n expect(LyriaGenerator.realizedFormat(\"audio/wav\")).toEqual({\n container: \"wav\",\n encoding: \"pcm_s16le\",\n sampleRate: 44100,\n channels: 2,\n })\n })\n})\n\ndescribe(\"LyriaGenerator capability guards (runtime)\", () => {\n it(\"streamGenerationFrom returns an Unsupported stream\", async () => {\n const program = LyriaGenerator.LyriaGenerator.use((s) =>\n Stream.runDrain(\n s.streamGenerationFrom(Stream.fromIterable([promptsInput([{ text: \"x\" }])]), {\n model: \"lyria-3-clip-preview\",\n prompt: \"\",\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(\"streamGenerationFrom\")\n }\n })\n})\n\ndescribe(\"LyriaGenerator Layer (compile-time)\", () => {\n it(\"leaves `MusicInteractiveSession` unsatisfied when using `streamGenerationFrom` against this Layer\", () => {\n const inputs = Stream.fromIterable([promptsInput([{ text: \"techno\" }])])\n const events = inputs.pipe(\n MusicGenerator.streamGenerationFrom({\n model: \"lyria-3-clip-preview\",\n prompt: \"\",\n }),\n )\n const provided = Stream.runDrain(events).pipe(Effect.provide(live))\n expectTypeOf(provided).toEqualTypeOf<\n Effect.Effect<void, AiError.AiError, MusicGenerator.MusicInteractiveSession>\n >()\n })\n\n it(\"sync `generate` returns GenerateResult and requires no marker\", () => {\n const gen = MusicGenerator.generate({\n model: \"lyria-3-clip-preview\",\n prompt: \"ambient\",\n }).pipe(Effect.provide(live))\n expectTypeOf(gen).toEqualTypeOf<Effect.Effect<GenerateResult, AiError.AiError, never>>()\n })\n\n it(\"chunked `streamGeneration` requires no marker\", () => {\n const _stream = Stream.runCollect(\n MusicGenerator.streamGeneration({\n model: \"lyria-3-clip-preview\",\n prompt: \"ambient\",\n }),\n ).pipe(Effect.provide(live))\n expect(_stream).toBeDefined()\n })\n\n it(\"Lyria-typed request accepts no `instrumental` field\", () => {\n const _req: LyriaGenerator.LyriaGenerateRequest = {\n model: \"lyria-3-clip-preview\",\n prompt: \"ambient\",\n // @ts-expect-error: instrumental was removed from LyriaGenerateRequest in v0.7\n instrumental: true,\n }\n expect(_req).toBeDefined()\n })\n\n it(\"Common request supports `duration: Duration.Duration`\", () => {\n const _req: MusicGenerator.CommonGenerateMusicRequest = {\n model: \"lyria-3-clip-preview\",\n prompt: \"ambient\",\n duration: Duration.seconds(30),\n }\n expect(_req).toBeDefined()\n })\n})\n"],"mappings":";;;;;;;AASA,MAAM,MAA6B,EAAE,QAAQ,SAAS,KAAK,WAAW,EAAE;AAGxE,MAAM,OAAO,MAAM,QAAQA,MAAqB,IAAI,EAAE,gBAAgB,MAAM;AAE5E,SAAS,6BAA6B;AACpC,IAAG,oBAAoB,YAAY;AACjC,eAAO,MAAM,OAAO,WAAWC,oBAAmC,MAAM,CAAC,CAAC,CAAC,KAAK,YAAY;AAC5F,eAAO,MAAM,OAAO,WAAWA,oBAAmC,MAAM,CAAC,CAAC,CAAC,KAAK,YAAY;GAC5F;AAEF,IAAG,8DAA8D,YAAY;EAC3E,MAAM,OAAO,MAAM,OAAO,eAAeA,oBAAmC,MAAM,CAAC;AACnF,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,wBAAwB;AAC/B,IAAG,sDAAsD;AACvD,eAAOC,eAA8B,YAAY,CAAC,CAAC,QAAQ;GACzD,WAAW;GACX,UAAU;GACV,YAAY;GACZ,UAAU;GACX,CAAC;AACF,eAAOA,eAA8B,YAAY,CAAC,CAAC,QAAQ;GACzD,WAAW;GACX,UAAU;GACV,YAAY;GACZ,UAAU;GACX,CAAC;GACF;EACF;AAEF,SAAS,oDAAoD;AAC3D,IAAG,sDAAsD,YAAY;EACnE,MAAM,UAAA,eAAwC,KAAK,MACjD,OAAO,SACL,EAAE,qBAAqB,OAAO,aAAa,CAAC,aAAa,CAAC,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE;GAC3E,OAAO;GACP,QAAQ;GACT,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,uBAAuB;;GAEtE;EACF;AAEF,SAAS,6CAA6C;AACpD,IAAG,2GAA2G;EAE5G,MAAM,SADS,OAAO,aAAa,CAAC,aAAa,CAAC,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,CAClD,CAAC,KACpBC,iBAAe,qBAAqB;GAClC,OAAO;GACP,QAAQ;GACT,CAAC,CACH;AAED,GAAA,GAAA,YAAA,cADiB,OAAO,SAAS,OAAO,CAAC,KAAK,OAAO,QAAQ,KAAK,CAC7C,CAAC,CAAC,eAEpB;GACH;AAEF,IAAG,uEAAuE;AAKxE,GAAA,GAAA,YAAA,cAJYA,iBAAe,SAAS;GAClC,OAAO;GACP,QAAQ;GACT,CAAC,CAAC,KAAK,OAAO,QAAQ,KAAK,CACZ,CAAC,CAAC,eAAsE;GACxF;AAEF,IAAG,uDAAuD;AAOxD,eANgB,OAAO,WACrBA,iBAAe,iBAAiB;GAC9B,OAAO;GACP,QAAQ;GACT,CAAC,CACH,CAAC,KAAK,OAAO,QAAQ,KAAK,CACb,CAAC,CAAC,aAAa;GAC7B;AAEF,IAAG,6DAA6D;AAO9D,eAAO;GALL,OAAO;GACP,QAAQ;GAER,cAAc;GAEL,CAAC,CAAC,aAAa;GAC1B;AAEF,IAAG,+DAA+D;AAMhE,eAAO;GAJL,OAAO;GACP,QAAQ;GACR,UAAU,SAAS,QAAQ,GAAG;GAErB,CAAC,CAAC,aAAa;GAC1B;EACF"}
package/dist/codec.d.mts CHANGED
@@ -1,11 +1,12 @@
1
1
  import { Option, Schema } from "effect";
2
+ import { CommonRequest } from "@effect-uai/core/LanguageModel";
2
3
  import { Turn } from "@effect-uai/core/Turn";
3
- import { Item } from "@effect-uai/core/Items";
4
+ import { HistoryItem } from "@effect-uai/core/Items";
4
5
  import { ToolDescriptor } from "@effect-uai/core/Tool";
5
6
 
6
7
  //#region src/codec.d.ts
7
8
  declare namespace codec_d_exports {
8
- export { AccumulatedFunctionCall, Accumulator, ChunkPart, ChunkResult, GenerationConfig, RequestBody, ThinkingConfig, WireChunk, accumulatorToTurn, buildRequestBody, emptyAccumulator, ingestChunk };
9
+ export { AccumulatedFunctionCall, Accumulator, ChunkPart, ChunkResult, GenerationConfig, RequestBody, ThinkingConfig, WireChunk, accumulatorToTurn, buildRequestBody, emptyAccumulator, hasUrlImageSource, ingestChunk };
9
10
  }
10
11
  declare const WireChunk: Schema.Struct<{
11
12
  readonly candidates: Schema.optional<Schema.$Array<Schema.Struct<{
@@ -98,11 +99,18 @@ type RequestBody = {
98
99
  readonly tools?: ReadonlyArray<RequestTool>;
99
100
  readonly toolConfig?: RequestToolConfig;
100
101
  };
101
- declare const buildRequestBody: (history: ReadonlyArray<Item>, generationConfig: Option.Option<GenerationConfig>, tools?: ReadonlyArray<ToolDescriptor>) => RequestBody;
102
+ /**
103
+ * URL-form images need Gemini's Files API (upload, then `fileData`); we
104
+ * don't pre-upload, so such a part would otherwise vanish from the request.
105
+ * The caller fails the request with `Unsupported` instead of silently
106
+ * dropping the image.
107
+ */
108
+ declare const hasUrlImageSource: (history: ReadonlyArray<HistoryItem>) => boolean;
109
+ declare const buildRequestBody: (history: ReadonlyArray<HistoryItem>, generationConfig: Option.Option<GenerationConfig>, tools?: ReadonlyArray<ToolDescriptor>, toolChoice?: CommonRequest["toolChoice"]) => RequestBody;
102
110
  type AccumulatedFunctionCall = {
103
111
  /** Synthesized id-or-name we surface as `call_id` on the canonical item. */readonly callId: string;
104
112
  readonly name: string; /** Wire id from Gemini 3, when present - echoed back on `functionResponse`. */
105
- readonly providerId: Option.Option<string>; /** Args as JSON-encoded string, mirroring `Items.FunctionCall.arguments`. */
113
+ readonly providerId: Option.Option<string>; /** Args as JSON-encoded string, mirroring `Items.ToolCall.arguments`. */
106
114
  readonly arguments: string;
107
115
  };
108
116
  type Accumulator = {
@@ -148,5 +156,5 @@ type ChunkResult = {
148
156
  declare const ingestChunk: (acc: Accumulator, chunk: WireChunk) => ChunkResult;
149
157
  declare const accumulatorToTurn: (acc: Accumulator) => Turn;
150
158
  //#endregion
151
- export { AccumulatedFunctionCall, Accumulator, ChunkPart, ChunkResult, GenerationConfig, RequestBody, ThinkingConfig, WireChunk, accumulatorToTurn, buildRequestBody, emptyAccumulator, ingestChunk, codec_d_exports as t };
159
+ export { AccumulatedFunctionCall, Accumulator, ChunkPart, ChunkResult, GenerationConfig, RequestBody, ThinkingConfig, WireChunk, accumulatorToTurn, buildRequestBody, emptyAccumulator, hasUrlImageSource, ingestChunk, codec_d_exports as t };
152
160
  //# sourceMappingURL=codec.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"codec.d.mts","names":[],"sources":["../src/codec.ts"],"mappings":";;;;;;;;;cAwDa,SAAA,EAAS,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAIV,SAAA,UAAmB,SAAA,CAAU,IAAA;AAAA,KAMpC,WAAA;EAAA,SACU,IAAA;AAAA;EAAA,SACA,UAAA;IAAA,SAAuB,QAAA;IAAA,SAA2B,IAAA;EAAA;AAAA;EAAA,SAClD,YAAA;IAAA,SAAyB,IAAA;IAAA,SAAuB,IAAA;EAAA;AAAA;EAAA,SAEhD,gBAAA;IAAA,SACE,IAAA;IAAA,SACA,QAAA,EAAU,MAAA;EAAA;AAAA;AAAA,KAItB,cAAA;EAAA,SACM,IAAA;EAAA,SACA,KAAA,EAAO,aAAA,CAAc,WAAA;AAAA;AAAA,KAG3B,wBAAA;EAAA,SACM,KAAA,EAAO,aAAA;IAAA,SAAyB,IAAA;EAAA;AAAA;;KAItC,WAAA;EAAA,SACM,oBAAA,EAAsB,aAAA;IAAA,SACpB,IAAA;IAAA,SACA,WAAA;IAAA,SACA,UAAA,EAAY,MAAA;EAAA;AAAA;AAAA,KAIpB,iBAAA;EAAA,SACM,qBAAA;IAAA,SACE,IAAA;IAAA,SACA,oBAAA,GAAuB,aAAA;EAAA;AAAA;AAAA,KAIxB,cAAA;EAAA,SACD,cAAA;AAAA;AAAA,KAGC,gBAAA;EAAA,SACD,WAAA;EAAA,SACA,eAAA;EAAA,SACA,IAAA;EAAA,SACA,cAAA,GAAiB,cAAA;WAEjB,gBAAA;WAEA,kBAAA,GAAqB,MAAA;AAAA;AAAA,KAGpB,WAAA;EAAA,SACD,QAAA,EAAU,aAAA,CAAc,cAAA;EAAA,SACxB,iBAAA,GAAoB,wBAAA;EAAA,SACpB,gBAAA,GAAmB,gBAAA;EAAA,SACnB,KAAA,GAAQ,aAAA,CAAc,WAAA;EAAA,SACtB,UAAA,GAAa,iBAAA;AAAA;AAAA,cAsNX,gBAAA,GACX,OAAA,EAAS,aAAA,CAAc,IAAA,GACvB,gBAAA,EAAkB,MAAA,CAAO,MAAA,CAAO,gBAAA,GAChC,KAAA,GAAO,aAAA,CAAc,cAAA,MACpB,WAAA;AAAA,KAiCS,uBAAA;uFAED,MAAA;EAAA,SACA,IAAA;WAEA,UAAA,EAAY,MAAA,CAAO,MAAA;WAEnB,SAAA;AAAA;AAAA,KAGC,WAAA;EAAA,SACD,IAAA;EAAA,SACA,SAAA;EAAA,SACA,aAAA,EAAe,aAAA,CAAc,uBAAA;EAAA,SAC7B,YAAA,EAAc,MAAA,CAAO,MAAA;EAAA,SACrB,KAAA;IAAA,SACE,YAAA;IAAA,SACA,aAAA;IAAA,SACA,YAAA;IAAA,SACA,oBAAA;MAAA,SAAkC,aAAA;IAAA;IAAA,SAClC,qBAAA;MAAA,SAAmC,gBAAA;IAAA;EAAA;AAAA;AAAA,cAInC,gBAAA,EAAkB,WAAA;;;;;;KAanB,SAAA;EAAA,SACG,IAAA;EAAA,SAAuB,IAAA;AAAA;EAAA,SACvB,IAAA;EAAA,SAA4B,IAAA;AAAA;EAAA,SAE5B,IAAA;EAAA,SACA,EAAA,EAAI,MAAA,CAAO,MAAA;EAAA,SACX,IAAA;EAAA,SACA,IAAA;AAAA;AAAA,KAGH,WAAA;EAAA,SACD,WAAA,EAAa,WAAA;EAAA,SACb,KAAA,EAAO,aAAA,CAAc,SAAA;EAAA,SACrB,QAAA;AAAA;AAAA,cA6FE,WAAA,GAAe,GAAA,EAAK,WAAA,EAAa,KAAA,EAAO,SAAA,KAAY,WAAA;AAAA,cA0CpD,iBAAA,GAAqB,GAAA,EAAK,WAAA,KAAc,IAAA"}
1
+ {"version":3,"file":"codec.d.mts","names":[],"sources":["../src/codec.ts"],"mappings":";;;;;;;;;;cA+Da,SAAA,EAAS,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAIV,SAAA,UAAmB,SAAA,CAAU,IAAA;AAAA,KAMpC,WAAA;EAAA,SACU,IAAA;AAAA;EAAA,SACA,UAAA;IAAA,SAAuB,QAAA;IAAA,SAA2B,IAAA;EAAA;AAAA;EAAA,SAClD,YAAA;IAAA,SAAyB,IAAA;IAAA,SAAuB,IAAA;EAAA;AAAA;EAAA,SAEhD,gBAAA;IAAA,SACE,IAAA;IAAA,SACA,QAAA,EAAU,MAAA;EAAA;AAAA;AAAA,KAItB,cAAA;EAAA,SACM,IAAA;EAAA,SACA,KAAA,EAAO,aAAA,CAAc,WAAA;AAAA;AAAA,KAG3B,wBAAA;EAAA,SACM,KAAA,EAAO,aAAA;IAAA,SAAyB,IAAA;EAAA;AAAA;;KAItC,WAAA;EAAA,SACM,oBAAA,EAAsB,aAAA;IAAA,SACpB,IAAA;IAAA,SACA,WAAA;IAAA,SACA,UAAA,EAAY,MAAA;EAAA;AAAA;AAAA,KAIpB,iBAAA;EAAA,SACM,qBAAA;IAAA,SACE,IAAA;IAAA,SACA,oBAAA,GAAuB,aAAA;EAAA;AAAA;AAAA,KAIxB,cAAA;EAAA,SACD,cAAA;AAAA;AAAA,KAGC,gBAAA;EAAA,SACD,WAAA;EAAA,SACA,eAAA;EAAA,SACA,IAAA;EAAA,SACA,cAAA,GAAiB,cAAA;WAEjB,gBAAA;WAEA,kBAAA,GAAqB,MAAA;AAAA;AAAA,KAGpB,WAAA;EAAA,SACD,QAAA,EAAU,aAAA,CAAc,cAAA;EAAA,SACxB,iBAAA,GAAoB,wBAAA;EAAA,SACpB,gBAAA,GAAmB,gBAAA;EAAA,SACnB,KAAA,GAAQ,aAAA,CAAc,WAAA;EAAA,SACtB,UAAA,GAAa,iBAAA;AAAA;;;;;;;cA4OX,iBAAA,GAAqB,OAAA,EAAS,aAAA,CAAc,WAAA;AAAA,cAO5C,gBAAA,GACX,OAAA,EAAS,aAAA,CAAc,WAAA,GACvB,gBAAA,EAAkB,MAAA,CAAO,MAAA,CAAO,gBAAA,GAChC,KAAA,GAAO,aAAA,CAAc,cAAA,GACrB,UAAA,GAAY,aAAA,mBACX,WAAA;AAAA,KAiCS,uBAAA;uFAED,MAAA;EAAA,SACA,IAAA;WAEA,UAAA,EAAY,MAAA,CAAO,MAAA;WAEnB,SAAA;AAAA;AAAA,KAGC,WAAA;EAAA,SACD,IAAA;EAAA,SACA,SAAA;EAAA,SACA,aAAA,EAAe,aAAA,CAAc,uBAAA;EAAA,SAC7B,YAAA,EAAc,MAAA,CAAO,MAAA;EAAA,SACrB,KAAA;IAAA,SACE,YAAA;IAAA,SACA,aAAA;IAAA,SACA,YAAA;IAAA,SACA,oBAAA;MAAA,SAAkC,aAAA;IAAA;IAAA,SAClC,qBAAA;MAAA,SAAmC,gBAAA;IAAA;EAAA;AAAA;AAAA,cAInC,gBAAA,EAAkB,WAAA;;;;;;KAanB,SAAA;EAAA,SACG,IAAA;EAAA,SAAuB,IAAA;AAAA;EAAA,SACvB,IAAA;EAAA,SAA4B,IAAA;AAAA;EAAA,SAE5B,IAAA;EAAA,SACA,EAAA,EAAI,MAAA,CAAO,MAAA;EAAA,SACX,IAAA;EAAA,SACA,IAAA;AAAA;AAAA,KAGH,WAAA;EAAA,SACD,WAAA,EAAa,WAAA;EAAA,SACb,KAAA,EAAO,aAAA,CAAc,SAAA;EAAA,SACrB,QAAA;AAAA;AAAA,cA6FE,WAAA,GAAe,GAAA,EAAK,WAAA,EAAa,KAAA,EAAO,SAAA,KAAY,WAAA;AAAA,cA0CpD,iBAAA,GAAqB,GAAA,EAAK,WAAA,KAAc,IAAA"}
package/dist/codec.mjs CHANGED
@@ -6,6 +6,7 @@ var codec_exports = /* @__PURE__ */ __exportAll({
6
6
  accumulatorToTurn: () => accumulatorToTurn,
7
7
  buildRequestBody: () => buildRequestBody,
8
8
  emptyAccumulator: () => emptyAccumulator,
9
+ hasUrlImageSource: () => hasUrlImageSource,
9
10
  ingestChunk: () => ingestChunk
10
11
  });
11
12
  const TextPart = Schema.Struct({
@@ -101,7 +102,7 @@ const parsedJsonObject = (fallback) => (encoded) => pipe(decodeJsonObject(encode
101
102
  const parsedArgs = parsedJsonObject(() => ({}));
102
103
  const parsedResponse = parsedJsonObject((raw) => ({ output: raw }));
103
104
  /**
104
- * `FunctionCallOutput` only carries `call_id`; Gemini's `functionResponse`
105
+ * `ToolCallOutput` only carries `call_id`; Gemini's `functionResponse`
105
106
  * requires the declared function `name`. Resolve the name by scanning prior
106
107
  * `function_call` items in the history for a matching `call_id`. If we
107
108
  * cannot resolve, fall back to `call_id` as the name - imperfect but
@@ -159,7 +160,23 @@ const toolDescriptorsToTools = (tools) => tools.length === 0 ? [] : [{ functionD
159
160
  description: t.description,
160
161
  parameters: sanitizeSchema(t.inputSchema)
161
162
  })) }];
162
- const buildRequestBody = (history, generationConfig, tools = []) => {
163
+ /**
164
+ * Map our cross-provider `toolChoice` onto Gemini's `functionCallingConfig`.
165
+ * Gemini honours the same three modes plus a function allow-list, so the
166
+ * choice is mapped, not dropped. `undefined` keeps the API default (`AUTO`).
167
+ */
168
+ const toolChoiceToFunctionCallingConfig = (toolChoice) => Match.value(toolChoice ?? "auto").pipe(Match.when("auto", () => ({ mode: "AUTO" })), Match.when("required", () => ({ mode: "ANY" })), Match.when("none", () => ({ mode: "NONE" })), Match.orElse((tc) => ({
169
+ mode: "ANY",
170
+ allowedFunctionNames: [tc.name]
171
+ })));
172
+ /**
173
+ * URL-form images need Gemini's Files API (upload, then `fileData`); we
174
+ * don't pre-upload, so such a part would otherwise vanish from the request.
175
+ * The caller fails the request with `Unsupported` instead of silently
176
+ * dropping the image.
177
+ */
178
+ const hasUrlImageSource = (history) => history.some((item) => item.type === "message" && item.content.some((b) => b.type === "input_image" && b.source._tag === "url"));
179
+ const buildRequestBody = (history, generationConfig, tools = [], toolChoice = void 0) => {
163
180
  const systemTexts = pipe(allMessages(history), Array$1.filterMap(systemMessageText));
164
181
  const contents = pipe(history, Array$1.filterMap(itemToContent(history)));
165
182
  const requestTools = toolDescriptorsToTools(tools);
@@ -172,7 +189,7 @@ const buildRequestBody = (history, generationConfig, tools = []) => {
172
189
  }),
173
190
  ...requestTools.length > 0 && {
174
191
  tools: requestTools,
175
- toolConfig: { functionCallingConfig: { mode: "AUTO" } }
192
+ toolConfig: { functionCallingConfig: toolChoiceToFunctionCallingConfig(toolChoice) }
176
193
  }
177
194
  };
178
195
  };
@@ -270,6 +287,6 @@ const accumulatorToTurn = (acc) => ({
270
287
  ]
271
288
  });
272
289
  //#endregion
273
- export { WireChunk, accumulatorToTurn, buildRequestBody, emptyAccumulator, ingestChunk, codec_exports as t };
290
+ export { WireChunk, accumulatorToTurn, buildRequestBody, emptyAccumulator, hasUrlImageSource, ingestChunk, codec_exports as t };
274
291
 
275
292
  //# sourceMappingURL=codec.mjs.map