@effect-uai/google 0.1.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.
package/dist/index.mjs CHANGED
@@ -1,289 +1,5 @@
1
- import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
- import { Array, Context, Effect, Layer, Match, Option, Redacted, Result, Schema, Stream, pipe } from "effect";
3
- import { HttpClient, HttpClientRequest } from "effect/unstable/http";
4
- import * as AiError from "@effect-uai/core/AiError";
5
- import { LanguageModel } from "@effect-uai/core/LanguageModel";
6
- import { JsonParseError } from "@effect-uai/core/JSONL";
7
- import * as SSE from "@effect-uai/core/SSE";
8
- import { matchType } from "@effect-uai/core/Match";
9
- //#region src/codec.ts
10
- var codec_exports = /* @__PURE__ */ __exportAll({
11
- WireChunk: () => WireChunk,
12
- accumulatorToTurn: () => accumulatorToTurn,
13
- buildRequestBody: () => buildRequestBody,
14
- emptyAccumulator: () => emptyAccumulator,
15
- ingestChunk: () => ingestChunk
16
- });
17
- const TextPart = Schema.Struct({
18
- text: Schema.String,
19
- /**
20
- * Gemini's flag for chain-of-thought parts. When `true`, the part is
21
- * reasoning trace, not the model's user-facing answer. Maps onto
22
- * `reasoning_delta { kind: "trace" }` in the canonical view.
23
- */
24
- thought: Schema.optional(Schema.Boolean)
25
- });
26
- const Part = Schema.Union([TextPart]);
27
- const Content = Schema.Struct({
28
- role: Schema.optional(Schema.String),
29
- parts: Schema.optional(Schema.Array(Part))
30
- });
31
- const Candidate = Schema.Struct({
32
- content: Schema.optional(Content),
33
- finishReason: Schema.optional(Schema.String),
34
- index: Schema.optional(Schema.Number)
35
- });
36
- const UsageMetadata = Schema.Struct({
37
- promptTokenCount: Schema.optional(Schema.Number),
38
- candidatesTokenCount: Schema.optional(Schema.Number),
39
- totalTokenCount: Schema.optional(Schema.Number),
40
- cachedContentTokenCount: Schema.optional(Schema.Number),
41
- thoughtsTokenCount: Schema.optional(Schema.Number)
42
- });
43
- const WireChunk = Schema.Struct({
44
- candidates: Schema.optional(Schema.Array(Candidate)),
45
- usageMetadata: Schema.optional(UsageMetadata)
46
- });
47
- const blockText = Match.type().pipe(matchType("input_text", (b) => b.text), matchType("input_image", () => ""), matchType("output_text", (b) => b.text), matchType("refusal", (b) => b.text), Match.exhaustive);
48
- const messageText = (message) => message.content.map(blockText).join("");
49
- const blockToParts = Match.type().pipe(matchType("input_text", (b) => b.text.length === 0 ? [] : [{ text: b.text }]), matchType("input_image", (b) => b.source._tag === "base64" ? [{ inlineData: {
50
- mimeType: b.source.media_type,
51
- data: b.source.data
52
- } }] : []), matchType("output_text", (b) => b.text.length === 0 ? [] : [{ text: b.text }]), matchType("refusal", () => []), Match.exhaustive);
53
- const messageToContent = (message) => {
54
- const parts = pipe(message.content, Array.flatMap(blockToParts));
55
- if (parts.length === 0) return Result.failVoid;
56
- return Match.value(message.role).pipe(Match.when("user", () => Result.succeed({
57
- role: "user",
58
- parts
59
- })), Match.when("assistant", () => Result.succeed({
60
- role: "model",
61
- parts
62
- })), Match.when("system", () => Result.failVoid), Match.exhaustive);
63
- };
64
- const systemMessageText = (message) => {
65
- if (message.role !== "system") return Result.failVoid;
66
- const text = messageText(message);
67
- return text.length === 0 ? Result.failVoid : Result.succeed(text);
68
- };
69
- const messages = (history) => pipe(history, Array.filterMap((item) => item.type === "message" ? Result.succeed(item) : Result.failVoid));
70
- const buildRequestBody = (history, generationConfig) => {
71
- const msgs = messages(history);
72
- const systemTexts = pipe(msgs, Array.filterMap(systemMessageText));
73
- return {
74
- contents: pipe(msgs, Array.filterMap(messageToContent)),
75
- ...systemTexts.length > 0 && { systemInstruction: { parts: [{ text: systemTexts.join("\n") }] } },
76
- ...Option.match(generationConfig, {
77
- onNone: () => ({}),
78
- onSome: (cfg) => ({ generationConfig: cfg })
79
- })
80
- };
81
- };
82
- const finishReasonToStop = (reason) => Option.match(reason, {
83
- onNone: () => "stop",
84
- onSome: (r) => r === "MAX_TOKENS" ? "max_tokens" : "stop"
85
- });
86
- const emptyAccumulator = {
87
- text: "",
88
- reasoning: "",
89
- finishReason: Option.none(),
90
- usage: {}
91
- };
92
- const chunkParts = (chunk) => pipe(chunk.candidates?.[0]?.content?.parts ?? [], Array.filterMap((p) => p.text.length === 0 ? Result.failVoid : Result.succeed({
93
- kind: p.thought === true ? "reasoning" : "text",
94
- text: p.text
95
- })));
96
- const sumByKind = (parts, kind) => pipe(parts, Array.filterMap((p) => p.kind === kind ? Result.succeed(p.text) : Result.failVoid)).join("");
97
- const mergeUsage = (prev, next) => next === void 0 ? prev : {
98
- ...prev,
99
- ...next.promptTokenCount !== void 0 && { input_tokens: next.promptTokenCount },
100
- ...next.candidatesTokenCount !== void 0 && { output_tokens: next.candidatesTokenCount },
101
- ...next.totalTokenCount !== void 0 && { total_tokens: next.totalTokenCount },
102
- ...next.cachedContentTokenCount !== void 0 && { input_tokens_details: { cached_tokens: next.cachedContentTokenCount } },
103
- ...next.thoughtsTokenCount !== void 0 && { output_tokens_details: { reasoning_tokens: next.thoughtsTokenCount } }
104
- };
105
- const ingestChunk = (acc, chunk) => {
106
- const parts = chunkParts(chunk);
107
- const finishReason = Option.fromNullishOr(chunk.candidates?.[0]?.finishReason);
108
- return {
109
- parts,
110
- finished: Option.isSome(finishReason),
111
- accumulator: {
112
- text: acc.text + sumByKind(parts, "text"),
113
- reasoning: acc.reasoning + sumByKind(parts, "reasoning"),
114
- finishReason: Option.orElse(finishReason, () => acc.finishReason),
115
- usage: mergeUsage(acc.usage, chunk.usageMetadata)
116
- }
117
- };
118
- };
119
- const accumulatorToTurn = (acc) => ({
120
- stop_reason: finishReasonToStop(acc.finishReason),
121
- usage: { ...acc.usage },
122
- items: [...acc.reasoning.length > 0 ? [{
123
- type: "reasoning",
124
- summary: acc.reasoning
125
- }] : [], ...acc.text.length === 0 ? [] : [{
126
- type: "message",
127
- role: "assistant",
128
- content: [{
129
- type: "output_text",
130
- text: acc.text
131
- }]
132
- }]]
133
- });
134
- //#endregion
135
- //#region src/Gemini.ts
136
- /**
137
- * Gemini's native event vocabulary. Aliased from `WireChunk` so the public
138
- * surface matches `@effect-uai/responses` and `@effect-uai/anthropic`. Each
139
- * `ProviderEvent` is a full `GenerateContentResponse` chunk (not a per-field
140
- * delta) - that's how Gemini's SSE works.
141
- */
142
- const ProviderEvent = WireChunk;
143
- /**
144
- * Provider-typed service tag. Yield this when you want Gemini-specific
145
- * options (`thinkingBudget`); yield the generic `LanguageModel` tag for
146
- * provider-portable code. Both are registered by `layer`.
147
- */
148
- var Gemini = class extends Context.Service()("@betalyra/effect-uai/providers/google/Gemini") {};
149
- const buildGenerationConfig = (request) => {
150
- const cfg = {
151
- ...request.temperature !== void 0 && { temperature: request.temperature },
152
- ...request.maxOutputTokens !== void 0 && { maxOutputTokens: request.maxOutputTokens },
153
- ...request.topP !== void 0 && { topP: request.topP },
154
- ...request.thinkingBudget !== void 0 && { thinkingConfig: { thinkingBudget: request.thinkingBudget } },
155
- ...request.structured !== void 0 && {
156
- responseMimeType: "application/json",
157
- responseJsonSchema: request.structured.schema["~standard"].jsonSchema.input({ target: "draft-2020-12" })
158
- }
159
- };
160
- return Object.keys(cfg).length === 0 ? Option.none() : Option.some(cfg);
161
- };
162
- const decodeChunk = Schema.decodeUnknownEffect(WireChunk);
163
- /**
164
- * Parse one SSE event's `data` payload into a `WireChunk`. Unlike the other
165
- * providers, Gemini's wire vocabulary is non-discriminated (a single
166
- * permissive struct, all fields optional) - there's no natural shape for
167
- * an `_unknown` variant, so JSON parse / schema decode failures flow
168
- * through as transport errors rather than being silently dropped.
169
- */
170
- const sseEventToChunk = (ev) => Effect.try({
171
- try: () => JSON.parse(ev.data),
172
- catch: (cause) => new JsonParseError({
173
- line: ev.data,
174
- cause
175
- })
176
- }).pipe(Effect.flatMap(decodeChunk));
177
- const httpStatusError = (status, body) => {
178
- const provider = "gemini";
179
- const raw = body;
180
- if (status === 429) return new AiError.RateLimited({
181
- provider,
182
- raw
183
- });
184
- if (status === 408 || status === 504) return new AiError.Timeout({
185
- provider,
186
- raw
187
- });
188
- if (status === 401) return new AiError.AuthFailed({
189
- provider,
190
- subtype: "auth",
191
- raw
192
- });
193
- if (status === 403) return new AiError.AuthFailed({
194
- provider,
195
- subtype: "permission",
196
- raw
197
- });
198
- if (status === 402) return new AiError.AuthFailed({
199
- provider,
200
- subtype: "billing",
201
- raw
202
- });
203
- if (status === 413) return new AiError.ContextLengthExceeded({
204
- provider,
205
- raw
206
- });
207
- if (status >= 500) return new AiError.Unavailable({
208
- provider,
209
- status,
210
- raw
211
- });
212
- return new AiError.InvalidRequest({
213
- provider,
214
- raw
215
- });
216
- };
217
- const buildNativeStream = (cfg) => {
218
- const baseUrl = cfg.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
219
- return (request) => Stream.unwrap(Effect.gen(function* () {
220
- const client = yield* HttpClient.HttpClient;
221
- const url = `${baseUrl}/models/${request.model}:streamGenerateContent?alt=sse`;
222
- const generationConfig = buildGenerationConfig(request);
223
- const body = buildRequestBody(request.history, generationConfig);
224
- const httpRequest = HttpClientRequest.post(url).pipe(HttpClientRequest.setHeader("x-goog-api-key", Redacted.value(cfg.apiKey)), HttpClientRequest.bodyJsonUnsafe(body), HttpClientRequest.accept("text/event-stream"));
225
- const response = yield* client.execute(httpRequest).pipe(Effect.mapError((cause) => new AiError.Unavailable({
226
- provider: "gemini",
227
- raw: cause
228
- })));
229
- if (response.status >= 400) {
230
- const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
231
- return Stream.fail(httpStatusError(response.status, text));
232
- }
233
- return response.stream.pipe(Stream.mapError((cause) => new AiError.Unavailable({
234
- provider: "gemini",
235
- raw: cause
236
- })), SSE.fromBytes, Stream.mapEffect((ev) => sseEventToChunk(ev).pipe(Effect.mapError((cause) => new AiError.Unavailable({
237
- provider: "gemini",
238
- raw: cause
239
- })))));
240
- }));
241
- };
242
- /**
243
- * Project a stream of native `ProviderEvent`s into canonical `TurnEvent`s.
244
- * Threads a fresh `Accumulator` so chunk-level text/usage merging works
245
- * across the run.
246
- */
247
- const toCanonical = (s) => s.pipe(Stream.mapAccum(() => emptyAccumulator, (acc, chunk) => {
248
- const result = ingestChunk(acc, chunk);
249
- const partDeltas = result.parts.map((p) => p.kind === "text" ? {
250
- type: "text_delta",
251
- text: p.text
252
- } : {
253
- type: "reasoning_delta",
254
- text: p.text,
255
- kind: "trace"
256
- });
257
- const deltas = result.finished ? [...partDeltas, {
258
- type: "turn_complete",
259
- turn: accumulatorToTurn(result.accumulator)
260
- }] : partDeltas;
261
- return [result.accumulator, deltas];
262
- }));
263
- /**
264
- * Build a `GeminiService` value. For Layer-based setup, prefer `layer`.
265
- */
266
- const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => {
267
- const streamNative = (request) => buildNativeStream(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client));
268
- return {
269
- streamNative,
270
- streamTurn: (request) => toCanonical(streamNative(request)),
271
- toCanonical
272
- };
273
- });
274
- /**
275
- * Layer that registers both the provider-specific `Gemini` tag and the
276
- * generic `LanguageModel` tag, sharing one underlying implementation.
277
- *
278
- * The generic tag accepts `CommonRequest`; the typed tag accepts the full
279
- * `GeminiRequest` surface.
280
- */
281
- const layer = (cfg) => {
282
- const typed = Layer.effect(Gemini, make(cfg));
283
- const generic = Layer.effect(LanguageModel, Effect.map(make(cfg), (s) => ({ streamTurn: (request) => s.streamTurn(request) })));
284
- return Layer.merge(typed, generic);
285
- };
286
- //#endregion
287
- export { Gemini, ProviderEvent, codec_exports as codec, layer, make, toCanonical };
288
-
289
- //# sourceMappingURL=index.mjs.map
1
+ import { t as codec_exports } from "./codec.mjs";
2
+ import { t as Gemini_exports } from "./Gemini.mjs";
3
+ import { t as GeminiEmbedding_exports } from "./GeminiEmbedding.mjs";
4
+ import "./models.mjs";
5
+ export { Gemini_exports as Gemini, GeminiEmbedding_exports as GeminiEmbedding, codec_exports as codec };
@@ -0,0 +1,27 @@
1
+ //#region src/models.d.ts
2
+ /**
3
+ * Known Gemini model identifiers (as of April 2026). The `(string & {})`
4
+ * tail keeps autocomplete on the literals while still accepting any string,
5
+ * so newly-released models work without an SDK update.
6
+ *
7
+ * Reference: https://ai.google.dev/gemini-api/docs/models
8
+ */
9
+ type GoogleModel = "gemini-3.1-pro-preview" | "gemini-3-flash-preview" | "gemini-3.1-flash-lite-preview" | "gemini-3.1-flash-live-preview" | "gemini-3.1-flash-tts-preview" | "gemini-2.5-pro" | "gemini-2.5-flash" | "gemini-2.5-flash-lite" | (string & {});
10
+ /**
11
+ * Known Gemini embedding model identifiers.
12
+ *
13
+ * - `gemini-embedding-2` — natively multimodal (text, image, audio, video,
14
+ * PDF). Does *not* honour the `taskType` field; instead prepend a task
15
+ * instruction to the text yourself.
16
+ * - `gemini-embedding-001` — text-only, supports the full `taskType` enum
17
+ * (`RETRIEVAL_QUERY` / `RETRIEVAL_DOCUMENT` / classification / etc.).
18
+ *
19
+ * The `(string & {})` tail accepts any string so newly-released models
20
+ * work without an SDK update.
21
+ *
22
+ * Reference: https://ai.google.dev/gemini-api/docs/embeddings
23
+ */
24
+ type GoogleEmbeddingModel = "gemini-embedding-2" | "gemini-embedding-001" | (string & {});
25
+ //#endregion
26
+ export { GoogleEmbeddingModel, GoogleModel };
27
+ //# sourceMappingURL=models.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAOA;;;;;AA0BA;KA1BY,WAAA;;;;;;;;;;;;;;;KA0BA,oBAAA"}
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/google",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Google Gemini provider for @effect-uai/core.",
5
5
  "keywords": [
6
6
  "ai",
@@ -20,6 +20,12 @@
20
20
  "url": "https://github.com/betalyra/effect-uai",
21
21
  "directory": "packages/providers/google"
22
22
  },
23
+ "files": [
24
+ "dist",
25
+ "src",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
23
29
  "type": "module",
24
30
  "main": "./dist/index.mjs",
25
31
  "types": "./dist/index.d.mts",
@@ -27,25 +33,27 @@
27
33
  ".": {
28
34
  "types": "./dist/index.d.mts",
29
35
  "import": "./dist/index.mjs"
36
+ },
37
+ "./Gemini": {
38
+ "types": "./dist/Gemini.d.mts",
39
+ "import": "./dist/Gemini.mjs"
40
+ },
41
+ "./GeminiEmbedding": {
42
+ "types": "./dist/GeminiEmbedding.d.mts",
43
+ "import": "./dist/GeminiEmbedding.mjs"
30
44
  }
31
45
  },
32
- "files": [
33
- "dist",
34
- "src",
35
- "README.md",
36
- "LICENSE"
37
- ],
38
46
  "publishConfig": {
39
47
  "access": "public"
40
48
  },
41
- "peerDependencies": {
42
- "effect": "4.0.0-beta.57",
43
- "@effect-uai/core": "^0.1.0"
44
- },
45
49
  "devDependencies": {
46
50
  "effect": "4.0.0-beta.57",
47
51
  "typescript": "^6.0.3",
48
- "@effect-uai/core": "0.1.0"
52
+ "@effect-uai/core": "0.3.0"
53
+ },
54
+ "peerDependencies": {
55
+ "@effect-uai/core": ">=0.2.0 <1",
56
+ "effect": "4.0.0-beta.57"
49
57
  },
50
58
  "scripts": {
51
59
  "build": "tsdown",
package/src/Gemini.ts CHANGED
@@ -32,7 +32,7 @@ export type ProviderEvent = WireChunk
32
32
  // Public types
33
33
  // ---------------------------------------------------------------------------
34
34
 
35
- export interface GeminiRequest extends Omit<CommonRequest, "model"> {
35
+ export type GeminiRequest = Omit<CommonRequest, "model"> & {
36
36
  /**
37
37
  * Narrows `CommonRequest.model` (`string`) to the typed `GoogleModel`
38
38
  * literal union for autocomplete.
@@ -46,7 +46,7 @@ export interface GeminiRequest extends Omit<CommonRequest, "model"> {
46
46
  readonly thinkingBudget?: number
47
47
  }
48
48
 
49
- export interface GeminiService {
49
+ export type GeminiService = {
50
50
  /**
51
51
  * Stream the provider's native event vocabulary (post-SSE-decode). For
52
52
  * Gemini, each event is a full `GenerateContentResponse` chunk. Use this
@@ -79,7 +79,7 @@ export class Gemini extends Context.Service<Gemini, GeminiService>()(
79
79
  "@betalyra/effect-uai/providers/google/Gemini",
80
80
  ) {}
81
81
 
82
- export interface Config {
82
+ export type Config = {
83
83
  readonly apiKey: Redacted.Redacted
84
84
  readonly baseUrl?: string
85
85
  }