@effect-uai/google 0.4.0 → 0.5.1

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.
@@ -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* Effect.fail(\n 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 }\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* Effect.fail(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* Effect.fail(\n 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 }\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":["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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"}
package/dist/codec.d.mts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { Option, Schema } from "effect";
2
2
  import { Turn } from "@effect-uai/core/Turn";
3
3
  import { Item } from "@effect-uai/core/Items";
4
+ import { ToolDescriptor } from "@effect-uai/core/Tool";
4
5
 
5
6
  //#region src/codec.d.ts
6
7
  declare namespace codec_d_exports {
7
- export { Accumulator, ChunkPart, ChunkResult, GenerationConfig, RequestBody, ThinkingConfig, WireChunk, accumulatorToTurn, buildRequestBody, emptyAccumulator, ingestChunk };
8
+ export { AccumulatedFunctionCall, Accumulator, ChunkPart, ChunkResult, GenerationConfig, RequestBody, ThinkingConfig, WireChunk, accumulatorToTurn, buildRequestBody, emptyAccumulator, ingestChunk };
8
9
  }
9
10
  declare const WireChunk: Schema.Struct<{
10
11
  readonly candidates: Schema.optional<Schema.$Array<Schema.Struct<{
@@ -18,6 +19,12 @@ declare const WireChunk: Schema.Struct<{
18
19
  * `reasoning_delta { kind: "trace" }` in the canonical view.
19
20
  */
20
21
  readonly thought: Schema.optional<Schema.Boolean>;
22
+ }>, Schema.Struct<{
23
+ readonly functionCall: Schema.Struct<{
24
+ readonly id: Schema.optional<Schema.String>;
25
+ readonly name: Schema.String;
26
+ readonly args: Schema.optional<Schema.Unknown>;
27
+ }>;
21
28
  }>]>>>;
22
29
  }>>;
23
30
  readonly finishReason: Schema.optional<Schema.String>;
@@ -39,6 +46,16 @@ type RequestPart = {
39
46
  readonly mimeType: string;
40
47
  readonly data: string;
41
48
  };
49
+ } | {
50
+ readonly functionCall: {
51
+ readonly name: string;
52
+ readonly args: unknown;
53
+ };
54
+ } | {
55
+ readonly functionResponse: {
56
+ readonly name: string;
57
+ readonly response: Record<string, unknown>;
58
+ };
42
59
  };
43
60
  type RequestContent = {
44
61
  readonly role: "user" | "model";
@@ -49,6 +66,20 @@ type RequestSystemInstruction = {
49
66
  readonly text: string;
50
67
  }>;
51
68
  };
69
+ /** Gemini's tool declaration. We translate one `ToolDescriptor` per entry. */
70
+ type RequestTool = {
71
+ readonly functionDeclarations: ReadonlyArray<{
72
+ readonly name: string;
73
+ readonly description: string;
74
+ readonly parameters: Record<string, unknown>;
75
+ }>;
76
+ };
77
+ type RequestToolConfig = {
78
+ readonly functionCallingConfig: {
79
+ readonly mode: "AUTO" | "ANY" | "NONE";
80
+ readonly allowedFunctionNames?: ReadonlyArray<string>;
81
+ };
82
+ };
52
83
  type ThinkingConfig = {
53
84
  readonly thinkingBudget: number;
54
85
  };
@@ -64,11 +95,20 @@ type RequestBody = {
64
95
  readonly contents: ReadonlyArray<RequestContent>;
65
96
  readonly systemInstruction?: RequestSystemInstruction;
66
97
  readonly generationConfig?: GenerationConfig;
98
+ readonly tools?: ReadonlyArray<RequestTool>;
99
+ readonly toolConfig?: RequestToolConfig;
100
+ };
101
+ declare const buildRequestBody: (history: ReadonlyArray<Item>, generationConfig: Option.Option<GenerationConfig>, tools?: ReadonlyArray<ToolDescriptor>) => RequestBody;
102
+ type AccumulatedFunctionCall = {
103
+ /** Synthesized id-or-name we surface as `call_id` on the canonical item. */readonly callId: string;
104
+ 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`. */
106
+ readonly arguments: string;
67
107
  };
68
- declare const buildRequestBody: (history: ReadonlyArray<Item>, generationConfig: Option.Option<GenerationConfig>) => RequestBody;
69
108
  type Accumulator = {
70
109
  readonly text: string;
71
110
  readonly reasoning: string;
111
+ readonly functionCalls: ReadonlyArray<AccumulatedFunctionCall>;
72
112
  readonly finishReason: Option.Option<string>;
73
113
  readonly usage: {
74
114
  readonly input_tokens?: number;
@@ -84,9 +124,9 @@ type Accumulator = {
84
124
  };
85
125
  declare const emptyAccumulator: Accumulator;
86
126
  /**
87
- * One part's worth of streamable text: either the model's answer
88
- * (`kind: "text"`) or chain-of-thought (`kind: "reasoning"`). Emitted
89
- * in wire order so consumers can interleave faithfully.
127
+ * One part's worth of streamable output. `text` and `reasoning` are
128
+ * incremental string deltas; `function_call` arrives whole-in-one-chunk
129
+ * (Gemini does not stream tool-call args).
90
130
  */
91
131
  type ChunkPart = {
92
132
  readonly kind: "text";
@@ -94,6 +134,11 @@ type ChunkPart = {
94
134
  } | {
95
135
  readonly kind: "reasoning";
96
136
  readonly text: string;
137
+ } | {
138
+ readonly kind: "function_call";
139
+ readonly id: Option.Option<string>;
140
+ readonly name: string;
141
+ readonly args: unknown;
97
142
  };
98
143
  type ChunkResult = {
99
144
  readonly accumulator: Accumulator;
@@ -103,5 +148,5 @@ type ChunkResult = {
103
148
  declare const ingestChunk: (acc: Accumulator, chunk: WireChunk) => ChunkResult;
104
149
  declare const accumulatorToTurn: (acc: Accumulator) => Turn;
105
150
  //#endregion
106
- export { Accumulator, ChunkPart, ChunkResult, GenerationConfig, RequestBody, ThinkingConfig, WireChunk, accumulatorToTurn, buildRequestBody, emptyAccumulator, ingestChunk, codec_d_exports as t };
151
+ export { AccumulatedFunctionCall, Accumulator, ChunkPart, ChunkResult, GenerationConfig, RequestBody, ThinkingConfig, WireChunk, accumulatorToTurn, buildRequestBody, emptyAccumulator, ingestChunk, codec_d_exports as t };
107
152
  //# sourceMappingURL=codec.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"codec.d.mts","names":[],"sources":["../src/codec.ts"],"mappings":";;;;;;;;cAwCa,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;AAAA,KAE5D,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;AAAA,KAG/B,cAAA;EAAA,SACD,cAAA;AAAA;AAAA,KAGC,gBAAA;EAAA,SACD,WAAA;EAAA,SACA,eAAA;EAAA,SACA,IAAA;EAAA,SACA,cAAA,GAAiB,cAAA,EA/BN;EAAA,SAiCX,gBAAA,WAjCW;EAAA,SAmCX,kBAAA,GAAqB,MAAA;AAAA;AAAA,KAGpB,WAAA;EAAA,SACD,QAAA,EAAU,aAAA,CAAc,cAAA;EAAA,SACxB,iBAAA,GAAoB,wBAAA;EAAA,SACpB,gBAAA,GAAmB,gBAAA;AAAA;AAAA,cAqEjB,gBAAA,GACX,OAAA,EAAS,aAAA,CAAc,IAAA,GACvB,gBAAA,EAAkB,MAAA,CAAO,MAAA,CAAO,gBAAA,MAC/B,WAAA;AAAA,KA6BS,WAAA;EAAA,SACD,IAAA;EAAA,SACA,SAAA;EAAA,SACA,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;;;;;;KAYnB,SAAA;EAAA,SACG,IAAA;EAAA,SAAuB,IAAA;AAAA;EAAA,SACvB,IAAA;EAAA,SAA4B,IAAA;AAAA;AAAA,KAE/B,WAAA;EAAA,SACD,WAAA,EAAa,WAAA;EAAA,SACb,KAAA,EAAO,aAAA,CAAc,SAAA;EAAA,SACrB,QAAA;AAAA;AAAA,cA2CE,WAAA,GAAe,GAAA,EAAK,WAAA,EAAa,KAAA,EAAO,SAAA,KAAY,WAAA;AAAA,cAepD,iBAAA,GAAqB,GAAA,EAAK,WAAA,KAAc,IAAA"}
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"}
package/dist/codec.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as __exportAll } from "./chunk-uyGKjUfl.mjs";
2
- import { Array, Encoding, Match, Option, Result, Schema, pipe } from "effect";
2
+ import { Array as Array$1, Encoding, Match, Option, Result, Schema, pipe } from "effect";
3
3
  //#region src/codec.ts
4
4
  var codec_exports = /* @__PURE__ */ __exportAll({
5
5
  WireChunk: () => WireChunk,
@@ -17,7 +17,18 @@ const TextPart = Schema.Struct({
17
17
  */
18
18
  thought: Schema.optional(Schema.Boolean)
19
19
  });
20
- const Part = Schema.Union([TextPart]);
20
+ /**
21
+ * Gemini function-call part. Args are delivered as a whole JSON *object*
22
+ * in a single chunk (Gemini does not stream tool-call args). On Gemini 3
23
+ * the response also carries an `id` per call, which we echo back on the
24
+ * corresponding `functionResponse`.
25
+ */
26
+ const FunctionCallPart = Schema.Struct({ functionCall: Schema.Struct({
27
+ id: Schema.optional(Schema.String),
28
+ name: Schema.String,
29
+ args: Schema.optional(Schema.Unknown)
30
+ }) });
31
+ const Part = Schema.Union([TextPart, FunctionCallPart]);
21
32
  const Content = Schema.Struct({
22
33
  role: Schema.optional(Schema.String),
23
34
  parts: Schema.optional(Schema.Array(Part))
@@ -65,7 +76,7 @@ const blockToParts = Match.type().pipe(Match.discriminatorsExhaustive("type")({
65
76
  refusal: () => []
66
77
  }));
67
78
  const messageToContent = (message) => {
68
- const parts = pipe(message.content, Array.flatMap(blockToParts));
79
+ const parts = pipe(message.content, Array$1.flatMap(blockToParts));
69
80
  if (parts.length === 0) return Result.failVoid;
70
81
  return Match.value(message.role).pipe(Match.when("user", () => Result.succeed({
71
82
  role: "user",
@@ -80,17 +91,89 @@ const systemMessageText = (message) => {
80
91
  const text = messageText(message);
81
92
  return text.length === 0 ? Result.failVoid : Result.succeed(text);
82
93
  };
83
- const messages = (history) => pipe(history, Array.filterMap((item) => item.type === "message" ? Result.succeed(item) : Result.failVoid));
84
- const buildRequestBody = (history, generationConfig) => {
85
- const msgs = messages(history);
86
- const systemTexts = pipe(msgs, Array.filterMap(systemMessageText));
94
+ const allMessages = (history) => pipe(history, Array$1.filterMap((item) => item.type === "message" ? Result.succeed(item) : Result.failVoid));
95
+ const JsonObject = Schema.Record(Schema.String, Schema.Unknown);
96
+ const decodeJsonObject = Schema.decodeResult(Schema.fromJsonString(JsonObject));
97
+ const parsedJsonObject = (fallback) => (encoded) => pipe(decodeJsonObject(encoded), Result.match({
98
+ onSuccess: (v) => v,
99
+ onFailure: () => fallback(encoded)
100
+ }));
101
+ const parsedArgs = parsedJsonObject(() => ({}));
102
+ const parsedResponse = parsedJsonObject((raw) => ({ output: raw }));
103
+ /**
104
+ * `FunctionCallOutput` only carries `call_id`; Gemini's `functionResponse`
105
+ * requires the declared function `name`. Resolve the name by scanning prior
106
+ * `function_call` items in the history for a matching `call_id`. If we
107
+ * cannot resolve, fall back to `call_id` as the name - imperfect but
108
+ * preserves stream shape so the model sees *some* response.
109
+ */
110
+ const isFunctionCallItem = (item) => item.type === "function_call";
111
+ const nameForCallId = (history, call_id) => pipe(history, Array$1.findFirst((item) => isFunctionCallItem(item) && item.call_id === call_id), Option.map((f) => f.name));
112
+ /**
113
+ * Extract the Gemini-3 wire id we stashed in `providerData` on the way out.
114
+ * Schema-driven so the shape lives in one place; failure → `Option.none()`.
115
+ */
116
+ const ProviderDataWithGeminiId = Schema.Struct({ gemini: Schema.Struct({ id: Schema.String }) });
117
+ const decodeProviderId = Schema.decodeUnknownResult(ProviderDataWithGeminiId);
118
+ const providerIdFor = (item) => pipe(decodeProviderId(item.providerData), Result.match({
119
+ onSuccess: (d) => Option.some(d.gemini.id),
120
+ onFailure: () => Option.none()
121
+ }));
122
+ const itemToContent = (history) => (item) => Match.value(item).pipe(Match.discriminatorsExhaustive("type")({
123
+ message: messageToContent,
124
+ function_call: (f) => Result.succeed({
125
+ role: "model",
126
+ parts: [{ functionCall: {
127
+ ...Option.match(providerIdFor(f), {
128
+ onSome: (id) => ({ id }),
129
+ onNone: () => ({})
130
+ }),
131
+ name: f.name,
132
+ args: parsedArgs(f.arguments)
133
+ } }]
134
+ }),
135
+ function_call_output: (o) => Result.succeed({
136
+ role: "user",
137
+ parts: [{ functionResponse: {
138
+ name: Option.getOrElse(nameForCallId(history, o.call_id), () => o.call_id),
139
+ response: parsedResponse(o.output)
140
+ } }]
141
+ }),
142
+ reasoning: () => Result.failVoid
143
+ }));
144
+ const UNSUPPORTED_SCHEMA_KEYS = new Set([
145
+ "$schema",
146
+ "$ref",
147
+ "$defs",
148
+ "definitions",
149
+ "additionalProperties",
150
+ "oneOf"
151
+ ]);
152
+ const sanitizeSchema = (schema) => {
153
+ if (Array.isArray(schema)) return schema.map(sanitizeSchema);
154
+ if (schema === null || typeof schema !== "object") return schema;
155
+ return pipe(Object.entries(schema), Array$1.filterMap(([k, v]) => UNSUPPORTED_SCHEMA_KEYS.has(k) ? Result.failVoid : Result.succeed([k, sanitizeSchema(v)])), Object.fromEntries);
156
+ };
157
+ const toolDescriptorsToTools = (tools) => tools.length === 0 ? [] : [{ functionDeclarations: tools.map((t) => ({
158
+ name: t.name,
159
+ description: t.description,
160
+ parameters: sanitizeSchema(t.inputSchema)
161
+ })) }];
162
+ const buildRequestBody = (history, generationConfig, tools = []) => {
163
+ const systemTexts = pipe(allMessages(history), Array$1.filterMap(systemMessageText));
164
+ const contents = pipe(history, Array$1.filterMap(itemToContent(history)));
165
+ const requestTools = toolDescriptorsToTools(tools);
87
166
  return {
88
- contents: pipe(msgs, Array.filterMap(messageToContent)),
167
+ contents,
89
168
  ...systemTexts.length > 0 && { systemInstruction: { parts: [{ text: systemTexts.join("\n") }] } },
90
169
  ...Option.match(generationConfig, {
91
170
  onNone: () => ({}),
92
171
  onSome: (cfg) => ({ generationConfig: cfg })
93
- })
172
+ }),
173
+ ...requestTools.length > 0 && {
174
+ tools: requestTools,
175
+ toolConfig: { functionCallingConfig: { mode: "AUTO" } }
176
+ }
94
177
  };
95
178
  };
96
179
  const finishReasonToStop = (reason) => Option.match(reason, {
@@ -100,14 +183,25 @@ const finishReasonToStop = (reason) => Option.match(reason, {
100
183
  const emptyAccumulator = {
101
184
  text: "",
102
185
  reasoning: "",
186
+ functionCalls: [],
103
187
  finishReason: Option.none(),
104
188
  usage: {}
105
189
  };
106
- const chunkParts = (chunk) => pipe(chunk.candidates?.[0]?.content?.parts ?? [], Array.filterMap((p) => p.text.length === 0 ? Result.failVoid : Result.succeed({
190
+ const isTextPart = (p) => "text" in p;
191
+ const textToChunkParts = (p) => p.text.length === 0 ? [] : [{
107
192
  kind: p.thought === true ? "reasoning" : "text",
108
193
  text: p.text
109
- })));
110
- const sumByKind = (parts, kind) => pipe(parts, Array.filterMap((p) => p.kind === kind ? Result.succeed(p.text) : Result.failVoid)).join("");
194
+ }];
195
+ const functionCallToChunkParts = (p) => [{
196
+ kind: "function_call",
197
+ id: Option.fromNullishOr(p.functionCall.id),
198
+ name: p.functionCall.name,
199
+ args: p.functionCall.args ?? {}
200
+ }];
201
+ const partToChunkParts = (p) => isTextPart(p) ? textToChunkParts(p) : functionCallToChunkParts(p);
202
+ const chunkParts = (chunk) => pipe(chunk.candidates?.[0]?.content?.parts ?? [], Array$1.flatMap(partToChunkParts));
203
+ const sumStrings = (parts, kind) => pipe(parts, Array$1.filterMap((p) => p.kind === kind ? Result.succeed(p.text) : Result.failVoid)).join("");
204
+ const collectFunctionCalls = (parts) => pipe(parts, Array$1.filterMap((p) => p.kind === "function_call" ? Result.succeed(p) : Result.failVoid));
111
205
  const mergeUsage = (prev, next) => next === void 0 ? prev : {
112
206
  ...prev,
113
207
  ...next.promptTokenCount !== void 0 && { input_tokens: next.promptTokenCount },
@@ -116,6 +210,22 @@ const mergeUsage = (prev, next) => next === void 0 ? prev : {
116
210
  ...next.cachedContentTokenCount !== void 0 && { input_tokens_details: { cached_tokens: next.cachedContentTokenCount } },
117
211
  ...next.thoughtsTokenCount !== void 0 && { output_tokens_details: { reasoning_tokens: next.thoughtsTokenCount } }
118
212
  };
213
+ /**
214
+ * Synthesize a stable `call_id` for a function call. Gemini 3 provides one
215
+ * via `functionCall.id`; older models do not, so we fall back to
216
+ * `<name>_<index>` based on prior calls' position in the accumulator.
217
+ */
218
+ const synthesizeCallId = (call, priorCalls) => Option.match(call.id, {
219
+ onSome: (id) => id,
220
+ onNone: () => `${call.name}_${priorCalls.length}`
221
+ });
222
+ const chunkCallToAccumulated = (prior, call) => ({
223
+ callId: synthesizeCallId(call, prior),
224
+ name: call.name,
225
+ providerId: call.id,
226
+ arguments: JSON.stringify(call.args)
227
+ });
228
+ const appendFunctionCalls = (prior, fromChunk) => fromChunk.reduce((acc, call) => [...acc, chunkCallToAccumulated(acc, call)], prior);
119
229
  const ingestChunk = (acc, chunk) => {
120
230
  const parts = chunkParts(chunk);
121
231
  const finishReason = Option.fromNullishOr(chunk.candidates?.[0]?.finishReason);
@@ -123,27 +233,41 @@ const ingestChunk = (acc, chunk) => {
123
233
  parts,
124
234
  finished: Option.isSome(finishReason),
125
235
  accumulator: {
126
- text: acc.text + sumByKind(parts, "text"),
127
- reasoning: acc.reasoning + sumByKind(parts, "reasoning"),
236
+ text: acc.text + sumStrings(parts, "text"),
237
+ reasoning: acc.reasoning + sumStrings(parts, "reasoning"),
238
+ functionCalls: appendFunctionCalls(acc.functionCalls, collectFunctionCalls(parts)),
128
239
  finishReason: Option.orElse(finishReason, () => acc.finishReason),
129
240
  usage: mergeUsage(acc.usage, chunk.usageMetadata)
130
241
  }
131
242
  };
132
243
  };
244
+ const reasoningItems = (acc) => acc.reasoning.length > 0 ? [{
245
+ type: "reasoning",
246
+ summary: acc.reasoning
247
+ }] : [];
248
+ const assistantMessageItems = (acc) => acc.text.length === 0 ? [] : [{
249
+ type: "message",
250
+ role: "assistant",
251
+ content: [{
252
+ type: "output_text",
253
+ text: acc.text
254
+ }]
255
+ }];
256
+ const functionCallItems = (acc) => pipe(acc.functionCalls, Array$1.map((c) => ({
257
+ type: "function_call",
258
+ call_id: c.callId,
259
+ name: c.name,
260
+ arguments: c.arguments,
261
+ ...Option.isSome(c.providerId) && { providerData: { gemini: { id: c.providerId.value } } }
262
+ })));
133
263
  const accumulatorToTurn = (acc) => ({
134
- stop_reason: finishReasonToStop(acc.finishReason),
264
+ stop_reason: acc.functionCalls.length > 0 ? "tool_calls" : finishReasonToStop(acc.finishReason),
135
265
  usage: { ...acc.usage },
136
- items: [...acc.reasoning.length > 0 ? [{
137
- type: "reasoning",
138
- summary: acc.reasoning
139
- }] : [], ...acc.text.length === 0 ? [] : [{
140
- type: "message",
141
- role: "assistant",
142
- content: [{
143
- type: "output_text",
144
- text: acc.text
145
- }]
146
- }]]
266
+ items: [
267
+ ...reasoningItems(acc),
268
+ ...assistantMessageItems(acc),
269
+ ...functionCallItems(acc)
270
+ ]
147
271
  });
148
272
  //#endregion
149
273
  export { WireChunk, accumulatorToTurn, buildRequestBody, emptyAccumulator, ingestChunk, codec_exports as t };
@@ -1 +1 @@
1
- {"version":3,"file":"codec.mjs","names":["Arr"],"sources":["../src/codec.ts"],"sourcesContent":["import { Array as Arr, Encoding, Match, Option, Result, Schema, pipe } from \"effect\"\nimport type { ContentBlock, InputImage, Item, Message } from \"@effect-uai/core/Items\"\nimport type { Turn } from \"@effect-uai/core/Turn\"\n\n// ---------------------------------------------------------------------------\n// Wire schemas - the subset of Gemini's streamGenerateContent payload we\n// consume. Reference: https://ai.google.dev/api/generate-content\n// ---------------------------------------------------------------------------\n\nconst TextPart = Schema.Struct({\n text: Schema.String,\n /**\n * Gemini's flag for chain-of-thought parts. When `true`, the part is\n * reasoning trace, not the model's user-facing answer. Maps onto\n * `reasoning_delta { kind: \"trace\" }` in the canonical view.\n */\n thought: Schema.optional(Schema.Boolean),\n})\n\nconst Part = Schema.Union([TextPart])\n\nconst Content = Schema.Struct({\n role: Schema.optional(Schema.String),\n parts: Schema.optional(Schema.Array(Part)),\n})\n\nconst Candidate = Schema.Struct({\n content: Schema.optional(Content),\n finishReason: Schema.optional(Schema.String),\n index: Schema.optional(Schema.Number),\n})\n\nconst UsageMetadata = Schema.Struct({\n promptTokenCount: Schema.optional(Schema.Number),\n candidatesTokenCount: Schema.optional(Schema.Number),\n totalTokenCount: Schema.optional(Schema.Number),\n cachedContentTokenCount: Schema.optional(Schema.Number),\n thoughtsTokenCount: Schema.optional(Schema.Number),\n})\n\nexport const WireChunk = Schema.Struct({\n candidates: Schema.optional(Schema.Array(Candidate)),\n usageMetadata: Schema.optional(UsageMetadata),\n})\nexport type WireChunk = typeof WireChunk.Type\n\n// ---------------------------------------------------------------------------\n// History → request body\n// ---------------------------------------------------------------------------\n\ntype RequestPart =\n | { readonly text: string }\n | { readonly inlineData: { readonly mimeType: string; readonly data: string } }\n\ntype RequestContent = {\n readonly role: \"user\" | \"model\"\n readonly parts: ReadonlyArray<RequestPart>\n}\n\ntype RequestSystemInstruction = {\n readonly parts: ReadonlyArray<{ readonly text: string }>\n}\n\nexport type ThinkingConfig = {\n readonly thinkingBudget: number\n}\n\nexport type GenerationConfig = {\n readonly temperature?: number\n readonly maxOutputTokens?: number\n readonly topP?: number\n readonly thinkingConfig?: ThinkingConfig\n /** Set together with `responseJsonSchema` to constrain output to JSON. */\n readonly responseMimeType?: string\n /** JSON Schema constraint on the output. */\n readonly responseJsonSchema?: Record<string, unknown>\n}\n\nexport type RequestBody = {\n readonly contents: ReadonlyArray<RequestContent>\n readonly systemInstruction?: RequestSystemInstruction\n readonly generationConfig?: GenerationConfig\n}\n\nconst blockText = Match.type<ContentBlock>().pipe(\n Match.discriminatorsExhaustive(\"type\")({\n input_text: (b) => b.text,\n input_image: () => \"\",\n output_text: (b) => b.text,\n refusal: (b) => b.text,\n }),\n)\n\nconst messageText = (message: Message): string => message.content.map(blockText).join(\"\")\n\n/**\n * Gemini's `inlineData` form expects a base64 payload. URL-form images\n * would need to go through Gemini's Files API (upload then `fileData`\n * with the returned URI); pre-uploading isn't free, so we skip those for\n * now and document as a follow-up.\n */\nconst imageSourceToParts = Match.type<InputImage[\"source\"]>().pipe(\n Match.tag(\"url\", (): ReadonlyArray<RequestPart> => []),\n Match.tag(\n \"base64\",\n (s): ReadonlyArray<RequestPart> => [{ inlineData: { mimeType: s.mimeType, data: s.base64 } }],\n ),\n Match.tag(\n \"bytes\",\n (s): ReadonlyArray<RequestPart> => [\n { inlineData: { mimeType: s.mimeType, data: Encoding.encodeBase64(s.bytes) } },\n ],\n ),\n Match.exhaustive,\n)\n\nconst blockToParts = Match.type<ContentBlock>().pipe(\n Match.discriminatorsExhaustive(\"type\")({\n input_text: (b): ReadonlyArray<RequestPart> => (b.text.length === 0 ? [] : [{ text: b.text }]),\n input_image: (b): ReadonlyArray<RequestPart> => imageSourceToParts(b.source),\n output_text: (b): ReadonlyArray<RequestPart> => (b.text.length === 0 ? [] : [{ text: b.text }]),\n // Refusals are assistant-side content; they don't round-trip into Gemini's\n // request body as parts. Skip.\n refusal: (): ReadonlyArray<RequestPart> => [],\n }),\n)\n\nconst messageToContent = (message: Message): Result.Result<RequestContent, void> => {\n const parts = pipe(message.content, Arr.flatMap(blockToParts))\n if (parts.length === 0) return Result.failVoid\n return Match.value(message.role).pipe(\n Match.when(\"user\", () => Result.succeed({ role: \"user\" as const, parts })),\n Match.when(\"assistant\", () => Result.succeed({ role: \"model\" as const, parts })),\n Match.when(\"system\", () => Result.failVoid),\n Match.exhaustive,\n )\n}\n\nconst systemMessageText = (message: Message): Result.Result<string, void> => {\n if (message.role !== \"system\") return Result.failVoid\n const text = messageText(message)\n return text.length === 0 ? Result.failVoid : Result.succeed(text)\n}\n\nconst messages = (history: ReadonlyArray<Item>): ReadonlyArray<Message> =>\n pipe(\n history,\n Arr.filterMap((item) => (item.type === \"message\" ? Result.succeed(item) : Result.failVoid)),\n )\n\nexport const buildRequestBody = (\n history: ReadonlyArray<Item>,\n generationConfig: Option.Option<GenerationConfig>,\n): RequestBody => {\n const msgs = messages(history)\n const systemTexts = pipe(msgs, Arr.filterMap(systemMessageText))\n const contents = pipe(msgs, Arr.filterMap(messageToContent))\n return {\n contents,\n ...(systemTexts.length > 0 && {\n systemInstruction: { parts: [{ text: systemTexts.join(\"\\n\") }] },\n }),\n ...Option.match(generationConfig, {\n onNone: () => ({}),\n onSome: (cfg) => ({ generationConfig: cfg }),\n }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Stream-level state - accumulate chunk text + final usage/finish.\n//\n// `Accumulator` is immutable; `ingestChunk` returns a fresh one per chunk.\n// Drive it via `Stream.mapAccum` in the consumer.\n// ---------------------------------------------------------------------------\n\nconst finishReasonToStop = (reason: Option.Option<string>): Turn[\"stop_reason\"] =>\n Option.match(reason, {\n onNone: () => \"stop\" as const,\n onSome: (r) => (r === \"MAX_TOKENS\" ? (\"max_tokens\" as const) : (\"stop\" as const)),\n })\n\nexport type Accumulator = {\n readonly text: string\n readonly reasoning: string\n readonly finishReason: Option.Option<string>\n readonly usage: {\n readonly input_tokens?: number\n readonly output_tokens?: number\n readonly total_tokens?: number\n readonly input_tokens_details?: { readonly cached_tokens?: number }\n readonly output_tokens_details?: { readonly reasoning_tokens?: number }\n }\n}\n\nexport const emptyAccumulator: Accumulator = {\n text: \"\",\n reasoning: \"\",\n finishReason: Option.none(),\n usage: {},\n}\n\n/**\n * One part's worth of streamable text: either the model's answer\n * (`kind: \"text\"`) or chain-of-thought (`kind: \"reasoning\"`). Emitted\n * in wire order so consumers can interleave faithfully.\n */\nexport type ChunkPart =\n | { readonly kind: \"text\"; readonly text: string }\n | { readonly kind: \"reasoning\"; readonly text: string }\n\nexport type ChunkResult = {\n readonly accumulator: Accumulator\n readonly parts: ReadonlyArray<ChunkPart>\n readonly finished: boolean\n}\n\nconst chunkParts = (chunk: WireChunk): ReadonlyArray<ChunkPart> =>\n pipe(\n chunk.candidates?.[0]?.content?.parts ?? [],\n Arr.filterMap((p) =>\n p.text.length === 0\n ? Result.failVoid\n : Result.succeed({\n kind: p.thought === true ? (\"reasoning\" as const) : (\"text\" as const),\n text: p.text,\n }),\n ),\n )\n\nconst sumByKind = (parts: ReadonlyArray<ChunkPart>, kind: ChunkPart[\"kind\"]): string =>\n pipe(\n parts,\n Arr.filterMap((p) => (p.kind === kind ? Result.succeed(p.text) : Result.failVoid)),\n ).join(\"\")\n\nconst mergeUsage = (\n prev: Accumulator[\"usage\"],\n next: WireChunk[\"usageMetadata\"],\n): Accumulator[\"usage\"] =>\n next === undefined\n ? prev\n : {\n ...prev,\n ...(next.promptTokenCount !== undefined && { input_tokens: next.promptTokenCount }),\n ...(next.candidatesTokenCount !== undefined && {\n output_tokens: next.candidatesTokenCount,\n }),\n ...(next.totalTokenCount !== undefined && { total_tokens: next.totalTokenCount }),\n ...(next.cachedContentTokenCount !== undefined && {\n input_tokens_details: { cached_tokens: next.cachedContentTokenCount },\n }),\n ...(next.thoughtsTokenCount !== undefined && {\n output_tokens_details: { reasoning_tokens: next.thoughtsTokenCount },\n }),\n }\n\nexport const ingestChunk = (acc: Accumulator, chunk: WireChunk): ChunkResult => {\n const parts = chunkParts(chunk)\n const finishReason = Option.fromNullishOr(chunk.candidates?.[0]?.finishReason)\n return {\n parts,\n finished: Option.isSome(finishReason),\n accumulator: {\n text: acc.text + sumByKind(parts, \"text\"),\n reasoning: acc.reasoning + sumByKind(parts, \"reasoning\"),\n finishReason: Option.orElse(finishReason, () => acc.finishReason),\n usage: mergeUsage(acc.usage, chunk.usageMetadata),\n },\n }\n}\n\nexport const accumulatorToTurn = (acc: Accumulator): Turn => ({\n stop_reason: finishReasonToStop(acc.finishReason),\n usage: { ...acc.usage },\n items: [\n ...(acc.reasoning.length > 0 ? [{ type: \"reasoning\" as const, summary: acc.reasoning }] : []),\n ...(acc.text.length === 0\n ? []\n : [\n {\n type: \"message\" as const,\n role: \"assistant\" as const,\n content: [{ type: \"output_text\" as const, text: acc.text }],\n },\n ]),\n ],\n})\n"],"mappings":";;;;;;;;;;AASA,MAAM,WAAW,OAAO,OAAO;CAC7B,MAAM,OAAO;;;;;;CAMb,SAAS,OAAO,SAAS,OAAO,QAAQ;CACzC,CAAC;AAEF,MAAM,OAAO,OAAO,MAAM,CAAC,SAAS,CAAC;AAErC,MAAM,UAAU,OAAO,OAAO;CAC5B,MAAM,OAAO,SAAS,OAAO,OAAO;CACpC,OAAO,OAAO,SAAS,OAAO,MAAM,KAAK,CAAC;CAC3C,CAAC;AAEF,MAAM,YAAY,OAAO,OAAO;CAC9B,SAAS,OAAO,SAAS,QAAQ;CACjC,cAAc,OAAO,SAAS,OAAO,OAAO;CAC5C,OAAO,OAAO,SAAS,OAAO,OAAO;CACtC,CAAC;AAEF,MAAM,gBAAgB,OAAO,OAAO;CAClC,kBAAkB,OAAO,SAAS,OAAO,OAAO;CAChD,sBAAsB,OAAO,SAAS,OAAO,OAAO;CACpD,iBAAiB,OAAO,SAAS,OAAO,OAAO;CAC/C,yBAAyB,OAAO,SAAS,OAAO,OAAO;CACvD,oBAAoB,OAAO,SAAS,OAAO,OAAO;CACnD,CAAC;AAEF,MAAa,YAAY,OAAO,OAAO;CACrC,YAAY,OAAO,SAAS,OAAO,MAAM,UAAU,CAAC;CACpD,eAAe,OAAO,SAAS,cAAc;CAC9C,CAAC;AAyCF,MAAM,YAAY,MAAM,MAAoB,CAAC,KAC3C,MAAM,yBAAyB,OAAO,CAAC;CACrC,aAAa,MAAM,EAAE;CACrB,mBAAmB;CACnB,cAAc,MAAM,EAAE;CACtB,UAAU,MAAM,EAAE;CACnB,CAAC,CACH;AAED,MAAM,eAAe,YAA6B,QAAQ,QAAQ,IAAI,UAAU,CAAC,KAAK,GAAG;;;;;;;AAQzF,MAAM,qBAAqB,MAAM,MAA4B,CAAC,KAC5D,MAAM,IAAI,aAAyC,EAAE,CAAC,EACtD,MAAM,IACJ,WACC,MAAkC,CAAC,EAAE,YAAY;CAAE,UAAU,EAAE;CAAU,MAAM,EAAE;CAAQ,EAAE,CAAC,CAC9F,EACD,MAAM,IACJ,UACC,MAAkC,CACjC,EAAE,YAAY;CAAE,UAAU,EAAE;CAAU,MAAM,SAAS,aAAa,EAAE,MAAM;CAAE,EAAE,CAC/E,CACF,EACD,MAAM,WACP;AAED,MAAM,eAAe,MAAM,MAAoB,CAAC,KAC9C,MAAM,yBAAyB,OAAO,CAAC;CACrC,aAAa,MAAmC,EAAE,KAAK,WAAW,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC;CAC7F,cAAc,MAAkC,mBAAmB,EAAE,OAAO;CAC5E,cAAc,MAAmC,EAAE,KAAK,WAAW,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC;CAG9F,eAA2C,EAAE;CAC9C,CAAC,CACH;AAED,MAAM,oBAAoB,YAA0D;CAClF,MAAM,QAAQ,KAAK,QAAQ,SAASA,MAAI,QAAQ,aAAa,CAAC;AAC9D,KAAI,MAAM,WAAW,EAAG,QAAO,OAAO;AACtC,QAAO,MAAM,MAAM,QAAQ,KAAK,CAAC,KAC/B,MAAM,KAAK,cAAc,OAAO,QAAQ;EAAE,MAAM;EAAiB;EAAO,CAAC,CAAC,EAC1E,MAAM,KAAK,mBAAmB,OAAO,QAAQ;EAAE,MAAM;EAAkB;EAAO,CAAC,CAAC,EAChF,MAAM,KAAK,gBAAgB,OAAO,SAAS,EAC3C,MAAM,WACP;;AAGH,MAAM,qBAAqB,YAAkD;AAC3E,KAAI,QAAQ,SAAS,SAAU,QAAO,OAAO;CAC7C,MAAM,OAAO,YAAY,QAAQ;AACjC,QAAO,KAAK,WAAW,IAAI,OAAO,WAAW,OAAO,QAAQ,KAAK;;AAGnE,MAAM,YAAY,YAChB,KACE,SACAA,MAAI,WAAW,SAAU,KAAK,SAAS,YAAY,OAAO,QAAQ,KAAK,GAAG,OAAO,SAAU,CAC5F;AAEH,MAAa,oBACX,SACA,qBACgB;CAChB,MAAM,OAAO,SAAS,QAAQ;CAC9B,MAAM,cAAc,KAAK,MAAMA,MAAI,UAAU,kBAAkB,CAAC;AAEhE,QAAO;EACL,UAFe,KAAK,MAAMA,MAAI,UAAU,iBAAiB,CAEjD;EACR,GAAI,YAAY,SAAS,KAAK,EAC5B,mBAAmB,EAAE,OAAO,CAAC,EAAE,MAAM,YAAY,KAAK,KAAK,EAAE,CAAC,EAAE,EACjE;EACD,GAAG,OAAO,MAAM,kBAAkB;GAChC,eAAe,EAAE;GACjB,SAAS,SAAS,EAAE,kBAAkB,KAAK;GAC5C,CAAC;EACH;;AAUH,MAAM,sBAAsB,WAC1B,OAAO,MAAM,QAAQ;CACnB,cAAc;CACd,SAAS,MAAO,MAAM,eAAgB,eAA0B;CACjE,CAAC;AAeJ,MAAa,mBAAgC;CAC3C,MAAM;CACN,WAAW;CACX,cAAc,OAAO,MAAM;CAC3B,OAAO,EAAE;CACV;AAiBD,MAAM,cAAc,UAClB,KACE,MAAM,aAAa,IAAI,SAAS,SAAS,EAAE,EAC3CA,MAAI,WAAW,MACb,EAAE,KAAK,WAAW,IACd,OAAO,WACP,OAAO,QAAQ;CACb,MAAM,EAAE,YAAY,OAAQ,cAAyB;CACrD,MAAM,EAAE;CACT,CAAC,CACP,CACF;AAEH,MAAM,aAAa,OAAiC,SAClD,KACE,OACAA,MAAI,WAAW,MAAO,EAAE,SAAS,OAAO,OAAO,QAAQ,EAAE,KAAK,GAAG,OAAO,SAAU,CACnF,CAAC,KAAK,GAAG;AAEZ,MAAM,cACJ,MACA,SAEA,SAAS,KAAA,IACL,OACA;CACE,GAAG;CACH,GAAI,KAAK,qBAAqB,KAAA,KAAa,EAAE,cAAc,KAAK,kBAAkB;CAClF,GAAI,KAAK,yBAAyB,KAAA,KAAa,EAC7C,eAAe,KAAK,sBACrB;CACD,GAAI,KAAK,oBAAoB,KAAA,KAAa,EAAE,cAAc,KAAK,iBAAiB;CAChF,GAAI,KAAK,4BAA4B,KAAA,KAAa,EAChD,sBAAsB,EAAE,eAAe,KAAK,yBAAyB,EACtE;CACD,GAAI,KAAK,uBAAuB,KAAA,KAAa,EAC3C,uBAAuB,EAAE,kBAAkB,KAAK,oBAAoB,EACrE;CACF;AAEP,MAAa,eAAe,KAAkB,UAAkC;CAC9E,MAAM,QAAQ,WAAW,MAAM;CAC/B,MAAM,eAAe,OAAO,cAAc,MAAM,aAAa,IAAI,aAAa;AAC9E,QAAO;EACL;EACA,UAAU,OAAO,OAAO,aAAa;EACrC,aAAa;GACX,MAAM,IAAI,OAAO,UAAU,OAAO,OAAO;GACzC,WAAW,IAAI,YAAY,UAAU,OAAO,YAAY;GACxD,cAAc,OAAO,OAAO,oBAAoB,IAAI,aAAa;GACjE,OAAO,WAAW,IAAI,OAAO,MAAM,cAAc;GAClD;EACF;;AAGH,MAAa,qBAAqB,SAA4B;CAC5D,aAAa,mBAAmB,IAAI,aAAa;CACjD,OAAO,EAAE,GAAG,IAAI,OAAO;CACvB,OAAO,CACL,GAAI,IAAI,UAAU,SAAS,IAAI,CAAC;EAAE,MAAM;EAAsB,SAAS,IAAI;EAAW,CAAC,GAAG,EAAE,EAC5F,GAAI,IAAI,KAAK,WAAW,IACpB,EAAE,GACF,CACE;EACE,MAAM;EACN,MAAM;EACN,SAAS,CAAC;GAAE,MAAM;GAAwB,MAAM,IAAI;GAAM,CAAC;EAC5D,CACF,CACN;CACF"}
1
+ {"version":3,"file":"codec.mjs","names":["Arr"],"sources":["../src/codec.ts"],"sourcesContent":["import { Array as Arr, Encoding, Match, Option, Result, Schema, pipe } from \"effect\"\nimport type { ContentBlock, FunctionCall, InputImage, Item, Message } from \"@effect-uai/core/Items\"\nimport type { ToolDescriptor } from \"@effect-uai/core/Tool\"\nimport type { Turn } from \"@effect-uai/core/Turn\"\n\n// ---------------------------------------------------------------------------\n// Wire schemas - the subset of Gemini's streamGenerateContent payload we\n// consume. Reference: https://ai.google.dev/api/generate-content\n// ---------------------------------------------------------------------------\n\nconst TextPart = Schema.Struct({\n text: Schema.String,\n /**\n * Gemini's flag for chain-of-thought parts. When `true`, the part is\n * reasoning trace, not the model's user-facing answer. Maps onto\n * `reasoning_delta { kind: \"trace\" }` in the canonical view.\n */\n thought: Schema.optional(Schema.Boolean),\n})\n\n/**\n * Gemini function-call part. Args are delivered as a whole JSON *object*\n * in a single chunk (Gemini does not stream tool-call args). On Gemini 3\n * the response also carries an `id` per call, which we echo back on the\n * corresponding `functionResponse`.\n */\nconst FunctionCallPart = Schema.Struct({\n functionCall: Schema.Struct({\n id: Schema.optional(Schema.String),\n name: Schema.String,\n args: Schema.optional(Schema.Unknown),\n }),\n})\n\nconst Part = Schema.Union([TextPart, FunctionCallPart])\ntype WireFunctionCallPart = typeof FunctionCallPart.Type\n\nconst Content = Schema.Struct({\n role: Schema.optional(Schema.String),\n parts: Schema.optional(Schema.Array(Part)),\n})\n\nconst Candidate = Schema.Struct({\n content: Schema.optional(Content),\n finishReason: Schema.optional(Schema.String),\n index: Schema.optional(Schema.Number),\n})\n\nconst UsageMetadata = Schema.Struct({\n promptTokenCount: Schema.optional(Schema.Number),\n candidatesTokenCount: Schema.optional(Schema.Number),\n totalTokenCount: Schema.optional(Schema.Number),\n cachedContentTokenCount: Schema.optional(Schema.Number),\n thoughtsTokenCount: Schema.optional(Schema.Number),\n})\n\nexport const WireChunk = Schema.Struct({\n candidates: Schema.optional(Schema.Array(Candidate)),\n usageMetadata: Schema.optional(UsageMetadata),\n})\nexport type WireChunk = typeof WireChunk.Type\n\n// ---------------------------------------------------------------------------\n// History → request body\n// ---------------------------------------------------------------------------\n\ntype RequestPart =\n | { readonly text: string }\n | { readonly inlineData: { readonly mimeType: string; readonly data: string } }\n | { readonly functionCall: { readonly name: string; readonly args: unknown } }\n | {\n readonly functionResponse: {\n readonly name: string\n readonly response: Record<string, unknown>\n }\n }\n\ntype RequestContent = {\n readonly role: \"user\" | \"model\"\n readonly parts: ReadonlyArray<RequestPart>\n}\n\ntype RequestSystemInstruction = {\n readonly parts: ReadonlyArray<{ readonly text: string }>\n}\n\n/** Gemini's tool declaration. We translate one `ToolDescriptor` per entry. */\ntype RequestTool = {\n readonly functionDeclarations: ReadonlyArray<{\n readonly name: string\n readonly description: string\n readonly parameters: Record<string, unknown>\n }>\n}\n\ntype RequestToolConfig = {\n readonly functionCallingConfig: {\n readonly mode: \"AUTO\" | \"ANY\" | \"NONE\"\n readonly allowedFunctionNames?: ReadonlyArray<string>\n }\n}\n\nexport type ThinkingConfig = {\n readonly thinkingBudget: number\n}\n\nexport type GenerationConfig = {\n readonly temperature?: number\n readonly maxOutputTokens?: number\n readonly topP?: number\n readonly thinkingConfig?: ThinkingConfig\n /** Set together with `responseJsonSchema` to constrain output to JSON. */\n readonly responseMimeType?: string\n /** JSON Schema constraint on the output. */\n readonly responseJsonSchema?: Record<string, unknown>\n}\n\nexport type RequestBody = {\n readonly contents: ReadonlyArray<RequestContent>\n readonly systemInstruction?: RequestSystemInstruction\n readonly generationConfig?: GenerationConfig\n readonly tools?: ReadonlyArray<RequestTool>\n readonly toolConfig?: RequestToolConfig\n}\n\nconst blockText = Match.type<ContentBlock>().pipe(\n Match.discriminatorsExhaustive(\"type\")({\n input_text: (b) => b.text,\n input_image: () => \"\",\n output_text: (b) => b.text,\n refusal: (b) => b.text,\n }),\n)\n\nconst messageText = (message: Message): string => message.content.map(blockText).join(\"\")\n\n/**\n * Gemini's `inlineData` form expects a base64 payload. URL-form images\n * would need to go through Gemini's Files API (upload then `fileData`\n * with the returned URI); pre-uploading isn't free, so we skip those for\n * now and document as a follow-up.\n */\nconst imageSourceToParts = Match.type<InputImage[\"source\"]>().pipe(\n Match.tag(\"url\", (): ReadonlyArray<RequestPart> => []),\n Match.tag(\n \"base64\",\n (s): ReadonlyArray<RequestPart> => [{ inlineData: { mimeType: s.mimeType, data: s.base64 } }],\n ),\n Match.tag(\n \"bytes\",\n (s): ReadonlyArray<RequestPart> => [\n { inlineData: { mimeType: s.mimeType, data: Encoding.encodeBase64(s.bytes) } },\n ],\n ),\n Match.exhaustive,\n)\n\nconst blockToParts = Match.type<ContentBlock>().pipe(\n Match.discriminatorsExhaustive(\"type\")({\n input_text: (b): ReadonlyArray<RequestPart> => (b.text.length === 0 ? [] : [{ text: b.text }]),\n input_image: (b): ReadonlyArray<RequestPart> => imageSourceToParts(b.source),\n output_text: (b): ReadonlyArray<RequestPart> => (b.text.length === 0 ? [] : [{ text: b.text }]),\n // Refusals are assistant-side content; they don't round-trip into Gemini's\n // request body as parts. Skip.\n refusal: (): ReadonlyArray<RequestPart> => [],\n }),\n)\n\nconst messageToContent = (message: Message): Result.Result<RequestContent, void> => {\n const parts = pipe(message.content, Arr.flatMap(blockToParts))\n if (parts.length === 0) return Result.failVoid\n return Match.value(message.role).pipe(\n Match.when(\"user\", () => Result.succeed({ role: \"user\" as const, parts })),\n Match.when(\"assistant\", () => Result.succeed({ role: \"model\" as const, parts })),\n Match.when(\"system\", () => Result.failVoid),\n Match.exhaustive,\n )\n}\n\nconst systemMessageText = (message: Message): Result.Result<string, void> => {\n if (message.role !== \"system\") return Result.failVoid\n const text = messageText(message)\n return text.length === 0 ? Result.failVoid : Result.succeed(text)\n}\n\nconst allMessages = (history: ReadonlyArray<Item>): ReadonlyArray<Message> =>\n pipe(\n history,\n Arr.filterMap((item) => (item.type === \"message\" ? Result.succeed(item) : Result.failVoid)),\n )\n\n// ---------------------------------------------------------------------------\n// Function-call round-trip\n//\n// `FunctionCall.arguments` and `FunctionCallOutput.output` are JSON-encoded\n// strings; Gemini's `functionCall.args` and `functionResponse.response`\n// expect parsed JSON *objects*. Decode via Schema:\n// - object payload → use as-is\n// - scalar / array / malformed → wrap as `{ output: <raw string> }` so the\n// model still sees *some* response without crashing the request.\n// ---------------------------------------------------------------------------\n\nconst JsonObject = Schema.Record(Schema.String, Schema.Unknown)\nconst decodeJsonObject = Schema.decodeResult(Schema.fromJsonString(JsonObject))\n\nconst parsedJsonObject =\n (fallback: (raw: string) => Record<string, unknown>) =>\n (encoded: string): Record<string, unknown> =>\n pipe(\n decodeJsonObject(encoded),\n Result.match({\n onSuccess: (v) => v,\n onFailure: () => fallback(encoded),\n }),\n )\n\nconst parsedArgs = parsedJsonObject(() => ({}))\nconst parsedResponse = parsedJsonObject((raw) => ({ output: raw }))\n\n/**\n * `FunctionCallOutput` only carries `call_id`; Gemini's `functionResponse`\n * requires the declared function `name`. Resolve the name by scanning prior\n * `function_call` items in the history for a matching `call_id`. If we\n * cannot resolve, fall back to `call_id` as the name - imperfect but\n * preserves stream shape so the model sees *some* response.\n */\nconst isFunctionCallItem = (item: Item): item is FunctionCall => item.type === \"function_call\"\n\nconst nameForCallId = (history: ReadonlyArray<Item>, call_id: string): Option.Option<string> =>\n pipe(\n history,\n Arr.findFirst(\n (item): item is FunctionCall => isFunctionCallItem(item) && item.call_id === call_id,\n ),\n Option.map((f) => f.name),\n )\n\n/**\n * Extract the Gemini-3 wire id we stashed in `providerData` on the way out.\n * Schema-driven so the shape lives in one place; failure → `Option.none()`.\n */\nconst ProviderDataWithGeminiId = Schema.Struct({\n gemini: Schema.Struct({ id: Schema.String }),\n})\nconst decodeProviderId = Schema.decodeUnknownResult(ProviderDataWithGeminiId)\n\nconst providerIdFor = (item: FunctionCall): Option.Option<string> =>\n pipe(\n decodeProviderId(item.providerData),\n Result.match({\n onSuccess: (d) => Option.some(d.gemini.id),\n onFailure: () => Option.none<string>(),\n }),\n )\n\nconst itemToContent =\n (history: ReadonlyArray<Item>) =>\n (item: Item): Result.Result<RequestContent, void> =>\n Match.value(item).pipe(\n Match.discriminatorsExhaustive(\"type\")({\n message: messageToContent,\n function_call: (f) =>\n Result.succeed({\n role: \"model\" as const,\n parts: [\n {\n functionCall: {\n ...Option.match(providerIdFor(f), {\n onSome: (id) => ({ id }),\n onNone: () => ({}),\n }),\n name: f.name,\n args: parsedArgs(f.arguments),\n },\n },\n ],\n }),\n function_call_output: (o) =>\n Result.succeed({\n role: \"user\" as const,\n parts: [\n {\n functionResponse: {\n name: Option.getOrElse(nameForCallId(history, o.call_id), () => o.call_id),\n response: parsedResponse(o.output),\n },\n },\n ],\n }),\n reasoning: () => Result.failVoid,\n }),\n )\n\n// ---------------------------------------------------------------------------\n// Tool descriptors → Gemini `functionDeclarations`. Gemini accepts only a\n// strict OpenAPI 3.0 subset for `parameters`; strip JSON-Schema keys it\n// rejects (`$schema`, `$ref`, `additionalProperties`, `oneOf`,\n// `definitions`).\n// ---------------------------------------------------------------------------\n\nconst UNSUPPORTED_SCHEMA_KEYS: ReadonlySet<string> = new Set([\n \"$schema\",\n \"$ref\",\n \"$defs\",\n \"definitions\",\n \"additionalProperties\",\n \"oneOf\",\n])\n\nconst sanitizeSchema = (schema: unknown): unknown => {\n if (Array.isArray(schema)) return schema.map(sanitizeSchema)\n if (schema === null || typeof schema !== \"object\") return schema\n return pipe(\n Object.entries(schema as Record<string, unknown>),\n Arr.filterMap(([k, v]) =>\n UNSUPPORTED_SCHEMA_KEYS.has(k)\n ? Result.failVoid\n : Result.succeed([k, sanitizeSchema(v)] as const),\n ),\n Object.fromEntries,\n )\n}\n\nconst toolDescriptorsToTools = (tools: ReadonlyArray<ToolDescriptor>): ReadonlyArray<RequestTool> =>\n tools.length === 0\n ? []\n : [\n {\n functionDeclarations: tools.map((t) => ({\n name: t.name,\n description: t.description,\n parameters: sanitizeSchema(t.inputSchema) as Record<string, unknown>,\n })),\n },\n ]\n\nexport const buildRequestBody = (\n history: ReadonlyArray<Item>,\n generationConfig: Option.Option<GenerationConfig>,\n tools: ReadonlyArray<ToolDescriptor> = [],\n): RequestBody => {\n const systemTexts = pipe(allMessages(history), Arr.filterMap(systemMessageText))\n const contents = pipe(history, Arr.filterMap(itemToContent(history)))\n const requestTools = toolDescriptorsToTools(tools)\n return {\n contents,\n ...(systemTexts.length > 0 && {\n systemInstruction: { parts: [{ text: systemTexts.join(\"\\n\") }] },\n }),\n ...Option.match(generationConfig, {\n onNone: () => ({}),\n onSome: (cfg) => ({ generationConfig: cfg }),\n }),\n ...(requestTools.length > 0 && {\n tools: requestTools,\n toolConfig: { functionCallingConfig: { mode: \"AUTO\" as const } },\n }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Stream-level state - accumulate chunk text + final usage/finish.\n//\n// `Accumulator` is immutable; `ingestChunk` returns a fresh one per chunk.\n// Drive it via `Stream.mapAccum` in the consumer.\n// ---------------------------------------------------------------------------\n\nconst finishReasonToStop = (reason: Option.Option<string>): Turn[\"stop_reason\"] =>\n Option.match(reason, {\n onNone: () => \"stop\" as const,\n onSome: (r) => (r === \"MAX_TOKENS\" ? (\"max_tokens\" as const) : (\"stop\" as const)),\n })\n\nexport type AccumulatedFunctionCall = {\n /** Synthesized id-or-name we surface as `call_id` on the canonical item. */\n readonly callId: string\n readonly name: string\n /** Wire id from Gemini 3, when present - echoed back on `functionResponse`. */\n readonly providerId: Option.Option<string>\n /** Args as JSON-encoded string, mirroring `Items.FunctionCall.arguments`. */\n readonly arguments: string\n}\n\nexport type Accumulator = {\n readonly text: string\n readonly reasoning: string\n readonly functionCalls: ReadonlyArray<AccumulatedFunctionCall>\n readonly finishReason: Option.Option<string>\n readonly usage: {\n readonly input_tokens?: number\n readonly output_tokens?: number\n readonly total_tokens?: number\n readonly input_tokens_details?: { readonly cached_tokens?: number }\n readonly output_tokens_details?: { readonly reasoning_tokens?: number }\n }\n}\n\nexport const emptyAccumulator: Accumulator = {\n text: \"\",\n reasoning: \"\",\n functionCalls: [],\n finishReason: Option.none(),\n usage: {},\n}\n\n/**\n * One part's worth of streamable output. `text` and `reasoning` are\n * incremental string deltas; `function_call` arrives whole-in-one-chunk\n * (Gemini does not stream tool-call args).\n */\nexport type ChunkPart =\n | { readonly kind: \"text\"; readonly text: string }\n | { readonly kind: \"reasoning\"; readonly text: string }\n | {\n readonly kind: \"function_call\"\n readonly id: Option.Option<string>\n readonly name: string\n readonly args: unknown\n }\n\nexport type ChunkResult = {\n readonly accumulator: Accumulator\n readonly parts: ReadonlyArray<ChunkPart>\n readonly finished: boolean\n}\n\nconst isTextPart = (p: typeof Part.Type): p is typeof TextPart.Type => \"text\" in p\n\nconst textToChunkParts = (p: typeof TextPart.Type): ReadonlyArray<ChunkPart> =>\n p.text.length === 0 ? [] : [{ kind: p.thought === true ? \"reasoning\" : \"text\", text: p.text }]\n\nconst functionCallToChunkParts = (p: WireFunctionCallPart): ReadonlyArray<ChunkPart> => [\n {\n kind: \"function_call\",\n id: Option.fromNullishOr(p.functionCall.id),\n name: p.functionCall.name,\n args: p.functionCall.args ?? {},\n },\n]\n\nconst partToChunkParts = (p: typeof Part.Type): ReadonlyArray<ChunkPart> =>\n isTextPart(p) ? textToChunkParts(p) : functionCallToChunkParts(p)\n\nconst chunkParts = (chunk: WireChunk): ReadonlyArray<ChunkPart> =>\n pipe(chunk.candidates?.[0]?.content?.parts ?? [], Arr.flatMap(partToChunkParts))\n\nconst sumStrings = (parts: ReadonlyArray<ChunkPart>, kind: \"text\" | \"reasoning\"): string =>\n pipe(\n parts,\n Arr.filterMap((p) => (p.kind === kind ? Result.succeed(p.text) : Result.failVoid)),\n ).join(\"\")\n\nconst collectFunctionCalls = (\n parts: ReadonlyArray<ChunkPart>,\n): ReadonlyArray<Extract<ChunkPart, { kind: \"function_call\" }>> =>\n pipe(\n parts,\n Arr.filterMap((p) => (p.kind === \"function_call\" ? Result.succeed(p) : Result.failVoid)),\n )\n\nconst mergeUsage = (\n prev: Accumulator[\"usage\"],\n next: WireChunk[\"usageMetadata\"],\n): Accumulator[\"usage\"] =>\n next === undefined\n ? prev\n : {\n ...prev,\n ...(next.promptTokenCount !== undefined && { input_tokens: next.promptTokenCount }),\n ...(next.candidatesTokenCount !== undefined && {\n output_tokens: next.candidatesTokenCount,\n }),\n ...(next.totalTokenCount !== undefined && { total_tokens: next.totalTokenCount }),\n ...(next.cachedContentTokenCount !== undefined && {\n input_tokens_details: { cached_tokens: next.cachedContentTokenCount },\n }),\n ...(next.thoughtsTokenCount !== undefined && {\n output_tokens_details: { reasoning_tokens: next.thoughtsTokenCount },\n }),\n }\n\n/**\n * Synthesize a stable `call_id` for a function call. Gemini 3 provides one\n * via `functionCall.id`; older models do not, so we fall back to\n * `<name>_<index>` based on prior calls' position in the accumulator.\n */\nconst synthesizeCallId = (\n call: Extract<ChunkPart, { kind: \"function_call\" }>,\n priorCalls: ReadonlyArray<AccumulatedFunctionCall>,\n): string =>\n Option.match(call.id, {\n onSome: (id) => id,\n onNone: () => `${call.name}_${priorCalls.length}`,\n })\n\nconst chunkCallToAccumulated = (\n prior: ReadonlyArray<AccumulatedFunctionCall>,\n call: Extract<ChunkPart, { kind: \"function_call\" }>,\n): AccumulatedFunctionCall => ({\n callId: synthesizeCallId(call, prior),\n name: call.name,\n providerId: call.id,\n // `functionCallToChunkParts` already replaced null/undefined with `{}`,\n // so `call.args` is always a JSON-encodable value here.\n arguments: JSON.stringify(call.args),\n})\n\nconst appendFunctionCalls = (\n prior: ReadonlyArray<AccumulatedFunctionCall>,\n fromChunk: ReadonlyArray<Extract<ChunkPart, { kind: \"function_call\" }>>,\n): ReadonlyArray<AccumulatedFunctionCall> =>\n fromChunk.reduce<ReadonlyArray<AccumulatedFunctionCall>>(\n (acc, call) => [...acc, chunkCallToAccumulated(acc, call)],\n prior,\n )\n\nexport const ingestChunk = (acc: Accumulator, chunk: WireChunk): ChunkResult => {\n const parts = chunkParts(chunk)\n const finishReason = Option.fromNullishOr(chunk.candidates?.[0]?.finishReason)\n return {\n parts,\n finished: Option.isSome(finishReason),\n accumulator: {\n text: acc.text + sumStrings(parts, \"text\"),\n reasoning: acc.reasoning + sumStrings(parts, \"reasoning\"),\n functionCalls: appendFunctionCalls(acc.functionCalls, collectFunctionCalls(parts)),\n finishReason: Option.orElse(finishReason, () => acc.finishReason),\n usage: mergeUsage(acc.usage, chunk.usageMetadata),\n },\n }\n}\n\nconst reasoningItems = (acc: Accumulator): ReadonlyArray<Item> =>\n acc.reasoning.length > 0 ? [{ type: \"reasoning\", summary: acc.reasoning }] : []\n\nconst assistantMessageItems = (acc: Accumulator): ReadonlyArray<Item> =>\n acc.text.length === 0\n ? []\n : [\n {\n type: \"message\",\n role: \"assistant\",\n content: [{ type: \"output_text\", text: acc.text }],\n },\n ]\n\nconst functionCallItems = (acc: Accumulator): ReadonlyArray<Item> =>\n pipe(\n acc.functionCalls,\n Arr.map((c) => ({\n type: \"function_call\" as const,\n call_id: c.callId,\n name: c.name,\n arguments: c.arguments,\n ...(Option.isSome(c.providerId) && { providerData: { gemini: { id: c.providerId.value } } }),\n })),\n )\n\nexport const accumulatorToTurn = (acc: Accumulator): Turn => ({\n stop_reason:\n acc.functionCalls.length > 0 ? (\"tool_calls\" as const) : finishReasonToStop(acc.finishReason),\n usage: { ...acc.usage },\n items: [...reasoningItems(acc), ...assistantMessageItems(acc), ...functionCallItems(acc)],\n})\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/google",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Google Gemini provider for @effect-uai/core.",
5
5
  "keywords": [
6
6
  "ai",
@@ -61,7 +61,7 @@
61
61
  "devDependencies": {
62
62
  "effect": "4.0.0-beta.57",
63
63
  "typescript": "^6.0.3",
64
- "@effect-uai/core": "0.4.0"
64
+ "@effect-uai/core": "0.5.1"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "@effect-uai/core": ">=0.2.0 <1",