@opencode/ai 2.0.15 → 2.0.17
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/README.md +441 -99
- package/dist/ai-client.d.ts +8 -0
- package/dist/ai-client.js +12 -0
- package/dist/experimental/evaluation-client.d.ts +3 -3
- package/dist/experimental/evaluation-client.js +1 -1
- package/dist/experimental/evaluation.js +1 -1
- package/dist/generation.d.ts +39 -29
- package/dist/generation.js +62 -36
- package/dist/image-client.d.ts +59 -17
- package/dist/image-client.js +16 -24
- package/dist/image.d.ts +394 -55
- package/dist/image.js +48 -57
- package/dist/index.d.ts +15 -2
- package/dist/index.js +10 -0
- package/dist/llm.d.ts +7 -5
- package/dist/llm.js +10 -4
- package/dist/media-client.d.ts +30 -0
- package/dist/media-client.js +51 -0
- package/dist/media-model.d.ts +43 -0
- package/dist/media-model.js +47 -0
- package/dist/media.d.ts +10 -9
- package/dist/media.js +11 -12
- package/dist/promise.d.ts +473 -15
- package/dist/promise.js +71 -13
- package/dist/protocols/alibaba-chat.d.ts +12 -0
- package/dist/protocols/alibaba-chat.js +4 -1
- package/dist/protocols/alibaba-messages.d.ts +1 -1
- package/dist/protocols/alibaba-messages.js +6 -4
- package/dist/protocols/alibaba-responses.d.ts +2 -2
- package/dist/protocols/anthropic-messages.d.ts +34 -34
- package/dist/protocols/anthropic-messages.js +14 -9
- package/dist/protocols/assemblyai-transcription.d.ts +41 -0
- package/dist/protocols/assemblyai-transcription.js +136 -0
- package/dist/protocols/bedrock-converse.d.ts +7 -0
- package/dist/protocols/bedrock-converse.js +39 -19
- package/dist/protocols/bfl-images.d.ts +38 -0
- package/dist/protocols/bfl-images.js +153 -0
- package/dist/protocols/cartesia-speech.d.ts +143 -0
- package/dist/protocols/cartesia-speech.js +120 -0
- package/dist/protocols/deepgram-speech.d.ts +135 -0
- package/dist/protocols/deepgram-speech.js +91 -0
- package/dist/protocols/deepgram-transcription.d.ts +26 -0
- package/dist/protocols/deepgram-transcription.js +125 -0
- package/dist/protocols/elevenlabs-speech.d.ts +138 -0
- package/dist/protocols/elevenlabs-speech.js +111 -0
- package/dist/protocols/fal-images.d.ts +25 -0
- package/dist/protocols/fal-images.js +104 -0
- package/dist/protocols/fal-video.d.ts +29 -0
- package/dist/protocols/fal-video.js +73 -0
- package/dist/protocols/gemini.d.ts +22 -22
- package/dist/protocols/gemini.js +19 -36
- package/dist/protocols/google-images.d.ts +3 -3
- package/dist/protocols/google-images.js +12 -34
- package/dist/protocols/google-speech.d.ts +146 -0
- package/dist/protocols/google-speech.js +88 -0
- package/dist/protocols/google-transcription.d.ts +174 -0
- package/dist/protocols/google-transcription.js +126 -0
- package/dist/protocols/google-video.d.ts +26 -0
- package/dist/protocols/google-video.js +142 -0
- package/dist/protocols/meta-images.d.ts +3 -4
- package/dist/protocols/meta-images.js +13 -29
- package/dist/protocols/meta-messages.d.ts +5 -5
- package/dist/protocols/meta-responses.d.ts +4 -4
- package/dist/protocols/meta-responses.js +6 -4
- package/dist/protocols/open-responses.d.ts +10 -9
- package/dist/protocols/open-responses.js +4 -9
- package/dist/protocols/openai-chat.d.ts +84 -0
- package/dist/protocols/openai-chat.js +28 -17
- package/dist/protocols/openai-compatible-chat.d.ts +12 -0
- package/dist/protocols/openai-compatible-responses.d.ts +2 -2
- package/dist/protocols/openai-images.d.ts +130 -7
- package/dist/protocols/openai-images.js +143 -91
- package/dist/protocols/openai-responses.d.ts +20 -20
- package/dist/protocols/openai-responses.js +31 -31
- package/dist/protocols/openai-speech.d.ts +136 -0
- package/dist/protocols/openai-speech.js +97 -0
- package/dist/protocols/openai-transcription.d.ts +211 -0
- package/dist/protocols/openai-transcription.js +202 -0
- package/dist/protocols/replicate-images.d.ts +28 -0
- package/dist/protocols/replicate-images.js +127 -0
- package/dist/protocols/runway-video.d.ts +38 -0
- package/dist/protocols/runway-video.js +140 -0
- package/dist/protocols/shared.d.ts +21 -17
- package/dist/protocols/shared.js +27 -36
- package/dist/protocols/stability-images.d.ts +39 -0
- package/dist/protocols/stability-images.js +136 -0
- package/dist/protocols/utils/fal-queue.d.ts +26 -0
- package/dist/protocols/utils/fal-queue.js +67 -0
- package/dist/protocols/utils/gemini-generate-content.d.ts +65 -0
- package/dist/protocols/utils/gemini-generate-content.js +65 -0
- package/dist/protocols/utils/gemini-json-schema.d.ts +3 -0
- package/dist/protocols/utils/gemini-json-schema.js +76 -0
- package/dist/protocols/utils/media-input.d.ts +23 -1
- package/dist/protocols/utils/media-input.js +40 -0
- package/dist/protocols/utils/responses-checkpoint.js +3 -7
- package/dist/protocols/utils/responses-compaction.d.ts +3 -1
- package/dist/protocols/utils/responses-compaction.js +16 -3
- package/dist/protocols/utils/speech-stream.d.ts +49 -0
- package/dist/protocols/utils/speech-stream.js +64 -0
- package/dist/protocols/utils/tool-schema.d.ts +2 -2
- package/dist/protocols/utils/tool-schema.js +62 -19
- package/dist/protocols/xai-images.d.ts +4 -4
- package/dist/protocols/xai-images.js +14 -40
- package/dist/protocols/xai-responses.d.ts +2 -2
- package/dist/protocols/xai-responses.js +1 -1
- package/dist/protocols/xai-video.d.ts +34 -0
- package/dist/protocols/xai-video.js +141 -0
- package/dist/protocols/zai-chat.d.ts +13 -1
- package/dist/protocols/zai-images.d.ts +2 -2
- package/dist/protocols/zai-images.js +11 -14
- package/dist/protocols/zai-messages.d.ts +1 -1
- package/dist/provider-error.js +10 -1
- package/dist/providers/alibaba.d.ts +15 -3
- package/dist/providers/amazon-bedrock-mantle.d.ts +14 -2
- package/dist/providers/amazon-bedrock.d.ts +2 -0
- package/dist/providers/amazon-bedrock.js +1 -0
- package/dist/providers/anthropic-compatible.d.ts +5 -5
- package/dist/providers/anthropic.d.ts +5 -5
- package/dist/providers/assemblyai.d.ts +25 -0
- package/dist/providers/assemblyai.js +24 -0
- package/dist/providers/azure.d.ts +20 -8
- package/dist/providers/azure.js +2 -2
- package/dist/providers/baseten.d.ts +24 -0
- package/dist/providers/black-forest-labs.d.ts +25 -0
- package/dist/providers/black-forest-labs.js +23 -0
- package/dist/providers/cartesia.d.ts +24 -0
- package/dist/providers/cartesia.js +17 -0
- package/dist/providers/cerebras.d.ts +24 -0
- package/dist/providers/cloudflare-ai-gateway.d.ts +42 -18
- package/dist/providers/cloudflare-workers-ai.d.ts +24 -0
- package/dist/providers/deepgram.d.ts +29 -0
- package/dist/providers/deepgram.js +25 -0
- package/dist/providers/deepinfra.d.ts +24 -0
- package/dist/providers/deepseek.d.ts +24 -0
- package/dist/providers/elevenlabs.d.ts +24 -0
- package/dist/providers/elevenlabs.js +23 -0
- package/dist/providers/fal.d.ts +29 -0
- package/dist/providers/fal.js +26 -0
- package/dist/providers/fireworks.d.ts +24 -0
- package/dist/providers/google-vertex-chat.d.ts +12 -0
- package/dist/providers/google-vertex-messages.d.ts +5 -5
- package/dist/providers/google-vertex-responses.d.ts +2 -2
- package/dist/providers/google-vertex.d.ts +6 -6
- package/dist/providers/google.d.ts +21 -6
- package/dist/providers/google.js +13 -9
- package/dist/providers/groq.d.ts +24 -0
- package/dist/providers/index.d.ts +9 -0
- package/dist/providers/index.js +9 -0
- package/dist/providers/meta.d.ts +22 -10
- package/dist/providers/meta.js +4 -8
- package/dist/providers/minimax.d.ts +19 -7
- package/dist/providers/moonshot.d.ts +19 -7
- package/dist/providers/moonshot.js +3 -3
- package/dist/providers/openai-compatible-responses.d.ts +2 -2
- package/dist/providers/openai-compatible.d.ts +12 -0
- package/dist/providers/openai-options.d.ts +3 -9
- package/dist/providers/openai-options.js +4 -7
- package/dist/providers/openai.d.ts +33 -12
- package/dist/providers/openai.js +17 -9
- package/dist/providers/opencode-zen.js +1 -1
- package/dist/providers/openrouter.d.ts +54 -7
- package/dist/providers/openrouter.js +10 -5
- package/dist/providers/replicate.d.ts +25 -0
- package/dist/providers/replicate.js +17 -0
- package/dist/providers/runway.d.ts +24 -0
- package/dist/providers/runway.js +17 -0
- package/dist/providers/stability.d.ts +28 -0
- package/dist/providers/stability.js +18 -0
- package/dist/providers/togetherai.d.ts +24 -0
- package/dist/providers/typesafe-ai.js +1 -1
- package/dist/providers/vercel-ai-gateway.js +1 -1
- package/dist/providers/xai.d.ts +17 -0
- package/dist/providers/xai.js +7 -9
- package/dist/providers/zai-coding-plan.d.ts +16 -4
- package/dist/providers/zai.d.ts +13 -1
- package/dist/providers/zai.js +4 -8
- package/dist/route/auth.d.ts +5 -2
- package/dist/route/auth.js +20 -13
- package/dist/route/client.d.ts +9 -7
- package/dist/route/client.js +6 -8
- package/dist/route/endpoint.d.ts +1 -0
- package/dist/route/endpoint.js +2 -2
- package/dist/route/executor-service.d.ts +4 -2
- package/dist/route/executor-service.js +2 -1
- package/dist/route/executor.d.ts +3 -1
- package/dist/route/executor.js +7 -0
- package/dist/route/framing.d.ts +15 -2
- package/dist/route/framing.js +53 -4
- package/dist/route/index.d.ts +1 -1
- package/dist/route/media-protocol.d.ts +145 -18
- package/dist/route/media-protocol.js +96 -28
- package/dist/route/media.d.ts +54 -9
- package/dist/route/media.js +194 -38
- package/dist/route/protocol.d.ts +3 -1
- package/dist/schema/events.d.ts +0 -6
- package/dist/schema/messages.d.ts +0 -3
- package/dist/schema/options.d.ts +10 -6
- package/dist/schema/options.js +10 -5
- package/dist/speech-client.d.ts +66 -0
- package/dist/speech-client.js +21 -0
- package/dist/speech.d.ts +1307 -0
- package/dist/speech.js +124 -0
- package/dist/testing.d.ts +2 -2
- package/dist/transcription-client.d.ts +82 -0
- package/dist/transcription-client.js +13 -0
- package/dist/transcription.d.ts +1492 -0
- package/dist/transcription.js +127 -0
- package/dist/utils/bytes.d.ts +1 -0
- package/dist/utils/bytes.js +10 -0
- package/dist/utils/json.d.ts +4 -0
- package/dist/utils/json.js +4 -0
- package/dist/utils/media-type.d.ts +3 -1
- package/dist/utils/media-type.js +25 -2
- package/dist/video-client.d.ts +59 -0
- package/dist/video-client.js +20 -0
- package/dist/video.d.ts +1351 -0
- package/dist/video.js +118 -0
- package/package.json +3 -3
- package/dist/protocols/utils/gemini-tool-schema.d.ts +0 -2
- package/dist/protocols/utils/gemini-tool-schema.js +0 -103
- package/dist/protocols/utils/meta-image.d.ts +0 -2
- package/dist/protocols/utils/meta-image.js +0 -13
- package/dist/protocols/utils/openai-image.d.ts +0 -5
- package/dist/protocols/utils/openai-image.js +0 -18
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { ImageModel, ImageResponse } from "../image.js";
|
|
3
|
+
import { Media } from "../media.js";
|
|
4
|
+
import { MediaProtocol } from "../route/media-protocol.js";
|
|
5
|
+
import { MediaRoute } from "../route/media.js";
|
|
6
|
+
import { mergeJsonRecords } from "../schema/index.js";
|
|
7
|
+
import { ProviderShared, optionalNull } from "./shared.js";
|
|
8
|
+
import { FalQueue } from "./utils/fal-queue.js";
|
|
9
|
+
import { MediaInput } from "./utils/media-input.js";
|
|
10
|
+
const route = MediaProtocol.identity({ id: "fal-images", name: "fal Images", provider: "fal" });
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// 2. Response schema
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const QueueResult = Schema.StructWithRest(Schema.Struct({
|
|
15
|
+
images: Schema.Array(Schema.Struct({
|
|
16
|
+
url: Schema.String,
|
|
17
|
+
width: optionalNull(Schema.Number),
|
|
18
|
+
height: optionalNull(Schema.Number),
|
|
19
|
+
content_type: optionalNull(Schema.String),
|
|
20
|
+
})),
|
|
21
|
+
seed: optionalNull(Schema.Number),
|
|
22
|
+
has_nsfw_concepts: optionalNull(Schema.Array(Schema.Boolean)),
|
|
23
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// 5. Request body construction
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
const sizing = (model) => {
|
|
28
|
+
if (/^fal-ai\/(nano-banana|flux-pro\/(v1\.1-ultra|kontext))/.test(model))
|
|
29
|
+
return "aspect_ratio";
|
|
30
|
+
if (model.startsWith("fal-ai/flux"))
|
|
31
|
+
return "image_size";
|
|
32
|
+
return undefined;
|
|
33
|
+
};
|
|
34
|
+
const validate = (request) => {
|
|
35
|
+
const id = request.model.id;
|
|
36
|
+
const field = sizing(id);
|
|
37
|
+
if (request.size !== undefined && request.aspectRatio !== undefined)
|
|
38
|
+
return Effect.fail(ProviderShared.invalidRequest(`${route.name} accepts either size or aspectRatio, not both`));
|
|
39
|
+
if (request.size !== undefined && field === "aspect_ratio")
|
|
40
|
+
return Effect.fail(route.unsupported("media.size", `${id} sizes by aspectRatio`));
|
|
41
|
+
if (request.aspectRatio !== undefined && field === "image_size")
|
|
42
|
+
return Effect.fail(route.unsupported("media.aspectRatio", `${id} sizes by size (image_size)`));
|
|
43
|
+
if ((request.images?.length ?? 0) > 1 && !takesImageList(id))
|
|
44
|
+
return Effect.fail(route.unsupported("media.images", `${id} takes one image_url; use an /edit or /multi endpoint for several images`));
|
|
45
|
+
return Effect.void;
|
|
46
|
+
};
|
|
47
|
+
// `/edit` and `/multi` (Kontext) endpoints take an `image_urls` list; image-to-image, fill, and Ultra take one
|
|
48
|
+
// `image_url` (beside `mask_url`).
|
|
49
|
+
const takesImageList = (model) => model.endsWith("/edit") || model.endsWith("/multi");
|
|
50
|
+
const fromRequest = Effect.fn("FalImages.fromRequest")(function* (request) {
|
|
51
|
+
yield* validate(request);
|
|
52
|
+
const images = yield* Effect.forEach(request.images ?? [], (image) => FalQueue.mediaUrl(image, route.name));
|
|
53
|
+
const list = takesImageList(request.model.id);
|
|
54
|
+
return MediaProtocol.json(mergeJsonRecords({
|
|
55
|
+
prompt: request.prompt,
|
|
56
|
+
num_images: request.n,
|
|
57
|
+
seed: request.seed,
|
|
58
|
+
image_size: request.size === undefined ? undefined : MediaInput.dimensions(request.size),
|
|
59
|
+
aspect_ratio: request.aspectRatio,
|
|
60
|
+
output_format: request.format,
|
|
61
|
+
image_urls: list && images.length > 0 ? images : undefined,
|
|
62
|
+
image_url: list ? undefined : images[0],
|
|
63
|
+
mask_url: request.mask === undefined ? undefined : yield* FalQueue.mediaUrl(request.mask, route.name),
|
|
64
|
+
}, request.providerOptions, request.http?.body) ?? {});
|
|
65
|
+
});
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// 6. Response decoding
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
const decodeQueueResult = route.decodeJson(QueueResult);
|
|
70
|
+
const decodeResult = Effect.fn("FalImages.decodeResult")(function* (response, context) {
|
|
71
|
+
const output = yield* decodeQueueResult(response);
|
|
72
|
+
const { images, seed, has_nsfw_concepts, ...rest } = output.value;
|
|
73
|
+
if (images.length === 0)
|
|
74
|
+
return yield* output.invalid(`${route.name} returned no images`);
|
|
75
|
+
// With the safety checker on, flagged images come back blacked out rather than omitted.
|
|
76
|
+
const flagged = (has_nsfw_concepts ?? []).flatMap((value, index) => (value ? [index] : []));
|
|
77
|
+
return new ImageResponse({
|
|
78
|
+
images: images.map((image) => {
|
|
79
|
+
const info = { width: image.width ?? undefined, height: image.height ?? undefined };
|
|
80
|
+
// `sync_mode: true` returns data URIs instead of hosted URLs.
|
|
81
|
+
return (Media.parseDataUrl(image.url, { info }) ??
|
|
82
|
+
Media.url(image.url, { mediaType: image.content_type ?? undefined, info }));
|
|
83
|
+
}),
|
|
84
|
+
notices: flagged.length === 0
|
|
85
|
+
? undefined
|
|
86
|
+
: flagged.map((index) => ({
|
|
87
|
+
type: "moderated",
|
|
88
|
+
message: `${route.name} flagged image ${index} as NSFW`,
|
|
89
|
+
})),
|
|
90
|
+
providerMetadata: { fal: { requestId: context.token.requestID, seed: seed ?? undefined, ...rest } },
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// 7. Protocol and route
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
export const protocol = FalQueue.protocol(route, {
|
|
97
|
+
from: fromRequest,
|
|
98
|
+
decodeResult,
|
|
99
|
+
});
|
|
100
|
+
export const model = (input) => ImageModel.fromRoute({ protocol, baseURL: FalQueue.DEFAULT_BASE_URL, path: ({ request }) => `/${request.model.id}` }, input);
|
|
101
|
+
export const FalImages = {
|
|
102
|
+
protocol,
|
|
103
|
+
model,
|
|
104
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { MediaProtocol } from "../route/media-protocol.js";
|
|
2
|
+
import { MediaRoute } from "../route/media.js";
|
|
3
|
+
import { type OpenString } from "../schema/index.js";
|
|
4
|
+
import { VideoModel, VideoResponse, type VideoRequestFor } from "../video.js";
|
|
5
|
+
/**
|
|
6
|
+
* Provider-native input. fal video endpoints are model-specific: `duration` is a string enum whose values differ per
|
|
7
|
+
* model (`"8s"` for Veo, `"5"` for Kling), and last-frame fields are named per model (`end_image_url`,
|
|
8
|
+
* `last_frame_url`, `tail_image_url`), so those pass through here instead of lowering from common fields.
|
|
9
|
+
*/
|
|
10
|
+
export type FalVideoOptions = {
|
|
11
|
+
readonly duration?: OpenString<"4s" | "6s" | "8s" | "5" | "10">;
|
|
12
|
+
} & Record<string, unknown>;
|
|
13
|
+
export type Request = VideoRequestFor<FalVideoOptions>;
|
|
14
|
+
export declare const protocol: MediaProtocol.Queued<Request, VideoResponse, {
|
|
15
|
+
readonly requestID: string;
|
|
16
|
+
readonly statusURL: string;
|
|
17
|
+
readonly responseURL: string;
|
|
18
|
+
readonly cancelURL: string;
|
|
19
|
+
}>;
|
|
20
|
+
export declare const model: (input: MediaRoute.ModelInput) => VideoModel<FalVideoOptions>;
|
|
21
|
+
export declare const FalVideo: {
|
|
22
|
+
readonly protocol: MediaProtocol.Queued<Request, VideoResponse, {
|
|
23
|
+
readonly requestID: string;
|
|
24
|
+
readonly statusURL: string;
|
|
25
|
+
readonly responseURL: string;
|
|
26
|
+
readonly cancelURL: string;
|
|
27
|
+
}>;
|
|
28
|
+
readonly model: (input: MediaRoute.ModelInput) => VideoModel<FalVideoOptions>;
|
|
29
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { Media } from "../media.js";
|
|
3
|
+
import { MediaProtocol } from "../route/media-protocol.js";
|
|
4
|
+
import { MediaRoute } from "../route/media.js";
|
|
5
|
+
import { mergeJsonRecords } from "../schema/index.js";
|
|
6
|
+
import { VideoModel, VideoResponse } from "../video.js";
|
|
7
|
+
import { optionalNull } from "./shared.js";
|
|
8
|
+
import { FalQueue } from "./utils/fal-queue.js";
|
|
9
|
+
const route = MediaProtocol.identity({ id: "fal-video", name: "fal Video", provider: "fal" });
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// 2. Response schema
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
const QueueResult = Schema.StructWithRest(Schema.Struct({
|
|
14
|
+
video: Schema.Struct({
|
|
15
|
+
url: Schema.String,
|
|
16
|
+
content_type: optionalNull(Schema.String),
|
|
17
|
+
file_name: optionalNull(Schema.String),
|
|
18
|
+
file_size: optionalNull(Schema.Number),
|
|
19
|
+
}),
|
|
20
|
+
seed: optionalNull(Schema.Number),
|
|
21
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// 5. Request body construction
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
const fromRequest = Effect.fn("FalVideo.fromRequest")(function* (request) {
|
|
26
|
+
if (request.frames?.last !== undefined)
|
|
27
|
+
return yield* route.unsupported("video.frames.last", `${route.name} names the last frame per model; pass it through providerOptions (e.g. end_image_url) instead of frames.last`);
|
|
28
|
+
const imageUrl = request.frames?.first === undefined ? undefined : yield* FalQueue.mediaUrl(request.frames.first, route.name);
|
|
29
|
+
const videoUrl = request.video === undefined ? undefined : yield* FalQueue.mediaUrl(request.video, route.name);
|
|
30
|
+
return MediaProtocol.json(mergeJsonRecords({
|
|
31
|
+
prompt: request.prompt,
|
|
32
|
+
negative_prompt: request.negativePrompt,
|
|
33
|
+
seed: request.seed,
|
|
34
|
+
aspect_ratio: request.aspectRatio,
|
|
35
|
+
resolution: request.resolution,
|
|
36
|
+
generate_audio: request.audio,
|
|
37
|
+
image_url: imageUrl,
|
|
38
|
+
video_url: videoUrl,
|
|
39
|
+
}, request.providerOptions, request.http?.body) ?? {});
|
|
40
|
+
});
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// 6. Response decoding
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
const decodeQueueResult = route.decodeJson(QueueResult);
|
|
45
|
+
const decodeResult = Effect.fn("FalVideo.decodeResult")(function* (response, context) {
|
|
46
|
+
const output = yield* decodeQueueResult(response);
|
|
47
|
+
const { video, seed, ...rest } = output.value;
|
|
48
|
+
return new VideoResponse({
|
|
49
|
+
videos: [Media.url(video.url, { mediaType: video.content_type ?? "video/mp4" })],
|
|
50
|
+
providerMetadata: {
|
|
51
|
+
fal: {
|
|
52
|
+
requestId: context.token.requestID,
|
|
53
|
+
seed: seed ?? undefined,
|
|
54
|
+
fileName: video.file_name ?? undefined,
|
|
55
|
+
fileSize: video.file_size ?? undefined,
|
|
56
|
+
...rest,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// 7. Protocol and route
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
export const protocol = FalQueue.protocol(route, {
|
|
65
|
+
unsupported: ["n", "durationSeconds", "references"],
|
|
66
|
+
from: fromRequest,
|
|
67
|
+
decodeResult,
|
|
68
|
+
});
|
|
69
|
+
export const model = (input) => VideoModel.fromRoute({ protocol, baseURL: FalQueue.DEFAULT_BASE_URL, path: ({ request }) => `/${request.model.id}` }, input);
|
|
70
|
+
export const FalVideo = {
|
|
71
|
+
protocol,
|
|
72
|
+
model,
|
|
73
|
+
};
|
|
@@ -10,14 +10,14 @@ export type ProviderOptionsInput = OptionsInput;
|
|
|
10
10
|
declare const Options: Schema.Struct<{
|
|
11
11
|
readonly cachedContent: Schema.optionalKey<Schema.middlewareDecoding<Schema.UndefinedOr<Schema.String>, never>>;
|
|
12
12
|
readonly safetySettings: Schema.optionalKey<Schema.middlewareDecoding<Schema.UndefinedOr<Schema.$Array<Schema.Struct<{
|
|
13
|
-
readonly category: Schema.declare<(
|
|
14
|
-
readonly threshold: Schema.declare<(
|
|
13
|
+
readonly category: Schema.declare<import("../schema/options.js").OpenString<"HARM_CATEGORY_UNSPECIFIED" | "HARM_CATEGORY_HATE_SPEECH" | "HARM_CATEGORY_DANGEROUS_CONTENT" | "HARM_CATEGORY_HARASSMENT" | "HARM_CATEGORY_SEXUALLY_EXPLICIT" | "HARM_CATEGORY_CIVIC_INTEGRITY">, import("../schema/options.js").OpenString<"HARM_CATEGORY_UNSPECIFIED" | "HARM_CATEGORY_HATE_SPEECH" | "HARM_CATEGORY_DANGEROUS_CONTENT" | "HARM_CATEGORY_HARASSMENT" | "HARM_CATEGORY_SEXUALLY_EXPLICIT" | "HARM_CATEGORY_CIVIC_INTEGRITY">>;
|
|
14
|
+
readonly threshold: Schema.declare<import("../schema/options.js").OpenString<"HARM_BLOCK_THRESHOLD_UNSPECIFIED" | "BLOCK_LOW_AND_ABOVE" | "BLOCK_MEDIUM_AND_ABOVE" | "BLOCK_ONLY_HIGH" | "BLOCK_NONE" | "OFF">, import("../schema/options.js").OpenString<"HARM_BLOCK_THRESHOLD_UNSPECIFIED" | "BLOCK_LOW_AND_ABOVE" | "BLOCK_MEDIUM_AND_ABOVE" | "BLOCK_ONLY_HIGH" | "BLOCK_NONE" | "OFF">>;
|
|
15
15
|
}>>>, never>>;
|
|
16
|
-
readonly serviceTier: Schema.optionalKey<Schema.middlewareDecoding<Schema.UndefinedOr<Schema.declare<(
|
|
16
|
+
readonly serviceTier: Schema.optionalKey<Schema.middlewareDecoding<Schema.UndefinedOr<Schema.declare<import("../schema/options.js").OpenString<"standard" | "flex" | "priority">, import("../schema/options.js").OpenString<"standard" | "flex" | "priority">>>, never>>;
|
|
17
17
|
readonly thinkingConfig: Schema.optionalKey<Schema.middlewareDecoding<Schema.UndefinedOr<Schema.Struct<{
|
|
18
18
|
readonly thinkingBudget: Schema.optionalKey<Schema.middlewareDecoding<Schema.UndefinedOr<Schema.Number>, never>>;
|
|
19
19
|
readonly includeThoughts: Schema.optionalKey<Schema.middlewareDecoding<Schema.UndefinedOr<Schema.Boolean>, never>>;
|
|
20
|
-
readonly thinkingLevel: Schema.optionalKey<Schema.middlewareDecoding<Schema.UndefinedOr<Schema.declare<(
|
|
20
|
+
readonly thinkingLevel: Schema.optionalKey<Schema.middlewareDecoding<Schema.UndefinedOr<Schema.declare<import("../schema/options.js").OpenString<"minimal" | "low" | "medium" | "high">, import("../schema/options.js").OpenString<"minimal" | "low" | "medium" | "high">>>, never>>;
|
|
21
21
|
}>>, never>>;
|
|
22
22
|
}>;
|
|
23
23
|
declare const GeminiBody: Schema.Struct<{
|
|
@@ -63,8 +63,8 @@ declare const GeminiBody: Schema.Struct<{
|
|
|
63
63
|
}>>;
|
|
64
64
|
labels: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
|
|
65
65
|
safetySettings: Schema.optional<Schema.$Array<Schema.Struct<{
|
|
66
|
-
readonly category: Schema.declare<(
|
|
67
|
-
readonly threshold: Schema.declare<(
|
|
66
|
+
readonly category: Schema.declare<import("../schema/options.js").OpenString<"HARM_CATEGORY_UNSPECIFIED" | "HARM_CATEGORY_HATE_SPEECH" | "HARM_CATEGORY_DANGEROUS_CONTENT" | "HARM_CATEGORY_HARASSMENT" | "HARM_CATEGORY_SEXUALLY_EXPLICIT" | "HARM_CATEGORY_CIVIC_INTEGRITY">, import("../schema/options.js").OpenString<"HARM_CATEGORY_UNSPECIFIED" | "HARM_CATEGORY_HATE_SPEECH" | "HARM_CATEGORY_DANGEROUS_CONTENT" | "HARM_CATEGORY_HARASSMENT" | "HARM_CATEGORY_SEXUALLY_EXPLICIT" | "HARM_CATEGORY_CIVIC_INTEGRITY">>;
|
|
67
|
+
readonly threshold: Schema.declare<import("../schema/options.js").OpenString<"HARM_BLOCK_THRESHOLD_UNSPECIFIED" | "BLOCK_LOW_AND_ABOVE" | "BLOCK_MEDIUM_AND_ABOVE" | "BLOCK_ONLY_HIGH" | "BLOCK_NONE" | "OFF">, import("../schema/options.js").OpenString<"HARM_BLOCK_THRESHOLD_UNSPECIFIED" | "BLOCK_LOW_AND_ABOVE" | "BLOCK_MEDIUM_AND_ABOVE" | "BLOCK_ONLY_HIGH" | "BLOCK_NONE" | "OFF">>;
|
|
68
68
|
}>>>;
|
|
69
69
|
serviceTier: Schema.optional<Schema.String>;
|
|
70
70
|
systemInstruction: Schema.optional<Schema.Struct<{
|
|
@@ -76,7 +76,7 @@ declare const GeminiBody: Schema.Struct<{
|
|
|
76
76
|
readonly functionDeclarations: Schema.$Array<Schema.Struct<{
|
|
77
77
|
readonly name: Schema.String;
|
|
78
78
|
readonly description: Schema.String;
|
|
79
|
-
readonly
|
|
79
|
+
readonly parametersJsonSchema: Schema.$Record<Schema.String, Schema.Unknown>;
|
|
80
80
|
}>>;
|
|
81
81
|
}>>>;
|
|
82
82
|
toolConfig: Schema.optional<Schema.Struct<{
|
|
@@ -97,7 +97,7 @@ declare const GeminiBody: Schema.Struct<{
|
|
|
97
97
|
readonly thinkingConfig: Schema.optional<Schema.Struct<{
|
|
98
98
|
readonly thinkingBudget: Schema.optional<Schema.Number>;
|
|
99
99
|
readonly includeThoughts: Schema.optional<Schema.Boolean>;
|
|
100
|
-
readonly thinkingLevel: Schema.optional<Schema.declare<(
|
|
100
|
+
readonly thinkingLevel: Schema.optional<Schema.declare<import("../schema/options.js").OpenString<"minimal" | "low" | "medium" | "high">, import("../schema/options.js").OpenString<"minimal" | "low" | "medium" | "high">>>;
|
|
101
101
|
}>>;
|
|
102
102
|
}>>;
|
|
103
103
|
}>;
|
|
@@ -148,11 +148,11 @@ export declare const protocol: Protocol<{
|
|
|
148
148
|
}[];
|
|
149
149
|
readonly tools?: readonly {
|
|
150
150
|
readonly functionDeclarations: readonly {
|
|
151
|
-
readonly description: string;
|
|
152
151
|
readonly name: string;
|
|
153
|
-
readonly
|
|
152
|
+
readonly description: string;
|
|
153
|
+
readonly parametersJsonSchema: {
|
|
154
154
|
readonly [x: string]: unknown;
|
|
155
|
-
}
|
|
155
|
+
};
|
|
156
156
|
}[];
|
|
157
157
|
}[] | undefined;
|
|
158
158
|
readonly serviceTier?: string | undefined;
|
|
@@ -164,8 +164,8 @@ export declare const protocol: Protocol<{
|
|
|
164
164
|
} | undefined;
|
|
165
165
|
readonly cachedContent?: string | undefined;
|
|
166
166
|
readonly safetySettings?: readonly {
|
|
167
|
-
readonly category: (
|
|
168
|
-
readonly threshold: (
|
|
167
|
+
readonly category: import("../schema/options.js").OpenString<"HARM_CATEGORY_UNSPECIFIED" | "HARM_CATEGORY_HATE_SPEECH" | "HARM_CATEGORY_DANGEROUS_CONTENT" | "HARM_CATEGORY_HARASSMENT" | "HARM_CATEGORY_SEXUALLY_EXPLICIT" | "HARM_CATEGORY_CIVIC_INTEGRITY">;
|
|
168
|
+
readonly threshold: import("../schema/options.js").OpenString<"HARM_BLOCK_THRESHOLD_UNSPECIFIED" | "BLOCK_LOW_AND_ABOVE" | "BLOCK_MEDIUM_AND_ABOVE" | "BLOCK_ONLY_HIGH" | "BLOCK_NONE" | "OFF">;
|
|
169
169
|
}[] | undefined;
|
|
170
170
|
readonly labels?: {
|
|
171
171
|
readonly [x: string]: string;
|
|
@@ -186,7 +186,7 @@ export declare const protocol: Protocol<{
|
|
|
186
186
|
readonly thinkingConfig?: {
|
|
187
187
|
readonly thinkingBudget?: number | undefined;
|
|
188
188
|
readonly includeThoughts?: boolean | undefined;
|
|
189
|
-
readonly thinkingLevel?: (
|
|
189
|
+
readonly thinkingLevel?: import("../schema/options.js").OpenString<"minimal" | "low" | "medium" | "high"> | undefined;
|
|
190
190
|
} | undefined;
|
|
191
191
|
readonly maxOutputTokens?: number | undefined;
|
|
192
192
|
} | undefined;
|
|
@@ -206,11 +206,11 @@ export declare const protocol: Protocol<{
|
|
|
206
206
|
readonly safetyRatings?: unknown;
|
|
207
207
|
} | null | undefined;
|
|
208
208
|
readonly usageMetadata?: {
|
|
209
|
-
readonly cachedContentTokenCount?: number | null | undefined;
|
|
210
|
-
readonly thoughtsTokenCount?: number | null | undefined;
|
|
211
209
|
readonly promptTokenCount?: number | null | undefined;
|
|
212
210
|
readonly candidatesTokenCount?: number | null | undefined;
|
|
213
211
|
readonly totalTokenCount?: number | null | undefined;
|
|
212
|
+
readonly cachedContentTokenCount?: number | null | undefined;
|
|
213
|
+
readonly thoughtsTokenCount?: number | null | undefined;
|
|
214
214
|
} | null | undefined;
|
|
215
215
|
}, {
|
|
216
216
|
route: string;
|
|
@@ -262,11 +262,11 @@ export declare const route: Route<{
|
|
|
262
262
|
}[];
|
|
263
263
|
readonly tools?: readonly {
|
|
264
264
|
readonly functionDeclarations: readonly {
|
|
265
|
-
readonly description: string;
|
|
266
265
|
readonly name: string;
|
|
267
|
-
readonly
|
|
266
|
+
readonly description: string;
|
|
267
|
+
readonly parametersJsonSchema: {
|
|
268
268
|
readonly [x: string]: unknown;
|
|
269
|
-
}
|
|
269
|
+
};
|
|
270
270
|
}[];
|
|
271
271
|
}[] | undefined;
|
|
272
272
|
readonly serviceTier?: string | undefined;
|
|
@@ -278,8 +278,8 @@ export declare const route: Route<{
|
|
|
278
278
|
} | undefined;
|
|
279
279
|
readonly cachedContent?: string | undefined;
|
|
280
280
|
readonly safetySettings?: readonly {
|
|
281
|
-
readonly category: (
|
|
282
|
-
readonly threshold: (
|
|
281
|
+
readonly category: import("../schema/options.js").OpenString<"HARM_CATEGORY_UNSPECIFIED" | "HARM_CATEGORY_HATE_SPEECH" | "HARM_CATEGORY_DANGEROUS_CONTENT" | "HARM_CATEGORY_HARASSMENT" | "HARM_CATEGORY_SEXUALLY_EXPLICIT" | "HARM_CATEGORY_CIVIC_INTEGRITY">;
|
|
282
|
+
readonly threshold: import("../schema/options.js").OpenString<"HARM_BLOCK_THRESHOLD_UNSPECIFIED" | "BLOCK_LOW_AND_ABOVE" | "BLOCK_MEDIUM_AND_ABOVE" | "BLOCK_ONLY_HIGH" | "BLOCK_NONE" | "OFF">;
|
|
283
283
|
}[] | undefined;
|
|
284
284
|
readonly labels?: {
|
|
285
285
|
readonly [x: string]: string;
|
|
@@ -300,7 +300,7 @@ export declare const route: Route<{
|
|
|
300
300
|
readonly thinkingConfig?: {
|
|
301
301
|
readonly thinkingBudget?: number | undefined;
|
|
302
302
|
readonly includeThoughts?: boolean | undefined;
|
|
303
|
-
readonly thinkingLevel?: (
|
|
303
|
+
readonly thinkingLevel?: import("../schema/options.js").OpenString<"minimal" | "low" | "medium" | "high"> | undefined;
|
|
304
304
|
} | undefined;
|
|
305
305
|
readonly maxOutputTokens?: number | undefined;
|
|
306
306
|
} | undefined;
|
package/dist/protocols/gemini.js
CHANGED
|
@@ -9,12 +9,13 @@ import { AIError, LLMEvent, Usage, } from "../schema/index.js";
|
|
|
9
9
|
import { classifyProviderFailure } from "../provider-error.js";
|
|
10
10
|
import { Media } from "../media.js";
|
|
11
11
|
import { JsonObject, knownString, lenient, optionalArray, optionalNull, ProviderShared } from "./shared.js";
|
|
12
|
-
import {
|
|
12
|
+
import { GeminiGenerateContent } from "./utils/gemini-generate-content.js";
|
|
13
13
|
import { Lifecycle } from "./utils/lifecycle.js";
|
|
14
|
-
import { ToolSchemaProjection } from "./utils/tool-schema.js";
|
|
15
14
|
const ADAPTER = "gemini";
|
|
16
15
|
// Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost.
|
|
17
16
|
const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator";
|
|
17
|
+
// Gemini 2.5 rejects a budget under the model's minimum: 512 on Flash-Lite, the highest, and 128 on Pro.
|
|
18
|
+
const MIN_THINKING_BUDGET = 512;
|
|
18
19
|
export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
19
20
|
// Gemini 3 rejects replayed function calls without a thought signature. Google's SDKs avoid that in normal chats by
|
|
20
21
|
// retaining complete model responses, but OpenCode reconstructs durable history and may encounter an unsigned call
|
|
@@ -102,7 +103,7 @@ const GeminiSystemInstruction = Schema.Struct({
|
|
|
102
103
|
const GeminiFunctionDeclaration = Schema.Struct({
|
|
103
104
|
name: Schema.String,
|
|
104
105
|
description: Schema.String,
|
|
105
|
-
|
|
106
|
+
parametersJsonSchema: JsonObject,
|
|
106
107
|
});
|
|
107
108
|
const GeminiTool = Schema.Struct({
|
|
108
109
|
functionDeclarations: Schema.Array(GeminiFunctionDeclaration),
|
|
@@ -186,34 +187,13 @@ const GeminiEvent = Schema.Struct({
|
|
|
186
187
|
usageMetadata: optionalNull(GeminiUsage),
|
|
187
188
|
});
|
|
188
189
|
// =============================================================================
|
|
189
|
-
// Tool Schema Conversion
|
|
190
|
-
// =============================================================================
|
|
191
|
-
// Tool-schema conversion has two distinct concerns:
|
|
192
|
-
//
|
|
193
|
-
// 1. Sanitize — fix common authoring mistakes Gemini rejects: integer/number
|
|
194
|
-
// enums (must be strings), `required` entries that don't match a property,
|
|
195
|
-
// untyped arrays (`items` must be present), and `properties`/`required`
|
|
196
|
-
// keys on non-object scalars. Mirrors OpenCode's historical Gemini rules.
|
|
197
|
-
//
|
|
198
|
-
// 2. Project — lossy mapping from JSON Schema to Gemini's schema dialect:
|
|
199
|
-
// drop empty root parameter schemas while preserving nested empty objects,
|
|
200
|
-
// expand type arrays into `anyOf`, derive `nullable: true` from null members,
|
|
201
|
-
// coerce `const` to `[const]` enum, recurse properties/items, and propagate
|
|
202
|
-
// only an allowlisted set of keys (description, required, format, type,
|
|
203
|
-
// nullable, enum, properties, items, allOf, anyOf, oneOf, minLength).
|
|
204
|
-
// Anything outside the allowlist (e.g. `additionalProperties`, `$ref`) is
|
|
205
|
-
// silently dropped.
|
|
206
|
-
//
|
|
207
|
-
// Sanitize runs first, then project. The implementation lives in
|
|
208
|
-
// `utils/gemini-tool-schema` so this protocol keeps the same shape as the other
|
|
209
|
-
// provider protocols.
|
|
210
|
-
// =============================================================================
|
|
211
190
|
// Request Lowering
|
|
212
191
|
// =============================================================================
|
|
213
|
-
|
|
192
|
+
// Tool schemas go in `parametersJsonSchema`, which accepts standard JSON Schema.
|
|
193
|
+
const lowerTool = (tool) => ({
|
|
214
194
|
name: tool.name,
|
|
215
195
|
description: tool.description,
|
|
216
|
-
|
|
196
|
+
parametersJsonSchema: tool.inputSchema,
|
|
217
197
|
});
|
|
218
198
|
const lowerToolConfig = (toolChoice) => ProviderShared.matchToolChoice("Gemini", toolChoice, {
|
|
219
199
|
auto: () => ({ functionCallingConfig: { mode: "AUTO" } }),
|
|
@@ -221,15 +201,10 @@ const lowerToolConfig = (toolChoice) => ProviderShared.matchToolChoice("Gemini",
|
|
|
221
201
|
required: () => ({ functionCallingConfig: { mode: "ANY" } }),
|
|
222
202
|
tool: (name) => ({ functionCallingConfig: { mode: "ANY", allowedFunctionNames: [name] } }),
|
|
223
203
|
});
|
|
224
|
-
// Gemini does not fetch public URLs; inline payloads and Gemini Files references are the accepted inputs.
|
|
225
204
|
const lowerContentPart = Effect.fn("Gemini.lowerContentPart")(function* (part) {
|
|
226
205
|
if (part.type === "text")
|
|
227
206
|
return { text: part.text };
|
|
228
|
-
|
|
229
|
-
if (source.type === "ref" && source.provider === "google")
|
|
230
|
-
return { fileData: { mimeType: part.media.mediaType, fileUri: source.id } };
|
|
231
|
-
const media = yield* ProviderShared.requireInlineMedia("Gemini", part.media);
|
|
232
|
-
return { inlineData: { mimeType: media.mime, data: media.base64 } };
|
|
207
|
+
return yield* GeminiGenerateContent.mediaPart("Gemini", part.media);
|
|
233
208
|
});
|
|
234
209
|
const providerMetadata = (key, metadata) => ({ [key]: metadata });
|
|
235
210
|
const thoughtSignature = (metadata, key) => {
|
|
@@ -382,7 +357,6 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request) {
|
|
|
382
357
|
const hasTools = flattened.tools.length > 0;
|
|
383
358
|
const generation = request.generation;
|
|
384
359
|
const options = yield* decodeOptions(request.providerOptions ?? {});
|
|
385
|
-
const toolSchemaCompatibility = request.model.compatibility?.toolSchema;
|
|
386
360
|
const generationConfig = {
|
|
387
361
|
maxOutputTokens: generation?.maxTokens,
|
|
388
362
|
temperature: generation?.temperature,
|
|
@@ -392,9 +366,16 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request) {
|
|
|
392
366
|
presencePenalty: generation?.presencePenalty,
|
|
393
367
|
seed: generation?.seed,
|
|
394
368
|
stopSequences: generation?.stop,
|
|
369
|
+
// Gemini accepts a budget above `maxOutputTokens`, but thinking then leaves the answer empty.
|
|
395
370
|
thinkingConfig: options.thinkingConfig === undefined
|
|
396
371
|
? undefined
|
|
397
|
-
: {
|
|
372
|
+
: {
|
|
373
|
+
...options.thinkingConfig,
|
|
374
|
+
includeThoughts: options.thinkingConfig.includeThoughts ?? true,
|
|
375
|
+
thinkingBudget: options.thinkingConfig.thinkingBudget === undefined
|
|
376
|
+
? undefined
|
|
377
|
+
: ProviderShared.fitThinkingBudget(options.thinkingConfig.thinkingBudget, generation?.maxTokens, MIN_THINKING_BUDGET),
|
|
378
|
+
},
|
|
398
379
|
};
|
|
399
380
|
return {
|
|
400
381
|
cachedContent: options.cachedContent,
|
|
@@ -405,7 +386,7 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request) {
|
|
|
405
386
|
tools: hasTools
|
|
406
387
|
? [
|
|
407
388
|
{
|
|
408
|
-
functionDeclarations: flattened.tools.map(
|
|
389
|
+
functionDeclarations: flattened.tools.map(lowerTool),
|
|
409
390
|
},
|
|
410
391
|
]
|
|
411
392
|
: undefined,
|
|
@@ -671,6 +652,8 @@ export const protocol = Protocol.make({
|
|
|
671
652
|
schema: GeminiBody,
|
|
672
653
|
from: fromRequest,
|
|
673
654
|
},
|
|
655
|
+
// Gemini's schema rules are this API's default, including for tuned endpoints whose IDs do not name Gemini.
|
|
656
|
+
sanitizer: "gemini",
|
|
674
657
|
stream: {
|
|
675
658
|
event: Protocol.jsonEvent(GeminiEvent),
|
|
676
659
|
initial: (request) => ({
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js";
|
|
2
2
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
3
3
|
import { MediaRoute } from "../route/media.js";
|
|
4
|
+
import { type OpenString } from "../schema/index.js";
|
|
4
5
|
export declare const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
5
|
-
export type GoogleImageString<Known extends string> = Known | (string & {});
|
|
6
6
|
/** Provider-native options. Common fields (`aspectRatio`, `seed`, `images`) live on the request. */
|
|
7
7
|
export type GoogleImageOptions = {
|
|
8
|
-
readonly imageSize?:
|
|
9
|
-
readonly thinkingLevel?:
|
|
8
|
+
readonly imageSize?: OpenString<"1K" | "2K" | "4K">;
|
|
9
|
+
readonly thinkingLevel?: OpenString<"MINIMAL" | "LOW" | "MEDIUM" | "HIGH">;
|
|
10
10
|
readonly includeThoughts?: boolean;
|
|
11
11
|
} & Record<string, unknown>;
|
|
12
12
|
export type Request = ImageRequestFor<GoogleImageOptions>;
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import { ImageModel, ImageResponse } from "../image.js";
|
|
3
|
-
import { Media } from "../media.js";
|
|
4
3
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
5
4
|
import { MediaRoute } from "../route/media.js";
|
|
6
|
-
import {
|
|
5
|
+
import { mergeJsonRecords } from "../schema/index.js";
|
|
7
6
|
import { ProviderShared } from "./shared.js";
|
|
7
|
+
import { GeminiGenerateContent } from "./utils/gemini-generate-content.js";
|
|
8
8
|
import { MediaInput } from "./utils/media-input.js";
|
|
9
|
-
const
|
|
10
|
-
const NAME = "Google Images";
|
|
11
|
-
const PROVIDER = ProviderID.make("google");
|
|
9
|
+
const route = MediaProtocol.identity({ id: "google-images", name: "Google Images", provider: "google" });
|
|
12
10
|
export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
13
11
|
// ---------------------------------------------------------------------------
|
|
14
12
|
// 2. Response schema
|
|
@@ -61,27 +59,10 @@ const generationConfig = (request) => {
|
|
|
61
59
|
thinkingConfig: Object.values(thinkingConfig).some((value) => value !== undefined) ? thinkingConfig : undefined,
|
|
62
60
|
}, native) ?? { responseModalities: ["IMAGE"] });
|
|
63
61
|
};
|
|
64
|
-
// Gemini does not fetch public URLs; inline payloads or Gemini Files references are the only accepted inputs.
|
|
65
|
-
const imagePart = (asset) => {
|
|
66
|
-
const inline = asset.inline();
|
|
67
|
-
if (inline)
|
|
68
|
-
return Effect.succeed({ inlineData: { mimeType: inline.mime, data: inline.base64 } });
|
|
69
|
-
const id = MediaInput.refID(asset, PROVIDER);
|
|
70
|
-
if (id)
|
|
71
|
-
return Effect.succeed({ fileData: { mimeType: asset.mediaType, fileUri: id } });
|
|
72
|
-
if (asset.source.type === "ref")
|
|
73
|
-
return Effect.fail(ProviderShared.invalidRequest("Google generateContent requires Gemini file references rather than other providers' file IDs"));
|
|
74
|
-
return Effect.fail(ProviderShared.invalidRequest("Google generateContent does not fetch public image URLs; use bytes, a data URL, or a Gemini file reference"));
|
|
75
|
-
};
|
|
76
62
|
const fromRequest = Effect.fn("GoogleImages.fromRequest")(function* (request) {
|
|
77
63
|
if (request.n !== undefined && request.n > 1)
|
|
78
|
-
return yield*
|
|
79
|
-
|
|
80
|
-
provider: PROVIDER,
|
|
81
|
-
route: ADAPTER,
|
|
82
|
-
message: `${NAME} generates one image per request; call it once per image instead of n=${request.n}`,
|
|
83
|
-
});
|
|
84
|
-
const parts = yield* Effect.forEach(request.images ?? [], imagePart);
|
|
64
|
+
return yield* route.unsupported("media.n", `${route.name} generates one image per request; call it once per image instead of n=${request.n}`);
|
|
65
|
+
const parts = yield* Effect.forEach(request.images ?? [], (image) => GeminiGenerateContent.mediaPart(route.name, image));
|
|
85
66
|
return MediaProtocol.json(mergeJsonRecords({
|
|
86
67
|
contents: [{ role: "user", parts: [{ text: request.prompt }, ...parts] }],
|
|
87
68
|
generationConfig: generationConfig(request),
|
|
@@ -90,8 +71,9 @@ const fromRequest = Effect.fn("GoogleImages.fromRequest")(function* (request) {
|
|
|
90
71
|
// ---------------------------------------------------------------------------
|
|
91
72
|
// 6. Response decoding
|
|
92
73
|
// ---------------------------------------------------------------------------
|
|
74
|
+
const decodeDocument = route.decodeJson(GoogleImageResponse);
|
|
93
75
|
const decodeResponse = Effect.fn("GoogleImages.decodeResponse")(function* (response) {
|
|
94
|
-
const output = yield*
|
|
76
|
+
const output = yield* decodeDocument(response);
|
|
95
77
|
const decoded = output.value;
|
|
96
78
|
const candidates = decoded.candidates ?? [];
|
|
97
79
|
const candidateMetadata = candidates.map((candidate, candidateIndex) => ({
|
|
@@ -122,7 +104,7 @@ const decodeResponse = Effect.fn("GoogleImages.decodeResponse")(function* (respo
|
|
|
122
104
|
thoughtSignature: part.thoughtSignature,
|
|
123
105
|
},
|
|
124
106
|
]));
|
|
125
|
-
const images = yield* Effect.forEach(encoded, (item) => MediaInput.decodedAsset(output.invalid, `${
|
|
107
|
+
const images = yield* Effect.forEach(encoded, (item) => MediaInput.decodedAsset(output.invalid, `${route.name} candidate ${item.candidateIndex} part ${item.partIndex}`, item.inlineData.data, item.inlineData.mimeType, {
|
|
126
108
|
providerMetadata: {
|
|
127
109
|
google: {
|
|
128
110
|
candidateIndex: item.candidate.index ?? item.candidateIndex,
|
|
@@ -137,7 +119,7 @@ const decodeResponse = Effect.fn("GoogleImages.decodeResponse")(function* (respo
|
|
|
137
119
|
}));
|
|
138
120
|
if (images.length === 0) {
|
|
139
121
|
const finishReasons = candidates.flatMap((candidate) => candidate.finishReason === undefined ? [] : [candidate.finishReason]);
|
|
140
|
-
return yield* output.invalid(`${
|
|
122
|
+
return yield* output.invalid(`${route.name} returned no final images${finishReasons.length === 0 ? "" : ` (finish reasons: ${finishReasons.join(", ")})`}; inspect body for prompt feedback and candidate details`);
|
|
141
123
|
}
|
|
142
124
|
// Candidates that stopped for a safety or policy reason are partial results, not a silent drop.
|
|
143
125
|
const notices = [
|
|
@@ -146,7 +128,7 @@ const decodeResponse = Effect.fn("GoogleImages.decodeResponse")(function* (respo
|
|
|
146
128
|
: [
|
|
147
129
|
{
|
|
148
130
|
type: "filtered",
|
|
149
|
-
message: `${
|
|
131
|
+
message: `${route.name} reported prompt feedback`,
|
|
150
132
|
providerMetadata: { google: { promptFeedback: decoded.promptFeedback } },
|
|
151
133
|
},
|
|
152
134
|
]),
|
|
@@ -155,7 +137,7 @@ const decodeResponse = Effect.fn("GoogleImages.decodeResponse")(function* (respo
|
|
|
155
137
|
: [
|
|
156
138
|
{
|
|
157
139
|
type: "filtered",
|
|
158
|
-
message: `${
|
|
140
|
+
message: `${route.name} candidate ${candidate.index ?? index} finished with ${candidate.finishReason}${candidate.finishMessage === undefined ? "" : `: ${candidate.finishMessage}`}`,
|
|
159
141
|
providerMetadata: {
|
|
160
142
|
google: {
|
|
161
143
|
candidateIndex: candidate.index ?? index,
|
|
@@ -198,16 +180,12 @@ const decodeResponse = Effect.fn("GoogleImages.decodeResponse")(function* (respo
|
|
|
198
180
|
// ---------------------------------------------------------------------------
|
|
199
181
|
// 7. Protocol and route
|
|
200
182
|
// ---------------------------------------------------------------------------
|
|
201
|
-
export const protocol = MediaProtocol.inline({
|
|
202
|
-
id: ADAPTER,
|
|
203
|
-
name: NAME,
|
|
183
|
+
export const protocol = MediaProtocol.inline(route, {
|
|
204
184
|
unsupported: ["mask", "size", "format"],
|
|
205
185
|
body: { from: fromRequest },
|
|
206
186
|
response: { decode: decodeResponse },
|
|
207
187
|
});
|
|
208
188
|
export const model = (input) => ImageModel.fromRoute({
|
|
209
|
-
id: ADAPTER,
|
|
210
|
-
provider: PROVIDER,
|
|
211
189
|
protocol,
|
|
212
190
|
baseURL: DEFAULT_BASE_URL,
|
|
213
191
|
path: ({ request }) => `/models/${request.model.id}:generateContent`,
|