@effect-uai/google 0.12.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Gemini.d.mts +15 -1
- package/dist/Gemini.d.mts.map +1 -1
- package/dist/Gemini.mjs +19 -42
- package/dist/Gemini.mjs.map +1 -1
- package/dist/GeminiImageGenerator.d.mts +149 -0
- package/dist/GeminiImageGenerator.d.mts.map +1 -0
- package/dist/GeminiImageGenerator.mjs +198 -0
- package/dist/GeminiImageGenerator.mjs.map +1 -0
- package/dist/LyriaGenerator.d.mts.map +1 -1
- package/dist/LyriaGenerator.mjs +3 -30
- package/dist/LyriaGenerator.mjs.map +1 -1
- package/dist/codec.d.mts +26 -2
- package/dist/codec.d.mts.map +1 -1
- package/dist/codec.mjs +87 -11
- package/dist/codec.mjs.map +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +2 -1
- package/dist/models.d.mts +15 -2
- package/dist/models.d.mts.map +1 -1
- package/package.json +6 -2
- package/src/Gemini.ts +37 -13
- package/src/GeminiImageGenerator.ts +390 -0
- package/src/LyriaGenerator.ts +6 -36
- package/src/codec.ts +122 -21
- package/src/index.ts +1 -0
- package/src/models.ts +24 -0
package/src/codec.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { Array as Arr, Encoding, Match, Option, Result, Schema, pipe } from "effect"
|
|
2
|
+
import * as AiError from "@effect-uai/core/AiError"
|
|
3
|
+
import type { ImageSource } from "@effect-uai/core/Image"
|
|
4
|
+
import { imageBase64 } from "@effect-uai/core/Image"
|
|
2
5
|
import type {
|
|
3
6
|
ContentBlock,
|
|
4
7
|
ToolCall,
|
|
@@ -43,8 +46,23 @@ const FunctionCallPart = Schema.Struct({
|
|
|
43
46
|
thoughtSignature: Schema.optional(Schema.String),
|
|
44
47
|
})
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
/**
|
|
50
|
+
* An image the model drew, as a part of its own turn. Only image models
|
|
51
|
+
* emit these, and only when the request asked for the IMAGE modality.
|
|
52
|
+
* `thought: true` marks an interim image drawn while planning, which is
|
|
53
|
+
* not the answer and is dropped the same way thought text is.
|
|
54
|
+
*/
|
|
55
|
+
const InlineDataPart = Schema.Struct({
|
|
56
|
+
inlineData: Schema.Struct({
|
|
57
|
+
mimeType: Schema.optional(Schema.String),
|
|
58
|
+
data: Schema.String,
|
|
59
|
+
}),
|
|
60
|
+
thought: Schema.optional(Schema.Boolean),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const Part = Schema.Union([TextPart, FunctionCallPart, InlineDataPart])
|
|
47
64
|
type WireFunctionCallPart = typeof FunctionCallPart.Type
|
|
65
|
+
type WireInlineDataPart = typeof InlineDataPart.Type
|
|
48
66
|
|
|
49
67
|
const Content = Schema.Struct({
|
|
50
68
|
role: Schema.optional(Schema.String),
|
|
@@ -180,11 +198,19 @@ export type ThinkingConfig = {
|
|
|
180
198
|
readonly thinkingBudget: number
|
|
181
199
|
}
|
|
182
200
|
|
|
201
|
+
export type ImageConfig = {
|
|
202
|
+
readonly aspectRatio?: string
|
|
203
|
+
readonly imageSize?: string
|
|
204
|
+
}
|
|
205
|
+
|
|
183
206
|
export type GenerationConfig = {
|
|
184
207
|
readonly temperature?: number
|
|
185
208
|
readonly maxOutputTokens?: number
|
|
186
209
|
readonly topP?: number
|
|
187
210
|
readonly thinkingConfig?: ThinkingConfig
|
|
211
|
+
/** Image models need `["TEXT", "IMAGE"]` or they answer in prose only. */
|
|
212
|
+
readonly responseModalities?: ReadonlyArray<"TEXT" | "IMAGE">
|
|
213
|
+
readonly imageConfig?: ImageConfig
|
|
188
214
|
/** Set together with `responseJsonSchema` to constrain output to JSON. */
|
|
189
215
|
readonly responseMimeType?: string
|
|
190
216
|
/** JSON Schema constraint on the output. */
|
|
@@ -204,6 +230,7 @@ const blockText = Match.type<ContentBlock>().pipe(
|
|
|
204
230
|
input_text: (b) => b.text,
|
|
205
231
|
input_image: () => "",
|
|
206
232
|
output_text: (b) => b.text,
|
|
233
|
+
output_image: () => "",
|
|
207
234
|
refusal: (b) => b.text,
|
|
208
235
|
}),
|
|
209
236
|
)
|
|
@@ -232,6 +259,9 @@ const blockToParts = Match.type<ContentBlock>().pipe(
|
|
|
232
259
|
input_text: (b): ReadonlyArray<RequestPart> => (b.text.length === 0 ? [] : [{ text: b.text }]),
|
|
233
260
|
input_image: (b): ReadonlyArray<RequestPart> => imageSourceToParts(b.source),
|
|
234
261
|
output_text: (b): ReadonlyArray<RequestPart> => (b.text.length === 0 ? [] : [{ text: b.text }]),
|
|
262
|
+
// An image the model drew goes back as a model-role `inlineData` part,
|
|
263
|
+
// which is how a multi-turn edit keeps its subject.
|
|
264
|
+
output_image: (b): ReadonlyArray<RequestPart> => imageSourceToParts(b.source),
|
|
235
265
|
// Refusals are assistant-side content; they don't round-trip into Gemini's
|
|
236
266
|
// request body as parts. Skip.
|
|
237
267
|
refusal: (): ReadonlyArray<RequestPart> => [],
|
|
@@ -460,6 +490,47 @@ const toolChoiceToFunctionCallingConfig = (
|
|
|
460
490
|
Match.orElse((tc) => ({ mode: "ANY" as const, allowedFunctionNames: [tc.name] })),
|
|
461
491
|
)
|
|
462
492
|
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
// HTTP failure mapping, shared by every Google surface
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
/** `provider` names the surface that failed, so errors read `gemini` or `lyria`. */
|
|
498
|
+
export const transportFailure =
|
|
499
|
+
(provider: string) =>
|
|
500
|
+
(cause: unknown): AiError.AiError =>
|
|
501
|
+
new AiError.Unavailable({ provider, raw: cause })
|
|
502
|
+
|
|
503
|
+
/** 413 is the inline-payload cap, which is a context limit on every surface. */
|
|
504
|
+
export const httpStatusError =
|
|
505
|
+
(provider: string) =>
|
|
506
|
+
(status: number, body: string): AiError.AiError =>
|
|
507
|
+
Match.value(status).pipe(
|
|
508
|
+
Match.when(429, (): AiError.AiError => new AiError.RateLimited({ provider, raw: body })),
|
|
509
|
+
Match.whenOr(408, 504, (): AiError.AiError => new AiError.Timeout({ provider, raw: body })),
|
|
510
|
+
Match.when(
|
|
511
|
+
401,
|
|
512
|
+
(): AiError.AiError => new AiError.AuthFailed({ provider, subtype: "auth", raw: body }),
|
|
513
|
+
),
|
|
514
|
+
Match.when(
|
|
515
|
+
403,
|
|
516
|
+
(): AiError.AiError =>
|
|
517
|
+
new AiError.AuthFailed({ provider, subtype: "permission", raw: body }),
|
|
518
|
+
),
|
|
519
|
+
Match.when(
|
|
520
|
+
402,
|
|
521
|
+
(): AiError.AiError => new AiError.AuthFailed({ provider, subtype: "billing", raw: body }),
|
|
522
|
+
),
|
|
523
|
+
Match.when(
|
|
524
|
+
413,
|
|
525
|
+
(): AiError.AiError => new AiError.ContextLengthExceeded({ provider, raw: body }),
|
|
526
|
+
),
|
|
527
|
+
Match.when(
|
|
528
|
+
(n) => n >= 500,
|
|
529
|
+
(n): AiError.AiError => new AiError.Unavailable({ provider, status: n, raw: body }),
|
|
530
|
+
),
|
|
531
|
+
Match.orElse((): AiError.AiError => new AiError.InvalidRequest({ provider, raw: body })),
|
|
532
|
+
)
|
|
533
|
+
|
|
463
534
|
/**
|
|
464
535
|
* URL-form images need Gemini's Files API (upload, then `fileData`); we
|
|
465
536
|
* don't pre-upload, so such a part would otherwise vanish from the request.
|
|
@@ -530,6 +601,8 @@ export type AccumulatedFunctionCall = {
|
|
|
530
601
|
export type Accumulator = {
|
|
531
602
|
readonly text: string
|
|
532
603
|
readonly reasoning: string
|
|
604
|
+
/** Images the model drew, in emission order. Image models only. */
|
|
605
|
+
readonly images: ReadonlyArray<ImageSource>
|
|
533
606
|
readonly functionCalls: ReadonlyArray<AccumulatedFunctionCall>
|
|
534
607
|
/** Grounding citations, accumulated (de-duped by url) across chunks. */
|
|
535
608
|
readonly annotations: ReadonlyArray<Annotation>
|
|
@@ -546,6 +619,7 @@ export type Accumulator = {
|
|
|
546
619
|
export const emptyAccumulator: Accumulator = {
|
|
547
620
|
text: "",
|
|
548
621
|
reasoning: "",
|
|
622
|
+
images: [],
|
|
549
623
|
functionCalls: [],
|
|
550
624
|
annotations: [],
|
|
551
625
|
finishReason: Option.none(),
|
|
@@ -560,6 +634,7 @@ export const emptyAccumulator: Accumulator = {
|
|
|
560
634
|
export type ChunkPart =
|
|
561
635
|
| { readonly kind: "text"; readonly text: string }
|
|
562
636
|
| { readonly kind: "reasoning"; readonly text: string }
|
|
637
|
+
| { readonly kind: "image"; readonly source: ImageSource }
|
|
563
638
|
| {
|
|
564
639
|
readonly kind: "function_call"
|
|
565
640
|
readonly id: Option.Option<string>
|
|
@@ -574,11 +649,20 @@ export type ChunkResult = {
|
|
|
574
649
|
readonly finished: boolean
|
|
575
650
|
}
|
|
576
651
|
|
|
577
|
-
const isTextPart = (p: typeof Part.Type): p is typeof TextPart.Type => "text" in p
|
|
578
|
-
|
|
579
652
|
const textToChunkParts = (p: typeof TextPart.Type): ReadonlyArray<ChunkPart> =>
|
|
580
653
|
p.text.length === 0 ? [] : [{ kind: p.thought === true ? "reasoning" : "text", text: p.text }]
|
|
581
654
|
|
|
655
|
+
/** Interim images drawn while planning are dropped, as thought text is. */
|
|
656
|
+
const inlineDataToChunkParts = (p: WireInlineDataPart): ReadonlyArray<ChunkPart> =>
|
|
657
|
+
p.thought === true
|
|
658
|
+
? []
|
|
659
|
+
: [
|
|
660
|
+
{
|
|
661
|
+
kind: "image",
|
|
662
|
+
source: imageBase64(p.inlineData.data, p.inlineData.mimeType ?? "image/png"),
|
|
663
|
+
},
|
|
664
|
+
]
|
|
665
|
+
|
|
582
666
|
const functionCallToChunkParts = (p: WireFunctionCallPart): ReadonlyArray<ChunkPart> => [
|
|
583
667
|
{
|
|
584
668
|
kind: "function_call",
|
|
@@ -589,8 +673,15 @@ const functionCallToChunkParts = (p: WireFunctionCallPart): ReadonlyArray<ChunkP
|
|
|
589
673
|
},
|
|
590
674
|
]
|
|
591
675
|
|
|
592
|
-
|
|
593
|
-
|
|
676
|
+
/** Parts are distinguished by which key they carry, not by a discriminator. */
|
|
677
|
+
const partToChunkParts: (p: typeof Part.Type) => ReadonlyArray<ChunkPart> = Match.type<
|
|
678
|
+
typeof Part.Type
|
|
679
|
+
>().pipe(
|
|
680
|
+
Match.when({ text: Match.string }, textToChunkParts),
|
|
681
|
+
Match.when({ inlineData: Match.defined }, inlineDataToChunkParts),
|
|
682
|
+
Match.when({ functionCall: Match.defined }, functionCallToChunkParts),
|
|
683
|
+
Match.exhaustive,
|
|
684
|
+
)
|
|
594
685
|
|
|
595
686
|
const chunkParts = (chunk: WireChunk): ReadonlyArray<ChunkPart> =>
|
|
596
687
|
pipe(chunk.candidates?.[0]?.content?.parts ?? [], Arr.flatMap(partToChunkParts))
|
|
@@ -601,6 +692,12 @@ const sumStrings = (parts: ReadonlyArray<ChunkPart>, kind: "text" | "reasoning")
|
|
|
601
692
|
Arr.filterMap((p) => (p.kind === kind ? Result.succeed(p.text) : Result.failVoid)),
|
|
602
693
|
).join("")
|
|
603
694
|
|
|
695
|
+
const collectImages = (parts: ReadonlyArray<ChunkPart>): ReadonlyArray<ImageSource> =>
|
|
696
|
+
pipe(
|
|
697
|
+
parts,
|
|
698
|
+
Arr.filterMap((p) => (p.kind === "image" ? Result.succeed(p.source) : Result.failVoid)),
|
|
699
|
+
)
|
|
700
|
+
|
|
604
701
|
const collectFunctionCalls = (
|
|
605
702
|
parts: ReadonlyArray<ChunkPart>,
|
|
606
703
|
): ReadonlyArray<Extract<ChunkPart, { kind: "function_call" }>> =>
|
|
@@ -683,6 +780,7 @@ export const ingestChunk = (acc: Accumulator, chunk: WireChunk): ChunkResult =>
|
|
|
683
780
|
accumulator: {
|
|
684
781
|
text: acc.text + sumStrings(parts, "text"),
|
|
685
782
|
reasoning: acc.reasoning + sumStrings(parts, "reasoning"),
|
|
783
|
+
images: Arr.appendAll(acc.images, collectImages(parts)),
|
|
686
784
|
functionCalls: appendFunctionCalls(acc.functionCalls, collectFunctionCalls(parts)),
|
|
687
785
|
annotations: mergeAnnotations(
|
|
688
786
|
acc.annotations,
|
|
@@ -697,22 +795,25 @@ export const ingestChunk = (acc: Accumulator, chunk: WireChunk): ChunkResult =>
|
|
|
697
795
|
const reasoningItems = (acc: Accumulator): ReadonlyArray<HistoryItem> =>
|
|
698
796
|
acc.reasoning.length > 0 ? [{ type: "reasoning", summary: acc.reasoning }] : []
|
|
699
797
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
},
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
798
|
+
/**
|
|
799
|
+
* Image models interleave text and image parts, and often answer with an
|
|
800
|
+
* image and no prose at all, so the message exists when either is present.
|
|
801
|
+
*/
|
|
802
|
+
const assistantMessageItems = (acc: Accumulator): ReadonlyArray<HistoryItem> => {
|
|
803
|
+
const content: ReadonlyArray<ContentBlock> = [
|
|
804
|
+
...(acc.text.length === 0
|
|
805
|
+
? []
|
|
806
|
+
: [
|
|
807
|
+
{
|
|
808
|
+
type: "output_text" as const,
|
|
809
|
+
text: acc.text,
|
|
810
|
+
...(acc.annotations.length > 0 && { annotations: acc.annotations }),
|
|
811
|
+
},
|
|
812
|
+
]),
|
|
813
|
+
...acc.images.map((source) => ({ type: "output_image" as const, source })),
|
|
814
|
+
]
|
|
815
|
+
return content.length === 0 ? [] : [{ type: "message", role: "assistant", content }]
|
|
816
|
+
}
|
|
716
817
|
|
|
717
818
|
const functionCallItems = (acc: Accumulator): ReadonlyArray<HistoryItem> =>
|
|
718
819
|
pipe(
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * as Gemini from "./Gemini.js"
|
|
2
2
|
export * as GeminiTools from "./GeminiTools.js"
|
|
3
3
|
export * as GeminiEmbedding from "./GeminiEmbedding.js"
|
|
4
|
+
export * as GeminiImageGenerator from "./GeminiImageGenerator.js"
|
|
4
5
|
export * as GeminiSynthesizer from "./GeminiSynthesizer.js"
|
|
5
6
|
export * as GoogleDeepResearch from "./GoogleDeepResearch.js"
|
|
6
7
|
export * as LyriaGenerator from "./LyriaGenerator.js"
|
package/src/models.ts
CHANGED
|
@@ -14,6 +14,30 @@ export type GoogleModel =
|
|
|
14
14
|
| "gemini-2.5-pro"
|
|
15
15
|
| "gemini-2.5-flash"
|
|
16
16
|
| "gemini-2.5-flash-lite"
|
|
17
|
+
// Image models answer in a chat turn like any other, with pictures among
|
|
18
|
+
// the parts. See {@link GeminiImageModel}.
|
|
19
|
+
| "gemini-3.1-flash-image"
|
|
20
|
+
| "gemini-3.1-flash-lite-image"
|
|
21
|
+
| "gemini-3-pro-image"
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
23
|
+
| (string & {})
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Known Gemini image-generation model identifiers. The `-preview` ids
|
|
27
|
+
* were shut down 2026-06-25 and are not listed; neither is the
|
|
28
|
+
* previous-generation `gemini-2.5-flash-image`, which the `(string & {})`
|
|
29
|
+
* tail still accepts.
|
|
30
|
+
*
|
|
31
|
+
* - `gemini-3.1-flash-image` — 512 / 1K / 2K / 4K, web and image search.
|
|
32
|
+
* - `gemini-3.1-flash-lite-image` — 1K only, cheapest, no search.
|
|
33
|
+
* - `gemini-3-pro-image` — 1K / 2K / 4K, the most reference images.
|
|
34
|
+
*
|
|
35
|
+
* Reference: https://ai.google.dev/gemini-api/docs/models
|
|
36
|
+
*/
|
|
37
|
+
export type GeminiImageModel =
|
|
38
|
+
| "gemini-3.1-flash-image"
|
|
39
|
+
| "gemini-3.1-flash-lite-image"
|
|
40
|
+
| "gemini-3-pro-image"
|
|
17
41
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
18
42
|
| (string & {})
|
|
19
43
|
|