@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.
@@ -0,0 +1,351 @@
1
+ import { Context, Effect, Encoding, Layer, Match, Redacted, Schema } from "effect"
2
+ import { HttpClient, HttpClientRequest } from "effect/unstable/http"
3
+ import * as AiError from "@effect-uai/core/AiError"
4
+ import type { Embedding, EmbedContentPart, EmbedInput, Usage } from "@effect-uai/core/Embedding"
5
+ import {
6
+ type CommonEmbedManyRequest,
7
+ type CommonEmbedRequest,
8
+ EmbeddingModel,
9
+ type EmbeddingModelService,
10
+ type EmbedManyResponse,
11
+ type EmbedResponse,
12
+ } from "@effect-uai/core/EmbeddingModel"
13
+ import type { ImageSource } from "@effect-uai/core/Image"
14
+ import type { GoogleEmbeddingModel } from "./models.js"
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Public types
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /**
21
+ * Gemini's task-type enum, exposed under our friendly names. Mapped to the
22
+ * wire `taskType` values inside the codec. Honoured by
23
+ * `gemini-embedding-001`; ignored by `gemini-embedding-2` (which expects
24
+ * task instructions in the prompt text instead).
25
+ */
26
+ export type GoogleEmbeddingTask =
27
+ | "query"
28
+ | "document"
29
+ | "similarity"
30
+ | "classification"
31
+ | "clustering"
32
+ | "qa"
33
+ | "fact_verification"
34
+ | "code_query"
35
+
36
+ export type GeminiEmbedRequest = Omit<CommonEmbedRequest, "model" | "task" | "encoding"> & {
37
+ /** Narrows `CommonEmbedRequest.model` to the typed Google union. */
38
+ readonly model: GoogleEmbeddingModel
39
+ /**
40
+ * Widens the cross-provider `"query" | "document"` to the full Google
41
+ * task enum. Optional. Honoured by `gemini-embedding-001`; ignored by
42
+ * `gemini-embedding-2`.
43
+ */
44
+ readonly task?: GoogleEmbeddingTask
45
+ /**
46
+ * Optional document title. Only meaningful for `RETRIEVAL_DOCUMENT`
47
+ * tasks on `gemini-embedding-001`.
48
+ */
49
+ readonly title?: string
50
+ }
51
+
52
+ export type GeminiEmbedManyRequest = Omit<GeminiEmbedRequest, "input"> & {
53
+ readonly inputs: ReadonlyArray<EmbedInput>
54
+ }
55
+
56
+ export type GeminiEmbeddingService = {
57
+ readonly embed: (request: GeminiEmbedRequest) => Effect.Effect<EmbedResponse, AiError.AiError>
58
+ readonly embedMany: (
59
+ request: GeminiEmbedManyRequest,
60
+ ) => Effect.Effect<EmbedManyResponse, AiError.AiError>
61
+ }
62
+
63
+ /**
64
+ * Provider-typed service tag. Yield this when you want Gemini-specific
65
+ * options (full `task` enum, `title`); yield the generic `EmbeddingModel`
66
+ * tag for provider-portable code. Both are registered by `layer`.
67
+ */
68
+ export class GeminiEmbedding extends Context.Service<GeminiEmbedding, GeminiEmbeddingService>()(
69
+ "@betalyra/effect-uai/providers/google/GeminiEmbedding",
70
+ ) {}
71
+
72
+ export type Config = {
73
+ readonly apiKey: Redacted.Redacted
74
+ readonly baseUrl?: string
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Codec - request body
79
+ // ---------------------------------------------------------------------------
80
+
81
+ const taskToWire: Record<GoogleEmbeddingTask, string> = {
82
+ query: "RETRIEVAL_QUERY",
83
+ document: "RETRIEVAL_DOCUMENT",
84
+ similarity: "SEMANTIC_SIMILARITY",
85
+ classification: "CLASSIFICATION",
86
+ clustering: "CLUSTERING",
87
+ qa: "QUESTION_ANSWERING",
88
+ fact_verification: "FACT_VERIFICATION",
89
+ code_query: "CODE_RETRIEVAL_QUERY",
90
+ }
91
+
92
+ type WireTextPart = {
93
+ readonly text: string
94
+ }
95
+ type WireInlineDataPart = {
96
+ readonly inlineData: { readonly mimeType: string; readonly data: string }
97
+ }
98
+ type WirePart = WireTextPart | WireInlineDataPart
99
+ type WireContent = {
100
+ readonly parts: ReadonlyArray<WirePart>
101
+ }
102
+
103
+ const urlRejected = (param: string): AiError.AiError =>
104
+ new AiError.InvalidRequest({
105
+ provider: "gemini",
106
+ param,
107
+ raw: "URL-form image inputs aren't supported; pass base64 or bytes",
108
+ })
109
+
110
+ /**
111
+ * Pre-uploading via Google's Files API isn't free, so we reject `url`
112
+ * sources up front rather than silently dropping them. Users with HTTPS
113
+ * URLs should fetch + pass `bytes` (or `base64`).
114
+ */
115
+ const imageSourceToPart = (param: string) =>
116
+ Match.type<ImageSource>().pipe(
117
+ Match.tag(
118
+ "base64",
119
+ (s): Effect.Effect<WirePart, AiError.AiError> =>
120
+ Effect.succeed({ inlineData: { mimeType: s.mimeType, data: s.base64 } }),
121
+ ),
122
+ Match.tag(
123
+ "bytes",
124
+ (s): Effect.Effect<WirePart, AiError.AiError> =>
125
+ Effect.succeed({
126
+ inlineData: { mimeType: s.mimeType, data: Encoding.encodeBase64(s.bytes) },
127
+ }),
128
+ ),
129
+ Match.tag(
130
+ "url",
131
+ (): Effect.Effect<WirePart, AiError.AiError> => Effect.fail(urlRejected(param)),
132
+ ),
133
+ Match.exhaustive,
134
+ )
135
+
136
+ const contentPartToPart = (
137
+ param: string,
138
+ part: EmbedContentPart,
139
+ ): Effect.Effect<WirePart, AiError.AiError> =>
140
+ "text" in part ? Effect.succeed({ text: part.text }) : imageSourceToPart(param)(part.image)
141
+
142
+ const inputToContent = (input: EmbedInput): Effect.Effect<WireContent, AiError.AiError> => {
143
+ if (typeof input === "string") return Effect.succeed({ parts: [{ text: input }] })
144
+ if ("text" in input) return Effect.succeed({ parts: [{ text: input.text }] })
145
+ if ("image" in input) {
146
+ return imageSourceToPart("input.image")(input.image).pipe(
147
+ Effect.map((part) => ({ parts: [part] })),
148
+ )
149
+ }
150
+ return Effect.forEach(input.content, (p) => contentPartToPart("input.content[].image", p)).pipe(
151
+ Effect.map((parts) => ({ parts })),
152
+ )
153
+ }
154
+
155
+ type WireSingleBody = {
156
+ readonly content: WireContent
157
+ readonly taskType?: string
158
+ readonly outputDimensionality?: number
159
+ readonly title?: string
160
+ }
161
+
162
+ const wireOptions = (
163
+ request: Pick<GeminiEmbedRequest, "task" | "dimensions" | "title">,
164
+ ): Omit<WireSingleBody, "content"> => ({
165
+ ...(request.task !== undefined && { taskType: taskToWire[request.task] }),
166
+ ...(request.dimensions !== undefined && { outputDimensionality: request.dimensions }),
167
+ ...(request.title !== undefined && { title: request.title }),
168
+ })
169
+
170
+ const buildSingleBody = (
171
+ request: GeminiEmbedRequest,
172
+ ): Effect.Effect<WireSingleBody, AiError.AiError> =>
173
+ inputToContent(request.input).pipe(
174
+ Effect.map((content) => ({ content, ...wireOptions(request) })),
175
+ )
176
+
177
+ type WireBatchEntry = WireSingleBody & {
178
+ readonly model: string
179
+ }
180
+ type WireBatchBody = {
181
+ readonly requests: ReadonlyArray<WireBatchEntry>
182
+ }
183
+
184
+ const buildBatchBody = (
185
+ request: GeminiEmbedManyRequest,
186
+ ): Effect.Effect<WireBatchBody, AiError.AiError> => {
187
+ const modelPath = `models/${request.model}`
188
+ const options = wireOptions(request)
189
+ return Effect.forEach(request.inputs, (input) =>
190
+ inputToContent(input).pipe(
191
+ Effect.map((content): WireBatchEntry => ({ model: modelPath, content, ...options })),
192
+ ),
193
+ ).pipe(Effect.map((requests) => ({ requests })))
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Codec - response
198
+ // ---------------------------------------------------------------------------
199
+
200
+ const WireEmbedding = Schema.Struct({
201
+ values: Schema.Array(Schema.Number),
202
+ })
203
+
204
+ const WireSingleResponse = Schema.Struct({
205
+ embedding: WireEmbedding,
206
+ })
207
+
208
+ const WireBatchResponse = Schema.Struct({
209
+ embeddings: Schema.Array(WireEmbedding),
210
+ })
211
+
212
+ const valuesToEmbedding = (values: ReadonlyArray<number>): Embedding => ({
213
+ _tag: "float32",
214
+ vector: Float32Array.from(values),
215
+ })
216
+
217
+ // Gemini's embed endpoints don't return token-count metadata. Estimating
218
+ // client-side would be provider-specific cruft; honest `undefined` is better.
219
+ const emptyUsage: Usage = {}
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // HTTP errors
223
+ // ---------------------------------------------------------------------------
224
+
225
+ const httpStatusError = (status: number, body: string): AiError.AiError => {
226
+ const provider = "gemini"
227
+ const raw = body
228
+ if (status === 429) return new AiError.RateLimited({ provider, raw })
229
+ if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })
230
+ if (status === 401) return new AiError.AuthFailed({ provider, subtype: "auth", raw })
231
+ if (status === 403) return new AiError.AuthFailed({ provider, subtype: "permission", raw })
232
+ if (status === 402) return new AiError.AuthFailed({ provider, subtype: "billing", raw })
233
+ if (status === 413) return new AiError.ContextLengthExceeded({ provider, raw })
234
+ if (status >= 500) return new AiError.Unavailable({ provider, status, raw })
235
+ return new AiError.InvalidRequest({ provider, raw })
236
+ }
237
+
238
+ const transportFailure = (cause: unknown): AiError.AiError =>
239
+ new AiError.Unavailable({ provider: "gemini", raw: cause })
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // HTTP plumbing - send a JSON body, error-map status, return parsed JSON.
243
+ // ---------------------------------------------------------------------------
244
+
245
+ const postJson = (
246
+ cfg: Config,
247
+ url: string,
248
+ body: unknown,
249
+ ): Effect.Effect<unknown, AiError.AiError, HttpClient.HttpClient> =>
250
+ Effect.gen(function* () {
251
+ const client = yield* HttpClient.HttpClient
252
+ const httpRequest = HttpClientRequest.post(url).pipe(
253
+ HttpClientRequest.setHeader("x-goog-api-key", Redacted.value(cfg.apiKey)),
254
+ HttpClientRequest.bodyJsonUnsafe(body),
255
+ )
256
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
257
+ if (response.status >= 400) {
258
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""))
259
+ return yield* Effect.fail(httpStatusError(response.status, text))
260
+ }
261
+ return yield* response.json.pipe(Effect.mapError(transportFailure))
262
+ })
263
+
264
+ const decodeSingle = (json: unknown) =>
265
+ Schema.decodeUnknownEffect(WireSingleResponse)(json).pipe(Effect.mapError(transportFailure))
266
+
267
+ const decodeBatch = (json: unknown) =>
268
+ Schema.decodeUnknownEffect(WireBatchResponse)(json).pipe(Effect.mapError(transportFailure))
269
+
270
+ const baseUrl = (cfg: Config): string =>
271
+ cfg.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta"
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // Service implementation
275
+ // ---------------------------------------------------------------------------
276
+
277
+ const embedImpl =
278
+ (cfg: Config) =>
279
+ (
280
+ request: GeminiEmbedRequest,
281
+ ): Effect.Effect<EmbedResponse, AiError.AiError, HttpClient.HttpClient> =>
282
+ buildSingleBody(request).pipe(
283
+ Effect.flatMap((body) =>
284
+ postJson(cfg, `${baseUrl(cfg)}/models/${request.model}:embedContent`, body),
285
+ ),
286
+ Effect.flatMap(decodeSingle),
287
+ Effect.map(
288
+ (decoded): EmbedResponse => ({
289
+ embedding: valuesToEmbedding(decoded.embedding.values),
290
+ usage: emptyUsage,
291
+ }),
292
+ ),
293
+ )
294
+
295
+ const embedManyImpl =
296
+ (cfg: Config) =>
297
+ (
298
+ request: GeminiEmbedManyRequest,
299
+ ): Effect.Effect<EmbedManyResponse, AiError.AiError, HttpClient.HttpClient> =>
300
+ buildBatchBody(request).pipe(
301
+ Effect.flatMap((body) =>
302
+ postJson(cfg, `${baseUrl(cfg)}/models/${request.model}:batchEmbedContents`, body),
303
+ ),
304
+ Effect.flatMap(decodeBatch),
305
+ Effect.map(
306
+ (decoded): EmbedManyResponse => ({
307
+ embeddings: decoded.embeddings.map((e) => valuesToEmbedding(e.values)),
308
+ usage: emptyUsage,
309
+ }),
310
+ ),
311
+ )
312
+
313
+ // ---------------------------------------------------------------------------
314
+ // Constructors
315
+ // ---------------------------------------------------------------------------
316
+
317
+ /**
318
+ * Build a `GeminiEmbeddingService` value. For Layer-based setup, prefer
319
+ * `layer`.
320
+ */
321
+ export const make = (
322
+ cfg: Config,
323
+ ): Effect.Effect<GeminiEmbeddingService, never, HttpClient.HttpClient> =>
324
+ Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
325
+ embed: (request) =>
326
+ embedImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
327
+ embedMany: (request) =>
328
+ embedManyImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
329
+ }))
330
+
331
+ /**
332
+ * Layer that registers both the provider-specific `GeminiEmbedding` tag
333
+ * and the generic `EmbeddingModel` tag, sharing one underlying
334
+ * implementation.
335
+ */
336
+ export const layer = (
337
+ cfg: Config,
338
+ ): Layer.Layer<GeminiEmbedding | EmbeddingModel, never, HttpClient.HttpClient> => {
339
+ const typed = Layer.effect(GeminiEmbedding, make(cfg))
340
+ const generic = Layer.effect(
341
+ EmbeddingModel,
342
+ Effect.map(
343
+ make(cfg),
344
+ (s): EmbeddingModelService => ({
345
+ embed: (req: CommonEmbedRequest) => s.embed(req as GeminiEmbedRequest),
346
+ embedMany: (req: CommonEmbedManyRequest) => s.embedMany(req as GeminiEmbedManyRequest),
347
+ }),
348
+ ),
349
+ )
350
+ return Layer.merge(typed, generic)
351
+ }
package/src/codec.ts CHANGED
@@ -1,6 +1,5 @@
1
- import { Array as Arr, Match, Option, Result, Schema, pipe } from "effect"
2
- import type { ContentBlock, Item, Message } from "@effect-uai/core/Items"
3
- import { matchType } from "@effect-uai/core/Match"
1
+ import { Array as Arr, Encoding, Match, Option, Result, Schema, pipe } from "effect"
2
+ import type { ContentBlock, InputImage, Item, Message } from "@effect-uai/core/Items"
4
3
  import type { Turn } from "@effect-uai/core/Turn"
5
4
 
6
5
  // ---------------------------------------------------------------------------
@@ -53,20 +52,20 @@ type RequestPart =
53
52
  | { readonly text: string }
54
53
  | { readonly inlineData: { readonly mimeType: string; readonly data: string } }
55
54
 
56
- interface RequestContent {
55
+ type RequestContent = {
57
56
  readonly role: "user" | "model"
58
57
  readonly parts: ReadonlyArray<RequestPart>
59
58
  }
60
59
 
61
- interface RequestSystemInstruction {
60
+ type RequestSystemInstruction = {
62
61
  readonly parts: ReadonlyArray<{ readonly text: string }>
63
62
  }
64
63
 
65
- export interface ThinkingConfig {
64
+ export type ThinkingConfig = {
66
65
  readonly thinkingBudget: number
67
66
  }
68
67
 
69
- export interface GenerationConfig {
68
+ export type GenerationConfig = {
70
69
  readonly temperature?: number
71
70
  readonly maxOutputTokens?: number
72
71
  readonly topP?: number
@@ -77,47 +76,55 @@ export interface GenerationConfig {
77
76
  readonly responseJsonSchema?: Record<string, unknown>
78
77
  }
79
78
 
80
- export interface RequestBody {
79
+ export type RequestBody = {
81
80
  readonly contents: ReadonlyArray<RequestContent>
82
81
  readonly systemInstruction?: RequestSystemInstruction
83
82
  readonly generationConfig?: GenerationConfig
84
83
  }
85
84
 
86
85
  const blockText = Match.type<ContentBlock>().pipe(
87
- matchType("input_text", (b) => b.text),
88
- matchType("input_image", () => ""),
89
- matchType("output_text", (b) => b.text),
90
- matchType("refusal", (b) => b.text),
91
- Match.exhaustive,
86
+ Match.discriminatorsExhaustive("type")({
87
+ input_text: (b) => b.text,
88
+ input_image: () => "",
89
+ output_text: (b) => b.text,
90
+ refusal: (b) => b.text,
91
+ }),
92
92
  )
93
93
 
94
94
  const messageText = (message: Message): string => message.content.map(blockText).join("")
95
95
 
96
- const blockToParts = Match.type<ContentBlock>().pipe(
97
- matchType(
98
- "input_text",
99
- (b): ReadonlyArray<RequestPart> => (b.text.length === 0 ? [] : [{ text: b.text }]),
100
- ),
101
- matchType(
102
- "input_image",
103
- (b): ReadonlyArray<RequestPart> =>
104
- // Gemini's `fileData` form expects a pre-uploaded Files API URI, not an
105
- // arbitrary HTTPS URL - we'd need to fetch + re-upload to support that.
106
- // Skip URL-form images for now; document as a follow-up.
107
- b.source._tag === "base64"
108
- ? [{ inlineData: { mimeType: b.source.media_type, data: b.source.data } }]
109
- : [],
96
+ /**
97
+ * Gemini's `inlineData` form expects a base64 payload. URL-form images
98
+ * would need to go through Gemini's Files API (upload then `fileData`
99
+ * with the returned URI); pre-uploading isn't free, so we skip those for
100
+ * now and document as a follow-up.
101
+ */
102
+ const imageSourceToParts = Match.type<InputImage["source"]>().pipe(
103
+ Match.tag("url", (): ReadonlyArray<RequestPart> => []),
104
+ Match.tag(
105
+ "base64",
106
+ (s): ReadonlyArray<RequestPart> => [{ inlineData: { mimeType: s.mimeType, data: s.base64 } }],
110
107
  ),
111
- matchType(
112
- "output_text",
113
- (b): ReadonlyArray<RequestPart> => (b.text.length === 0 ? [] : [{ text: b.text }]),
108
+ Match.tag(
109
+ "bytes",
110
+ (s): ReadonlyArray<RequestPart> => [
111
+ { inlineData: { mimeType: s.mimeType, data: Encoding.encodeBase64(s.bytes) } },
112
+ ],
114
113
  ),
115
- // Refusals are assistant-side content; they don't round-trip into Gemini's
116
- // request body as parts. Skip.
117
- matchType("refusal", (): ReadonlyArray<RequestPart> => []),
118
114
  Match.exhaustive,
119
115
  )
120
116
 
117
+ const blockToParts = Match.type<ContentBlock>().pipe(
118
+ Match.discriminatorsExhaustive("type")({
119
+ input_text: (b): ReadonlyArray<RequestPart> => (b.text.length === 0 ? [] : [{ text: b.text }]),
120
+ input_image: (b): ReadonlyArray<RequestPart> => imageSourceToParts(b.source),
121
+ output_text: (b): ReadonlyArray<RequestPart> => (b.text.length === 0 ? [] : [{ text: b.text }]),
122
+ // Refusals are assistant-side content; they don't round-trip into Gemini's
123
+ // request body as parts. Skip.
124
+ refusal: (): ReadonlyArray<RequestPart> => [],
125
+ }),
126
+ )
127
+
121
128
  const messageToContent = (message: Message): Result.Result<RequestContent, void> => {
122
129
  const parts = pipe(message.content, Arr.flatMap(blockToParts))
123
130
  if (parts.length === 0) return Result.failVoid
@@ -173,7 +180,7 @@ const finishReasonToStop = (reason: Option.Option<string>): Turn["stop_reason"]
173
180
  onSome: (r) => (r === "MAX_TOKENS" ? ("max_tokens" as const) : ("stop" as const)),
174
181
  })
175
182
 
176
- export interface Accumulator {
183
+ export type Accumulator = {
177
184
  readonly text: string
178
185
  readonly reasoning: string
179
186
  readonly finishReason: Option.Option<string>
@@ -202,7 +209,7 @@ export type ChunkPart =
202
209
  | { readonly kind: "text"; readonly text: string }
203
210
  | { readonly kind: "reasoning"; readonly text: string }
204
211
 
205
- export interface ChunkResult {
212
+ export type ChunkResult = {
206
213
  readonly accumulator: Accumulator
207
214
  readonly parts: ReadonlyArray<ChunkPart>
208
215
  readonly finished: boolean
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
- export * from "./Gemini.js"
2
- export * from "./models.js"
1
+ export * as Gemini from "./Gemini.js"
2
+ export * as GeminiEmbedding from "./GeminiEmbedding.js"
3
3
  export * as codec from "./codec.js"
4
+ export * from "./models.js"
package/src/models.ts CHANGED
@@ -16,3 +16,23 @@ export type GoogleModel =
16
16
  | "gemini-2.5-flash-lite"
17
17
  // eslint-disable-next-line @typescript-eslint/ban-types
18
18
  | (string & {})
19
+
20
+ /**
21
+ * Known Gemini embedding model identifiers.
22
+ *
23
+ * - `gemini-embedding-2` — natively multimodal (text, image, audio, video,
24
+ * PDF). Does *not* honour the `taskType` field; instead prepend a task
25
+ * instruction to the text yourself.
26
+ * - `gemini-embedding-001` — text-only, supports the full `taskType` enum
27
+ * (`RETRIEVAL_QUERY` / `RETRIEVAL_DOCUMENT` / classification / etc.).
28
+ *
29
+ * The `(string & {})` tail accepts any string so newly-released models
30
+ * work without an SDK update.
31
+ *
32
+ * Reference: https://ai.google.dev/gemini-api/docs/embeddings
33
+ */
34
+ export type GoogleEmbeddingModel =
35
+ | "gemini-embedding-2"
36
+ | "gemini-embedding-001"
37
+ // eslint-disable-next-line @typescript-eslint/ban-types
38
+ | (string & {})
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/codec.ts","../src/models.ts","../src/Gemini.ts"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","names":["Arr"],"sources":["../src/codec.ts","../src/Gemini.ts"],"sourcesContent":["import { Array as Arr, Match, Option, Result, Schema, pipe } from \"effect\"\nimport type { ContentBlock, Item, Message } from \"@effect-uai/core/Items\"\nimport { matchType } from \"@effect-uai/core/Match\"\nimport type { Turn } from \"@effect-uai/core/Turn\"\n\n// ---------------------------------------------------------------------------\n// Wire schemas - the subset of Gemini's streamGenerateContent payload we\n// consume. Reference: https://ai.google.dev/api/generate-content\n// ---------------------------------------------------------------------------\n\nconst TextPart = Schema.Struct({\n text: Schema.String,\n /**\n * Gemini's flag for chain-of-thought parts. When `true`, the part is\n * reasoning trace, not the model's user-facing answer. Maps onto\n * `reasoning_delta { kind: \"trace\" }` in the canonical view.\n */\n thought: Schema.optional(Schema.Boolean),\n})\n\nconst Part = Schema.Union([TextPart])\n\nconst Content = Schema.Struct({\n role: Schema.optional(Schema.String),\n parts: Schema.optional(Schema.Array(Part)),\n})\n\nconst Candidate = Schema.Struct({\n content: Schema.optional(Content),\n finishReason: Schema.optional(Schema.String),\n index: Schema.optional(Schema.Number),\n})\n\nconst UsageMetadata = Schema.Struct({\n promptTokenCount: Schema.optional(Schema.Number),\n candidatesTokenCount: Schema.optional(Schema.Number),\n totalTokenCount: Schema.optional(Schema.Number),\n cachedContentTokenCount: Schema.optional(Schema.Number),\n thoughtsTokenCount: Schema.optional(Schema.Number),\n})\n\nexport const WireChunk = Schema.Struct({\n candidates: Schema.optional(Schema.Array(Candidate)),\n usageMetadata: Schema.optional(UsageMetadata),\n})\nexport type WireChunk = typeof WireChunk.Type\n\n// ---------------------------------------------------------------------------\n// History → request body\n// ---------------------------------------------------------------------------\n\ntype RequestPart =\n | { readonly text: string }\n | { readonly inlineData: { readonly mimeType: string; readonly data: string } }\n\ninterface RequestContent {\n readonly role: \"user\" | \"model\"\n readonly parts: ReadonlyArray<RequestPart>\n}\n\ninterface RequestSystemInstruction {\n readonly parts: ReadonlyArray<{ readonly text: string }>\n}\n\nexport interface ThinkingConfig {\n readonly thinkingBudget: number\n}\n\nexport interface GenerationConfig {\n readonly temperature?: number\n readonly maxOutputTokens?: number\n readonly topP?: number\n readonly thinkingConfig?: ThinkingConfig\n /** Set together with `responseJsonSchema` to constrain output to JSON. */\n readonly responseMimeType?: string\n /** JSON Schema constraint on the output. */\n readonly responseJsonSchema?: Record<string, unknown>\n}\n\nexport interface RequestBody {\n readonly contents: ReadonlyArray<RequestContent>\n readonly systemInstruction?: RequestSystemInstruction\n readonly generationConfig?: GenerationConfig\n}\n\nconst blockText = Match.type<ContentBlock>().pipe(\n matchType(\"input_text\", (b) => b.text),\n matchType(\"input_image\", () => \"\"),\n matchType(\"output_text\", (b) => b.text),\n matchType(\"refusal\", (b) => b.text),\n Match.exhaustive,\n)\n\nconst messageText = (message: Message): string => message.content.map(blockText).join(\"\")\n\nconst blockToParts = Match.type<ContentBlock>().pipe(\n matchType(\n \"input_text\",\n (b): ReadonlyArray<RequestPart> => (b.text.length === 0 ? [] : [{ text: b.text }]),\n ),\n matchType(\n \"input_image\",\n (b): ReadonlyArray<RequestPart> =>\n // Gemini's `fileData` form expects a pre-uploaded Files API URI, not an\n // arbitrary HTTPS URL - we'd need to fetch + re-upload to support that.\n // Skip URL-form images for now; document as a follow-up.\n b.source._tag === \"base64\"\n ? [{ inlineData: { mimeType: b.source.media_type, data: b.source.data } }]\n : [],\n ),\n matchType(\n \"output_text\",\n (b): ReadonlyArray<RequestPart> => (b.text.length === 0 ? [] : [{ text: b.text }]),\n ),\n // Refusals are assistant-side content; they don't round-trip into Gemini's\n // request body as parts. Skip.\n matchType(\"refusal\", (): ReadonlyArray<RequestPart> => []),\n Match.exhaustive,\n)\n\nconst messageToContent = (message: Message): Result.Result<RequestContent, void> => {\n const parts = pipe(message.content, Arr.flatMap(blockToParts))\n if (parts.length === 0) return Result.failVoid\n return Match.value(message.role).pipe(\n Match.when(\"user\", () => Result.succeed({ role: \"user\" as const, parts })),\n Match.when(\"assistant\", () => Result.succeed({ role: \"model\" as const, parts })),\n Match.when(\"system\", () => Result.failVoid),\n Match.exhaustive,\n )\n}\n\nconst systemMessageText = (message: Message): Result.Result<string, void> => {\n if (message.role !== \"system\") return Result.failVoid\n const text = messageText(message)\n return text.length === 0 ? Result.failVoid : Result.succeed(text)\n}\n\nconst messages = (history: ReadonlyArray<Item>): ReadonlyArray<Message> =>\n pipe(\n history,\n Arr.filterMap((item) => (item.type === \"message\" ? Result.succeed(item) : Result.failVoid)),\n )\n\nexport const buildRequestBody = (\n history: ReadonlyArray<Item>,\n generationConfig: Option.Option<GenerationConfig>,\n): RequestBody => {\n const msgs = messages(history)\n const systemTexts = pipe(msgs, Arr.filterMap(systemMessageText))\n const contents = pipe(msgs, Arr.filterMap(messageToContent))\n return {\n contents,\n ...(systemTexts.length > 0 && {\n systemInstruction: { parts: [{ text: systemTexts.join(\"\\n\") }] },\n }),\n ...Option.match(generationConfig, {\n onNone: () => ({}),\n onSome: (cfg) => ({ generationConfig: cfg }),\n }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Stream-level state - accumulate chunk text + final usage/finish.\n//\n// `Accumulator` is immutable; `ingestChunk` returns a fresh one per chunk.\n// Drive it via `Stream.mapAccum` in the consumer.\n// ---------------------------------------------------------------------------\n\nconst finishReasonToStop = (reason: Option.Option<string>): Turn[\"stop_reason\"] =>\n Option.match(reason, {\n onNone: () => \"stop\" as const,\n onSome: (r) => (r === \"MAX_TOKENS\" ? (\"max_tokens\" as const) : (\"stop\" as const)),\n })\n\nexport interface Accumulator {\n readonly text: string\n readonly reasoning: string\n readonly finishReason: Option.Option<string>\n readonly usage: {\n readonly input_tokens?: number\n readonly output_tokens?: number\n readonly total_tokens?: number\n readonly input_tokens_details?: { readonly cached_tokens?: number }\n readonly output_tokens_details?: { readonly reasoning_tokens?: number }\n }\n}\n\nexport const emptyAccumulator: Accumulator = {\n text: \"\",\n reasoning: \"\",\n finishReason: Option.none(),\n usage: {},\n}\n\n/**\n * One part's worth of streamable text: either the model's answer\n * (`kind: \"text\"`) or chain-of-thought (`kind: \"reasoning\"`). Emitted\n * in wire order so consumers can interleave faithfully.\n */\nexport type ChunkPart =\n | { readonly kind: \"text\"; readonly text: string }\n | { readonly kind: \"reasoning\"; readonly text: string }\n\nexport interface ChunkResult {\n readonly accumulator: Accumulator\n readonly parts: ReadonlyArray<ChunkPart>\n readonly finished: boolean\n}\n\nconst chunkParts = (chunk: WireChunk): ReadonlyArray<ChunkPart> =>\n pipe(\n chunk.candidates?.[0]?.content?.parts ?? [],\n Arr.filterMap((p) =>\n p.text.length === 0\n ? Result.failVoid\n : Result.succeed({\n kind: p.thought === true ? (\"reasoning\" as const) : (\"text\" as const),\n text: p.text,\n }),\n ),\n )\n\nconst sumByKind = (parts: ReadonlyArray<ChunkPart>, kind: ChunkPart[\"kind\"]): string =>\n pipe(\n parts,\n Arr.filterMap((p) => (p.kind === kind ? Result.succeed(p.text) : Result.failVoid)),\n ).join(\"\")\n\nconst mergeUsage = (\n prev: Accumulator[\"usage\"],\n next: WireChunk[\"usageMetadata\"],\n): Accumulator[\"usage\"] =>\n next === undefined\n ? prev\n : {\n ...prev,\n ...(next.promptTokenCount !== undefined && { input_tokens: next.promptTokenCount }),\n ...(next.candidatesTokenCount !== undefined && {\n output_tokens: next.candidatesTokenCount,\n }),\n ...(next.totalTokenCount !== undefined && { total_tokens: next.totalTokenCount }),\n ...(next.cachedContentTokenCount !== undefined && {\n input_tokens_details: { cached_tokens: next.cachedContentTokenCount },\n }),\n ...(next.thoughtsTokenCount !== undefined && {\n output_tokens_details: { reasoning_tokens: next.thoughtsTokenCount },\n }),\n }\n\nexport const ingestChunk = (acc: Accumulator, chunk: WireChunk): ChunkResult => {\n const parts = chunkParts(chunk)\n const finishReason = Option.fromNullishOr(chunk.candidates?.[0]?.finishReason)\n return {\n parts,\n finished: Option.isSome(finishReason),\n accumulator: {\n text: acc.text + sumByKind(parts, \"text\"),\n reasoning: acc.reasoning + sumByKind(parts, \"reasoning\"),\n finishReason: Option.orElse(finishReason, () => acc.finishReason),\n usage: mergeUsage(acc.usage, chunk.usageMetadata),\n },\n }\n}\n\nexport const accumulatorToTurn = (acc: Accumulator): Turn => ({\n stop_reason: finishReasonToStop(acc.finishReason),\n usage: { ...acc.usage },\n items: [\n ...(acc.reasoning.length > 0 ? [{ type: \"reasoning\" as const, summary: acc.reasoning }] : []),\n ...(acc.text.length === 0\n ? []\n : [\n {\n type: \"message\" as const,\n role: \"assistant\" as const,\n content: [{ type: \"output_text\" as const, text: acc.text }],\n },\n ]),\n ],\n})\n","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 interface GeminiRequest extends 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 interface 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 interface 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":";;;;;;;;;;;;;;;;AAUA,MAAM,WAAW,OAAO,OAAO;CAC7B,MAAM,OAAO;;;;;;CAMb,SAAS,OAAO,SAAS,OAAO,QAAQ;CACzC,CAAC;AAEF,MAAM,OAAO,OAAO,MAAM,CAAC,SAAS,CAAC;AAErC,MAAM,UAAU,OAAO,OAAO;CAC5B,MAAM,OAAO,SAAS,OAAO,OAAO;CACpC,OAAO,OAAO,SAAS,OAAO,MAAM,KAAK,CAAC;CAC3C,CAAC;AAEF,MAAM,YAAY,OAAO,OAAO;CAC9B,SAAS,OAAO,SAAS,QAAQ;CACjC,cAAc,OAAO,SAAS,OAAO,OAAO;CAC5C,OAAO,OAAO,SAAS,OAAO,OAAO;CACtC,CAAC;AAEF,MAAM,gBAAgB,OAAO,OAAO;CAClC,kBAAkB,OAAO,SAAS,OAAO,OAAO;CAChD,sBAAsB,OAAO,SAAS,OAAO,OAAO;CACpD,iBAAiB,OAAO,SAAS,OAAO,OAAO;CAC/C,yBAAyB,OAAO,SAAS,OAAO,OAAO;CACvD,oBAAoB,OAAO,SAAS,OAAO,OAAO;CACnD,CAAC;AAEF,MAAa,YAAY,OAAO,OAAO;CACrC,YAAY,OAAO,SAAS,OAAO,MAAM,UAAU,CAAC;CACpD,eAAe,OAAO,SAAS,cAAc;CAC9C,CAAC;AAyCF,MAAM,YAAY,MAAM,MAAoB,CAAC,KAC3C,UAAU,eAAe,MAAM,EAAE,KAAK,EACtC,UAAU,qBAAqB,GAAG,EAClC,UAAU,gBAAgB,MAAM,EAAE,KAAK,EACvC,UAAU,YAAY,MAAM,EAAE,KAAK,EACnC,MAAM,WACP;AAED,MAAM,eAAe,YAA6B,QAAQ,QAAQ,IAAI,UAAU,CAAC,KAAK,GAAG;AAEzF,MAAM,eAAe,MAAM,MAAoB,CAAC,KAC9C,UACE,eACC,MAAmC,EAAE,KAAK,WAAW,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAClF,EACD,UACE,gBACC,MAIC,EAAE,OAAO,SAAS,WACd,CAAC,EAAE,YAAY;CAAE,UAAU,EAAE,OAAO;CAAY,MAAM,EAAE,OAAO;CAAM,EAAE,CAAC,GACxE,EAAE,CACT,EACD,UACE,gBACC,MAAmC,EAAE,KAAK,WAAW,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAClF,EAGD,UAAU,iBAA6C,EAAE,CAAC,EAC1D,MAAM,WACP;AAED,MAAM,oBAAoB,YAA0D;CAClF,MAAM,QAAQ,KAAK,QAAQ,SAASA,MAAI,QAAQ,aAAa,CAAC;AAC9D,KAAI,MAAM,WAAW,EAAG,QAAO,OAAO;AACtC,QAAO,MAAM,MAAM,QAAQ,KAAK,CAAC,KAC/B,MAAM,KAAK,cAAc,OAAO,QAAQ;EAAE,MAAM;EAAiB;EAAO,CAAC,CAAC,EAC1E,MAAM,KAAK,mBAAmB,OAAO,QAAQ;EAAE,MAAM;EAAkB;EAAO,CAAC,CAAC,EAChF,MAAM,KAAK,gBAAgB,OAAO,SAAS,EAC3C,MAAM,WACP;;AAGH,MAAM,qBAAqB,YAAkD;AAC3E,KAAI,QAAQ,SAAS,SAAU,QAAO,OAAO;CAC7C,MAAM,OAAO,YAAY,QAAQ;AACjC,QAAO,KAAK,WAAW,IAAI,OAAO,WAAW,OAAO,QAAQ,KAAK;;AAGnE,MAAM,YAAY,YAChB,KACE,SACAA,MAAI,WAAW,SAAU,KAAK,SAAS,YAAY,OAAO,QAAQ,KAAK,GAAG,OAAO,SAAU,CAC5F;AAEH,MAAa,oBACX,SACA,qBACgB;CAChB,MAAM,OAAO,SAAS,QAAQ;CAC9B,MAAM,cAAc,KAAK,MAAMA,MAAI,UAAU,kBAAkB,CAAC;AAEhE,QAAO;EACL,UAFe,KAAK,MAAMA,MAAI,UAAU,iBAAiB,CAEjD;EACR,GAAI,YAAY,SAAS,KAAK,EAC5B,mBAAmB,EAAE,OAAO,CAAC,EAAE,MAAM,YAAY,KAAK,KAAK,EAAE,CAAC,EAAE,EACjE;EACD,GAAG,OAAO,MAAM,kBAAkB;GAChC,eAAe,EAAE;GACjB,SAAS,SAAS,EAAE,kBAAkB,KAAK;GAC5C,CAAC;EACH;;AAUH,MAAM,sBAAsB,WAC1B,OAAO,MAAM,QAAQ;CACnB,cAAc;CACd,SAAS,MAAO,MAAM,eAAgB,eAA0B;CACjE,CAAC;AAeJ,MAAa,mBAAgC;CAC3C,MAAM;CACN,WAAW;CACX,cAAc,OAAO,MAAM;CAC3B,OAAO,EAAE;CACV;AAiBD,MAAM,cAAc,UAClB,KACE,MAAM,aAAa,IAAI,SAAS,SAAS,EAAE,EAC3CA,MAAI,WAAW,MACb,EAAE,KAAK,WAAW,IACd,OAAO,WACP,OAAO,QAAQ;CACb,MAAM,EAAE,YAAY,OAAQ,cAAyB;CACrD,MAAM,EAAE;CACT,CAAC,CACP,CACF;AAEH,MAAM,aAAa,OAAiC,SAClD,KACE,OACAA,MAAI,WAAW,MAAO,EAAE,SAAS,OAAO,OAAO,QAAQ,EAAE,KAAK,GAAG,OAAO,SAAU,CACnF,CAAC,KAAK,GAAG;AAEZ,MAAM,cACJ,MACA,SAEA,SAAS,KAAA,IACL,OACA;CACE,GAAG;CACH,GAAI,KAAK,qBAAqB,KAAA,KAAa,EAAE,cAAc,KAAK,kBAAkB;CAClF,GAAI,KAAK,yBAAyB,KAAA,KAAa,EAC7C,eAAe,KAAK,sBACrB;CACD,GAAI,KAAK,oBAAoB,KAAA,KAAa,EAAE,cAAc,KAAK,iBAAiB;CAChF,GAAI,KAAK,4BAA4B,KAAA,KAAa,EAChD,sBAAsB,EAAE,eAAe,KAAK,yBAAyB,EACtE;CACD,GAAI,KAAK,uBAAuB,KAAA,KAAa,EAC3C,uBAAuB,EAAE,kBAAkB,KAAK,oBAAoB,EACrE;CACF;AAEP,MAAa,eAAe,KAAkB,UAAkC;CAC9E,MAAM,QAAQ,WAAW,MAAM;CAC/B,MAAM,eAAe,OAAO,cAAc,MAAM,aAAa,IAAI,aAAa;AAC9E,QAAO;EACL;EACA,UAAU,OAAO,OAAO,aAAa;EACrC,aAAa;GACX,MAAM,IAAI,OAAO,UAAU,OAAO,OAAO;GACzC,WAAW,IAAI,YAAY,UAAU,OAAO,YAAY;GACxD,cAAc,OAAO,OAAO,oBAAoB,IAAI,aAAa;GACjE,OAAO,WAAW,IAAI,OAAO,MAAM,cAAc;GAClD;EACF;;AAGH,MAAa,qBAAqB,SAA4B;CAC5D,aAAa,mBAAmB,IAAI,aAAa;CACjD,OAAO,EAAE,GAAG,IAAI,OAAO;CACvB,OAAO,CACL,GAAI,IAAI,UAAU,SAAS,IAAI,CAAC;EAAE,MAAM;EAAsB,SAAS,IAAI;EAAW,CAAC,GAAG,EAAE,EAC5F,GAAI,IAAI,KAAK,WAAW,IACpB,EAAE,GACF,CACE;EACE,MAAM;EACN,MAAM;EACN,SAAS,CAAC;GAAE,MAAM;GAAwB,MAAM,IAAI;GAAM,CAAC;EAC5D,CACF,CACN;CACF;;;;;;;;;AC7PD,MAAa,gBAAgB;;;;;;AAkD7B,IAAa,SAAb,cAA4B,QAAQ,SAAgC,CAClE,+CACD,CAAC;AAWF,MAAM,yBAAyB,YAA4D;CACzF,MAAM,MAAwB;EAC5B,GAAI,QAAQ,gBAAgB,KAAA,KAAa,EAAE,aAAa,QAAQ,aAAa;EAC7E,GAAI,QAAQ,oBAAoB,KAAA,KAAa,EAAE,iBAAiB,QAAQ,iBAAiB;EACzF,GAAI,QAAQ,SAAS,KAAA,KAAa,EAAE,MAAM,QAAQ,MAAM;EACxD,GAAI,QAAQ,mBAAmB,KAAA,KAAa,EAC1C,gBAAgB,EAAE,gBAAgB,QAAQ,gBAAgB,EAC3D;EACD,GAAI,QAAQ,eAAe,KAAA,KAAa;GACtC,kBAAkB;GAClB,oBAAoB,QAAQ,WAAW,OAAO,aAAa,WAAW,MAAM,EAC1E,QAAQ,iBACT,CAAC;GACH;EACF;AACD,QAAO,OAAO,KAAK,IAAI,CAAC,WAAW,IAAI,OAAO,MAAM,GAAG,OAAO,KAAK,IAAI;;AAOzE,MAAM,cAAc,OAAO,oBAAoB,UAAU;;;;;;;;AASzD,MAAM,mBAAmB,OACvB,OAAO,IAAI;CACT,WAAW,KAAK,MAAM,GAAG,KAAK;CAC9B,QAAQ,UAAU,IAAI,eAAe;EAAE,MAAM,GAAG;EAAM;EAAO,CAAC;CAC/D,CAAC,CAAC,KAAK,OAAO,QAAQ,YAAY,CAAC;AAMtC,MAAM,mBAAmB,QAAgB,SAAkC;CACzE,MAAM,WAAW;CACjB,MAAM,MAAM;AACZ,KAAI,WAAW,IAAK,QAAO,IAAI,QAAQ,YAAY;EAAE;EAAU;EAAK,CAAC;AACrE,KAAI,WAAW,OAAO,WAAW,IAAK,QAAO,IAAI,QAAQ,QAAQ;EAAE;EAAU;EAAK,CAAC;AACnF,KAAI,WAAW,IAAK,QAAO,IAAI,QAAQ,WAAW;EAAE;EAAU,SAAS;EAAQ;EAAK,CAAC;AACrF,KAAI,WAAW,IAAK,QAAO,IAAI,QAAQ,WAAW;EAAE;EAAU,SAAS;EAAc;EAAK,CAAC;AAC3F,KAAI,WAAW,IAAK,QAAO,IAAI,QAAQ,WAAW;EAAE;EAAU,SAAS;EAAW;EAAK,CAAC;AACxF,KAAI,WAAW,IAAK,QAAO,IAAI,QAAQ,sBAAsB;EAAE;EAAU;EAAK,CAAC;AAC/E,KAAI,UAAU,IAAK,QAAO,IAAI,QAAQ,YAAY;EAAE;EAAU;EAAQ;EAAK,CAAC;AAC5E,QAAO,IAAI,QAAQ,eAAe;EAAE;EAAU;EAAK,CAAC;;AAGtD,MAAM,qBAAqB,QAAgB;CACzC,MAAM,UAAU,IAAI,WAAW;AAC/B,SACE,YAEA,OAAO,OACL,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAO,WAAW;EACjC,MAAM,MAAM,GAAG,QAAQ,UAAU,QAAQ,MAAM;EAC/C,MAAM,mBAAmB,sBAAsB,QAAQ;EACvD,MAAM,OAAO,iBAAiB,QAAQ,SAAS,iBAAiB;EAChE,MAAM,cAAc,kBAAkB,KAAK,IAAI,CAAC,KAC9C,kBAAkB,UAAU,kBAAkB,SAAS,MAAM,IAAI,OAAO,CAAC,EACzE,kBAAkB,eAAe,KAAK,EACtC,kBAAkB,OAAO,oBAAoB,CAC9C;EACD,MAAM,WAAW,OAAO,OACrB,QAAQ,YAAY,CACpB,KACC,OAAO,UACJ,UACC,IAAI,QAAQ,YAAY;GAAE,UAAU;GAAU,KAAK;GAAO,CAAC,CAC9D,CACF;AACH,MAAI,SAAS,UAAU,KAAK;GAC1B,MAAM,OAAO,OAAO,SAAS,KAAK,KAAK,OAAO,oBAAoB,GAAG,CAAC;AACtE,UAAO,OAAO,KAAK,gBAAgB,SAAS,QAAQ,KAAK,CAAC;;AAG5D,SAAO,SAAS,OAAO,KACrB,OAAO,UACJ,UAA2B,IAAI,QAAQ,YAAY;GAAE,UAAU;GAAU,KAAK;GAAO,CAAC,CACxF,EACD,IAAI,WACJ,OAAO,WAAW,OAChB,gBAAgB,GAAG,CAAC,KAClB,OAAO,UACJ,UACC,IAAI,QAAQ,YAAY;GAAE,UAAU;GAAU,KAAK;GAAO,CAAC,CAC9D,CACF,CACF,CACF;GACD,CACH;;;;;;;AAQL,MAAa,eACX,MAEA,EAAE,KACA,OAAO,eACC,mBACL,KAAK,UAAU;CACd,MAAM,SAAS,YAAY,KAAK,MAAM;CACtC,MAAM,aAAa,OAAO,MAAM,KAAK,MACnC,EAAE,SAAS,SACP;EAAE,MAAM;EAAuB,MAAM,EAAE;EAAM,GAC7C;EAAE,MAAM;EAA4B,MAAM,EAAE;EAAM,MAAM;EAAkB,CAC/E;CACD,MAAM,SAAS,OAAO,WAClB,CACE,GAAG,YACH;EACE,MAAM;EACN,MAAM,kBAAkB,OAAO,YAAY;EAC5C,CACF,GACD;AACJ,QAAO,CAAC,OAAO,aAAa,OAAO;EAEtC,CACF;;;;AASH,MAAa,QAAQ,QACnB,OAAO,IAAI,WAAW,WAAW,UAAU,GAAG,WAAW;CACvD,MAAM,gBAA+C,YACnD,kBAAkB,IAAI,CAAC,QAAQ,CAAC,KAAK,OAAO,eAAe,WAAW,YAAY,OAAO,CAAC;AAC5F,QAAO;EACL;EACA,aAAa,YAAY,YAAY,aAAa,QAAQ,CAAC;EAC3D;EACD;EACD;;;;;;;;AASJ,MAAa,SACX,QACsE;CACtE,MAAM,QAAQ,MAAM,OAAO,QAAQ,KAAK,IAAI,CAAC;CAC7C,MAAM,UAAU,MAAM,OACpB,eACA,OAAO,IACL,KAAK,IAAI,GACR,OAA6B,EAC5B,aAAa,YAAY,EAAE,WAAW,QAAyB,EAChE,EACF,CACF;AACD,QAAO,MAAM,MAAM,OAAO,QAAQ"}