@effect-uai/google 0.5.0 → 0.5.2
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 +8 -1
- package/dist/Gemini.d.mts.map +1 -1
- package/dist/Gemini.mjs +8 -3
- package/dist/Gemini.mjs.map +1 -1
- package/package.json +2 -2
- package/src/Gemini.ts +13 -2
package/dist/Gemini.d.mts
CHANGED
|
@@ -4,7 +4,7 @@ import { Context, Effect, Layer, Redacted, Schema, Stream } from "effect";
|
|
|
4
4
|
import { HttpClient } from "effect/unstable/http";
|
|
5
5
|
import * as AiError from "@effect-uai/core/AiError";
|
|
6
6
|
import { CommonRequest, LanguageModel } from "@effect-uai/core/LanguageModel";
|
|
7
|
-
import { TurnEvent } from "@effect-uai/core/Turn";
|
|
7
|
+
import { Turn, TurnEvent } from "@effect-uai/core/Turn";
|
|
8
8
|
|
|
9
9
|
//#region src/Gemini.d.ts
|
|
10
10
|
declare namespace Gemini_d_exports {
|
|
@@ -70,6 +70,13 @@ type GeminiService = {
|
|
|
70
70
|
* `streamNative |> toCanonical`.
|
|
71
71
|
*/
|
|
72
72
|
readonly streamTurn: (request: GeminiRequest) => Stream.Stream<TurnEvent, AiError.AiError>;
|
|
73
|
+
/**
|
|
74
|
+
* Drain a single turn and return the assembled `Turn`. Derived from
|
|
75
|
+
* `streamTurn` — Gemini's non-streaming `generateContent` could
|
|
76
|
+
* back this directly, but routing through streaming keeps a single
|
|
77
|
+
* accumulator path.
|
|
78
|
+
*/
|
|
79
|
+
readonly turn: (request: GeminiRequest) => Effect.Effect<Turn, AiError.AiError>;
|
|
73
80
|
/**
|
|
74
81
|
* Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.
|
|
75
82
|
* Threads a fresh `Accumulator` so chunk-level text/usage merging happens
|
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":";;;;;;;;;;;;;;;;;;cA8Ba,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,cA8IE,WAAA,SACX,CAAA,EAAG,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,CAAA,EAAG,CAAA,MAClC,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,CAAA,EAAG,CAAA;;;;cAoClB,IAAA,GAAQ,GAAA,EAAK,MAAA,KAAS,MAAA,CAAO,MAAA,CAAO,aAAA,SAAsB,UAAA,CAAW,UAAA;;;;;;;;cAoBrE,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,MAAA,GAAS,aAAA,SAAsB,UAAA,CAAW,UAAA"}
|
package/dist/Gemini.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { WireChunk, accumulatorToTurn, buildRequestBody, emptyAccumulator, inges
|
|
|
3
3
|
import { Context, Effect, Layer, Match, Option, Redacted, Schema, Stream } from "effect";
|
|
4
4
|
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
5
5
|
import * as AiError from "@effect-uai/core/AiError";
|
|
6
|
-
import { LanguageModel } from "@effect-uai/core/LanguageModel";
|
|
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
9
|
import { TurnEvent } from "@effect-uai/core/Turn";
|
|
@@ -166,9 +166,11 @@ const toCanonical = (s) => s.pipe(Stream.mapAccum(() => emptyAccumulator, (acc,
|
|
|
166
166
|
*/
|
|
167
167
|
const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => {
|
|
168
168
|
const streamNative = (request) => buildNativeStream(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client));
|
|
169
|
+
const streamTurn = (request) => toCanonical(streamNative(request));
|
|
169
170
|
return {
|
|
170
171
|
streamNative,
|
|
171
|
-
streamTurn
|
|
172
|
+
streamTurn,
|
|
173
|
+
turn: turnFromStream(streamTurn),
|
|
172
174
|
toCanonical
|
|
173
175
|
};
|
|
174
176
|
});
|
|
@@ -181,7 +183,10 @@ const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => {
|
|
|
181
183
|
*/
|
|
182
184
|
const layer = (cfg) => {
|
|
183
185
|
const typed = Layer.effect(Gemini, make(cfg));
|
|
184
|
-
const generic = Layer.effect(LanguageModel, Effect.map(make(cfg), (s) => ({
|
|
186
|
+
const generic = Layer.effect(LanguageModel, Effect.map(make(cfg), (s) => ({
|
|
187
|
+
streamTurn: (request) => s.streamTurn(request),
|
|
188
|
+
turn: (request) => s.turn(request)
|
|
189
|
+
})));
|
|
185
190
|
return Layer.merge(typed, generic);
|
|
186
191
|
};
|
|
187
192
|
//#endregion
|
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} from \"@effect-uai/core/LanguageModel\"\nimport { JsonParseError } from \"@effect-uai/core/JSONL\"\nimport * as SSE from \"@effect-uai/core/SSE\"\nimport { TurnEvent } from \"@effect-uai/core/Turn\"\nimport {\n type ChunkPart,\n type GenerationConfig,\n WireChunk,\n accumulatorToTurn,\n buildRequestBody,\n emptyAccumulator,\n ingestChunk,\n} from \"./codec.js\"\nimport { Match } from \"effect\"\nimport type { GoogleModel } from \"./models.js\"\n\n/**\n * Gemini's native event vocabulary. Aliased from `WireChunk` so the public\n * surface matches `@effect-uai/responses` and `@effect-uai/anthropic`. Each\n * `ProviderEvent` is a full `GenerateContentResponse` chunk (not a per-field\n * delta) - that's how Gemini's SSE works.\n */\nexport const ProviderEvent = WireChunk\nexport type ProviderEvent = WireChunk\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type GeminiRequest = Omit<CommonRequest, \"model\"> & {\n /**\n * Narrows `CommonRequest.model` (`string`) to the typed `GoogleModel`\n * literal union for autocomplete.\n */\n readonly model: GoogleModel\n /**\n * Gemini 2.5 thinking budget. `0` disables thinking entirely (lowest\n * latency); higher values let the model think longer. Forwarded as\n * `generationConfig.thinkingConfig.thinkingBudget`.\n */\n readonly thinkingBudget?: number\n}\n\nexport type GeminiService = {\n /**\n * Stream the provider's native event vocabulary (post-SSE-decode). For\n * Gemini, each event is a full `GenerateContentResponse` chunk. Use this\n * when you need access to fields the canonical view doesn't surface\n * (`thoughtSignature`, granular `safetyRatings`, etc.). For\n * provider-portable code, use `streamTurn`.\n */\n readonly streamNative: (request: GeminiRequest) => Stream.Stream<ProviderEvent, AiError.AiError>\n /**\n * Stream canonical `TurnEvent`s. Implemented as\n * `streamNative |> toCanonical`.\n */\n readonly streamTurn: (request: GeminiRequest) => Stream.Stream<TurnEvent, AiError.AiError>\n /**\n * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.\n * Threads a fresh `Accumulator` so chunk-level text/usage merging happens\n * per stream.\n */\n readonly toCanonical: <E, R>(\n s: Stream.Stream<ProviderEvent, E, R>,\n ) => Stream.Stream<TurnEvent, E, R>\n}\n\n/**\n * Provider-typed service tag. Yield this when you want Gemini-specific\n * options (`thinkingBudget`); yield the generic `LanguageModel` tag for\n * provider-portable code. Both are registered by `layer`.\n */\nexport class Gemini extends Context.Service<Gemini, GeminiService>()(\n \"@betalyra/effect-uai/providers/google/Gemini\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Request body\n// ---------------------------------------------------------------------------\n\nconst buildGenerationConfig = (request: GeminiRequest): Option.Option<GenerationConfig> => {\n const cfg: GenerationConfig = {\n ...(request.temperature !== undefined && { temperature: request.temperature }),\n ...(request.maxOutputTokens !== undefined && { maxOutputTokens: request.maxOutputTokens }),\n ...(request.topP !== undefined && { topP: request.topP }),\n ...(request.thinkingBudget !== undefined && {\n thinkingConfig: { thinkingBudget: request.thinkingBudget },\n }),\n ...(request.structured !== undefined && {\n responseMimeType: \"application/json\",\n responseJsonSchema: request.structured.schema[\"~standard\"].jsonSchema.input({\n target: \"draft-2020-12\",\n }),\n }),\n }\n return Object.keys(cfg).length === 0 ? Option.none() : Option.some(cfg)\n}\n\n// ---------------------------------------------------------------------------\n// SSE event → wire chunk\n// ---------------------------------------------------------------------------\n\nconst decodeChunk = Schema.decodeUnknownEffect(WireChunk)\n\n/**\n * Parse one SSE event's `data` payload into a `WireChunk`. Unlike the other\n * providers, Gemini's wire vocabulary is non-discriminated (a single\n * permissive struct, all fields optional) - there's no natural shape for\n * an `_unknown` variant, so JSON parse / schema decode failures flow\n * through as transport errors rather than being silently dropped.\n */\nconst parseJsonUnknown = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))\n\nconst sseEventToChunk = (ev: SSE.Event) =>\n parseJsonUnknown(ev.data).pipe(\n Effect.mapError((cause) => new JsonParseError({ line: ev.data, cause })),\n Effect.flatMap(decodeChunk),\n )\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"gemini\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status === 413) return new AiError.ContextLengthExceeded({ provider, raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\nconst buildNativeStream = (cfg: Config) => {\n const baseUrl = cfg.baseUrl ?? \"https://generativelanguage.googleapis.com/v1beta\"\n return (\n request: GeminiRequest,\n ): Stream.Stream<ProviderEvent, AiError.AiError, HttpClient.HttpClient> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const url = `${baseUrl}/models/${request.model}:streamGenerateContent?alt=sse`\n const generationConfig = buildGenerationConfig(request)\n const body = buildRequestBody(request.history, generationConfig, request.tools ?? [])\n const httpRequest = HttpClientRequest.post(url).pipe(\n HttpClientRequest.setHeader(\"x-goog-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n HttpClientRequest.accept(\"text/event-stream\"),\n )\n const response = yield* client\n .execute(httpRequest)\n .pipe(\n Effect.mapError(\n (cause): AiError.AiError =>\n new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n )\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return Stream.fail(httpStatusError(response.status, text))\n }\n\n return response.stream.pipe(\n Stream.mapError(\n (cause): AiError.AiError => new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n SSE.fromBytes,\n Stream.mapEffect((ev) =>\n sseEventToChunk(ev).pipe(\n Effect.mapError(\n (cause): AiError.AiError =>\n new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n ),\n ),\n )\n }),\n )\n}\n\n/**\n * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.\n * Threads a fresh `Accumulator` so chunk-level text/usage merging works\n * across the run.\n */\n/**\n * Map one `ChunkPart` to the `TurnEvent`(s) it produces. Function calls\n * emit two events back-to-back since Gemini delivers args whole: a\n * `tool_call_start` immediately followed by a `tool_call_args_delta` with\n * the full JSON-encoded args. The `call_id` is the synthesized id from\n * the accumulator (Gemini-3 wire id when present, `<name>_<index>` otherwise).\n */\nconst partToTurnEvents = (\n part: ChunkPart,\n callIdAt: (name: string, indexFromTail: number) => string,\n): ReadonlyArray<TurnEvent> =>\n Match.value(part).pipe(\n Match.discriminatorsExhaustive(\"kind\")({\n text: (p): ReadonlyArray<TurnEvent> => [TurnEvent.TextDelta({ text: p.text })],\n reasoning: (p): ReadonlyArray<TurnEvent> => [\n TurnEvent.ReasoningDelta({ text: p.text, kind: \"trace\" }),\n ],\n function_call: (p): ReadonlyArray<TurnEvent> => {\n const call_id = callIdAt(p.name, 0)\n return [\n TurnEvent.ToolCallStart({ call_id, name: p.name }),\n TurnEvent.ToolCallArgsDelta({ call_id, delta: JSON.stringify(p.args ?? {}) }),\n ]\n },\n }),\n )\n\nexport const toCanonical = <E, R>(\n s: Stream.Stream<ProviderEvent, E, R>,\n): Stream.Stream<TurnEvent, E, R> =>\n s.pipe(\n Stream.mapAccum(\n () => emptyAccumulator,\n (acc, chunk) => {\n const result = ingestChunk(acc, chunk)\n // The accumulator already contains the synthesized call_ids for any\n // function_call parts in *this* chunk - they are the tail. Walk back\n // by chunk-local function-call index to recover them.\n const newCalls = result.parts.filter((p) => p.kind === \"function_call\")\n const tailCalls = result.accumulator.functionCalls.slice(\n result.accumulator.functionCalls.length - newCalls.length,\n )\n let consumedCalls = 0\n const partDeltas: ReadonlyArray<TurnEvent> = result.parts.flatMap((p) =>\n partToTurnEvents(p, () => {\n const id = tailCalls[consumedCalls]?.callId ?? p.kind\n consumedCalls += 1\n return id\n }),\n )\n const deltas: ReadonlyArray<TurnEvent> = result.finished\n ? [...partDeltas, TurnEvent.TurnComplete({ turn: accumulatorToTurn(result.accumulator) })]\n : partDeltas\n return [result.accumulator, deltas] as const\n },\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `GeminiService` value. For Layer-based setup, prefer `layer`.\n */\nexport const make = (cfg: Config): Effect.Effect<GeminiService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => {\n const streamNative: GeminiService[\"streamNative\"] = (request) =>\n buildNativeStream(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client))\n return {\n streamNative,\n streamTurn: (request) => toCanonical(streamNative(request)),\n toCanonical,\n }\n })\n\n/**\n * Layer that registers both the provider-specific `Gemini` tag and the\n * generic `LanguageModel` tag, sharing one underlying implementation.\n *\n * The generic tag accepts `CommonRequest`; the typed tag accepts the full\n * `GeminiRequest` surface.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<Gemini | LanguageModel, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(Gemini, make(cfg))\n const generic = Layer.effect(\n LanguageModel,\n Effect.map(\n make(cfg),\n (s): LanguageModelService => ({\n streamTurn: (request) => s.streamTurn(request as GeminiRequest),\n }),\n ),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"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 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 const url = `${baseUrl}/models/${request.model}:streamGenerateContent?alt=sse`\n const generationConfig = buildGenerationConfig(request)\n const body = buildRequestBody(request.history, generationConfig, request.tools ?? [])\n const httpRequest = HttpClientRequest.post(url).pipe(\n HttpClientRequest.setHeader(\"x-goog-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n HttpClientRequest.accept(\"text/event-stream\"),\n )\n const response = yield* client\n .execute(httpRequest)\n .pipe(\n Effect.mapError(\n (cause): AiError.AiError =>\n new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n )\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return Stream.fail(httpStatusError(response.status, text))\n }\n\n return response.stream.pipe(\n Stream.mapError(\n (cause): AiError.AiError => new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n SSE.fromBytes,\n Stream.mapEffect((ev) =>\n sseEventToChunk(ev).pipe(\n Effect.mapError(\n (cause): AiError.AiError =>\n new AiError.Unavailable({ provider: \"gemini\", raw: cause }),\n ),\n ),\n ),\n )\n }),\n )\n}\n\n/**\n * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.\n * Threads a fresh `Accumulator` so chunk-level text/usage merging works\n * across the run.\n */\n/**\n * Map one `ChunkPart` to the `TurnEvent`(s) it produces. Function calls\n * emit two events back-to-back since Gemini delivers args whole: a\n * `tool_call_start` immediately followed by a `tool_call_args_delta` with\n * the full JSON-encoded args. The `call_id` is the synthesized id from\n * the accumulator (Gemini-3 wire id when present, `<name>_<index>` otherwise).\n */\nconst partToTurnEvents = (\n part: ChunkPart,\n callIdAt: (name: string, indexFromTail: number) => string,\n): ReadonlyArray<TurnEvent> =>\n Match.value(part).pipe(\n Match.discriminatorsExhaustive(\"kind\")({\n text: (p): ReadonlyArray<TurnEvent> => [TurnEvent.TextDelta({ text: p.text })],\n reasoning: (p): ReadonlyArray<TurnEvent> => [\n TurnEvent.ReasoningDelta({ text: p.text, kind: \"trace\" }),\n ],\n function_call: (p): ReadonlyArray<TurnEvent> => {\n const call_id = callIdAt(p.name, 0)\n return [\n TurnEvent.ToolCallStart({ call_id, name: p.name }),\n TurnEvent.ToolCallArgsDelta({ call_id, delta: JSON.stringify(p.args ?? {}) }),\n ]\n },\n }),\n )\n\nexport const toCanonical = <E, R>(\n s: Stream.Stream<ProviderEvent, E, R>,\n): Stream.Stream<TurnEvent, E, R> =>\n s.pipe(\n Stream.mapAccum(\n () => emptyAccumulator,\n (acc, chunk) => {\n const result = ingestChunk(acc, chunk)\n // The accumulator already contains the synthesized call_ids for any\n // function_call parts in *this* chunk - they are the tail. Walk back\n // by chunk-local function-call index to recover them.\n const newCalls = result.parts.filter((p) => p.kind === \"function_call\")\n const tailCalls = result.accumulator.functionCalls.slice(\n result.accumulator.functionCalls.length - newCalls.length,\n )\n let consumedCalls = 0\n const partDeltas: ReadonlyArray<TurnEvent> = result.parts.flatMap((p) =>\n partToTurnEvents(p, () => {\n const id = tailCalls[consumedCalls]?.callId ?? p.kind\n consumedCalls += 1\n return id\n }),\n )\n const deltas: ReadonlyArray<TurnEvent> = result.finished\n ? [...partDeltas, TurnEvent.TurnComplete({ turn: accumulatorToTurn(result.accumulator) })]\n : partDeltas\n return [result.accumulator, deltas] as const\n },\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `GeminiService` value. For Layer-based setup, prefer `layer`.\n */\nexport const make = (cfg: Config): Effect.Effect<GeminiService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => {\n const streamNative: GeminiService[\"streamNative\"] = (request) =>\n buildNativeStream(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client))\n 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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-uai/google",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Google Gemini provider for @effect-uai/core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"effect": "4.0.0-beta.57",
|
|
63
63
|
"typescript": "^6.0.3",
|
|
64
|
-
"@effect-uai/core": "0.5.
|
|
64
|
+
"@effect-uai/core": "0.5.2"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"@effect-uai/core": ">=0.2.0 <1",
|
package/src/Gemini.ts
CHANGED
|
@@ -5,10 +5,11 @@ import {
|
|
|
5
5
|
type CommonRequest,
|
|
6
6
|
LanguageModel,
|
|
7
7
|
type LanguageModelService,
|
|
8
|
+
turnFromStream,
|
|
8
9
|
} from "@effect-uai/core/LanguageModel"
|
|
9
10
|
import { JsonParseError } from "@effect-uai/core/JSONL"
|
|
10
11
|
import * as SSE from "@effect-uai/core/SSE"
|
|
11
|
-
import { TurnEvent } from "@effect-uai/core/Turn"
|
|
12
|
+
import { type Turn, TurnEvent } from "@effect-uai/core/Turn"
|
|
12
13
|
import {
|
|
13
14
|
type ChunkPart,
|
|
14
15
|
type GenerationConfig,
|
|
@@ -62,6 +63,13 @@ export type GeminiService = {
|
|
|
62
63
|
* `streamNative |> toCanonical`.
|
|
63
64
|
*/
|
|
64
65
|
readonly streamTurn: (request: GeminiRequest) => Stream.Stream<TurnEvent, AiError.AiError>
|
|
66
|
+
/**
|
|
67
|
+
* Drain a single turn and return the assembled `Turn`. Derived from
|
|
68
|
+
* `streamTurn` — Gemini's non-streaming `generateContent` could
|
|
69
|
+
* back this directly, but routing through streaming keeps a single
|
|
70
|
+
* accumulator path.
|
|
71
|
+
*/
|
|
72
|
+
readonly turn: (request: GeminiRequest) => Effect.Effect<Turn, AiError.AiError>
|
|
65
73
|
/**
|
|
66
74
|
* Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.
|
|
67
75
|
* Threads a fresh `Accumulator` so chunk-level text/usage merging happens
|
|
@@ -267,9 +275,11 @@ export const make = (cfg: Config): Effect.Effect<GeminiService, never, HttpClien
|
|
|
267
275
|
Effect.map(HttpClient.HttpClient.asEffect(), (client) => {
|
|
268
276
|
const streamNative: GeminiService["streamNative"] = (request) =>
|
|
269
277
|
buildNativeStream(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client))
|
|
278
|
+
const streamTurn: GeminiService["streamTurn"] = (request) => toCanonical(streamNative(request))
|
|
270
279
|
return {
|
|
271
280
|
streamNative,
|
|
272
|
-
streamTurn
|
|
281
|
+
streamTurn,
|
|
282
|
+
turn: turnFromStream(streamTurn),
|
|
273
283
|
toCanonical,
|
|
274
284
|
}
|
|
275
285
|
})
|
|
@@ -291,6 +301,7 @@ export const layer = (
|
|
|
291
301
|
make(cfg),
|
|
292
302
|
(s): LanguageModelService => ({
|
|
293
303
|
streamTurn: (request) => s.streamTurn(request as GeminiRequest),
|
|
304
|
+
turn: (request) => s.turn(request as GeminiRequest),
|
|
294
305
|
}),
|
|
295
306
|
),
|
|
296
307
|
)
|