@effect-uai/google 0.8.0 → 0.10.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 +1 -0
- package/dist/Gemini.d.mts.map +1 -1
- package/dist/Gemini.mjs +2 -1
- package/dist/Gemini.mjs.map +1 -1
- package/dist/GeminiSynthesizer.d.mts +14 -4
- package/dist/GeminiSynthesizer.d.mts.map +1 -1
- package/dist/GeminiSynthesizer.mjs +2 -2
- package/dist/GeminiSynthesizer.mjs.map +1 -1
- package/dist/codec.d.mts +7 -2
- package/dist/codec.d.mts.map +1 -1
- package/dist/codec.mjs +47 -24
- package/dist/codec.mjs.map +1 -1
- package/package.json +2 -2
- package/src/Gemini.ts +2 -1
- package/src/GeminiSynthesizer.ts +6 -8
- package/src/codec.ts +56 -17
package/dist/Gemini.d.mts
CHANGED
|
@@ -29,6 +29,7 @@ declare const ProviderEvent: Schema.Struct<{
|
|
|
29
29
|
readonly name: Schema.String;
|
|
30
30
|
readonly args: Schema.optional<Schema.Unknown>;
|
|
31
31
|
}>;
|
|
32
|
+
readonly thoughtSignature: Schema.optional<Schema.String>;
|
|
32
33
|
}>]>>>;
|
|
33
34
|
}>>;
|
|
34
35
|
readonly finishReason: Schema.optional<Schema.String>;
|
package/dist/Gemini.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Gemini.d.mts","names":[],"sources":["../src/Gemini.ts"],"mappings":";;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"Gemini.d.mts","names":[],"sources":["../src/Gemini.ts"],"mappings":";;;;;;;;;;;;;;;;;;cAgCa,aAAA,EAAa,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;KACd,aAAA,GAAgB,SAAA;AAAA,KAMhB,aAAA,GAAgB,IAAA,CAAK,aAAA;;;;;WAKtB,KAAA,EAAO,WAAA;;;;;;WAMP,cAAA;AAAA;AAAA,KAGC,aAAA;;;;;;;;WAQD,YAAA,GAAe,OAAA,EAAS,aAAA,KAAkB,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,OAAA,CAAQ,OAAA;EA7BhE;;;;EAAA,SAkCf,UAAA,GAAa,OAAA,EAAS,aAAA,KAAkB,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,OAAA,CAAQ,OAAA;;;;;;;WAOzE,IAAA,GAAO,OAAA,EAAS,aAAA,KAAkB,MAAA,CAAO,MAAA,CAAO,IAAA,EAAM,OAAA,CAAQ,OAAA;;;;;;WAM9D,WAAA,SACP,CAAA,EAAG,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,CAAA,EAAG,CAAA,MAChC,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,CAAA,EAAG,CAAA;AAAA;AAAA,cAClC,WAAA;;;;;;cAOY,MAAA,SAAe,WAAA;AAAA,KAIhB,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;AAAA;AAAA,cA2JE,WAAA,SACX,CAAA,EAAG,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,CAAA,EAAG,CAAA,MAClC,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,CAAA,EAAG,CAAA;;;;cAoClB,IAAA,GAAQ,GAAA,EAAK,MAAA,KAAS,MAAA,CAAO,MAAA,CAAO,aAAA,SAAsB,UAAA,CAAW,UAAA;;;;;;;;cAoBrE,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,MAAA,GAAS,aAAA,SAAsB,UAAA,CAAW,UAAA"}
|
package/dist/Gemini.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import * as AiError from "@effect-uai/core/AiError";
|
|
|
6
6
|
import { LanguageModel, turnFromStream } from "@effect-uai/core/LanguageModel";
|
|
7
7
|
import { JsonParseError } from "@effect-uai/core/JSONL";
|
|
8
8
|
import * as SSE from "@effect-uai/core/SSE";
|
|
9
|
+
import { descriptorsOf } from "@effect-uai/core/Tool";
|
|
9
10
|
import { TurnEvent } from "@effect-uai/core/Turn";
|
|
10
11
|
//#region src/Gemini.ts
|
|
11
12
|
var Gemini_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -105,7 +106,7 @@ const buildNativeStream = (cfg) => {
|
|
|
105
106
|
});
|
|
106
107
|
const url = `${baseUrl}/models/${request.model}:streamGenerateContent?alt=sse`;
|
|
107
108
|
const generationConfig = buildGenerationConfig(request);
|
|
108
|
-
const body = buildRequestBody(request.history, generationConfig, request.tools
|
|
109
|
+
const body = buildRequestBody(request.history, generationConfig, descriptorsOf(request.tools), request.toolChoice);
|
|
109
110
|
const httpRequest = HttpClientRequest.post(url).pipe(HttpClientRequest.setHeader("x-goog-api-key", Redacted.value(cfg.apiKey)), HttpClientRequest.bodyJsonUnsafe(body), HttpClientRequest.accept("text/event-stream"));
|
|
110
111
|
const response = yield* client.execute(httpRequest).pipe(Effect.mapError((cause) => new AiError.Unavailable({
|
|
111
112
|
provider: "gemini",
|
package/dist/Gemini.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Gemini.mjs","names":[],"sources":["../src/Gemini.ts"],"sourcesContent":["import { Context, Effect, Layer, Option, Redacted, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport {\n type CommonRequest,\n LanguageModel,\n type LanguageModelService,\n turnFromStream,\n} from \"@effect-uai/core/LanguageModel\"\nimport { JsonParseError } from \"@effect-uai/core/JSONL\"\nimport * as SSE from \"@effect-uai/core/SSE\"\nimport { type Turn, TurnEvent } from \"@effect-uai/core/Turn\"\nimport {\n type ChunkPart,\n type GenerationConfig,\n WireChunk,\n accumulatorToTurn,\n buildRequestBody,\n emptyAccumulator,\n hasUrlImageSource,\n ingestChunk,\n} from \"./codec.js\"\nimport { Match } from \"effect\"\nimport type { GoogleModel } from \"./models.js\"\n\n/**\n * Gemini's native event vocabulary. Aliased from `WireChunk` so the public\n * surface matches `@effect-uai/responses` and `@effect-uai/anthropic`. Each\n * `ProviderEvent` is a full `GenerateContentResponse` chunk (not a per-field\n * delta) - that's how Gemini's SSE works.\n */\nexport const ProviderEvent = WireChunk\nexport type ProviderEvent = WireChunk\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type GeminiRequest = Omit<CommonRequest, \"model\"> & {\n /**\n * Narrows `CommonRequest.model` (`string`) to the typed `GoogleModel`\n * literal union for autocomplete.\n */\n readonly model: GoogleModel\n /**\n * Gemini 2.5 thinking budget. `0` disables thinking entirely (lowest\n * latency); higher values let the model think longer. Forwarded as\n * `generationConfig.thinkingConfig.thinkingBudget`.\n */\n readonly thinkingBudget?: number\n}\n\nexport type GeminiService = {\n /**\n * Stream the provider's native event vocabulary (post-SSE-decode). For\n * Gemini, each event is a full `GenerateContentResponse` chunk. Use this\n * when you need access to fields the canonical view doesn't surface\n * (`thoughtSignature`, granular `safetyRatings`, etc.). For\n * provider-portable code, use `streamTurn`.\n */\n readonly streamNative: (request: GeminiRequest) => Stream.Stream<ProviderEvent, AiError.AiError>\n /**\n * Stream canonical `TurnEvent`s. Implemented as\n * `streamNative |> toCanonical`.\n */\n readonly streamTurn: (request: GeminiRequest) => Stream.Stream<TurnEvent, AiError.AiError>\n /**\n * Drain a single turn and return the assembled `Turn`. Derived from\n * `streamTurn` — Gemini's non-streaming `generateContent` could\n * back this directly, but routing through streaming keeps a single\n * accumulator path.\n */\n readonly turn: (request: GeminiRequest) => Effect.Effect<Turn, AiError.AiError>\n /**\n * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.\n * Threads a fresh `Accumulator` so chunk-level text/usage merging happens\n * per stream.\n */\n readonly toCanonical: <E, R>(\n s: Stream.Stream<ProviderEvent, E, R>,\n ) => Stream.Stream<TurnEvent, E, R>\n}\n\n/**\n * Provider-typed service tag. Yield this when you want Gemini-specific\n * options (`thinkingBudget`); yield the generic `LanguageModel` tag for\n * provider-portable code. Both are registered by `layer`.\n */\nexport class Gemini extends Context.Service<Gemini, GeminiService>()(\n \"@betalyra/effect-uai/providers/google/Gemini\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Request body\n// ---------------------------------------------------------------------------\n\nconst buildGenerationConfig = (request: GeminiRequest): Option.Option<GenerationConfig> => {\n const cfg: GenerationConfig = {\n ...(request.temperature !== undefined && { temperature: request.temperature }),\n ...(request.maxOutputTokens !== undefined && { maxOutputTokens: request.maxOutputTokens }),\n ...(request.topP !== undefined && { topP: request.topP }),\n ...(request.thinkingBudget !== undefined && {\n thinkingConfig: { thinkingBudget: request.thinkingBudget },\n }),\n ...(request.structured !== undefined && {\n responseMimeType: \"application/json\",\n responseJsonSchema: request.structured.schema[\"~standard\"].jsonSchema.input({\n target: \"draft-2020-12\",\n }),\n }),\n }\n return Object.keys(cfg).length === 0 ? Option.none() : Option.some(cfg)\n}\n\n// ---------------------------------------------------------------------------\n// SSE event → wire chunk\n// ---------------------------------------------------------------------------\n\nconst decodeChunk = Schema.decodeUnknownEffect(WireChunk)\n\n/**\n * Parse one SSE event's `data` payload into a `WireChunk`. Unlike the other\n * providers, Gemini's wire vocabulary is non-discriminated (a single\n * permissive struct, all fields optional) - there's no natural shape for\n * an `_unknown` variant, so JSON parse / schema decode failures flow\n * through as transport errors rather than being silently dropped.\n */\nconst parseJsonUnknown = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))\n\nconst sseEventToChunk = (ev: SSE.Event) =>\n parseJsonUnknown(ev.data).pipe(\n Effect.mapError((cause) => new JsonParseError({ line: ev.data, cause })),\n Effect.flatMap(decodeChunk),\n )\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"gemini\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status === 413) return new AiError.ContextLengthExceeded({ provider, raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\nconst buildNativeStream = (cfg: Config) => {\n const baseUrl = cfg.baseUrl ?? \"https://generativelanguage.googleapis.com/v1beta\"\n return (\n request: GeminiRequest,\n ): Stream.Stream<ProviderEvent, AiError.AiError, HttpClient.HttpClient> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n if (hasUrlImageSource(request.history)) {\n return yield* new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"imageInput\",\n reason:\n \"Gemini needs URL images pre-uploaded via the Files API; pass the image as base64 or raw bytes instead.\",\n })\n }\n const url = `${baseUrl}/models/${request.model}:streamGenerateContent?alt=sse`\n const generationConfig = buildGenerationConfig(request)\n const body = buildRequestBody(\n request.history,\n generationConfig,\n request.tools ?? [],\n request.toolChoice,\n )\n const httpRequest = HttpClientRequest.post(url).pipe(\n HttpClientRequest.setHeader(\"x-goog-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n HttpClientRequest.accept(\"text/event-stream\"),\n )\n const response = yield* client\n .execute(httpRequest)\n .pipe(\n Effect.mapError(\n (cause): AiError.AiError =>\n new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n )\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return Stream.fail(httpStatusError(response.status, text))\n }\n\n return response.stream.pipe(\n Stream.mapError(\n (cause): AiError.AiError => new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n SSE.fromBytes,\n Stream.mapEffect((ev) =>\n sseEventToChunk(ev).pipe(\n Effect.mapError(\n (cause): AiError.AiError =>\n new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n ),\n ),\n )\n }),\n )\n}\n\n/**\n * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.\n * Threads a fresh `Accumulator` so chunk-level text/usage merging works\n * across the run.\n */\n/**\n * Map one `ChunkPart` to the `TurnEvent`(s) it produces. Function calls\n * emit two events back-to-back since Gemini delivers args whole: a\n * `tool_call_start` immediately followed by a `tool_call_args_delta` with\n * the full JSON-encoded args. The `call_id` is the synthesized id from\n * the accumulator (Gemini-3 wire id when present, `<name>_<index>` otherwise).\n */\nconst partToTurnEvents = (\n part: ChunkPart,\n callIdAt: (name: string, indexFromTail: number) => string,\n): ReadonlyArray<TurnEvent> =>\n Match.value(part).pipe(\n Match.discriminatorsExhaustive(\"kind\")({\n text: (p): ReadonlyArray<TurnEvent> => [TurnEvent.TextDelta({ text: p.text })],\n reasoning: (p): ReadonlyArray<TurnEvent> => [\n TurnEvent.ReasoningDelta({ text: p.text, kind: \"trace\" }),\n ],\n function_call: (p): ReadonlyArray<TurnEvent> => {\n const call_id = callIdAt(p.name, 0)\n return [\n TurnEvent.ToolCallStart({ call_id, name: p.name }),\n TurnEvent.ToolCallArgsDelta({ call_id, delta: JSON.stringify(p.args ?? {}) }),\n ]\n },\n }),\n )\n\nexport const toCanonical = <E, R>(\n s: Stream.Stream<ProviderEvent, E, R>,\n): Stream.Stream<TurnEvent, E, R> =>\n s.pipe(\n Stream.mapAccum(\n () => emptyAccumulator,\n (acc, chunk) => {\n const result = ingestChunk(acc, chunk)\n // The accumulator already contains the synthesized call_ids for any\n // function_call parts in *this* chunk - they are the tail. Walk back\n // by chunk-local function-call index to recover them.\n const newCalls = result.parts.filter((p) => p.kind === \"function_call\")\n const tailCalls = result.accumulator.functionCalls.slice(\n result.accumulator.functionCalls.length - newCalls.length,\n )\n let consumedCalls = 0\n const partDeltas: ReadonlyArray<TurnEvent> = result.parts.flatMap((p) =>\n partToTurnEvents(p, () => {\n const id = tailCalls[consumedCalls]?.callId ?? p.kind\n consumedCalls += 1\n return id\n }),\n )\n const deltas: ReadonlyArray<TurnEvent> = result.finished\n ? [...partDeltas, TurnEvent.TurnComplete({ turn: accumulatorToTurn(result.accumulator) })]\n : partDeltas\n return [result.accumulator, deltas] as const\n },\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `GeminiService` value. For Layer-based setup, prefer `layer`.\n */\nexport const make = (cfg: Config): Effect.Effect<GeminiService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => {\n const streamNative: GeminiService[\"streamNative\"] = (request) =>\n buildNativeStream(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client))\n const streamTurn: GeminiService[\"streamTurn\"] = (request) => toCanonical(streamNative(request))\n return {\n streamNative,\n streamTurn,\n turn: turnFromStream(streamTurn),\n toCanonical,\n }\n })\n\n/**\n * Layer that registers both the provider-specific `Gemini` tag and the\n * generic `LanguageModel` tag, sharing one underlying implementation.\n *\n * The generic tag accepts `CommonRequest`; the typed tag accepts the full\n * `GeminiRequest` surface.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<Gemini | LanguageModel, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(Gemini, make(cfg))\n const generic = Layer.effect(\n LanguageModel,\n Effect.map(\n make(cfg),\n (s): LanguageModelService => ({\n streamTurn: (request) => s.streamTurn(request as GeminiRequest),\n turn: (request) => s.turn(request as GeminiRequest),\n }),\n ),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Gemini.mjs","names":[],"sources":["../src/Gemini.ts"],"sourcesContent":["import { Context, Effect, Layer, Option, Redacted, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport {\n type CommonRequest,\n LanguageModel,\n type LanguageModelService,\n turnFromStream,\n} from \"@effect-uai/core/LanguageModel\"\nimport { JsonParseError } from \"@effect-uai/core/JSONL\"\nimport * as SSE from \"@effect-uai/core/SSE\"\nimport { descriptorsOf } from \"@effect-uai/core/Tool\"\nimport { type Turn, TurnEvent } from \"@effect-uai/core/Turn\"\nimport {\n type ChunkPart,\n type GenerationConfig,\n WireChunk,\n accumulatorToTurn,\n buildRequestBody,\n emptyAccumulator,\n hasUrlImageSource,\n ingestChunk,\n} from \"./codec.js\"\nimport { Match } from \"effect\"\nimport type { GoogleModel } from \"./models.js\"\n\n/**\n * Gemini's native event vocabulary. Aliased from `WireChunk` so the public\n * surface matches `@effect-uai/responses` and `@effect-uai/anthropic`. Each\n * `ProviderEvent` is a full `GenerateContentResponse` chunk (not a per-field\n * delta) - that's how Gemini's SSE works.\n */\nexport const ProviderEvent = WireChunk\nexport type ProviderEvent = WireChunk\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type GeminiRequest = Omit<CommonRequest, \"model\"> & {\n /**\n * Narrows `CommonRequest.model` (`string`) to the typed `GoogleModel`\n * literal union for autocomplete.\n */\n readonly model: GoogleModel\n /**\n * Gemini 2.5 thinking budget. `0` disables thinking entirely (lowest\n * latency); higher values let the model think longer. Forwarded as\n * `generationConfig.thinkingConfig.thinkingBudget`.\n */\n readonly thinkingBudget?: number\n}\n\nexport type GeminiService = {\n /**\n * Stream the provider's native event vocabulary (post-SSE-decode). For\n * Gemini, each event is a full `GenerateContentResponse` chunk. Use this\n * when you need access to fields the canonical view doesn't surface\n * (`thoughtSignature`, granular `safetyRatings`, etc.). For\n * provider-portable code, use `streamTurn`.\n */\n readonly streamNative: (request: GeminiRequest) => Stream.Stream<ProviderEvent, AiError.AiError>\n /**\n * Stream canonical `TurnEvent`s. Implemented as\n * `streamNative |> toCanonical`.\n */\n readonly streamTurn: (request: GeminiRequest) => Stream.Stream<TurnEvent, AiError.AiError>\n /**\n * Drain a single turn and return the assembled `Turn`. Derived from\n * `streamTurn` — Gemini's non-streaming `generateContent` could\n * back this directly, but routing through streaming keeps a single\n * accumulator path.\n */\n readonly turn: (request: GeminiRequest) => Effect.Effect<Turn, AiError.AiError>\n /**\n * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.\n * Threads a fresh `Accumulator` so chunk-level text/usage merging happens\n * per stream.\n */\n readonly toCanonical: <E, R>(\n s: Stream.Stream<ProviderEvent, E, R>,\n ) => Stream.Stream<TurnEvent, E, R>\n}\n\n/**\n * Provider-typed service tag. Yield this when you want Gemini-specific\n * options (`thinkingBudget`); yield the generic `LanguageModel` tag for\n * provider-portable code. Both are registered by `layer`.\n */\nexport class Gemini extends Context.Service<Gemini, GeminiService>()(\n \"@betalyra/effect-uai/providers/google/Gemini\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Request body\n// ---------------------------------------------------------------------------\n\nconst buildGenerationConfig = (request: GeminiRequest): Option.Option<GenerationConfig> => {\n const cfg: GenerationConfig = {\n ...(request.temperature !== undefined && { temperature: request.temperature }),\n ...(request.maxOutputTokens !== undefined && { maxOutputTokens: request.maxOutputTokens }),\n ...(request.topP !== undefined && { topP: request.topP }),\n ...(request.thinkingBudget !== undefined && {\n thinkingConfig: { thinkingBudget: request.thinkingBudget },\n }),\n ...(request.structured !== undefined && {\n responseMimeType: \"application/json\",\n responseJsonSchema: request.structured.schema[\"~standard\"].jsonSchema.input({\n target: \"draft-2020-12\",\n }),\n }),\n }\n return Object.keys(cfg).length === 0 ? Option.none() : Option.some(cfg)\n}\n\n// ---------------------------------------------------------------------------\n// SSE event → wire chunk\n// ---------------------------------------------------------------------------\n\nconst decodeChunk = Schema.decodeUnknownEffect(WireChunk)\n\n/**\n * Parse one SSE event's `data` payload into a `WireChunk`. Unlike the other\n * providers, Gemini's wire vocabulary is non-discriminated (a single\n * permissive struct, all fields optional) - there's no natural shape for\n * an `_unknown` variant, so JSON parse / schema decode failures flow\n * through as transport errors rather than being silently dropped.\n */\nconst parseJsonUnknown = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))\n\nconst sseEventToChunk = (ev: SSE.Event) =>\n parseJsonUnknown(ev.data).pipe(\n Effect.mapError((cause) => new JsonParseError({ line: ev.data, cause })),\n Effect.flatMap(decodeChunk),\n )\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"gemini\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status === 413) return new AiError.ContextLengthExceeded({ provider, raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\nconst buildNativeStream = (cfg: Config) => {\n const baseUrl = cfg.baseUrl ?? \"https://generativelanguage.googleapis.com/v1beta\"\n return (\n request: GeminiRequest,\n ): Stream.Stream<ProviderEvent, AiError.AiError, HttpClient.HttpClient> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n if (hasUrlImageSource(request.history)) {\n return yield* new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"imageInput\",\n reason:\n \"Gemini needs URL images pre-uploaded via the Files API; pass the image as base64 or raw bytes instead.\",\n })\n }\n const url = `${baseUrl}/models/${request.model}:streamGenerateContent?alt=sse`\n const generationConfig = buildGenerationConfig(request)\n const body = buildRequestBody(\n request.history,\n generationConfig,\n descriptorsOf(request.tools),\n request.toolChoice,\n )\n const httpRequest = HttpClientRequest.post(url).pipe(\n HttpClientRequest.setHeader(\"x-goog-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n HttpClientRequest.accept(\"text/event-stream\"),\n )\n const response = yield* client\n .execute(httpRequest)\n .pipe(\n Effect.mapError(\n (cause): AiError.AiError =>\n new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n )\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return Stream.fail(httpStatusError(response.status, text))\n }\n\n return response.stream.pipe(\n Stream.mapError(\n (cause): AiError.AiError => new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n SSE.fromBytes,\n Stream.mapEffect((ev) =>\n sseEventToChunk(ev).pipe(\n Effect.mapError(\n (cause): AiError.AiError =>\n new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n ),\n ),\n )\n }),\n )\n}\n\n/**\n * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.\n * Threads a fresh `Accumulator` so chunk-level text/usage merging works\n * across the run.\n */\n/**\n * Map one `ChunkPart` to the `TurnEvent`(s) it produces. Function calls\n * emit two events back-to-back since Gemini delivers args whole: a\n * `tool_call_start` immediately followed by a `tool_call_args_delta` with\n * the full JSON-encoded args. The `call_id` is the synthesized id from\n * the accumulator (Gemini-3 wire id when present, `<name>_<index>` otherwise).\n */\nconst partToTurnEvents = (\n part: ChunkPart,\n callIdAt: (name: string, indexFromTail: number) => string,\n): ReadonlyArray<TurnEvent> =>\n Match.value(part).pipe(\n Match.discriminatorsExhaustive(\"kind\")({\n text: (p): ReadonlyArray<TurnEvent> => [TurnEvent.TextDelta({ text: p.text })],\n reasoning: (p): ReadonlyArray<TurnEvent> => [\n TurnEvent.ReasoningDelta({ text: p.text, kind: \"trace\" }),\n ],\n function_call: (p): ReadonlyArray<TurnEvent> => {\n const call_id = callIdAt(p.name, 0)\n return [\n TurnEvent.ToolCallStart({ call_id, name: p.name }),\n TurnEvent.ToolCallArgsDelta({ call_id, delta: JSON.stringify(p.args ?? {}) }),\n ]\n },\n }),\n )\n\nexport const toCanonical = <E, R>(\n s: Stream.Stream<ProviderEvent, E, R>,\n): Stream.Stream<TurnEvent, E, R> =>\n s.pipe(\n Stream.mapAccum(\n () => emptyAccumulator,\n (acc, chunk) => {\n const result = ingestChunk(acc, chunk)\n // The accumulator already contains the synthesized call_ids for any\n // function_call parts in *this* chunk - they are the tail. Walk back\n // by chunk-local function-call index to recover them.\n const newCalls = result.parts.filter((p) => p.kind === \"function_call\")\n const tailCalls = result.accumulator.functionCalls.slice(\n result.accumulator.functionCalls.length - newCalls.length,\n )\n let consumedCalls = 0\n const partDeltas: ReadonlyArray<TurnEvent> = result.parts.flatMap((p) =>\n partToTurnEvents(p, () => {\n const id = tailCalls[consumedCalls]?.callId ?? p.kind\n consumedCalls += 1\n return id\n }),\n )\n const deltas: ReadonlyArray<TurnEvent> = result.finished\n ? [...partDeltas, TurnEvent.TurnComplete({ turn: accumulatorToTurn(result.accumulator) })]\n : partDeltas\n return [result.accumulator, deltas] as const\n },\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `GeminiService` value. For Layer-based setup, prefer `layer`.\n */\nexport const make = (cfg: Config): Effect.Effect<GeminiService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => {\n const streamNative: GeminiService[\"streamNative\"] = (request) =>\n buildNativeStream(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client))\n const streamTurn: GeminiService[\"streamTurn\"] = (request) => toCanonical(streamNative(request))\n return {\n streamNative,\n streamTurn,\n turn: turnFromStream(streamTurn),\n toCanonical,\n }\n })\n\n/**\n * Layer that registers both the provider-specific `Gemini` tag and the\n * generic `LanguageModel` tag, sharing one underlying implementation.\n *\n * The generic tag accepts `CommonRequest`; the typed tag accepts the full\n * `GeminiRequest` surface.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<Gemini | LanguageModel, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(Gemini, make(cfg))\n const generic = Layer.effect(\n LanguageModel,\n Effect.map(\n make(cfg),\n (s): LanguageModelService => ({\n streamTurn: (request) => s.streamTurn(request as GeminiRequest),\n turn: (request) => s.turn(request as GeminiRequest),\n }),\n ),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { GeminiTtsModel, GeminiVoiceName } from "./models.mjs";
|
|
2
|
-
import { Context, Effect, Layer, Redacted, Stream } from "effect";
|
|
2
|
+
import { Context, Effect, Layer, Redacted, Result, Stream } from "effect";
|
|
3
3
|
import { HttpClient } from "effect/unstable/http";
|
|
4
4
|
import * as AiError from "@effect-uai/core/AiError";
|
|
5
|
-
import { CommonSynthesizeRequest, SpeechSynthesizerService } from "@effect-uai/core/SpeechSynthesizer";
|
|
5
|
+
import { CommonSynthesizeRequest, SpeechSynthesizer, SpeechSynthesizerService } from "@effect-uai/core/SpeechSynthesizer";
|
|
6
6
|
import { AudioBlob, AudioChunk } from "@effect-uai/core/Audio";
|
|
7
7
|
|
|
8
8
|
//#region src/GeminiSynthesizer.d.ts
|
|
@@ -26,7 +26,17 @@ type Config = {
|
|
|
26
26
|
readonly apiKey: Redacted.Redacted;
|
|
27
27
|
readonly baseUrl?: string;
|
|
28
28
|
};
|
|
29
|
-
declare const realizeOutput:
|
|
29
|
+
declare const realizeOutput: (u: "mp3" | "wav" | "ogg" | "opus" | "flac" | "aac" | "webm" | "raw") => Result.Result<readonly [{
|
|
30
|
+
readonly container: "raw";
|
|
31
|
+
readonly encoding: "pcm_s16le";
|
|
32
|
+
readonly sampleRate: 24000;
|
|
33
|
+
readonly channels: 1;
|
|
34
|
+
}, (b: Uint8Array) => Uint8Array<ArrayBufferLike>] | readonly [{
|
|
35
|
+
readonly container: "wav";
|
|
36
|
+
readonly encoding: "pcm_s16le";
|
|
37
|
+
readonly sampleRate: 24000;
|
|
38
|
+
readonly channels: 1;
|
|
39
|
+
}, (b: Uint8Array) => Uint8Array<ArrayBufferLike>], AiError.Unsupported>;
|
|
30
40
|
declare const make: (cfg: Config) => Effect.Effect<GeminiSynthesizerService, never, HttpClient.HttpClient>;
|
|
31
41
|
/**
|
|
32
42
|
* Layer registers both `GeminiSynthesizer` and the generic
|
|
@@ -34,7 +44,7 @@ declare const make: (cfg: Config) => Effect.Effect<GeminiSynthesizerService, nev
|
|
|
34
44
|
* TTS is sync-only at the wire level, so `streamSynthesisFrom` against
|
|
35
45
|
* this Layer alone is a compile-time error.
|
|
36
46
|
*/
|
|
37
|
-
declare const layer: (cfg: Config) => Layer.Layer<
|
|
47
|
+
declare const layer: (cfg: Config) => Layer.Layer<GeminiSynthesizer | SpeechSynthesizer, never, HttpClient.HttpClient>;
|
|
38
48
|
//#endregion
|
|
39
49
|
export { Config, GeminiSynthesizeRequest, GeminiSynthesizer, GeminiSynthesizerService, layer, make, realizeOutput, GeminiSynthesizer_d_exports as t };
|
|
40
50
|
//# sourceMappingURL=GeminiSynthesizer.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GeminiSynthesizer.d.mts","names":[],"sources":["../src/GeminiSynthesizer.ts"],"mappings":";;;;;;;;;;;KAsBY,uBAAA,GAA0B,IAAA,CAAK,uBAAA;EAAA,SAChC,KAAA,EAAO,cAAA;EAAA,SACP,OAAA,EAAS,eAAA;AAAA;AAAA,KAGR,wBAAA;EAAA,SACD,UAAA,GAAa,CAAA,EAAG,uBAAA,KAA4B,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,OAAA,CAAQ,OAAA;EAAA,SAC7E,eAAA,GACP,CAAA,EAAG,uBAAA,KACA,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,CAAQ,OAAA;EAAA,SAC9B,mBAAA,EAAqB,wBAAA;EAAA,SACrB,kBAAA,EAAoB,wBAAA;EAAA,SACpB,wBAAA,EAA0B,wBAAA;AAAA;AAAA,cACpC,sBAAA;cAEY,iBAAA,SAA0B,sBAAA;AAAA,KAK3B,MAAA;EAAA,SAAoB,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SAAmB,OAAA;AAAA;AAAA,cAUvD,aAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"GeminiSynthesizer.d.mts","names":[],"sources":["../src/GeminiSynthesizer.ts"],"mappings":";;;;;;;;;;;KAsBY,uBAAA,GAA0B,IAAA,CAAK,uBAAA;EAAA,SAChC,KAAA,EAAO,cAAA;EAAA,SACP,OAAA,EAAS,eAAA;AAAA;AAAA,KAGR,wBAAA;EAAA,SACD,UAAA,GAAa,CAAA,EAAG,uBAAA,KAA4B,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,OAAA,CAAQ,OAAA;EAAA,SAC7E,eAAA,GACP,CAAA,EAAG,uBAAA,KACA,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,CAAQ,OAAA;EAAA,SAC9B,mBAAA,EAAqB,wBAAA;EAAA,SACrB,kBAAA,EAAoB,wBAAA;EAAA,SACpB,wBAAA,EAA0B,wBAAA;AAAA;AAAA,cACpC,sBAAA;cAEY,iBAAA,SAA0B,sBAAA;AAAA,KAK3B,MAAA;EAAA,SAAoB,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SAAmB,OAAA;AAAA;AAAA,cAUvD,aAAA,GAAa,CAAA,uEAAA,MAAA,CAAA,MAAA;EAAA;;;;OAIhB,UAAA,KAAU,UAAA,CAAA,eAAA;EAAA;;;;OAMV,UAAA,KAAU,UAAA,CAAA,eAAA,IAAA,OAAA,CAAA,WAAA;AAAA,cA+JP,IAAA,GAAQ,GAAA,EAAK,MAAA,KAAM,MAAA,CAAA,MAAA,CAAA,wBAAA,SAAA,UAAA,CAAA,UAAA;;;;;;;cAuBnB,KAAA,GAAS,GAAA,EAAK,MAAA,KAAM,KAAA,CAAA,KAAA,CAAA,iBAAA,GAAA,iBAAA,SAAA,UAAA,CAAA,UAAA"}
|
|
@@ -45,11 +45,11 @@ const ttsBody = (r) => ({
|
|
|
45
45
|
});
|
|
46
46
|
const baseUrl = (cfg) => cfg.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
47
47
|
const synthesizeImpl = (cfg) => (request) => Effect.gen(function* () {
|
|
48
|
-
if (request.pronunciations !== void 0 && request.pronunciations.length > 0) return yield*
|
|
48
|
+
if (request.pronunciations !== void 0 && request.pronunciations.length > 0) return yield* new AiError.Unsupported({
|
|
49
49
|
provider: "gemini",
|
|
50
50
|
capability: "pronunciations",
|
|
51
51
|
reason: "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."
|
|
52
|
-
})
|
|
52
|
+
});
|
|
53
53
|
yield* Capabilities.warnDroppedWhen(request.speed, {
|
|
54
54
|
provider: "gemini",
|
|
55
55
|
capability: "speed",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GeminiSynthesizer.mjs","names":[],"sources":["../src/GeminiSynthesizer.ts"],"sourcesContent":["import { Context, Effect, Layer, Match, Redacted, Result, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioBlob, AudioChunk, AudioFormat } from \"@effect-uai/core/Audio\"\nimport * as Capabilities from \"@effect-uai/core/Capabilities\"\nimport {\n type CommonSynthesizeRequest,\n SpeechSynthesizer,\n type SpeechSynthesizerService,\n} from \"@effect-uai/core/SpeechSynthesizer\"\nimport {\n decodeBase64Audio,\n httpStatusError,\n transportFailure,\n wrapPcmAsWav,\n} from \"./geminiSpeechCodec.js\"\nimport type { GeminiTtsModel, GeminiVoiceName } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type GeminiSynthesizeRequest = Omit<CommonSynthesizeRequest, \"model\" | \"voiceId\"> & {\n readonly model: GeminiTtsModel\n readonly voiceId: GeminiVoiceName\n}\n\nexport type GeminiSynthesizerService = {\n readonly synthesize: (r: GeminiSynthesizeRequest) => Effect.Effect<AudioBlob, AiError.AiError>\n readonly streamSynthesis: (\n r: GeminiSynthesizeRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError>\n readonly streamSynthesisFrom: SpeechSynthesizerService[\"streamSynthesisFrom\"]\n readonly synthesizeDialogue: SpeechSynthesizerService[\"synthesizeDialogue\"]\n readonly streamSynthesizeDialogue: SpeechSynthesizerService[\"streamSynthesizeDialogue\"]\n}\n\nexport class GeminiSynthesizer extends Context.Service<\n GeminiSynthesizer,\n GeminiSynthesizerService\n>()(\"@betalyra/effect-uai/providers/google/GeminiSynthesizer\") {}\n\nexport type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }\n\n// ---------------------------------------------------------------------------\n// Codec — Gemini TTS always emits 24 kHz mono signed-16 PCM. Caller picks\n// raw bytes or WAV-wrapped.\n// ---------------------------------------------------------------------------\n\nconst PCM_RATE = 24000\nconst PCM_CHANNELS = 1\n\nexport const realizeOutput = Match.type<AudioFormat[\"container\"]>().pipe(\n Match.when(\"raw\", () =>\n Result.succeed([\n { container: \"raw\", encoding: \"pcm_s16le\", sampleRate: PCM_RATE, channels: PCM_CHANNELS },\n (b: Uint8Array) => b,\n ] as const),\n ),\n Match.when(\"wav\", () =>\n Result.succeed([\n { container: \"wav\", encoding: \"pcm_s16le\", sampleRate: PCM_RATE, channels: PCM_CHANNELS },\n (b: Uint8Array) => wrapPcmAsWav(b, PCM_RATE, PCM_CHANNELS),\n ] as const),\n ),\n Match.whenOr(\"mp3\", \"opus\", \"aac\", \"flac\", \"ogg\", \"webm\", (c) =>\n Result.fail(\n new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"outputFormat\",\n reason: `Gemini TTS emits raw 16-bit PCM at 24 kHz. Request \"raw\" or \"wav\"; ${c} is unavailable.`,\n }),\n ),\n ),\n Match.exhaustive,\n)\n\n// ---------------------------------------------------------------------------\n// Wire\n// ---------------------------------------------------------------------------\n\nconst Wire = Schema.Struct({\n candidates: Schema.optional(\n Schema.Array(\n Schema.Struct({\n content: Schema.optional(\n Schema.Struct({\n parts: Schema.optional(\n Schema.Array(\n Schema.Struct({\n inlineData: Schema.optional(\n Schema.Struct({\n mimeType: Schema.optional(Schema.String),\n data: Schema.String,\n }),\n ),\n }),\n ),\n ),\n }),\n ),\n }),\n ),\n ),\n})\nconst decodeWire = Schema.decodeUnknownEffect(Wire)\n\nconst findInline = (wire: typeof Wire.Type) =>\n (wire.candidates ?? [])\n .flatMap((c) => c.content?.parts ?? [])\n .find((p) => p.inlineData !== undefined)?.inlineData\n\nconst ttsBody = (r: GeminiSynthesizeRequest) => ({\n contents: [{ parts: [{ text: r.text }] }],\n generationConfig: {\n responseModalities: [\"AUDIO\"],\n speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: r.voiceId } } },\n },\n})\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config) => cfg.baseUrl ?? \"https://generativelanguage.googleapis.com/v1beta\"\n\nconst synthesizeImpl = (cfg: Config) => (request: GeminiSynthesizeRequest) =>\n Effect.gen(function* () {\n // Gemini `:generateContent` TTS has no phoneme field. Pronunciations\n // are load-bearing (bucket 1), so reject rather than mispronounce.\n if (request.pronunciations !== undefined && request.pronunciations.length > 0) {\n return yield* Effect.fail(\n new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"pronunciations\",\n reason:\n \"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.\",\n }),\n )\n }\n // No speaking-rate or language parameters on this endpoint; bucket 2.\n yield* Capabilities.warnDroppedWhen(request.speed, {\n provider: \"gemini\",\n capability: \"speed\",\n field: \"speed\",\n reason: \"Gemini `:generateContent` TTS has no speaking-rate parameter.\",\n })\n yield* Capabilities.warnDroppedWhen(request.languageCode, {\n provider: \"gemini\",\n capability: \"languageCode\",\n field: \"languageCode\",\n reason: \"Gemini `:generateContent` TTS has no language parameter.\",\n })\n const client = yield* HttpClient.HttpClient\n const [format, wrap] = yield* realizeOutput(request.outputFormat?.container ?? \"raw\")\n const httpRequest = HttpClientRequest.post(\n `${baseUrl(cfg)}/models/${request.model}:generateContent`,\n ).pipe(\n HttpClientRequest.setHeader(\"x-goog-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(ttsBody(request)),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const inline = yield* decodeWire(json).pipe(\n Effect.map(findInline),\n Effect.mapError((cause) => new AiError.GenerationFailed({ provider: \"gemini\", raw: cause })),\n Effect.flatMap((found) =>\n found === undefined\n ? Effect.fail(\n new AiError.GenerationFailed({\n provider: \"gemini\",\n raw: {\n message: \"Gemini TTS response had no inlineData audio part.\",\n hint: \"Likely a prompt-safety rejection or the model didn't honour responseModalities.\",\n response: json,\n },\n }),\n )\n : Effect.succeed(found),\n ),\n )\n const pcm = yield* decodeBase64Audio(inline.data)\n return { format, bytes: wrap(pcm) } satisfies AudioBlob\n })\n\nconst unsupportedStreamFrom: SpeechSynthesizerService[\"streamSynthesisFrom\"] = () =>\n Stream.fail(\n new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"streamSynthesisFrom\",\n reason:\n \"Gemini TTS via generateContent is sync-only. Use Cloud Text-to-Speech (Chirp 3 HD) for incremental text-in.\",\n }),\n )\n\nconst unsupportedDialogue: SpeechSynthesizerService[\"synthesizeDialogue\"] = () =>\n Effect.fail(\n new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"synthesizeDialogue\",\n reason:\n \"Gemini API (generativelanguage.googleapis.com) does not expose a multi-speaker endpoint. Use Cloud Text-to-Speech (`@effect-uai/google-speech`) with Gemini TTS for multi-speaker.\",\n }),\n )\n\nconst unsupportedStreamDialogue: SpeechSynthesizerService[\"streamSynthesizeDialogue\"] = () =>\n Stream.fail(\n new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"streamSynthesizeDialogue\",\n reason:\n \"Gemini API (generativelanguage.googleapis.com) does not expose a multi-speaker endpoint. Use Cloud Text-to-Speech (`@effect-uai/google-speech`) with Gemini TTS for multi-speaker.\",\n }),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (cfg: Config) =>\n Effect.map(\n HttpClient.HttpClient.asEffect(),\n (client): GeminiSynthesizerService => ({\n synthesize: (r) =>\n synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n streamSynthesis: (r) =>\n Stream.fromEffect(synthesizeImpl(cfg)(r)).pipe(\n Stream.map((b): AudioChunk => ({ bytes: b.bytes })),\n Stream.provideService(HttpClient.HttpClient, client),\n ),\n streamSynthesisFrom: unsupportedStreamFrom,\n synthesizeDialogue: unsupportedDialogue,\n streamSynthesizeDialogue: unsupportedStreamDialogue,\n }),\n )\n\n/**\n * Layer registers both `GeminiSynthesizer` and the generic\n * `SpeechSynthesizer`. Does NOT register `TtsIncrementalText` — Gemini\n * TTS is sync-only at the wire level, so `streamSynthesisFrom` against\n * this Layer alone is a compile-time error.\n */\nexport const layer = (cfg: Config) =>\n Layer.merge(\n Layer.effect(GeminiSynthesizer, make(cfg)),\n Layer.effect(\n SpeechSynthesizer,\n Effect.map(\n make(cfg),\n (s): SpeechSynthesizerService => ({\n synthesize: (req: CommonSynthesizeRequest) =>\n s.synthesize(req as GeminiSynthesizeRequest),\n streamSynthesis: (req: CommonSynthesizeRequest) =>\n s.streamSynthesis(req as GeminiSynthesizeRequest),\n streamSynthesisFrom: s.streamSynthesisFrom,\n synthesizeDialogue: s.synthesizeDialogue,\n streamSynthesizeDialogue: s.streamSynthesizeDialogue,\n }),\n ),\n ),\n )\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GeminiSynthesizer.mjs","names":[],"sources":["../src/GeminiSynthesizer.ts"],"sourcesContent":["import { Context, Effect, Layer, Match, Redacted, Result, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioBlob, AudioChunk, AudioFormat } from \"@effect-uai/core/Audio\"\nimport * as Capabilities from \"@effect-uai/core/Capabilities\"\nimport {\n type CommonSynthesizeRequest,\n SpeechSynthesizer,\n type SpeechSynthesizerService,\n} from \"@effect-uai/core/SpeechSynthesizer\"\nimport {\n decodeBase64Audio,\n httpStatusError,\n transportFailure,\n wrapPcmAsWav,\n} from \"./geminiSpeechCodec.js\"\nimport type { GeminiTtsModel, GeminiVoiceName } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type GeminiSynthesizeRequest = Omit<CommonSynthesizeRequest, \"model\" | \"voiceId\"> & {\n readonly model: GeminiTtsModel\n readonly voiceId: GeminiVoiceName\n}\n\nexport type GeminiSynthesizerService = {\n readonly synthesize: (r: GeminiSynthesizeRequest) => Effect.Effect<AudioBlob, AiError.AiError>\n readonly streamSynthesis: (\n r: GeminiSynthesizeRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError>\n readonly streamSynthesisFrom: SpeechSynthesizerService[\"streamSynthesisFrom\"]\n readonly synthesizeDialogue: SpeechSynthesizerService[\"synthesizeDialogue\"]\n readonly streamSynthesizeDialogue: SpeechSynthesizerService[\"streamSynthesizeDialogue\"]\n}\n\nexport class GeminiSynthesizer extends Context.Service<\n GeminiSynthesizer,\n GeminiSynthesizerService\n>()(\"@betalyra/effect-uai/providers/google/GeminiSynthesizer\") {}\n\nexport type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }\n\n// ---------------------------------------------------------------------------\n// Codec — Gemini TTS always emits 24 kHz mono signed-16 PCM. Caller picks\n// raw bytes or WAV-wrapped.\n// ---------------------------------------------------------------------------\n\nconst PCM_RATE = 24000\nconst PCM_CHANNELS = 1\n\nexport const realizeOutput = Match.type<AudioFormat[\"container\"]>().pipe(\n Match.when(\"raw\", () =>\n Result.succeed([\n { container: \"raw\", encoding: \"pcm_s16le\", sampleRate: PCM_RATE, channels: PCM_CHANNELS },\n (b: Uint8Array) => b,\n ] as const),\n ),\n Match.when(\"wav\", () =>\n Result.succeed([\n { container: \"wav\", encoding: \"pcm_s16le\", sampleRate: PCM_RATE, channels: PCM_CHANNELS },\n (b: Uint8Array) => wrapPcmAsWav(b, PCM_RATE, PCM_CHANNELS),\n ] as const),\n ),\n Match.whenOr(\"mp3\", \"opus\", \"aac\", \"flac\", \"ogg\", \"webm\", (c) =>\n Result.fail(\n new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"outputFormat\",\n reason: `Gemini TTS emits raw 16-bit PCM at 24 kHz. Request \"raw\" or \"wav\"; ${c} is unavailable.`,\n }),\n ),\n ),\n Match.exhaustive,\n)\n\n// ---------------------------------------------------------------------------\n// Wire\n// ---------------------------------------------------------------------------\n\nconst Wire = Schema.Struct({\n candidates: Schema.optional(\n Schema.Array(\n Schema.Struct({\n content: Schema.optional(\n Schema.Struct({\n parts: Schema.optional(\n Schema.Array(\n Schema.Struct({\n inlineData: Schema.optional(\n Schema.Struct({\n mimeType: Schema.optional(Schema.String),\n data: Schema.String,\n }),\n ),\n }),\n ),\n ),\n }),\n ),\n }),\n ),\n ),\n})\nconst decodeWire = Schema.decodeUnknownEffect(Wire)\n\nconst findInline = (wire: typeof Wire.Type) =>\n (wire.candidates ?? [])\n .flatMap((c) => c.content?.parts ?? [])\n .find((p) => p.inlineData !== undefined)?.inlineData\n\nconst ttsBody = (r: GeminiSynthesizeRequest) => ({\n contents: [{ parts: [{ text: r.text }] }],\n generationConfig: {\n responseModalities: [\"AUDIO\"],\n speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: r.voiceId } } },\n },\n})\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config) => cfg.baseUrl ?? \"https://generativelanguage.googleapis.com/v1beta\"\n\nconst synthesizeImpl = (cfg: Config) => (request: GeminiSynthesizeRequest) =>\n Effect.gen(function* () {\n // Gemini `:generateContent` TTS has no phoneme field. Pronunciations\n // are load-bearing (bucket 1), so reject rather than mispronounce.\n if (request.pronunciations !== undefined && request.pronunciations.length > 0) {\n return yield* new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"pronunciations\",\n reason:\n \"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.\",\n })\n }\n // No speaking-rate or language parameters on this endpoint; bucket 2.\n yield* Capabilities.warnDroppedWhen(request.speed, {\n provider: \"gemini\",\n capability: \"speed\",\n field: \"speed\",\n reason: \"Gemini `:generateContent` TTS has no speaking-rate parameter.\",\n })\n yield* Capabilities.warnDroppedWhen(request.languageCode, {\n provider: \"gemini\",\n capability: \"languageCode\",\n field: \"languageCode\",\n reason: \"Gemini `:generateContent` TTS has no language parameter.\",\n })\n const client = yield* HttpClient.HttpClient\n const [format, wrap] = yield* realizeOutput(request.outputFormat?.container ?? \"raw\")\n const httpRequest = HttpClientRequest.post(\n `${baseUrl(cfg)}/models/${request.model}:generateContent`,\n ).pipe(\n HttpClientRequest.setHeader(\"x-goog-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(ttsBody(request)),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const inline = yield* decodeWire(json).pipe(\n Effect.map(findInline),\n Effect.mapError((cause) => new AiError.GenerationFailed({ provider: \"gemini\", raw: cause })),\n Effect.flatMap((found) =>\n found === undefined\n ? Effect.fail(\n new AiError.GenerationFailed({\n provider: \"gemini\",\n raw: {\n message: \"Gemini TTS response had no inlineData audio part.\",\n hint: \"Likely a prompt-safety rejection or the model didn't honour responseModalities.\",\n response: json,\n },\n }),\n )\n : Effect.succeed(found),\n ),\n )\n const pcm = yield* decodeBase64Audio(inline.data)\n return { format, bytes: wrap(pcm) } satisfies AudioBlob\n })\n\nconst unsupportedStreamFrom: SpeechSynthesizerService[\"streamSynthesisFrom\"] = () =>\n Stream.fail(\n new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"streamSynthesisFrom\",\n reason:\n \"Gemini TTS via generateContent is sync-only. Use Cloud Text-to-Speech (Chirp 3 HD) for incremental text-in.\",\n }),\n )\n\nconst unsupportedDialogue: SpeechSynthesizerService[\"synthesizeDialogue\"] = () =>\n Effect.fail(\n new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"synthesizeDialogue\",\n reason:\n \"Gemini API (generativelanguage.googleapis.com) does not expose a multi-speaker endpoint. Use Cloud Text-to-Speech (`@effect-uai/google-speech`) with Gemini TTS for multi-speaker.\",\n }),\n )\n\nconst unsupportedStreamDialogue: SpeechSynthesizerService[\"streamSynthesizeDialogue\"] = () =>\n Stream.fail(\n new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"streamSynthesizeDialogue\",\n reason:\n \"Gemini API (generativelanguage.googleapis.com) does not expose a multi-speaker endpoint. Use Cloud Text-to-Speech (`@effect-uai/google-speech`) with Gemini TTS for multi-speaker.\",\n }),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (cfg: Config) =>\n Effect.map(\n HttpClient.HttpClient.asEffect(),\n (client): GeminiSynthesizerService => ({\n synthesize: (r) =>\n synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n streamSynthesis: (r) =>\n Stream.fromEffect(synthesizeImpl(cfg)(r)).pipe(\n Stream.map((b): AudioChunk => ({ bytes: b.bytes })),\n Stream.provideService(HttpClient.HttpClient, client),\n ),\n streamSynthesisFrom: unsupportedStreamFrom,\n synthesizeDialogue: unsupportedDialogue,\n streamSynthesizeDialogue: unsupportedStreamDialogue,\n }),\n )\n\n/**\n * Layer registers both `GeminiSynthesizer` and the generic\n * `SpeechSynthesizer`. Does NOT register `TtsIncrementalText` — Gemini\n * TTS is sync-only at the wire level, so `streamSynthesisFrom` against\n * this Layer alone is a compile-time error.\n */\nexport const layer = (cfg: Config) =>\n Layer.merge(\n Layer.effect(GeminiSynthesizer, make(cfg)),\n Layer.effect(\n SpeechSynthesizer,\n Effect.map(\n make(cfg),\n (s): SpeechSynthesizerService => ({\n synthesize: (req: CommonSynthesizeRequest) =>\n s.synthesize(req as GeminiSynthesizeRequest),\n streamSynthesis: (req: CommonSynthesizeRequest) =>\n s.streamSynthesis(req as GeminiSynthesizeRequest),\n streamSynthesisFrom: s.streamSynthesisFrom,\n synthesizeDialogue: s.synthesizeDialogue,\n streamSynthesizeDialogue: s.streamSynthesizeDialogue,\n }),\n ),\n ),\n )\n"],"mappings":"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"}
|
package/dist/codec.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Option, Schema } from "effect";
|
|
2
2
|
import { CommonRequest } from "@effect-uai/core/LanguageModel";
|
|
3
|
+
import { ToolDescriptor } from "@effect-uai/core/Tool";
|
|
3
4
|
import { Turn } from "@effect-uai/core/Turn";
|
|
4
5
|
import { HistoryItem } from "@effect-uai/core/Items";
|
|
5
|
-
import { ToolDescriptor } from "@effect-uai/core/Tool";
|
|
6
6
|
|
|
7
7
|
//#region src/codec.d.ts
|
|
8
8
|
declare namespace codec_d_exports {
|
|
@@ -26,6 +26,7 @@ declare const WireChunk: Schema.Struct<{
|
|
|
26
26
|
readonly name: Schema.String;
|
|
27
27
|
readonly args: Schema.optional<Schema.Unknown>;
|
|
28
28
|
}>;
|
|
29
|
+
readonly thoughtSignature: Schema.optional<Schema.String>;
|
|
29
30
|
}>]>>>;
|
|
30
31
|
}>>;
|
|
31
32
|
readonly finishReason: Schema.optional<Schema.String>;
|
|
@@ -49,9 +50,11 @@ type RequestPart = {
|
|
|
49
50
|
};
|
|
50
51
|
} | {
|
|
51
52
|
readonly functionCall: {
|
|
53
|
+
readonly id?: string;
|
|
52
54
|
readonly name: string;
|
|
53
55
|
readonly args: unknown;
|
|
54
56
|
};
|
|
57
|
+
readonly thoughtSignature?: string;
|
|
55
58
|
} | {
|
|
56
59
|
readonly functionResponse: {
|
|
57
60
|
readonly name: string;
|
|
@@ -110,7 +113,8 @@ declare const buildRequestBody: (history: ReadonlyArray<HistoryItem>, generation
|
|
|
110
113
|
type AccumulatedFunctionCall = {
|
|
111
114
|
/** Synthesized id-or-name we surface as `call_id` on the canonical item. */readonly callId: string;
|
|
112
115
|
readonly name: string; /** Wire id from Gemini 3, when present - echoed back on `functionResponse`. */
|
|
113
|
-
readonly providerId: Option.Option<string>; /**
|
|
116
|
+
readonly providerId: Option.Option<string>; /** Thought signature from Gemini 3, replayed on the next request's part. */
|
|
117
|
+
readonly signature: Option.Option<string>; /** Args as JSON-encoded string, mirroring `Items.ToolCall.arguments`. */
|
|
114
118
|
readonly arguments: string;
|
|
115
119
|
};
|
|
116
120
|
type Accumulator = {
|
|
@@ -145,6 +149,7 @@ type ChunkPart = {
|
|
|
145
149
|
} | {
|
|
146
150
|
readonly kind: "function_call";
|
|
147
151
|
readonly id: Option.Option<string>;
|
|
152
|
+
readonly signature: Option.Option<string>;
|
|
148
153
|
readonly name: string;
|
|
149
154
|
readonly args: unknown;
|
|
150
155
|
};
|
package/dist/codec.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codec.d.mts","names":[],"sources":["../src/codec.ts"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"codec.d.mts","names":[],"sources":["../src/codec.ts"],"mappings":";;;;;;;;;;cAkEa,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,SAElD,YAAA;IAAA,SACE,EAAA;IAAA,SACA,IAAA;IAAA,SACA,IAAA;EAAA;EAAA,SAEF,gBAAA;AAAA;EAAA,SAGA,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;;;;;;;cA8PX,iBAAA,GAAqB,OAAA,EAAS,aAAA,CAAc,WAAA;AAAA,cAO5C,gBAAA,GACX,OAAA,EAAS,aAAA,CAAc,WAAA,GACvB,gBAAA,EAAkB,MAAA,CAAO,MAAA,CAAO,gBAAA,GAChC,KAAA,GAAO,aAAA,CAAc,cAAA,GACrB,UAAA,GAAY,aAAA,mBACX,WAAA;AAAA,KAiCS,uBAAA;uFAED,MAAA;EAAA,SACA,IAAA;WAEA,UAAA,EAAY,MAAA,CAAO,MAAA;WAEnB,SAAA,EAAW,MAAA,CAAO,MAAA;WAElB,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,SAAA,EAAW,MAAA,CAAO,MAAA;EAAA,SAClB,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,cA+FE,WAAA,GAAe,GAAA,EAAK,WAAA,EAAa,KAAA,EAAO,SAAA,KAAY,WAAA;AAAA,cAgDpD,iBAAA,GAAqB,GAAA,EAAK,WAAA,KAAc,IAAA"}
|
package/dist/codec.mjs
CHANGED
|
@@ -24,11 +24,14 @@ const TextPart = Schema.Struct({
|
|
|
24
24
|
* the response also carries an `id` per call, which we echo back on the
|
|
25
25
|
* corresponding `functionResponse`.
|
|
26
26
|
*/
|
|
27
|
-
const FunctionCallPart = Schema.Struct({
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
const FunctionCallPart = Schema.Struct({
|
|
28
|
+
functionCall: Schema.Struct({
|
|
29
|
+
id: Schema.optional(Schema.String),
|
|
30
|
+
name: Schema.String,
|
|
31
|
+
args: Schema.optional(Schema.Unknown)
|
|
32
|
+
}),
|
|
33
|
+
thoughtSignature: Schema.optional(Schema.String)
|
|
34
|
+
});
|
|
32
35
|
const Part = Schema.Union([TextPart, FunctionCallPart]);
|
|
33
36
|
const Content = Schema.Struct({
|
|
34
37
|
role: Schema.optional(Schema.String),
|
|
@@ -111,27 +114,39 @@ const parsedResponse = parsedJsonObject((raw) => ({ output: raw }));
|
|
|
111
114
|
const isFunctionCallItem = (item) => item.type === "function_call";
|
|
112
115
|
const nameForCallId = (history, call_id) => pipe(history, Array$1.findFirst((item) => isFunctionCallItem(item) && item.call_id === call_id), Option.map((f) => f.name));
|
|
113
116
|
/**
|
|
114
|
-
* Extract the Gemini-3 wire id we stashed in
|
|
115
|
-
* Schema-driven so the shape lives in one
|
|
117
|
+
* Extract the Gemini-3 wire id and thought signature we stashed in
|
|
118
|
+
* `providerData` on the way out. Schema-driven so the shape lives in one
|
|
119
|
+
* place; failure or absence → `Option.none()`.
|
|
116
120
|
*/
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
const ProviderDataGemini = Schema.Struct({ gemini: Schema.Struct({
|
|
122
|
+
id: Schema.optional(Schema.String),
|
|
123
|
+
thoughtSignature: Schema.optional(Schema.String)
|
|
124
|
+
}) });
|
|
125
|
+
const decodeGemini = Schema.decodeUnknownResult(ProviderDataGemini);
|
|
126
|
+
const geminiField = (item, pick) => pipe(decodeGemini(item.providerData), Result.match({
|
|
127
|
+
onSuccess: (d) => Option.fromNullishOr(pick(d.gemini)),
|
|
121
128
|
onFailure: () => Option.none()
|
|
122
129
|
}));
|
|
130
|
+
const providerIdFor = (item) => geminiField(item, (g) => g.id);
|
|
131
|
+
const signatureFor = (item) => geminiField(item, (g) => g.thoughtSignature);
|
|
123
132
|
const itemToContent = (history) => (item) => Match.value(item).pipe(Match.discriminatorsExhaustive("type")({
|
|
124
133
|
message: messageToContent,
|
|
125
134
|
function_call: (f) => Result.succeed({
|
|
126
135
|
role: "model",
|
|
127
|
-
parts: [{
|
|
128
|
-
...Option.match(
|
|
129
|
-
onSome: (
|
|
136
|
+
parts: [{
|
|
137
|
+
...Option.match(signatureFor(f), {
|
|
138
|
+
onSome: (thoughtSignature) => ({ thoughtSignature }),
|
|
130
139
|
onNone: () => ({})
|
|
131
140
|
}),
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
functionCall: {
|
|
142
|
+
...Option.match(providerIdFor(f), {
|
|
143
|
+
onSome: (id) => ({ id }),
|
|
144
|
+
onNone: () => ({})
|
|
145
|
+
}),
|
|
146
|
+
name: f.name,
|
|
147
|
+
args: parsedArgs(f.arguments)
|
|
148
|
+
}
|
|
149
|
+
}]
|
|
135
150
|
}),
|
|
136
151
|
function_call_output: (o) => Result.succeed({
|
|
137
152
|
role: "user",
|
|
@@ -212,6 +227,7 @@ const textToChunkParts = (p) => p.text.length === 0 ? [] : [{
|
|
|
212
227
|
const functionCallToChunkParts = (p) => [{
|
|
213
228
|
kind: "function_call",
|
|
214
229
|
id: Option.fromNullishOr(p.functionCall.id),
|
|
230
|
+
signature: Option.fromNullishOr(p.thoughtSignature),
|
|
215
231
|
name: p.functionCall.name,
|
|
216
232
|
args: p.functionCall.args ?? {}
|
|
217
233
|
}];
|
|
@@ -240,6 +256,7 @@ const chunkCallToAccumulated = (prior, call) => ({
|
|
|
240
256
|
callId: synthesizeCallId(call, prior),
|
|
241
257
|
name: call.name,
|
|
242
258
|
providerId: call.id,
|
|
259
|
+
signature: call.signature,
|
|
243
260
|
arguments: JSON.stringify(call.args)
|
|
244
261
|
});
|
|
245
262
|
const appendFunctionCalls = (prior, fromChunk) => fromChunk.reduce((acc, call) => [...acc, chunkCallToAccumulated(acc, call)], prior);
|
|
@@ -270,13 +287,19 @@ const assistantMessageItems = (acc) => acc.text.length === 0 ? [] : [{
|
|
|
270
287
|
text: acc.text
|
|
271
288
|
}]
|
|
272
289
|
}];
|
|
273
|
-
const functionCallItems = (acc) => pipe(acc.functionCalls, Array$1.map((c) =>
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
290
|
+
const functionCallItems = (acc) => pipe(acc.functionCalls, Array$1.map((c) => {
|
|
291
|
+
const gemini = {
|
|
292
|
+
...Option.isSome(c.providerId) && { id: c.providerId.value },
|
|
293
|
+
...Option.isSome(c.signature) && { thoughtSignature: c.signature.value }
|
|
294
|
+
};
|
|
295
|
+
return {
|
|
296
|
+
type: "function_call",
|
|
297
|
+
call_id: c.callId,
|
|
298
|
+
name: c.name,
|
|
299
|
+
arguments: c.arguments,
|
|
300
|
+
...Object.keys(gemini).length > 0 && { providerData: { gemini } }
|
|
301
|
+
};
|
|
302
|
+
}));
|
|
280
303
|
const accumulatorToTurn = (acc) => ({
|
|
281
304
|
stop_reason: acc.functionCalls.length > 0 ? "tool_calls" : finishReasonToStop(acc.finishReason),
|
|
282
305
|
usage: { ...acc.usage },
|
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\"\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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"}
|
|
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 // Gemini 3 thinking models attach an opaque signature to each function-call\n // part that must be replayed on the next request, or the call is rejected.\n thoughtSignature: Schema.optional(Schema.String),\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 | {\n readonly functionCall: {\n readonly id?: string\n readonly name: string\n readonly args: unknown\n }\n readonly thoughtSignature?: string\n }\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 and thought signature we stashed in\n * `providerData` on the way out. Schema-driven so the shape lives in one\n * place; failure or absence → `Option.none()`.\n */\nconst ProviderDataGemini = Schema.Struct({\n gemini: Schema.Struct({\n id: Schema.optional(Schema.String),\n thoughtSignature: Schema.optional(Schema.String),\n }),\n})\nconst decodeGemini = Schema.decodeUnknownResult(ProviderDataGemini)\n\nconst geminiField = (\n item: ToolCall,\n pick: (g: {\n readonly id?: string | undefined\n readonly thoughtSignature?: string | undefined\n }) => string | undefined,\n): Option.Option<string> =>\n pipe(\n decodeGemini(item.providerData),\n Result.match({\n onSuccess: (d) => Option.fromNullishOr(pick(d.gemini)),\n onFailure: () => Option.none<string>(),\n }),\n )\n\nconst providerIdFor = (item: ToolCall): Option.Option<string> => geminiField(item, (g) => g.id)\nconst signatureFor = (item: ToolCall): Option.Option<string> =>\n geminiField(item, (g) => g.thoughtSignature)\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 ...Option.match(signatureFor(f), {\n onSome: (thoughtSignature) => ({ thoughtSignature }),\n onNone: () => ({}),\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 /** Thought signature from Gemini 3, replayed on the next request's part. */\n readonly signature: 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 signature: 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 signature: Option.fromNullishOr(p.thoughtSignature),\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 signature: call.signature,\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 const gemini = {\n ...(Option.isSome(c.providerId) && { id: c.providerId.value }),\n ...(Option.isSome(c.signature) && { thoughtSignature: c.signature.value }),\n }\n return {\n type: \"function_call\" as const,\n call_id: c.callId,\n name: c.name,\n arguments: c.arguments,\n ...(Object.keys(gemini).length > 0 && { providerData: { gemini } }),\n }\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.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Google Gemini provider for @effect-uai/core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"effect": "4.0.0-beta.57",
|
|
59
59
|
"typescript": "^6.0.3",
|
|
60
|
-
"@effect-uai/core": "0.
|
|
60
|
+
"@effect-uai/core": "0.10.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@effect-uai/core": ">=0.2.0 <1",
|
package/src/Gemini.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "@effect-uai/core/LanguageModel"
|
|
10
10
|
import { JsonParseError } from "@effect-uai/core/JSONL"
|
|
11
11
|
import * as SSE from "@effect-uai/core/SSE"
|
|
12
|
+
import { descriptorsOf } from "@effect-uai/core/Tool"
|
|
12
13
|
import { type Turn, TurnEvent } from "@effect-uai/core/Turn"
|
|
13
14
|
import {
|
|
14
15
|
type ChunkPart,
|
|
@@ -176,7 +177,7 @@ const buildNativeStream = (cfg: Config) => {
|
|
|
176
177
|
const body = buildRequestBody(
|
|
177
178
|
request.history,
|
|
178
179
|
generationConfig,
|
|
179
|
-
request.tools
|
|
180
|
+
descriptorsOf(request.tools),
|
|
180
181
|
request.toolChoice,
|
|
181
182
|
)
|
|
182
183
|
const httpRequest = HttpClientRequest.post(url).pipe(
|
package/src/GeminiSynthesizer.ts
CHANGED
|
@@ -129,14 +129,12 @@ const synthesizeImpl = (cfg: Config) => (request: GeminiSynthesizeRequest) =>
|
|
|
129
129
|
// Gemini `:generateContent` TTS has no phoneme field. Pronunciations
|
|
130
130
|
// are load-bearing (bucket 1), so reject rather than mispronounce.
|
|
131
131
|
if (request.pronunciations !== undefined && request.pronunciations.length > 0) {
|
|
132
|
-
return yield*
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}),
|
|
139
|
-
)
|
|
132
|
+
return yield* new AiError.Unsupported({
|
|
133
|
+
provider: "gemini",
|
|
134
|
+
capability: "pronunciations",
|
|
135
|
+
reason:
|
|
136
|
+
"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.",
|
|
137
|
+
})
|
|
140
138
|
}
|
|
141
139
|
// No speaking-rate or language parameters on this endpoint; bucket 2.
|
|
142
140
|
yield* Capabilities.warnDroppedWhen(request.speed, {
|
package/src/codec.ts
CHANGED
|
@@ -37,6 +37,9 @@ const FunctionCallPart = Schema.Struct({
|
|
|
37
37
|
name: Schema.String,
|
|
38
38
|
args: Schema.optional(Schema.Unknown),
|
|
39
39
|
}),
|
|
40
|
+
// Gemini 3 thinking models attach an opaque signature to each function-call
|
|
41
|
+
// part that must be replayed on the next request, or the call is rejected.
|
|
42
|
+
thoughtSignature: Schema.optional(Schema.String),
|
|
40
43
|
})
|
|
41
44
|
|
|
42
45
|
const Part = Schema.Union([TextPart, FunctionCallPart])
|
|
@@ -74,7 +77,14 @@ export type WireChunk = typeof WireChunk.Type
|
|
|
74
77
|
type RequestPart =
|
|
75
78
|
| { readonly text: string }
|
|
76
79
|
| { readonly inlineData: { readonly mimeType: string; readonly data: string } }
|
|
77
|
-
| {
|
|
80
|
+
| {
|
|
81
|
+
readonly functionCall: {
|
|
82
|
+
readonly id?: string
|
|
83
|
+
readonly name: string
|
|
84
|
+
readonly args: unknown
|
|
85
|
+
}
|
|
86
|
+
readonly thoughtSignature?: string
|
|
87
|
+
}
|
|
78
88
|
| {
|
|
79
89
|
readonly functionResponse: {
|
|
80
90
|
readonly name: string
|
|
@@ -244,23 +254,37 @@ const nameForCallId = (
|
|
|
244
254
|
)
|
|
245
255
|
|
|
246
256
|
/**
|
|
247
|
-
* Extract the Gemini-3 wire id we stashed in
|
|
248
|
-
* Schema-driven so the shape lives in one
|
|
257
|
+
* Extract the Gemini-3 wire id and thought signature we stashed in
|
|
258
|
+
* `providerData` on the way out. Schema-driven so the shape lives in one
|
|
259
|
+
* place; failure or absence → `Option.none()`.
|
|
249
260
|
*/
|
|
250
|
-
const
|
|
251
|
-
gemini: Schema.Struct({
|
|
261
|
+
const ProviderDataGemini = Schema.Struct({
|
|
262
|
+
gemini: Schema.Struct({
|
|
263
|
+
id: Schema.optional(Schema.String),
|
|
264
|
+
thoughtSignature: Schema.optional(Schema.String),
|
|
265
|
+
}),
|
|
252
266
|
})
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
const
|
|
267
|
+
const decodeGemini = Schema.decodeUnknownResult(ProviderDataGemini)
|
|
268
|
+
|
|
269
|
+
const geminiField = (
|
|
270
|
+
item: ToolCall,
|
|
271
|
+
pick: (g: {
|
|
272
|
+
readonly id?: string | undefined
|
|
273
|
+
readonly thoughtSignature?: string | undefined
|
|
274
|
+
}) => string | undefined,
|
|
275
|
+
): Option.Option<string> =>
|
|
256
276
|
pipe(
|
|
257
|
-
|
|
277
|
+
decodeGemini(item.providerData),
|
|
258
278
|
Result.match({
|
|
259
|
-
onSuccess: (d) => Option.
|
|
279
|
+
onSuccess: (d) => Option.fromNullishOr(pick(d.gemini)),
|
|
260
280
|
onFailure: () => Option.none<string>(),
|
|
261
281
|
}),
|
|
262
282
|
)
|
|
263
283
|
|
|
284
|
+
const providerIdFor = (item: ToolCall): Option.Option<string> => geminiField(item, (g) => g.id)
|
|
285
|
+
const signatureFor = (item: ToolCall): Option.Option<string> =>
|
|
286
|
+
geminiField(item, (g) => g.thoughtSignature)
|
|
287
|
+
|
|
264
288
|
const itemToContent =
|
|
265
289
|
(history: ReadonlyArray<HistoryItem>) =>
|
|
266
290
|
(item: HistoryItem): Result.Result<RequestContent, void> =>
|
|
@@ -272,6 +296,10 @@ const itemToContent =
|
|
|
272
296
|
role: "model" as const,
|
|
273
297
|
parts: [
|
|
274
298
|
{
|
|
299
|
+
...Option.match(signatureFor(f), {
|
|
300
|
+
onSome: (thoughtSignature) => ({ thoughtSignature }),
|
|
301
|
+
onNone: () => ({}),
|
|
302
|
+
}),
|
|
275
303
|
functionCall: {
|
|
276
304
|
...Option.match(providerIdFor(f), {
|
|
277
305
|
onSome: (id) => ({ id }),
|
|
@@ -414,6 +442,8 @@ export type AccumulatedFunctionCall = {
|
|
|
414
442
|
readonly name: string
|
|
415
443
|
/** Wire id from Gemini 3, when present - echoed back on `functionResponse`. */
|
|
416
444
|
readonly providerId: Option.Option<string>
|
|
445
|
+
/** Thought signature from Gemini 3, replayed on the next request's part. */
|
|
446
|
+
readonly signature: Option.Option<string>
|
|
417
447
|
/** Args as JSON-encoded string, mirroring `Items.ToolCall.arguments`. */
|
|
418
448
|
readonly arguments: string
|
|
419
449
|
}
|
|
@@ -451,6 +481,7 @@ export type ChunkPart =
|
|
|
451
481
|
| {
|
|
452
482
|
readonly kind: "function_call"
|
|
453
483
|
readonly id: Option.Option<string>
|
|
484
|
+
readonly signature: Option.Option<string>
|
|
454
485
|
readonly name: string
|
|
455
486
|
readonly args: unknown
|
|
456
487
|
}
|
|
@@ -470,6 +501,7 @@ const functionCallToChunkParts = (p: WireFunctionCallPart): ReadonlyArray<ChunkP
|
|
|
470
501
|
{
|
|
471
502
|
kind: "function_call",
|
|
472
503
|
id: Option.fromNullishOr(p.functionCall.id),
|
|
504
|
+
signature: Option.fromNullishOr(p.thoughtSignature),
|
|
473
505
|
name: p.functionCall.name,
|
|
474
506
|
args: p.functionCall.args ?? {},
|
|
475
507
|
},
|
|
@@ -537,6 +569,7 @@ const chunkCallToAccumulated = (
|
|
|
537
569
|
callId: synthesizeCallId(call, prior),
|
|
538
570
|
name: call.name,
|
|
539
571
|
providerId: call.id,
|
|
572
|
+
signature: call.signature,
|
|
540
573
|
// `functionCallToChunkParts` already replaced null/undefined with `{}`,
|
|
541
574
|
// so `call.args` is always a JSON-encodable value here.
|
|
542
575
|
arguments: JSON.stringify(call.args),
|
|
@@ -584,13 +617,19 @@ const assistantMessageItems = (acc: Accumulator): ReadonlyArray<HistoryItem> =>
|
|
|
584
617
|
const functionCallItems = (acc: Accumulator): ReadonlyArray<HistoryItem> =>
|
|
585
618
|
pipe(
|
|
586
619
|
acc.functionCalls,
|
|
587
|
-
Arr.map((c) =>
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
620
|
+
Arr.map((c) => {
|
|
621
|
+
const gemini = {
|
|
622
|
+
...(Option.isSome(c.providerId) && { id: c.providerId.value }),
|
|
623
|
+
...(Option.isSome(c.signature) && { thoughtSignature: c.signature.value }),
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
type: "function_call" as const,
|
|
627
|
+
call_id: c.callId,
|
|
628
|
+
name: c.name,
|
|
629
|
+
arguments: c.arguments,
|
|
630
|
+
...(Object.keys(gemini).length > 0 && { providerData: { gemini } }),
|
|
631
|
+
}
|
|
632
|
+
}),
|
|
594
633
|
)
|
|
595
634
|
|
|
596
635
|
export const accumulatorToTurn = (acc: Accumulator): Turn => ({
|