@effect-uai/google 0.6.0 → 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.
- package/dist/Gemini.d.mts.map +1 -1
- package/dist/Gemini.mjs +7 -2
- package/dist/Gemini.mjs.map +1 -1
- package/dist/GeminiEmbedding.d.mts.map +1 -1
- package/dist/GeminiEmbedding.mjs +3 -3
- package/dist/GeminiEmbedding.mjs.map +1 -1
- package/dist/GeminiSynthesizer.d.mts.map +1 -1
- package/dist/GeminiSynthesizer.mjs +22 -4
- package/dist/GeminiSynthesizer.mjs.map +1 -1
- package/dist/GeminiSynthesizer.test.mjs +14 -48
- package/dist/GeminiSynthesizer.test.mjs.map +1 -1
- package/dist/LyriaGenerator.d.mts +18 -18
- package/dist/LyriaGenerator.d.mts.map +1 -1
- package/dist/LyriaGenerator.mjs +59 -42
- package/dist/LyriaGenerator.mjs.map +1 -1
- package/dist/LyriaGenerator.test.mjs +26 -72
- package/dist/LyriaGenerator.test.mjs.map +1 -1
- package/dist/codec.d.mts +11 -3
- package/dist/codec.d.mts.map +1 -1
- package/dist/codec.mjs +20 -3
- package/dist/codec.mjs.map +1 -1
- package/dist/index.d.mts +2 -3
- package/dist/index.mjs +1 -2
- package/dist/models.d.mts +1 -10
- package/dist/models.d.mts.map +1 -1
- package/package.json +2 -6
- package/src/Gemini.ts +15 -1
- package/src/GeminiEmbedding.ts +14 -9
- package/src/GeminiSynthesizer.test.ts +13 -67
- package/src/GeminiSynthesizer.ts +30 -4
- package/src/LyriaGenerator.test.ts +34 -78
- package/src/LyriaGenerator.ts +92 -73
- package/src/codec.ts +31 -1
- package/src/index.ts +0 -1
- package/src/models.ts +0 -17
- package/dist/GeminiTranscriber.d.mts +0 -42
- package/dist/GeminiTranscriber.d.mts.map +0 -1
- package/dist/GeminiTranscriber.mjs +0 -80
- package/dist/GeminiTranscriber.mjs.map +0 -1
- package/dist/GeminiTranscriber.test.d.mts +0 -1
- package/dist/GeminiTranscriber.test.mjs +0 -125
- package/dist/GeminiTranscriber.test.mjs.map +0 -1
- package/src/GeminiTranscriber.test.ts +0 -136
- package/src/GeminiTranscriber.ts +0 -204
package/dist/codec.mjs.map
CHANGED
|
@@ -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 {\n ContentBlock,\n ToolCall,\n InputImage,\n HistoryItem,\n Message,\n} 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<HistoryItem>): 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// `ToolCall.arguments` and `ToolCallOutput.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 * `ToolCallOutput` 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: HistoryItem): item is ToolCall => item.type === \"function_call\"\n\nconst nameForCallId = (\n history: ReadonlyArray<HistoryItem>,\n call_id: string,\n): Option.Option<string> =>\n pipe(\n history,\n Arr.findFirst((item): item is ToolCall => isFunctionCallItem(item) && item.call_id === call_id),\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: ToolCall): 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<HistoryItem>) =>\n (item: HistoryItem): 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<HistoryItem>,\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.ToolCall.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<HistoryItem> =>\n acc.reasoning.length > 0 ? [{ type: \"reasoning\", summary: acc.reasoning }] : []\n\nconst assistantMessageItems = (acc: Accumulator): ReadonlyArray<HistoryItem> =>\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<HistoryItem> =>\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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"}
|
|
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 {\n ContentBlock,\n ToolCall,\n InputImage,\n HistoryItem,\n Message,\n} from \"@effect-uai/core/Items\"\nimport type { ToolDescriptor } from \"@effect-uai/core/Tool\"\nimport type { Turn } from \"@effect-uai/core/Turn\"\nimport type { CommonRequest } from \"@effect-uai/core/LanguageModel\"\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<HistoryItem>): 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// `ToolCall.arguments` and `ToolCallOutput.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 * `ToolCallOutput` 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: HistoryItem): item is ToolCall => item.type === \"function_call\"\n\nconst nameForCallId = (\n history: ReadonlyArray<HistoryItem>,\n call_id: string,\n): Option.Option<string> =>\n pipe(\n history,\n Arr.findFirst((item): item is ToolCall => isFunctionCallItem(item) && item.call_id === call_id),\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: ToolCall): 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<HistoryItem>) =>\n (item: HistoryItem): 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\n/**\n * Map our cross-provider `toolChoice` onto Gemini's `functionCallingConfig`.\n * Gemini honours the same three modes plus a function allow-list, so the\n * choice is mapped, not dropped. `undefined` keeps the API default (`AUTO`).\n */\nconst toolChoiceToFunctionCallingConfig = (\n toolChoice: CommonRequest[\"toolChoice\"],\n): RequestToolConfig[\"functionCallingConfig\"] =>\n Match.value(toolChoice ?? \"auto\").pipe(\n Match.when(\"auto\", () => ({ mode: \"AUTO\" as const })),\n Match.when(\"required\", () => ({ mode: \"ANY\" as const })),\n Match.when(\"none\", () => ({ mode: \"NONE\" as const })),\n Match.orElse((tc) => ({ mode: \"ANY\" as const, allowedFunctionNames: [tc.name] })),\n )\n\n/**\n * URL-form images need Gemini's Files API (upload, then `fileData`); we\n * don't pre-upload, so such a part would otherwise vanish from the request.\n * The caller fails the request with `Unsupported` instead of silently\n * dropping the image.\n */\nexport const hasUrlImageSource = (history: ReadonlyArray<HistoryItem>): boolean =>\n history.some(\n (item) =>\n item.type === \"message\" &&\n item.content.some((b) => b.type === \"input_image\" && b.source._tag === \"url\"),\n )\n\nexport const buildRequestBody = (\n history: ReadonlyArray<HistoryItem>,\n generationConfig: Option.Option<GenerationConfig>,\n tools: ReadonlyArray<ToolDescriptor> = [],\n toolChoice: CommonRequest[\"toolChoice\"] = undefined,\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: toolChoiceToFunctionCallingConfig(toolChoice) },\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.ToolCall.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<HistoryItem> =>\n acc.reasoning.length > 0 ? [{ type: \"reasoning\", summary: acc.reasoning }] : []\n\nconst assistantMessageItems = (acc: Accumulator): ReadonlyArray<HistoryItem> =>\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<HistoryItem> =>\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/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { t as codec_d_exports } from "./codec.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { GeminiTtsModel, GeminiVoiceName, GoogleEmbeddingModel, GoogleModel, LyriaModel } from "./models.mjs";
|
|
3
3
|
import { t as Gemini_d_exports } from "./Gemini.mjs";
|
|
4
4
|
import { t as GeminiEmbedding_d_exports } from "./GeminiEmbedding.mjs";
|
|
5
5
|
import { t as GeminiSynthesizer_d_exports } from "./GeminiSynthesizer.mjs";
|
|
6
|
-
import { t as GeminiTranscriber_d_exports } from "./GeminiTranscriber.mjs";
|
|
7
6
|
import { t as LyriaGenerator_d_exports } from "./LyriaGenerator.mjs";
|
|
8
|
-
export { Gemini_d_exports as Gemini, GeminiEmbedding_d_exports as GeminiEmbedding,
|
|
7
|
+
export { Gemini_d_exports as Gemini, GeminiEmbedding_d_exports as GeminiEmbedding, GeminiSynthesizer_d_exports as GeminiSynthesizer, GeminiTtsModel, GeminiVoiceName, GoogleEmbeddingModel, GoogleModel, LyriaGenerator_d_exports as LyriaGenerator, LyriaModel, codec_d_exports as codec };
|
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,6 @@ import { t as codec_exports } from "./codec.mjs";
|
|
|
2
2
|
import { t as Gemini_exports } from "./Gemini.mjs";
|
|
3
3
|
import { t as GeminiEmbedding_exports } from "./GeminiEmbedding.mjs";
|
|
4
4
|
import { t as GeminiSynthesizer_exports } from "./GeminiSynthesizer.mjs";
|
|
5
|
-
import { t as GeminiTranscriber_exports } from "./GeminiTranscriber.mjs";
|
|
6
5
|
import { t as LyriaGenerator_exports } from "./LyriaGenerator.mjs";
|
|
7
6
|
import "./models.mjs";
|
|
8
|
-
export { Gemini_exports as Gemini, GeminiEmbedding_exports as GeminiEmbedding, GeminiSynthesizer_exports as GeminiSynthesizer,
|
|
7
|
+
export { Gemini_exports as Gemini, GeminiEmbedding_exports as GeminiEmbedding, GeminiSynthesizer_exports as GeminiSynthesizer, LyriaGenerator_exports as LyriaGenerator, codec_exports as codec };
|
package/dist/models.d.mts
CHANGED
|
@@ -28,15 +28,6 @@ type GoogleModel = "gemini-3.1-pro-preview" | "gemini-3-flash-preview" | "gemini
|
|
|
28
28
|
* Reference: https://ai.google.dev/gemini-api/docs/speech-generation
|
|
29
29
|
*/
|
|
30
30
|
type GeminiTtsModel = "gemini-2.5-flash-preview-tts" | "gemini-2.5-pro-preview-tts" | "gemini-3.1-flash-tts-preview" | (string & {});
|
|
31
|
-
/**
|
|
32
|
-
* Known Gemini models that accept audio input via `inlineData` /
|
|
33
|
-
* `fileData` on `generateContent`. Used by `GeminiTranscriber` —
|
|
34
|
-
* transcription is prompt-driven on top of these models, not a
|
|
35
|
-
* dedicated endpoint.
|
|
36
|
-
*
|
|
37
|
-
* Reference: https://ai.google.dev/gemini-api/docs/audio
|
|
38
|
-
*/
|
|
39
|
-
type GeminiSttModel = "gemini-3-flash-preview" | "gemini-3.1-pro-preview" | "gemini-3.1-flash-lite-preview" | "gemini-2.5-flash" | "gemini-2.5-pro" | (string & {});
|
|
40
31
|
/**
|
|
41
32
|
* 30 prebuilt voice names for Gemini TTS. No custom-voice / cloning path
|
|
42
33
|
* is exposed on this surface, so the tail is stock-only (no
|
|
@@ -57,5 +48,5 @@ type GeminiVoiceName = "Zephyr" | "Puck" | "Charon" | "Kore" | "Fenrir" | "Leda"
|
|
|
57
48
|
type LyriaModel = "lyria-3-clip-preview" | "lyria-3-pro-preview" | (string & {});
|
|
58
49
|
type GoogleEmbeddingModel = "gemini-embedding-2" | "gemini-embedding-001" | (string & {});
|
|
59
50
|
//#endregion
|
|
60
|
-
export {
|
|
51
|
+
export { GeminiTtsModel, GeminiVoiceName, GoogleEmbeddingModel, GoogleModel, LyriaModel };
|
|
61
52
|
//# sourceMappingURL=models.d.mts.map
|
package/dist/models.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAOA;;;;;AAgCA;KAhCY,WAAA;;;;
|
|
1
|
+
{"version":3,"file":"models.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAOA;;;;;AAgCA;KAhCY,WAAA;;;;AA8CZ;;;;;AAyCA;;;;;AAMA;;;;;;;KA7DY,cAAA;;;;;;;;KAcA,eAAA;;;;;;;;;;KAyCA,UAAA;AAAA,KAMA,oBAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-uai/google",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Google Gemini provider for @effect-uai/core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -46,10 +46,6 @@
|
|
|
46
46
|
"types": "./dist/GeminiSynthesizer.d.mts",
|
|
47
47
|
"import": "./dist/GeminiSynthesizer.mjs"
|
|
48
48
|
},
|
|
49
|
-
"./GeminiTranscriber": {
|
|
50
|
-
"types": "./dist/GeminiTranscriber.d.mts",
|
|
51
|
-
"import": "./dist/GeminiTranscriber.mjs"
|
|
52
|
-
},
|
|
53
49
|
"./LyriaGenerator": {
|
|
54
50
|
"types": "./dist/LyriaGenerator.d.mts",
|
|
55
51
|
"import": "./dist/LyriaGenerator.mjs"
|
|
@@ -61,7 +57,7 @@
|
|
|
61
57
|
"devDependencies": {
|
|
62
58
|
"effect": "4.0.0-beta.57",
|
|
63
59
|
"typescript": "^6.0.3",
|
|
64
|
-
"@effect-uai/core": "0.
|
|
60
|
+
"@effect-uai/core": "0.7.0"
|
|
65
61
|
},
|
|
66
62
|
"peerDependencies": {
|
|
67
63
|
"@effect-uai/core": ">=0.2.0 <1",
|
package/src/Gemini.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
accumulatorToTurn,
|
|
18
18
|
buildRequestBody,
|
|
19
19
|
emptyAccumulator,
|
|
20
|
+
hasUrlImageSource,
|
|
20
21
|
ingestChunk,
|
|
21
22
|
} from "./codec.js"
|
|
22
23
|
import { Match } from "effect"
|
|
@@ -162,9 +163,22 @@ const buildNativeStream = (cfg: Config) => {
|
|
|
162
163
|
Stream.unwrap(
|
|
163
164
|
Effect.gen(function* () {
|
|
164
165
|
const client = yield* HttpClient.HttpClient
|
|
166
|
+
if (hasUrlImageSource(request.history)) {
|
|
167
|
+
return yield* new AiError.Unsupported({
|
|
168
|
+
provider: "gemini",
|
|
169
|
+
capability: "imageInput",
|
|
170
|
+
reason:
|
|
171
|
+
"Gemini needs URL images pre-uploaded via the Files API; pass the image as base64 or raw bytes instead.",
|
|
172
|
+
})
|
|
173
|
+
}
|
|
165
174
|
const url = `${baseUrl}/models/${request.model}:streamGenerateContent?alt=sse`
|
|
166
175
|
const generationConfig = buildGenerationConfig(request)
|
|
167
|
-
const body = buildRequestBody(
|
|
176
|
+
const body = buildRequestBody(
|
|
177
|
+
request.history,
|
|
178
|
+
generationConfig,
|
|
179
|
+
request.tools ?? [],
|
|
180
|
+
request.toolChoice,
|
|
181
|
+
)
|
|
168
182
|
const httpRequest = HttpClientRequest.post(url).pipe(
|
|
169
183
|
HttpClientRequest.setHeader("x-goog-api-key", Redacted.value(cfg.apiKey)),
|
|
170
184
|
HttpClientRequest.bodyJsonUnsafe(body),
|
package/src/GeminiEmbedding.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
Usage,
|
|
9
9
|
} from "@effect-uai/core/Embedding"
|
|
10
10
|
import {
|
|
11
|
+
assertEncoding,
|
|
11
12
|
type CommonEmbedManyRequest,
|
|
12
13
|
type CommonEmbedRequest,
|
|
13
14
|
type EmbedEncoding,
|
|
@@ -348,20 +349,24 @@ export const layer = (
|
|
|
348
349
|
Effect.map(
|
|
349
350
|
make(cfg),
|
|
350
351
|
(s): EmbeddingModelService => ({
|
|
351
|
-
// Gemini's endpoints
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
352
|
+
// Gemini's embed endpoints emit float32 only. Reject a non-float32
|
|
353
|
+
// `encoding` (bucket 1) so a float32 vector isn't handed back
|
|
354
|
+
// mislabeled as the requested type. `task` is left silent: it's a
|
|
355
|
+
// per-model gap (honored by gemini-embedding-001, ignored by
|
|
356
|
+
// gemini-embedding-2 server-side), and per capabilities.md §2.3 we
|
|
357
|
+
// don't maintain per-model tables; there's no wire error to translate.
|
|
355
358
|
embed: <E extends EmbedEncoding | undefined = undefined>(
|
|
356
359
|
req: Omit<CommonEmbedRequest, "encoding"> & { readonly encoding?: E },
|
|
357
|
-
) =>
|
|
360
|
+
) =>
|
|
361
|
+
assertEncoding(req.encoding, ["float32"], "gemini").pipe(
|
|
362
|
+
Effect.andThen(s.embed(req as GeminiEmbedRequest)),
|
|
363
|
+
) as Effect.Effect<EmbedResponse<E>, AiError.AiError>,
|
|
358
364
|
embedMany: <E extends EmbedEncoding | undefined = undefined>(
|
|
359
365
|
req: Omit<CommonEmbedManyRequest, "encoding"> & { readonly encoding?: E },
|
|
360
366
|
) =>
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
>,
|
|
367
|
+
assertEncoding(req.encoding, ["float32"], "gemini").pipe(
|
|
368
|
+
Effect.andThen(s.embedMany(req as GeminiEmbedManyRequest)),
|
|
369
|
+
) as Effect.Effect<EmbedManyResponse<E>, AiError.AiError>,
|
|
365
370
|
}),
|
|
366
371
|
),
|
|
367
372
|
)
|
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { describe, expect, expectTypeOf, it } from "vitest"
|
|
4
|
-
import type * as AiError from "@effect-uai/core/AiError"
|
|
5
|
-
import type { AudioBlob } from "@effect-uai/core/Audio"
|
|
6
|
-
import * as SpeechSynthesizer from "@effect-uai/core/SpeechSynthesizer"
|
|
1
|
+
import { Result } from "effect"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
7
3
|
import * as GeminiSynthesizer from "./GeminiSynthesizer.js"
|
|
8
4
|
|
|
9
|
-
const cfg: GeminiSynthesizer.Config = { apiKey: Redacted.make("test-key") }
|
|
10
|
-
// FetchHttpClient is required for `make`, but these tests only exercise
|
|
11
|
-
// the codec and the compile-time / runtime Unsupported branches — no
|
|
12
|
-
// real HTTP call is made.
|
|
13
|
-
const live = Layer.provide(GeminiSynthesizer.layer(cfg), FetchHttpClient.layer)
|
|
14
|
-
|
|
15
5
|
describe("GeminiSynthesizer.realizeOutput", () => {
|
|
16
|
-
it("maps raw to native PCM 24 kHz mono with an identity wrapper",
|
|
17
|
-
const
|
|
6
|
+
it("maps raw to native PCM 24 kHz mono with an identity wrapper", () => {
|
|
7
|
+
const result = GeminiSynthesizer.realizeOutput("raw")
|
|
8
|
+
expect(Result.isSuccess(result)).toBe(true)
|
|
9
|
+
const [format, wrap] = Result.getOrThrow(result)
|
|
18
10
|
expect(format).toEqual({
|
|
19
11
|
container: "raw",
|
|
20
12
|
encoding: "pcm_s16le",
|
|
@@ -25,8 +17,8 @@ describe("GeminiSynthesizer.realizeOutput", () => {
|
|
|
25
17
|
expect(wrap(pcm)).toBe(pcm)
|
|
26
18
|
})
|
|
27
19
|
|
|
28
|
-
it("maps wav to a RIFF/WAVE wrapper that prepends a 44-byte header",
|
|
29
|
-
const [format, wrap] =
|
|
20
|
+
it("maps wav to a RIFF/WAVE wrapper that prepends a 44-byte header", () => {
|
|
21
|
+
const [format, wrap] = Result.getOrThrow(GeminiSynthesizer.realizeOutput("wav"))
|
|
30
22
|
expect(format.container).toBe("wav")
|
|
31
23
|
const out = wrap(new Uint8Array([0, 1, 2, 3]))
|
|
32
24
|
expect(out.length).toBe(44 + 4)
|
|
@@ -38,56 +30,10 @@ describe("GeminiSynthesizer.realizeOutput", () => {
|
|
|
38
30
|
expect(ascii(36, 4)).toBe("data")
|
|
39
31
|
})
|
|
40
32
|
|
|
41
|
-
it("fails Unsupported for mp3 / opus / aac / flac / ogg / webm",
|
|
42
|
-
const
|
|
43
|
-
expect(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
expect(JSON.stringify(exit.cause)).toContain("outputFormat")
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
describe("GeminiSynthesizer capability guards (runtime)", () => {
|
|
52
|
-
it("streamSynthesisFrom returns an Unsupported stream", async () => {
|
|
53
|
-
const program = GeminiSynthesizer.GeminiSynthesizer.use((s) =>
|
|
54
|
-
Stream.runDrain(
|
|
55
|
-
s.streamSynthesisFrom(Stream.fromIterable(["hi"]), {
|
|
56
|
-
model: "gemini-2.5-flash-preview-tts",
|
|
57
|
-
voiceId: "Kore",
|
|
58
|
-
}),
|
|
59
|
-
),
|
|
60
|
-
)
|
|
61
|
-
const exit = await Effect.runPromiseExit(program.pipe(Effect.provide(live)))
|
|
62
|
-
expect(exit._tag).toBe("Failure")
|
|
63
|
-
if (exit._tag === "Failure") {
|
|
64
|
-
expect(JSON.stringify(exit.cause)).toContain("Unsupported")
|
|
65
|
-
expect(JSON.stringify(exit.cause)).toContain("streamSynthesisFrom")
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
describe("GeminiSynthesizer Layer (compile-time)", () => {
|
|
71
|
-
it("leaves `TtsIncrementalText` unsatisfied when using `streamSynthesisFrom` against this Layer", () => {
|
|
72
|
-
const tokens: Stream.Stream<string> = Stream.fromIterable(["a"])
|
|
73
|
-
const audio = tokens.pipe(
|
|
74
|
-
SpeechSynthesizer.streamSynthesisFrom({
|
|
75
|
-
model: "gemini-2.5-flash-preview-tts",
|
|
76
|
-
voiceId: "Kore",
|
|
77
|
-
}),
|
|
78
|
-
)
|
|
79
|
-
const provided = Stream.runDrain(audio).pipe(Effect.provide(live))
|
|
80
|
-
expectTypeOf(provided).toEqualTypeOf<
|
|
81
|
-
Effect.Effect<void, AiError.AiError, SpeechSynthesizer.TtsIncrementalText>
|
|
82
|
-
>()
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it("sync `synthesize` requires no marker", () => {
|
|
86
|
-
const synth = SpeechSynthesizer.synthesize({
|
|
87
|
-
text: "hi",
|
|
88
|
-
model: "gemini-2.5-flash-preview-tts",
|
|
89
|
-
voiceId: "Kore",
|
|
90
|
-
}).pipe(Effect.provide(live))
|
|
91
|
-
expectTypeOf(synth).toEqualTypeOf<Effect.Effect<AudioBlob, AiError.AiError, never>>()
|
|
33
|
+
it("fails Unsupported for mp3 / opus / aac / flac / ogg / webm", () => {
|
|
34
|
+
const result = GeminiSynthesizer.realizeOutput("mp3")
|
|
35
|
+
expect(Result.isFailure(result)).toBe(true)
|
|
36
|
+
expect(JSON.stringify(result)).toContain("Unsupported")
|
|
37
|
+
expect(JSON.stringify(result)).toContain("outputFormat")
|
|
92
38
|
})
|
|
93
39
|
})
|
package/src/GeminiSynthesizer.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Context, Effect, Layer, Match, Redacted, Schema, Stream } from "effect"
|
|
1
|
+
import { Context, Effect, Layer, Match, Redacted, Result, Schema, Stream } from "effect"
|
|
2
2
|
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
|
3
3
|
import * as AiError from "@effect-uai/core/AiError"
|
|
4
4
|
import type { AudioBlob, AudioChunk, AudioFormat } from "@effect-uai/core/Audio"
|
|
5
|
+
import * as Capabilities from "@effect-uai/core/Capabilities"
|
|
5
6
|
import {
|
|
6
7
|
type CommonSynthesizeRequest,
|
|
7
8
|
SpeechSynthesizer,
|
|
@@ -51,19 +52,19 @@ const PCM_CHANNELS = 1
|
|
|
51
52
|
|
|
52
53
|
export const realizeOutput = Match.type<AudioFormat["container"]>().pipe(
|
|
53
54
|
Match.when("raw", () =>
|
|
54
|
-
|
|
55
|
+
Result.succeed([
|
|
55
56
|
{ container: "raw", encoding: "pcm_s16le", sampleRate: PCM_RATE, channels: PCM_CHANNELS },
|
|
56
57
|
(b: Uint8Array) => b,
|
|
57
58
|
] as const),
|
|
58
59
|
),
|
|
59
60
|
Match.when("wav", () =>
|
|
60
|
-
|
|
61
|
+
Result.succeed([
|
|
61
62
|
{ container: "wav", encoding: "pcm_s16le", sampleRate: PCM_RATE, channels: PCM_CHANNELS },
|
|
62
63
|
(b: Uint8Array) => wrapPcmAsWav(b, PCM_RATE, PCM_CHANNELS),
|
|
63
64
|
] as const),
|
|
64
65
|
),
|
|
65
66
|
Match.whenOr("mp3", "opus", "aac", "flac", "ogg", "webm", (c) =>
|
|
66
|
-
|
|
67
|
+
Result.fail(
|
|
67
68
|
new AiError.Unsupported({
|
|
68
69
|
provider: "gemini",
|
|
69
70
|
capability: "outputFormat",
|
|
@@ -125,6 +126,31 @@ const baseUrl = (cfg: Config) => cfg.baseUrl ?? "https://generativelanguage.goog
|
|
|
125
126
|
|
|
126
127
|
const synthesizeImpl = (cfg: Config) => (request: GeminiSynthesizeRequest) =>
|
|
127
128
|
Effect.gen(function* () {
|
|
129
|
+
// Gemini `:generateContent` TTS has no phoneme field. Pronunciations
|
|
130
|
+
// are load-bearing (bucket 1), so reject rather than mispronounce.
|
|
131
|
+
if (request.pronunciations !== undefined && request.pronunciations.length > 0) {
|
|
132
|
+
return yield* Effect.fail(
|
|
133
|
+
new AiError.Unsupported({
|
|
134
|
+
provider: "gemini",
|
|
135
|
+
capability: "pronunciations",
|
|
136
|
+
reason:
|
|
137
|
+
"Gemini `:generateContent` TTS has no phoneme field. Use Cloud Text-to-Speech (Chirp 3 HD) or a provider with inline phonemes (Inworld) for pronunciation overrides.",
|
|
138
|
+
}),
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
// No speaking-rate or language parameters on this endpoint; bucket 2.
|
|
142
|
+
yield* Capabilities.warnDroppedWhen(request.speed, {
|
|
143
|
+
provider: "gemini",
|
|
144
|
+
capability: "speed",
|
|
145
|
+
field: "speed",
|
|
146
|
+
reason: "Gemini `:generateContent` TTS has no speaking-rate parameter.",
|
|
147
|
+
})
|
|
148
|
+
yield* Capabilities.warnDroppedWhen(request.languageCode, {
|
|
149
|
+
provider: "gemini",
|
|
150
|
+
capability: "languageCode",
|
|
151
|
+
field: "languageCode",
|
|
152
|
+
reason: "Gemini `:generateContent` TTS has no language parameter.",
|
|
153
|
+
})
|
|
128
154
|
const client = yield* HttpClient.HttpClient
|
|
129
155
|
const [format, wrap] = yield* realizeOutput(request.outputFormat?.container ?? "raw")
|
|
130
156
|
const httpRequest = HttpClientRequest.post(
|
|
@@ -1,81 +1,17 @@
|
|
|
1
|
-
import { Effect, Layer, Redacted, Stream } from "effect"
|
|
1
|
+
import { Duration, Effect, Layer, Redacted, Stream } from "effect"
|
|
2
2
|
import { FetchHttpClient } from "effect/unstable/http"
|
|
3
3
|
import { describe, expect, expectTypeOf, it } from "vitest"
|
|
4
4
|
import type * as AiError from "@effect-uai/core/AiError"
|
|
5
|
-
import
|
|
5
|
+
import type { GenerateResult } from "@effect-uai/core/Music"
|
|
6
6
|
import { promptsInput } from "@effect-uai/core/Music"
|
|
7
|
+
import * as MusicGenerator from "@effect-uai/core/MusicGenerator"
|
|
7
8
|
import * as LyriaGenerator from "./LyriaGenerator.js"
|
|
8
9
|
|
|
9
10
|
const cfg: LyriaGenerator.Config = { apiKey: Redacted.make("test-key") }
|
|
10
11
|
// FetchHttpClient is required for `make`, but these tests only exercise the
|
|
11
|
-
// compile-time gating and runtime Unsupported branches
|
|
12
|
+
// compile-time gating and runtime Unsupported branches, no real HTTP is made.
|
|
12
13
|
const live = Layer.provide(LyriaGenerator.layer(cfg), FetchHttpClient.layer)
|
|
13
14
|
|
|
14
|
-
describe("buildPrompt", () => {
|
|
15
|
-
it("returns a plain string when prompts is a string", () => {
|
|
16
|
-
const out = LyriaGenerator.buildPrompt({
|
|
17
|
-
model: "lyria-3-clip-preview",
|
|
18
|
-
prompts: "upbeat indie pop",
|
|
19
|
-
})
|
|
20
|
-
expect(out).toBe("upbeat indie pop")
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it("joins weighted prompts with weight annotations", () => {
|
|
24
|
-
const out = LyriaGenerator.buildPrompt({
|
|
25
|
-
model: "lyria-3-clip-preview",
|
|
26
|
-
prompts: [
|
|
27
|
-
{ text: "minimal techno", weight: 1.0 },
|
|
28
|
-
{ text: "1980s synthwave", weight: 0.3 },
|
|
29
|
-
],
|
|
30
|
-
})
|
|
31
|
-
expect(out).toContain("minimal techno")
|
|
32
|
-
expect(out).toContain("1980s synthwave (weight 0.3)")
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it("omits the weight annotation for weight === 1", () => {
|
|
36
|
-
const out = LyriaGenerator.buildPrompt({
|
|
37
|
-
model: "lyria-3-clip-preview",
|
|
38
|
-
prompts: [{ text: "ambient", weight: 1 }],
|
|
39
|
-
})
|
|
40
|
-
expect(out).toBe("ambient")
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it("splices in bpm, scale, duration, and instrumental hints", () => {
|
|
44
|
-
const out = LyriaGenerator.buildPrompt({
|
|
45
|
-
model: "lyria-3-clip-preview",
|
|
46
|
-
prompts: "house",
|
|
47
|
-
bpm: 124,
|
|
48
|
-
scale: "C_MAJOR",
|
|
49
|
-
durationSeconds: 30,
|
|
50
|
-
instrumental: true,
|
|
51
|
-
})
|
|
52
|
-
expect(out).toContain("Instrumental only")
|
|
53
|
-
expect(out).toContain("BPM: 124")
|
|
54
|
-
expect(out).toContain("Key/scale: C_MAJOR")
|
|
55
|
-
expect(out).toContain("Target duration: 30s")
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it("appends a lyrics block when lyrics is set and instrumental is not true", () => {
|
|
59
|
-
const out = LyriaGenerator.buildPrompt({
|
|
60
|
-
model: "lyria-3-clip-preview",
|
|
61
|
-
prompts: "indie rock",
|
|
62
|
-
lyrics: "[Verse]\nhello world",
|
|
63
|
-
})
|
|
64
|
-
expect(out).toContain("Lyrics:")
|
|
65
|
-
expect(out).toContain("[Verse]")
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it("drops the lyrics block when instrumental is true", () => {
|
|
69
|
-
const out = LyriaGenerator.buildPrompt({
|
|
70
|
-
model: "lyria-3-clip-preview",
|
|
71
|
-
prompts: "indie rock",
|
|
72
|
-
lyrics: "[Verse]\nhello world",
|
|
73
|
-
instrumental: true,
|
|
74
|
-
})
|
|
75
|
-
expect(out).not.toContain("Lyrics:")
|
|
76
|
-
})
|
|
77
|
-
})
|
|
78
|
-
|
|
79
15
|
describe("containerToMimeType", () => {
|
|
80
16
|
it("maps mp3 and wav", async () => {
|
|
81
17
|
expect(await Effect.runPromise(LyriaGenerator.containerToMimeType("mp3"))).toBe("audio/mp3")
|
|
@@ -115,7 +51,7 @@ describe("LyriaGenerator capability guards (runtime)", () => {
|
|
|
115
51
|
Stream.runDrain(
|
|
116
52
|
s.streamGenerationFrom(Stream.fromIterable([promptsInput([{ text: "x" }])]), {
|
|
117
53
|
model: "lyria-3-clip-preview",
|
|
118
|
-
|
|
54
|
+
prompt: "",
|
|
119
55
|
}),
|
|
120
56
|
),
|
|
121
57
|
)
|
|
@@ -131,32 +67,52 @@ describe("LyriaGenerator capability guards (runtime)", () => {
|
|
|
131
67
|
describe("LyriaGenerator Layer (compile-time)", () => {
|
|
132
68
|
it("leaves `MusicInteractiveSession` unsatisfied when using `streamGenerationFrom` against this Layer", () => {
|
|
133
69
|
const inputs = Stream.fromIterable([promptsInput([{ text: "techno" }])])
|
|
134
|
-
const
|
|
70
|
+
const events = inputs.pipe(
|
|
135
71
|
MusicGenerator.streamGenerationFrom({
|
|
136
72
|
model: "lyria-3-clip-preview",
|
|
137
|
-
|
|
73
|
+
prompt: "",
|
|
138
74
|
}),
|
|
139
75
|
)
|
|
140
|
-
const provided = Stream.runDrain(
|
|
76
|
+
const provided = Stream.runDrain(events).pipe(Effect.provide(live))
|
|
141
77
|
expectTypeOf(provided).toEqualTypeOf<
|
|
142
78
|
Effect.Effect<void, AiError.AiError, MusicGenerator.MusicInteractiveSession>
|
|
143
79
|
>()
|
|
144
80
|
})
|
|
145
81
|
|
|
146
|
-
it("sync `generate`
|
|
82
|
+
it("sync `generate` returns GenerateResult and requires no marker", () => {
|
|
147
83
|
const gen = MusicGenerator.generate({
|
|
148
84
|
model: "lyria-3-clip-preview",
|
|
149
|
-
|
|
85
|
+
prompt: "ambient",
|
|
150
86
|
}).pipe(Effect.provide(live))
|
|
151
|
-
expectTypeOf(gen).toEqualTypeOf<
|
|
152
|
-
|
|
153
|
-
|
|
87
|
+
expectTypeOf(gen).toEqualTypeOf<Effect.Effect<GenerateResult, AiError.AiError, never>>()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("chunked `streamGeneration` requires no marker", () => {
|
|
154
91
|
const _stream = Stream.runCollect(
|
|
155
92
|
MusicGenerator.streamGeneration({
|
|
156
93
|
model: "lyria-3-clip-preview",
|
|
157
|
-
|
|
94
|
+
prompt: "ambient",
|
|
158
95
|
}),
|
|
159
96
|
).pipe(Effect.provide(live))
|
|
160
97
|
expect(_stream).toBeDefined()
|
|
161
98
|
})
|
|
99
|
+
|
|
100
|
+
it("Lyria-typed request accepts no `instrumental` field", () => {
|
|
101
|
+
const _req: LyriaGenerator.LyriaGenerateRequest = {
|
|
102
|
+
model: "lyria-3-clip-preview",
|
|
103
|
+
prompt: "ambient",
|
|
104
|
+
// @ts-expect-error: instrumental was removed from LyriaGenerateRequest in v0.7
|
|
105
|
+
instrumental: true,
|
|
106
|
+
}
|
|
107
|
+
expect(_req).toBeDefined()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it("Common request supports `duration: Duration.Duration`", () => {
|
|
111
|
+
const _req: MusicGenerator.CommonGenerateMusicRequest = {
|
|
112
|
+
model: "lyria-3-clip-preview",
|
|
113
|
+
prompt: "ambient",
|
|
114
|
+
duration: Duration.seconds(30),
|
|
115
|
+
}
|
|
116
|
+
expect(_req).toBeDefined()
|
|
117
|
+
})
|
|
162
118
|
})
|