@effect-uai/google 0.2.0 → 0.3.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.
@@ -0,0 +1,105 @@
1
+ import { WireChunk } from "./codec.mjs";
2
+ import { GoogleModel } from "./models.mjs";
3
+ import { Context, Effect, Layer, Redacted, Schema, Stream } from "effect";
4
+ import { HttpClient } from "effect/unstable/http";
5
+ import * as AiError from "@effect-uai/core/AiError";
6
+ import { CommonRequest, LanguageModel } from "@effect-uai/core/LanguageModel";
7
+ import { TurnEvent } from "@effect-uai/core/Turn";
8
+
9
+ //#region src/Gemini.d.ts
10
+ declare namespace Gemini_d_exports {
11
+ export { Config, Gemini, GeminiRequest, GeminiService, ProviderEvent, layer, make, toCanonical };
12
+ }
13
+ /**
14
+ * Gemini's native event vocabulary. Aliased from `WireChunk` so the public
15
+ * surface matches `@effect-uai/responses` and `@effect-uai/anthropic`. Each
16
+ * `ProviderEvent` is a full `GenerateContentResponse` chunk (not a per-field
17
+ * delta) - that's how Gemini's SSE works.
18
+ */
19
+ declare const ProviderEvent: Schema.Struct<{
20
+ readonly candidates: Schema.optional<Schema.$Array<Schema.Struct<{
21
+ readonly content: Schema.optional<Schema.Struct<{
22
+ readonly role: Schema.optional<Schema.String>;
23
+ readonly parts: Schema.optional<Schema.$Array<Schema.Union<readonly [Schema.Struct<{
24
+ readonly text: Schema.String;
25
+ readonly thought: Schema.optional<Schema.Boolean>;
26
+ }>]>>>;
27
+ }>>;
28
+ readonly finishReason: Schema.optional<Schema.String>;
29
+ readonly index: Schema.optional<Schema.Number>;
30
+ }>>>;
31
+ readonly usageMetadata: Schema.optional<Schema.Struct<{
32
+ readonly promptTokenCount: Schema.optional<Schema.Number>;
33
+ readonly candidatesTokenCount: Schema.optional<Schema.Number>;
34
+ readonly totalTokenCount: Schema.optional<Schema.Number>;
35
+ readonly cachedContentTokenCount: Schema.optional<Schema.Number>;
36
+ readonly thoughtsTokenCount: Schema.optional<Schema.Number>;
37
+ }>>;
38
+ }>;
39
+ type ProviderEvent = WireChunk;
40
+ type GeminiRequest = Omit<CommonRequest, "model"> & {
41
+ /**
42
+ * Narrows `CommonRequest.model` (`string`) to the typed `GoogleModel`
43
+ * literal union for autocomplete.
44
+ */
45
+ readonly model: GoogleModel;
46
+ /**
47
+ * Gemini 2.5 thinking budget. `0` disables thinking entirely (lowest
48
+ * latency); higher values let the model think longer. Forwarded as
49
+ * `generationConfig.thinkingConfig.thinkingBudget`.
50
+ */
51
+ readonly thinkingBudget?: number;
52
+ };
53
+ type GeminiService = {
54
+ /**
55
+ * Stream the provider's native event vocabulary (post-SSE-decode). For
56
+ * Gemini, each event is a full `GenerateContentResponse` chunk. Use this
57
+ * when you need access to fields the canonical view doesn't surface
58
+ * (`thoughtSignature`, granular `safetyRatings`, etc.). For
59
+ * provider-portable code, use `streamTurn`.
60
+ */
61
+ readonly streamNative: (request: GeminiRequest) => Stream.Stream<ProviderEvent, AiError.AiError>;
62
+ /**
63
+ * Stream canonical `TurnEvent`s. Implemented as
64
+ * `streamNative |> toCanonical`.
65
+ */
66
+ readonly streamTurn: (request: GeminiRequest) => Stream.Stream<TurnEvent, AiError.AiError>;
67
+ /**
68
+ * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.
69
+ * Threads a fresh `Accumulator` so chunk-level text/usage merging happens
70
+ * per stream.
71
+ */
72
+ readonly toCanonical: <E, R>(s: Stream.Stream<ProviderEvent, E, R>) => Stream.Stream<TurnEvent, E, R>;
73
+ };
74
+ declare const Gemini_base: Context.ServiceClass<Gemini, "@betalyra/effect-uai/providers/google/Gemini", GeminiService>;
75
+ /**
76
+ * Provider-typed service tag. Yield this when you want Gemini-specific
77
+ * options (`thinkingBudget`); yield the generic `LanguageModel` tag for
78
+ * provider-portable code. Both are registered by `layer`.
79
+ */
80
+ declare class Gemini extends Gemini_base {}
81
+ type Config = {
82
+ readonly apiKey: Redacted.Redacted;
83
+ readonly baseUrl?: string;
84
+ };
85
+ /**
86
+ * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.
87
+ * Threads a fresh `Accumulator` so chunk-level text/usage merging works
88
+ * across the run.
89
+ */
90
+ declare const toCanonical: <E, R>(s: Stream.Stream<ProviderEvent, E, R>) => Stream.Stream<TurnEvent, E, R>;
91
+ /**
92
+ * Build a `GeminiService` value. For Layer-based setup, prefer `layer`.
93
+ */
94
+ declare const make: (cfg: Config) => Effect.Effect<GeminiService, never, HttpClient.HttpClient>;
95
+ /**
96
+ * Layer that registers both the provider-specific `Gemini` tag and the
97
+ * generic `LanguageModel` tag, sharing one underlying implementation.
98
+ *
99
+ * The generic tag accepts `CommonRequest`; the typed tag accepts the full
100
+ * `GeminiRequest` surface.
101
+ */
102
+ declare const layer: (cfg: Config) => Layer.Layer<Gemini | LanguageModel, never, HttpClient.HttpClient>;
103
+ //#endregion
104
+ export { Config, Gemini, GeminiRequest, GeminiService, ProviderEvent, layer, make, Gemini_d_exports as t, toCanonical };
105
+ //# sourceMappingURL=Gemini.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Gemini.d.mts","names":[],"sources":["../src/Gemini.ts"],"mappings":";;;;;;;;;;;;;;;;;;cA2Ba,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;;;;;;WAMzE,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;;;;;;cAiHE,WAAA,SACX,CAAA,EAAG,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,CAAA,EAAG,CAAA,MAClC,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,CAAA,EAAG,CAAA;;;;cAgClB,IAAA,GAAQ,GAAA,EAAK,MAAA,KAAS,MAAA,CAAO,MAAA,CAAO,aAAA,SAAsB,UAAA,CAAW,UAAA;;;;;;;;cAkBrE,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,MAAA,GAAS,aAAA,SAAsB,UAAA,CAAW,UAAA"}
@@ -0,0 +1,170 @@
1
+ import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
+ import { WireChunk, accumulatorToTurn, buildRequestBody, emptyAccumulator, ingestChunk } from "./codec.mjs";
3
+ import { Context, Effect, Layer, Option, Redacted, Schema, Stream } from "effect";
4
+ import { HttpClient, HttpClientRequest } from "effect/unstable/http";
5
+ import * as AiError from "@effect-uai/core/AiError";
6
+ import { LanguageModel } from "@effect-uai/core/LanguageModel";
7
+ import { JsonParseError } from "@effect-uai/core/JSONL";
8
+ import * as SSE from "@effect-uai/core/SSE";
9
+ //#region src/Gemini.ts
10
+ var Gemini_exports = /* @__PURE__ */ __exportAll({
11
+ Gemini: () => Gemini,
12
+ ProviderEvent: () => ProviderEvent,
13
+ layer: () => layer,
14
+ make: () => make,
15
+ toCanonical: () => toCanonical
16
+ });
17
+ /**
18
+ * Gemini's native event vocabulary. Aliased from `WireChunk` so the public
19
+ * surface matches `@effect-uai/responses` and `@effect-uai/anthropic`. Each
20
+ * `ProviderEvent` is a full `GenerateContentResponse` chunk (not a per-field
21
+ * delta) - that's how Gemini's SSE works.
22
+ */
23
+ const ProviderEvent = WireChunk;
24
+ /**
25
+ * Provider-typed service tag. Yield this when you want Gemini-specific
26
+ * options (`thinkingBudget`); yield the generic `LanguageModel` tag for
27
+ * provider-portable code. Both are registered by `layer`.
28
+ */
29
+ var Gemini = class extends Context.Service()("@betalyra/effect-uai/providers/google/Gemini") {};
30
+ const buildGenerationConfig = (request) => {
31
+ const cfg = {
32
+ ...request.temperature !== void 0 && { temperature: request.temperature },
33
+ ...request.maxOutputTokens !== void 0 && { maxOutputTokens: request.maxOutputTokens },
34
+ ...request.topP !== void 0 && { topP: request.topP },
35
+ ...request.thinkingBudget !== void 0 && { thinkingConfig: { thinkingBudget: request.thinkingBudget } },
36
+ ...request.structured !== void 0 && {
37
+ responseMimeType: "application/json",
38
+ responseJsonSchema: request.structured.schema["~standard"].jsonSchema.input({ target: "draft-2020-12" })
39
+ }
40
+ };
41
+ return Object.keys(cfg).length === 0 ? Option.none() : Option.some(cfg);
42
+ };
43
+ const decodeChunk = Schema.decodeUnknownEffect(WireChunk);
44
+ /**
45
+ * Parse one SSE event's `data` payload into a `WireChunk`. Unlike the other
46
+ * providers, Gemini's wire vocabulary is non-discriminated (a single
47
+ * permissive struct, all fields optional) - there's no natural shape for
48
+ * an `_unknown` variant, so JSON parse / schema decode failures flow
49
+ * through as transport errors rather than being silently dropped.
50
+ */
51
+ const sseEventToChunk = (ev) => Effect.try({
52
+ try: () => JSON.parse(ev.data),
53
+ catch: (cause) => new JsonParseError({
54
+ line: ev.data,
55
+ cause
56
+ })
57
+ }).pipe(Effect.flatMap(decodeChunk));
58
+ const httpStatusError = (status, body) => {
59
+ const provider = "gemini";
60
+ const raw = body;
61
+ if (status === 429) return new AiError.RateLimited({
62
+ provider,
63
+ raw
64
+ });
65
+ if (status === 408 || status === 504) return new AiError.Timeout({
66
+ provider,
67
+ raw
68
+ });
69
+ if (status === 401) return new AiError.AuthFailed({
70
+ provider,
71
+ subtype: "auth",
72
+ raw
73
+ });
74
+ if (status === 403) return new AiError.AuthFailed({
75
+ provider,
76
+ subtype: "permission",
77
+ raw
78
+ });
79
+ if (status === 402) return new AiError.AuthFailed({
80
+ provider,
81
+ subtype: "billing",
82
+ raw
83
+ });
84
+ if (status === 413) return new AiError.ContextLengthExceeded({
85
+ provider,
86
+ raw
87
+ });
88
+ if (status >= 500) return new AiError.Unavailable({
89
+ provider,
90
+ status,
91
+ raw
92
+ });
93
+ return new AiError.InvalidRequest({
94
+ provider,
95
+ raw
96
+ });
97
+ };
98
+ const buildNativeStream = (cfg) => {
99
+ const baseUrl = cfg.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
100
+ return (request) => Stream.unwrap(Effect.gen(function* () {
101
+ const client = yield* HttpClient.HttpClient;
102
+ const url = `${baseUrl}/models/${request.model}:streamGenerateContent?alt=sse`;
103
+ const generationConfig = buildGenerationConfig(request);
104
+ const body = buildRequestBody(request.history, generationConfig);
105
+ const httpRequest = HttpClientRequest.post(url).pipe(HttpClientRequest.setHeader("x-goog-api-key", Redacted.value(cfg.apiKey)), HttpClientRequest.bodyJsonUnsafe(body), HttpClientRequest.accept("text/event-stream"));
106
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError((cause) => new AiError.Unavailable({
107
+ provider: "gemini",
108
+ raw: cause
109
+ })));
110
+ if (response.status >= 400) {
111
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
112
+ return Stream.fail(httpStatusError(response.status, text));
113
+ }
114
+ return response.stream.pipe(Stream.mapError((cause) => new AiError.Unavailable({
115
+ provider: "gemini",
116
+ raw: cause
117
+ })), SSE.fromBytes, Stream.mapEffect((ev) => sseEventToChunk(ev).pipe(Effect.mapError((cause) => new AiError.Unavailable({
118
+ provider: "gemini",
119
+ raw: cause
120
+ })))));
121
+ }));
122
+ };
123
+ /**
124
+ * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.
125
+ * Threads a fresh `Accumulator` so chunk-level text/usage merging works
126
+ * across the run.
127
+ */
128
+ const toCanonical = (s) => s.pipe(Stream.mapAccum(() => emptyAccumulator, (acc, chunk) => {
129
+ const result = ingestChunk(acc, chunk);
130
+ const partDeltas = result.parts.map((p) => p.kind === "text" ? {
131
+ type: "text_delta",
132
+ text: p.text
133
+ } : {
134
+ type: "reasoning_delta",
135
+ text: p.text,
136
+ kind: "trace"
137
+ });
138
+ const deltas = result.finished ? [...partDeltas, {
139
+ type: "turn_complete",
140
+ turn: accumulatorToTurn(result.accumulator)
141
+ }] : partDeltas;
142
+ return [result.accumulator, deltas];
143
+ }));
144
+ /**
145
+ * Build a `GeminiService` value. For Layer-based setup, prefer `layer`.
146
+ */
147
+ const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => {
148
+ const streamNative = (request) => buildNativeStream(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client));
149
+ return {
150
+ streamNative,
151
+ streamTurn: (request) => toCanonical(streamNative(request)),
152
+ toCanonical
153
+ };
154
+ });
155
+ /**
156
+ * Layer that registers both the provider-specific `Gemini` tag and the
157
+ * generic `LanguageModel` tag, sharing one underlying implementation.
158
+ *
159
+ * The generic tag accepts `CommonRequest`; the typed tag accepts the full
160
+ * `GeminiRequest` surface.
161
+ */
162
+ const layer = (cfg) => {
163
+ const typed = Layer.effect(Gemini, make(cfg));
164
+ const generic = Layer.effect(LanguageModel, Effect.map(make(cfg), (s) => ({ streamTurn: (request) => s.streamTurn(request) })));
165
+ return Layer.merge(typed, generic);
166
+ };
167
+ //#endregion
168
+ export { Gemini, ProviderEvent, layer, make, Gemini_exports as t, toCanonical };
169
+
170
+ //# sourceMappingURL=Gemini.mjs.map
@@ -0,0 +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 type { TurnEvent } from \"@effect-uai/core/Turn\"\nimport {\n type GenerationConfig,\n WireChunk,\n accumulatorToTurn,\n buildRequestBody,\n emptyAccumulator,\n ingestChunk,\n} from \"./codec.js\"\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 sseEventToChunk = (ev: SSE.Event) =>\n Effect.try({\n try: () => JSON.parse(ev.data) as unknown,\n catch: (cause) => new JsonParseError({ line: ev.data, cause }),\n }).pipe(Effect.flatMap(decodeChunk))\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)\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 */\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 const partDeltas = result.parts.map((p) =>\n p.kind === \"text\"\n ? { type: \"text_delta\" as const, text: p.text }\n : { type: \"reasoning_delta\" as const, text: p.text, kind: \"trace\" as const },\n )\n const deltas = result.finished\n ? [\n ...partDeltas,\n {\n type: \"turn_complete\" as const,\n turn: accumulatorToTurn(result.accumulator),\n },\n ]\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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"}
@@ -0,0 +1,64 @@
1
+ import { GoogleEmbeddingModel } from "./models.mjs";
2
+ import { Context, Effect, Layer, Redacted } from "effect";
3
+ import { HttpClient } from "effect/unstable/http";
4
+ import * as AiError from "@effect-uai/core/AiError";
5
+ import { CommonEmbedRequest, EmbedManyResponse, EmbedResponse, EmbeddingModel } from "@effect-uai/core/EmbeddingModel";
6
+ import { EmbedInput } from "@effect-uai/core/Embedding";
7
+
8
+ //#region src/GeminiEmbedding.d.ts
9
+ declare namespace GeminiEmbedding_d_exports {
10
+ export { Config, GeminiEmbedManyRequest, GeminiEmbedRequest, GeminiEmbedding, GeminiEmbeddingService, GoogleEmbeddingTask, layer, make };
11
+ }
12
+ /**
13
+ * Gemini's task-type enum, exposed under our friendly names. Mapped to the
14
+ * wire `taskType` values inside the codec. Honoured by
15
+ * `gemini-embedding-001`; ignored by `gemini-embedding-2` (which expects
16
+ * task instructions in the prompt text instead).
17
+ */
18
+ type GoogleEmbeddingTask = "query" | "document" | "similarity" | "classification" | "clustering" | "qa" | "fact_verification" | "code_query";
19
+ type GeminiEmbedRequest = Omit<CommonEmbedRequest, "model" | "task" | "encoding"> & {
20
+ /** Narrows `CommonEmbedRequest.model` to the typed Google union. */readonly model: GoogleEmbeddingModel;
21
+ /**
22
+ * Widens the cross-provider `"query" | "document"` to the full Google
23
+ * task enum. Optional. Honoured by `gemini-embedding-001`; ignored by
24
+ * `gemini-embedding-2`.
25
+ */
26
+ readonly task?: GoogleEmbeddingTask;
27
+ /**
28
+ * Optional document title. Only meaningful for `RETRIEVAL_DOCUMENT`
29
+ * tasks on `gemini-embedding-001`.
30
+ */
31
+ readonly title?: string;
32
+ };
33
+ type GeminiEmbedManyRequest = Omit<GeminiEmbedRequest, "input"> & {
34
+ readonly inputs: ReadonlyArray<EmbedInput>;
35
+ };
36
+ type GeminiEmbeddingService = {
37
+ readonly embed: (request: GeminiEmbedRequest) => Effect.Effect<EmbedResponse, AiError.AiError>;
38
+ readonly embedMany: (request: GeminiEmbedManyRequest) => Effect.Effect<EmbedManyResponse, AiError.AiError>;
39
+ };
40
+ declare const GeminiEmbedding_base: Context.ServiceClass<GeminiEmbedding, "@betalyra/effect-uai/providers/google/GeminiEmbedding", GeminiEmbeddingService>;
41
+ /**
42
+ * Provider-typed service tag. Yield this when you want Gemini-specific
43
+ * options (full `task` enum, `title`); yield the generic `EmbeddingModel`
44
+ * tag for provider-portable code. Both are registered by `layer`.
45
+ */
46
+ declare class GeminiEmbedding extends GeminiEmbedding_base {}
47
+ type Config = {
48
+ readonly apiKey: Redacted.Redacted;
49
+ readonly baseUrl?: string;
50
+ };
51
+ /**
52
+ * Build a `GeminiEmbeddingService` value. For Layer-based setup, prefer
53
+ * `layer`.
54
+ */
55
+ declare const make: (cfg: Config) => Effect.Effect<GeminiEmbeddingService, never, HttpClient.HttpClient>;
56
+ /**
57
+ * Layer that registers both the provider-specific `GeminiEmbedding` tag
58
+ * and the generic `EmbeddingModel` tag, sharing one underlying
59
+ * implementation.
60
+ */
61
+ declare const layer: (cfg: Config) => Layer.Layer<GeminiEmbedding | EmbeddingModel, never, HttpClient.HttpClient>;
62
+ //#endregion
63
+ export { Config, GeminiEmbedManyRequest, GeminiEmbedRequest, GeminiEmbedding, GeminiEmbeddingService, GoogleEmbeddingTask, layer, make, GeminiEmbedding_d_exports as t };
64
+ //# sourceMappingURL=GeminiEmbedding.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeminiEmbedding.d.mts","names":[],"sources":["../src/GeminiEmbedding.ts"],"mappings":";;;;;;;;;;;;;;;;;KAyBY,mBAAA;AAAA,KAUA,kBAAA,GAAqB,IAAA,CAAK,kBAAA;+EAE3B,KAAA,EAAO,oBAAA;;;;AAZlB;;WAkBW,IAAA,GAAO,mBAAA;EAlBa;;AAU/B;;EAV+B,SAuBpB,KAAA;AAAA;AAAA,KAGC,sBAAA,GAAyB,IAAA,CAAK,kBAAA;EAAA,SAC/B,MAAA,EAAQ,aAAA,CAAc,UAAA;AAAA;AAAA,KAGrB,sBAAA;EAAA,SACD,KAAA,GAAQ,OAAA,EAAS,kBAAA,KAAuB,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,OAAA,CAAQ,OAAA;EAAA,SAC7E,SAAA,GACP,OAAA,EAAS,sBAAA,KACN,MAAA,CAAO,MAAA,CAAO,iBAAA,EAAmB,OAAA,CAAQ,OAAA;AAAA;AAAA,cAC/C,oBAAA;;;;;;cAOY,eAAA,SAAwB,oBAAA;AAAA,KAIzB,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;AAAA;;;;;cAuPE,IAAA,GACX,GAAA,EAAK,MAAA,KACJ,MAAA,CAAO,MAAA,CAAO,sBAAA,SAA+B,UAAA,CAAW,UAAA;;;;;;cAa9C,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,eAAA,GAAkB,cAAA,SAAuB,UAAA,CAAW,UAAA"}
@@ -0,0 +1,167 @@
1
+ import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
+ import { Context, Effect, Encoding, Layer, Match, Redacted, Schema } from "effect";
3
+ import { HttpClient, HttpClientRequest } from "effect/unstable/http";
4
+ import * as AiError from "@effect-uai/core/AiError";
5
+ import { EmbeddingModel } from "@effect-uai/core/EmbeddingModel";
6
+ //#region src/GeminiEmbedding.ts
7
+ var GeminiEmbedding_exports = /* @__PURE__ */ __exportAll({
8
+ GeminiEmbedding: () => GeminiEmbedding,
9
+ layer: () => layer,
10
+ make: () => make
11
+ });
12
+ /**
13
+ * Provider-typed service tag. Yield this when you want Gemini-specific
14
+ * options (full `task` enum, `title`); yield the generic `EmbeddingModel`
15
+ * tag for provider-portable code. Both are registered by `layer`.
16
+ */
17
+ var GeminiEmbedding = class extends Context.Service()("@betalyra/effect-uai/providers/google/GeminiEmbedding") {};
18
+ const taskToWire = {
19
+ query: "RETRIEVAL_QUERY",
20
+ document: "RETRIEVAL_DOCUMENT",
21
+ similarity: "SEMANTIC_SIMILARITY",
22
+ classification: "CLASSIFICATION",
23
+ clustering: "CLUSTERING",
24
+ qa: "QUESTION_ANSWERING",
25
+ fact_verification: "FACT_VERIFICATION",
26
+ code_query: "CODE_RETRIEVAL_QUERY"
27
+ };
28
+ const urlRejected = (param) => new AiError.InvalidRequest({
29
+ provider: "gemini",
30
+ param,
31
+ raw: "URL-form image inputs aren't supported; pass base64 or bytes"
32
+ });
33
+ /**
34
+ * Pre-uploading via Google's Files API isn't free, so we reject `url`
35
+ * sources up front rather than silently dropping them. Users with HTTPS
36
+ * URLs should fetch + pass `bytes` (or `base64`).
37
+ */
38
+ const imageSourceToPart = (param) => Match.type().pipe(Match.tag("base64", (s) => Effect.succeed({ inlineData: {
39
+ mimeType: s.mimeType,
40
+ data: s.base64
41
+ } })), Match.tag("bytes", (s) => Effect.succeed({ inlineData: {
42
+ mimeType: s.mimeType,
43
+ data: Encoding.encodeBase64(s.bytes)
44
+ } })), Match.tag("url", () => Effect.fail(urlRejected(param))), Match.exhaustive);
45
+ const contentPartToPart = (param, part) => "text" in part ? Effect.succeed({ text: part.text }) : imageSourceToPart(param)(part.image);
46
+ const inputToContent = (input) => {
47
+ if (typeof input === "string") return Effect.succeed({ parts: [{ text: input }] });
48
+ if ("text" in input) return Effect.succeed({ parts: [{ text: input.text }] });
49
+ if ("image" in input) return imageSourceToPart("input.image")(input.image).pipe(Effect.map((part) => ({ parts: [part] })));
50
+ return Effect.forEach(input.content, (p) => contentPartToPart("input.content[].image", p)).pipe(Effect.map((parts) => ({ parts })));
51
+ };
52
+ const wireOptions = (request) => ({
53
+ ...request.task !== void 0 && { taskType: taskToWire[request.task] },
54
+ ...request.dimensions !== void 0 && { outputDimensionality: request.dimensions },
55
+ ...request.title !== void 0 && { title: request.title }
56
+ });
57
+ const buildSingleBody = (request) => inputToContent(request.input).pipe(Effect.map((content) => ({
58
+ content,
59
+ ...wireOptions(request)
60
+ })));
61
+ const buildBatchBody = (request) => {
62
+ const modelPath = `models/${request.model}`;
63
+ const options = wireOptions(request);
64
+ return Effect.forEach(request.inputs, (input) => inputToContent(input).pipe(Effect.map((content) => ({
65
+ model: modelPath,
66
+ content,
67
+ ...options
68
+ })))).pipe(Effect.map((requests) => ({ requests })));
69
+ };
70
+ const WireEmbedding = Schema.Struct({ values: Schema.Array(Schema.Number) });
71
+ const WireSingleResponse = Schema.Struct({ embedding: WireEmbedding });
72
+ const WireBatchResponse = Schema.Struct({ embeddings: Schema.Array(WireEmbedding) });
73
+ const valuesToEmbedding = (values) => ({
74
+ _tag: "float32",
75
+ vector: Float32Array.from(values)
76
+ });
77
+ const emptyUsage = {};
78
+ const httpStatusError = (status, body) => {
79
+ const provider = "gemini";
80
+ const raw = body;
81
+ if (status === 429) return new AiError.RateLimited({
82
+ provider,
83
+ raw
84
+ });
85
+ if (status === 408 || status === 504) return new AiError.Timeout({
86
+ provider,
87
+ raw
88
+ });
89
+ if (status === 401) return new AiError.AuthFailed({
90
+ provider,
91
+ subtype: "auth",
92
+ raw
93
+ });
94
+ if (status === 403) return new AiError.AuthFailed({
95
+ provider,
96
+ subtype: "permission",
97
+ raw
98
+ });
99
+ if (status === 402) return new AiError.AuthFailed({
100
+ provider,
101
+ subtype: "billing",
102
+ raw
103
+ });
104
+ if (status === 413) return new AiError.ContextLengthExceeded({
105
+ provider,
106
+ raw
107
+ });
108
+ if (status >= 500) return new AiError.Unavailable({
109
+ provider,
110
+ status,
111
+ raw
112
+ });
113
+ return new AiError.InvalidRequest({
114
+ provider,
115
+ raw
116
+ });
117
+ };
118
+ const transportFailure = (cause) => new AiError.Unavailable({
119
+ provider: "gemini",
120
+ raw: cause
121
+ });
122
+ const postJson = (cfg, url, body) => Effect.gen(function* () {
123
+ const client = yield* HttpClient.HttpClient;
124
+ const httpRequest = HttpClientRequest.post(url).pipe(HttpClientRequest.setHeader("x-goog-api-key", Redacted.value(cfg.apiKey)), HttpClientRequest.bodyJsonUnsafe(body));
125
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
126
+ if (response.status >= 400) {
127
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
128
+ return yield* Effect.fail(httpStatusError(response.status, text));
129
+ }
130
+ return yield* response.json.pipe(Effect.mapError(transportFailure));
131
+ });
132
+ const decodeSingle = (json) => Schema.decodeUnknownEffect(WireSingleResponse)(json).pipe(Effect.mapError(transportFailure));
133
+ const decodeBatch = (json) => Schema.decodeUnknownEffect(WireBatchResponse)(json).pipe(Effect.mapError(transportFailure));
134
+ const baseUrl = (cfg) => cfg.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
135
+ const embedImpl = (cfg) => (request) => buildSingleBody(request).pipe(Effect.flatMap((body) => postJson(cfg, `${baseUrl(cfg)}/models/${request.model}:embedContent`, body)), Effect.flatMap(decodeSingle), Effect.map((decoded) => ({
136
+ embedding: valuesToEmbedding(decoded.embedding.values),
137
+ usage: emptyUsage
138
+ })));
139
+ const embedManyImpl = (cfg) => (request) => buildBatchBody(request).pipe(Effect.flatMap((body) => postJson(cfg, `${baseUrl(cfg)}/models/${request.model}:batchEmbedContents`, body)), Effect.flatMap(decodeBatch), Effect.map((decoded) => ({
140
+ embeddings: decoded.embeddings.map((e) => valuesToEmbedding(e.values)),
141
+ usage: emptyUsage
142
+ })));
143
+ /**
144
+ * Build a `GeminiEmbeddingService` value. For Layer-based setup, prefer
145
+ * `layer`.
146
+ */
147
+ const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
148
+ embed: (request) => embedImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
149
+ embedMany: (request) => embedManyImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client))
150
+ }));
151
+ /**
152
+ * Layer that registers both the provider-specific `GeminiEmbedding` tag
153
+ * and the generic `EmbeddingModel` tag, sharing one underlying
154
+ * implementation.
155
+ */
156
+ const layer = (cfg) => {
157
+ const typed = Layer.effect(GeminiEmbedding, make(cfg));
158
+ const generic = Layer.effect(EmbeddingModel, Effect.map(make(cfg), (s) => ({
159
+ embed: (req) => s.embed(req),
160
+ embedMany: (req) => s.embedMany(req)
161
+ })));
162
+ return Layer.merge(typed, generic);
163
+ };
164
+ //#endregion
165
+ export { GeminiEmbedding, layer, make, GeminiEmbedding_exports as t };
166
+
167
+ //# sourceMappingURL=GeminiEmbedding.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeminiEmbedding.mjs","names":[],"sources":["../src/GeminiEmbedding.ts"],"sourcesContent":["import { Context, Effect, Encoding, Layer, Match, Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { Embedding, EmbedContentPart, EmbedInput, Usage } from \"@effect-uai/core/Embedding\"\nimport {\n type CommonEmbedManyRequest,\n type CommonEmbedRequest,\n EmbeddingModel,\n type EmbeddingModelService,\n type EmbedManyResponse,\n type EmbedResponse,\n} from \"@effect-uai/core/EmbeddingModel\"\nimport type { ImageSource } from \"@effect-uai/core/Image\"\nimport type { GoogleEmbeddingModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Gemini's task-type enum, exposed under our friendly names. Mapped to the\n * wire `taskType` values inside the codec. Honoured by\n * `gemini-embedding-001`; ignored by `gemini-embedding-2` (which expects\n * task instructions in the prompt text instead).\n */\nexport type GoogleEmbeddingTask =\n | \"query\"\n | \"document\"\n | \"similarity\"\n | \"classification\"\n | \"clustering\"\n | \"qa\"\n | \"fact_verification\"\n | \"code_query\"\n\nexport type GeminiEmbedRequest = Omit<CommonEmbedRequest, \"model\" | \"task\" | \"encoding\"> & {\n /** Narrows `CommonEmbedRequest.model` to the typed Google union. */\n readonly model: GoogleEmbeddingModel\n /**\n * Widens the cross-provider `\"query\" | \"document\"` to the full Google\n * task enum. Optional. Honoured by `gemini-embedding-001`; ignored by\n * `gemini-embedding-2`.\n */\n readonly task?: GoogleEmbeddingTask\n /**\n * Optional document title. Only meaningful for `RETRIEVAL_DOCUMENT`\n * tasks on `gemini-embedding-001`.\n */\n readonly title?: string\n}\n\nexport type GeminiEmbedManyRequest = Omit<GeminiEmbedRequest, \"input\"> & {\n readonly inputs: ReadonlyArray<EmbedInput>\n}\n\nexport type GeminiEmbeddingService = {\n readonly embed: (request: GeminiEmbedRequest) => Effect.Effect<EmbedResponse, AiError.AiError>\n readonly embedMany: (\n request: GeminiEmbedManyRequest,\n ) => Effect.Effect<EmbedManyResponse, AiError.AiError>\n}\n\n/**\n * Provider-typed service tag. Yield this when you want Gemini-specific\n * options (full `task` enum, `title`); yield the generic `EmbeddingModel`\n * tag for provider-portable code. Both are registered by `layer`.\n */\nexport class GeminiEmbedding extends Context.Service<GeminiEmbedding, GeminiEmbeddingService>()(\n \"@betalyra/effect-uai/providers/google/GeminiEmbedding\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec - request body\n// ---------------------------------------------------------------------------\n\nconst taskToWire: Record<GoogleEmbeddingTask, string> = {\n query: \"RETRIEVAL_QUERY\",\n document: \"RETRIEVAL_DOCUMENT\",\n similarity: \"SEMANTIC_SIMILARITY\",\n classification: \"CLASSIFICATION\",\n clustering: \"CLUSTERING\",\n qa: \"QUESTION_ANSWERING\",\n fact_verification: \"FACT_VERIFICATION\",\n code_query: \"CODE_RETRIEVAL_QUERY\",\n}\n\ntype WireTextPart = {\n readonly text: string\n}\ntype WireInlineDataPart = {\n readonly inlineData: { readonly mimeType: string; readonly data: string }\n}\ntype WirePart = WireTextPart | WireInlineDataPart\ntype WireContent = {\n readonly parts: ReadonlyArray<WirePart>\n}\n\nconst urlRejected = (param: string): AiError.AiError =>\n new AiError.InvalidRequest({\n provider: \"gemini\",\n param,\n raw: \"URL-form image inputs aren't supported; pass base64 or bytes\",\n })\n\n/**\n * Pre-uploading via Google's Files API isn't free, so we reject `url`\n * sources up front rather than silently dropping them. Users with HTTPS\n * URLs should fetch + pass `bytes` (or `base64`).\n */\nconst imageSourceToPart = (param: string) =>\n Match.type<ImageSource>().pipe(\n Match.tag(\n \"base64\",\n (s): Effect.Effect<WirePart, AiError.AiError> =>\n Effect.succeed({ inlineData: { mimeType: s.mimeType, data: s.base64 } }),\n ),\n Match.tag(\n \"bytes\",\n (s): Effect.Effect<WirePart, AiError.AiError> =>\n Effect.succeed({\n inlineData: { mimeType: s.mimeType, data: Encoding.encodeBase64(s.bytes) },\n }),\n ),\n Match.tag(\n \"url\",\n (): Effect.Effect<WirePart, AiError.AiError> => Effect.fail(urlRejected(param)),\n ),\n Match.exhaustive,\n )\n\nconst contentPartToPart = (\n param: string,\n part: EmbedContentPart,\n): Effect.Effect<WirePart, AiError.AiError> =>\n \"text\" in part ? Effect.succeed({ text: part.text }) : imageSourceToPart(param)(part.image)\n\nconst inputToContent = (input: EmbedInput): Effect.Effect<WireContent, AiError.AiError> => {\n if (typeof input === \"string\") return Effect.succeed({ parts: [{ text: input }] })\n if (\"text\" in input) return Effect.succeed({ parts: [{ text: input.text }] })\n if (\"image\" in input) {\n return imageSourceToPart(\"input.image\")(input.image).pipe(\n Effect.map((part) => ({ parts: [part] })),\n )\n }\n return Effect.forEach(input.content, (p) => contentPartToPart(\"input.content[].image\", p)).pipe(\n Effect.map((parts) => ({ parts })),\n )\n}\n\ntype WireSingleBody = {\n readonly content: WireContent\n readonly taskType?: string\n readonly outputDimensionality?: number\n readonly title?: string\n}\n\nconst wireOptions = (\n request: Pick<GeminiEmbedRequest, \"task\" | \"dimensions\" | \"title\">,\n): Omit<WireSingleBody, \"content\"> => ({\n ...(request.task !== undefined && { taskType: taskToWire[request.task] }),\n ...(request.dimensions !== undefined && { outputDimensionality: request.dimensions }),\n ...(request.title !== undefined && { title: request.title }),\n})\n\nconst buildSingleBody = (\n request: GeminiEmbedRequest,\n): Effect.Effect<WireSingleBody, AiError.AiError> =>\n inputToContent(request.input).pipe(\n Effect.map((content) => ({ content, ...wireOptions(request) })),\n )\n\ntype WireBatchEntry = WireSingleBody & {\n readonly model: string\n}\ntype WireBatchBody = {\n readonly requests: ReadonlyArray<WireBatchEntry>\n}\n\nconst buildBatchBody = (\n request: GeminiEmbedManyRequest,\n): Effect.Effect<WireBatchBody, AiError.AiError> => {\n const modelPath = `models/${request.model}`\n const options = wireOptions(request)\n return Effect.forEach(request.inputs, (input) =>\n inputToContent(input).pipe(\n Effect.map((content): WireBatchEntry => ({ model: modelPath, content, ...options })),\n ),\n ).pipe(Effect.map((requests) => ({ requests })))\n}\n\n// ---------------------------------------------------------------------------\n// Codec - response\n// ---------------------------------------------------------------------------\n\nconst WireEmbedding = Schema.Struct({\n values: Schema.Array(Schema.Number),\n})\n\nconst WireSingleResponse = Schema.Struct({\n embedding: WireEmbedding,\n})\n\nconst WireBatchResponse = Schema.Struct({\n embeddings: Schema.Array(WireEmbedding),\n})\n\nconst valuesToEmbedding = (values: ReadonlyArray<number>): Embedding => ({\n _tag: \"float32\",\n vector: Float32Array.from(values),\n})\n\n// Gemini's embed endpoints don't return token-count metadata. Estimating\n// client-side would be provider-specific cruft; honest `undefined` is better.\nconst emptyUsage: Usage = {}\n\n// ---------------------------------------------------------------------------\n// HTTP errors\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 transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"gemini\", raw: cause })\n\n// ---------------------------------------------------------------------------\n// HTTP plumbing - send a JSON body, error-map status, return parsed JSON.\n// ---------------------------------------------------------------------------\n\nconst postJson = (\n cfg: Config,\n url: string,\n body: unknown,\n): Effect.Effect<unknown, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(url).pipe(\n HttpClientRequest.setHeader(\"x-goog-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* Effect.fail(httpStatusError(response.status, text))\n }\n return yield* response.json.pipe(Effect.mapError(transportFailure))\n })\n\nconst decodeSingle = (json: unknown) =>\n Schema.decodeUnknownEffect(WireSingleResponse)(json).pipe(Effect.mapError(transportFailure))\n\nconst decodeBatch = (json: unknown) =>\n Schema.decodeUnknownEffect(WireBatchResponse)(json).pipe(Effect.mapError(transportFailure))\n\nconst baseUrl = (cfg: Config): string =>\n cfg.baseUrl ?? \"https://generativelanguage.googleapis.com/v1beta\"\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst embedImpl =\n (cfg: Config) =>\n (\n request: GeminiEmbedRequest,\n ): Effect.Effect<EmbedResponse, AiError.AiError, HttpClient.HttpClient> =>\n buildSingleBody(request).pipe(\n Effect.flatMap((body) =>\n postJson(cfg, `${baseUrl(cfg)}/models/${request.model}:embedContent`, body),\n ),\n Effect.flatMap(decodeSingle),\n Effect.map(\n (decoded): EmbedResponse => ({\n embedding: valuesToEmbedding(decoded.embedding.values),\n usage: emptyUsage,\n }),\n ),\n )\n\nconst embedManyImpl =\n (cfg: Config) =>\n (\n request: GeminiEmbedManyRequest,\n ): Effect.Effect<EmbedManyResponse, AiError.AiError, HttpClient.HttpClient> =>\n buildBatchBody(request).pipe(\n Effect.flatMap((body) =>\n postJson(cfg, `${baseUrl(cfg)}/models/${request.model}:batchEmbedContents`, body),\n ),\n Effect.flatMap(decodeBatch),\n Effect.map(\n (decoded): EmbedManyResponse => ({\n embeddings: decoded.embeddings.map((e) => valuesToEmbedding(e.values)),\n usage: emptyUsage,\n }),\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `GeminiEmbeddingService` value. For Layer-based setup, prefer\n * `layer`.\n */\nexport const make = (\n cfg: Config,\n): Effect.Effect<GeminiEmbeddingService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n embed: (request) =>\n embedImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n embedMany: (request) =>\n embedManyImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n }))\n\n/**\n * Layer that registers both the provider-specific `GeminiEmbedding` tag\n * and the generic `EmbeddingModel` tag, sharing one underlying\n * implementation.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<GeminiEmbedding | EmbeddingModel, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(GeminiEmbedding, make(cfg))\n const generic = Layer.effect(\n EmbeddingModel,\n Effect.map(\n make(cfg),\n (s): EmbeddingModelService => ({\n embed: (req: CommonEmbedRequest) => s.embed(req as GeminiEmbedRequest),\n embedMany: (req: CommonEmbedManyRequest) => s.embedMany(req as GeminiEmbedManyRequest),\n }),\n ),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}