@opencode/ai 2.0.16 → 2.0.18
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 +187 -129
- 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 +6 -10
- package/dist/generation.js +17 -20
- package/dist/image-client.d.ts +59 -24
- package/dist/image-client.js +16 -40
- package/dist/image.d.ts +15 -27
- package/dist/image.js +4 -15
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -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 +9 -10
- package/dist/media-model.js +10 -12
- package/dist/media.js +2 -2
- package/dist/promise.d.ts +68 -30
- package/dist/promise.js +52 -31
- 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/anthropic-messages.d.ts +34 -34
- package/dist/protocols/anthropic-messages.js +14 -8
- package/dist/protocols/assemblyai-transcription.d.ts +2 -1
- package/dist/protocols/assemblyai-transcription.js +14 -16
- package/dist/protocols/bedrock-converse.d.ts +7 -0
- package/dist/protocols/bedrock-converse.js +37 -11
- package/dist/protocols/bfl-images.d.ts +7 -1
- package/dist/protocols/bfl-images.js +36 -36
- package/dist/protocols/cartesia-speech.d.ts +18 -2
- package/dist/protocols/cartesia-speech.js +10 -16
- package/dist/protocols/deepgram-speech.d.ts +19 -3
- package/dist/protocols/deepgram-speech.js +11 -12
- package/dist/protocols/deepgram-transcription.d.ts +2 -1
- package/dist/protocols/deepgram-transcription.js +9 -13
- package/dist/protocols/elevenlabs-speech.d.ts +19 -3
- package/dist/protocols/elevenlabs-speech.js +9 -13
- package/dist/protocols/fal-images.d.ts +2 -1
- package/dist/protocols/fal-images.js +30 -40
- package/dist/protocols/fal-video.d.ts +2 -2
- package/dist/protocols/fal-video.js +9 -24
- package/dist/protocols/gemini.d.ts +13 -13
- package/dist/protocols/gemini.js +16 -7
- package/dist/protocols/google-images.d.ts +3 -3
- package/dist/protocols/google-images.js +11 -21
- package/dist/protocols/google-speech.d.ts +16 -0
- package/dist/protocols/google-speech.js +20 -16
- package/dist/protocols/google-transcription.d.ts +2 -1
- package/dist/protocols/google-transcription.js +8 -20
- package/dist/protocols/google-video.d.ts +2 -2
- package/dist/protocols/google-video.js +14 -30
- package/dist/protocols/meta-images.d.ts +3 -4
- package/dist/protocols/meta-images.js +12 -21
- package/dist/protocols/meta-messages.d.ts +5 -5
- package/dist/protocols/meta-responses.js +6 -4
- package/dist/protocols/open-responses.d.ts +4 -3
- package/dist/protocols/open-responses.js +4 -8
- package/dist/protocols/openai-chat.js +3 -4
- package/dist/protocols/openai-images.d.ts +7 -5
- package/dist/protocols/openai-images.js +65 -66
- package/dist/protocols/openai-responses.d.ts +10 -10
- package/dist/protocols/openai-responses.js +31 -30
- package/dist/protocols/openai-speech.d.ts +21 -1
- package/dist/protocols/openai-speech.js +11 -12
- package/dist/protocols/openai-transcription.d.ts +4 -0
- package/dist/protocols/openai-transcription.js +44 -32
- package/dist/protocols/replicate-images.js +11 -17
- package/dist/protocols/runway-video.d.ts +3 -3
- package/dist/protocols/runway-video.js +11 -17
- package/dist/protocols/shared.d.ts +11 -17
- package/dist/protocols/shared.js +11 -40
- package/dist/protocols/stability-images.d.ts +2 -1
- package/dist/protocols/stability-images.js +24 -36
- package/dist/protocols/utils/fal-queue.d.ts +1 -3
- package/dist/protocols/utils/fal-queue.js +4 -6
- package/dist/protocols/utils/media-input.d.ts +15 -1
- package/dist/protocols/utils/media-input.js +22 -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 +3 -3
- package/dist/protocols/utils/speech-stream.js +1 -4
- package/dist/protocols/utils/tool-schema.d.ts +2 -2
- package/dist/protocols/utils/tool-schema.js +29 -9
- package/dist/protocols/xai-images.d.ts +4 -4
- package/dist/protocols/xai-images.js +14 -29
- package/dist/protocols/xai-responses.js +1 -1
- package/dist/protocols/xai-video.js +12 -18
- 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 +7 -1
- package/dist/providers/alibaba.d.ts +1 -1
- 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 +1 -1
- package/dist/providers/assemblyai.js +5 -10
- package/dist/providers/azure.d.ts +4 -4
- package/dist/providers/azure.js +2 -2
- package/dist/providers/black-forest-labs.d.ts +1 -1
- package/dist/providers/black-forest-labs.js +5 -10
- package/dist/providers/cartesia.d.ts +1 -1
- package/dist/providers/cartesia.js +5 -10
- package/dist/providers/cloudflare-ai-gateway.d.ts +14 -14
- package/dist/providers/deepgram.d.ts +1 -1
- package/dist/providers/deepgram.js +6 -12
- package/dist/providers/elevenlabs.d.ts +1 -1
- package/dist/providers/elevenlabs.js +5 -10
- package/dist/providers/fal.d.ts +1 -1
- package/dist/providers/fal.js +5 -12
- package/dist/providers/google-vertex-messages.d.ts +5 -5
- package/dist/providers/google-vertex.d.ts +3 -3
- package/dist/providers/google.d.ts +3 -3
- package/dist/providers/google.js +7 -12
- package/dist/providers/meta.d.ts +8 -8
- package/dist/providers/meta.js +4 -8
- package/dist/providers/minimax.d.ts +5 -5
- package/dist/providers/moonshot.d.ts +5 -5
- package/dist/providers/openai-options.d.ts +3 -9
- package/dist/providers/openai-options.js +4 -7
- package/dist/providers/openai.d.ts +9 -10
- package/dist/providers/openai.js +11 -12
- package/dist/providers/opencode-zen.js +1 -1
- package/dist/providers/openrouter.d.ts +6 -7
- package/dist/providers/openrouter.js +10 -5
- package/dist/providers/replicate.d.ts +1 -1
- package/dist/providers/replicate.js +5 -10
- package/dist/providers/runway.d.ts +1 -1
- package/dist/providers/runway.js +5 -10
- package/dist/providers/stability.d.ts +1 -1
- package/dist/providers/stability.js +6 -11
- package/dist/providers/typesafe-ai.js +1 -1
- package/dist/providers/vercel-ai-gateway.js +1 -1
- package/dist/providers/xai.js +5 -10
- package/dist/providers/zai-coding-plan.d.ts +1 -1
- package/dist/providers/zai.js +4 -8
- package/dist/route/auth.d.ts +1 -1
- package/dist/route/auth.js +14 -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 +10 -1
- package/dist/route/framing.js +45 -5
- package/dist/route/index.d.ts +1 -1
- package/dist/route/media-protocol.d.ts +49 -35
- package/dist/route/media-protocol.js +77 -65
- package/dist/route/media.d.ts +10 -16
- package/dist/route/media.js +46 -62
- package/dist/route/protocol.d.ts +3 -1
- package/dist/schema/options.d.ts +6 -3
- package/dist/schema/options.js +7 -3
- package/dist/speech-client.d.ts +62 -17
- package/dist/speech-client.js +17 -21
- package/dist/speech.d.ts +178 -21
- package/dist/speech.js +13 -8
- package/dist/testing.d.ts +2 -2
- package/dist/transcription-client.d.ts +78 -24
- package/dist/transcription-client.js +9 -40
- package/dist/transcription.d.ts +15 -27
- package/dist/transcription.js +4 -10
- package/dist/utils/json.d.ts +4 -0
- package/dist/utils/json.js +4 -0
- package/dist/utils/media-type.d.ts +2 -1
- package/dist/utils/media-type.js +3 -1
- package/dist/video-client.d.ts +55 -24
- package/dist/video-client.js +16 -36
- package/dist/video.d.ts +18 -26
- package/dist/video.js +15 -16
- package/package.json +3 -3
- 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
|
@@ -3,13 +3,11 @@ import { ImageModel, ImageResponse } from "../image.js";
|
|
|
3
3
|
import { Media } from "../media.js";
|
|
4
4
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
5
5
|
import { MediaRoute } from "../route/media.js";
|
|
6
|
-
import {
|
|
6
|
+
import { mergeJsonRecords } from "../schema/index.js";
|
|
7
7
|
import { ProviderShared, optionalNull } from "./shared.js";
|
|
8
8
|
import { FalQueue } from "./utils/fal-queue.js";
|
|
9
9
|
import { MediaInput } from "./utils/media-input.js";
|
|
10
|
-
const
|
|
11
|
-
const NAME = "fal Images";
|
|
12
|
-
const PROVIDER = ProviderID.make("fal");
|
|
10
|
+
const route = MediaProtocol.identity({ id: "fal-images", name: "fal Images", provider: "fal" });
|
|
13
11
|
// ---------------------------------------------------------------------------
|
|
14
12
|
// 2. Response schema
|
|
15
13
|
// ---------------------------------------------------------------------------
|
|
@@ -27,37 +25,32 @@ const QueueResult = Schema.StructWithRest(Schema.Struct({
|
|
|
27
25
|
// 5. Request body construction
|
|
28
26
|
// ---------------------------------------------------------------------------
|
|
29
27
|
const sizing = (model) => {
|
|
30
|
-
if (/^fal-ai\/(nano-banana|flux-pro\/v1\.1-ultra)/.test(model))
|
|
28
|
+
if (/^fal-ai\/(nano-banana|flux-pro\/(v1\.1-ultra|kontext))/.test(model))
|
|
31
29
|
return "aspect_ratio";
|
|
32
30
|
if (model.startsWith("fal-ai/flux"))
|
|
33
31
|
return "image_size";
|
|
34
32
|
return undefined;
|
|
35
33
|
};
|
|
36
|
-
const unsupported = (model, field, message) => ProviderShared.unsupportedOperation({
|
|
37
|
-
operation: `media.${field}`,
|
|
38
|
-
provider: PROVIDER,
|
|
39
|
-
route: ADAPTER,
|
|
40
|
-
message: `${model} ${message}`,
|
|
41
|
-
});
|
|
42
34
|
const validate = (request) => {
|
|
43
35
|
const id = request.model.id;
|
|
44
36
|
const field = sizing(id);
|
|
45
37
|
if (request.size !== undefined && request.aspectRatio !== undefined)
|
|
46
|
-
return Effect.fail(ProviderShared.invalidRequest(`${
|
|
38
|
+
return Effect.fail(ProviderShared.invalidRequest(`${route.name} accepts either size or aspectRatio, not both`));
|
|
47
39
|
if (request.size !== undefined && field === "aspect_ratio")
|
|
48
|
-
return Effect.fail(unsupported(
|
|
40
|
+
return Effect.fail(route.unsupported("media.size", `${id} sizes by aspectRatio`));
|
|
49
41
|
if (request.aspectRatio !== undefined && field === "image_size")
|
|
50
|
-
return Effect.fail(unsupported(
|
|
51
|
-
if ((request.images?.length ?? 0) > 1 && !
|
|
52
|
-
return Effect.fail(unsupported(
|
|
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`));
|
|
53
45
|
return Effect.void;
|
|
54
46
|
};
|
|
55
|
-
// `/edit` endpoints take an `image_urls` list; image-to-image, fill, and Ultra take one
|
|
56
|
-
|
|
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");
|
|
57
50
|
const fromRequest = Effect.fn("FalImages.fromRequest")(function* (request) {
|
|
58
51
|
yield* validate(request);
|
|
59
|
-
const images = yield* Effect.forEach(request.images ?? [], (image) => FalQueue.mediaUrl(image,
|
|
60
|
-
const
|
|
52
|
+
const images = yield* Effect.forEach(request.images ?? [], (image) => FalQueue.mediaUrl(image, route.name));
|
|
53
|
+
const list = takesImageList(request.model.id);
|
|
61
54
|
return MediaProtocol.json(mergeJsonRecords({
|
|
62
55
|
prompt: request.prompt,
|
|
63
56
|
num_images: request.n,
|
|
@@ -65,49 +58,46 @@ const fromRequest = Effect.fn("FalImages.fromRequest")(function* (request) {
|
|
|
65
58
|
image_size: request.size === undefined ? undefined : MediaInput.dimensions(request.size),
|
|
66
59
|
aspect_ratio: request.aspectRatio,
|
|
67
60
|
output_format: request.format,
|
|
68
|
-
image_urls:
|
|
69
|
-
image_url:
|
|
70
|
-
mask_url: request.mask === undefined ? undefined : yield* FalQueue.mediaUrl(request.mask,
|
|
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),
|
|
71
64
|
}, request.providerOptions, request.http?.body) ?? {});
|
|
72
65
|
});
|
|
73
66
|
// ---------------------------------------------------------------------------
|
|
74
67
|
// 6. Response decoding
|
|
75
68
|
// ---------------------------------------------------------------------------
|
|
76
|
-
const decodeQueueResult =
|
|
69
|
+
const decodeQueueResult = route.decodeJson(QueueResult);
|
|
77
70
|
const decodeResult = Effect.fn("FalImages.decodeResult")(function* (response, context) {
|
|
78
71
|
const output = yield* decodeQueueResult(response);
|
|
79
72
|
const { images, seed, has_nsfw_concepts, ...rest } = output.value;
|
|
80
73
|
if (images.length === 0)
|
|
81
|
-
return yield* output.invalid(`${
|
|
74
|
+
return yield* output.invalid(`${route.name} returned no images`);
|
|
82
75
|
// With the safety checker on, flagged images come back blacked out rather than omitted.
|
|
83
76
|
const flagged = (has_nsfw_concepts ?? []).flatMap((value, index) => (value ? [index] : []));
|
|
84
77
|
return new ImageResponse({
|
|
85
|
-
images: images.map((image) =>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
}),
|
|
89
84
|
notices: flagged.length === 0
|
|
90
85
|
? undefined
|
|
91
|
-
: flagged.map((index) => ({
|
|
86
|
+
: flagged.map((index) => ({
|
|
87
|
+
type: "moderated",
|
|
88
|
+
message: `${route.name} flagged image ${index} as NSFW`,
|
|
89
|
+
})),
|
|
92
90
|
providerMetadata: { fal: { requestId: context.token.requestID, seed: seed ?? undefined, ...rest } },
|
|
93
91
|
});
|
|
94
92
|
});
|
|
95
93
|
// ---------------------------------------------------------------------------
|
|
96
94
|
// 7. Protocol and route
|
|
97
95
|
// ---------------------------------------------------------------------------
|
|
98
|
-
export const protocol = FalQueue.protocol({
|
|
99
|
-
id: ADAPTER,
|
|
100
|
-
name: NAME,
|
|
96
|
+
export const protocol = FalQueue.protocol(route, {
|
|
101
97
|
from: fromRequest,
|
|
102
98
|
decodeResult,
|
|
103
99
|
});
|
|
104
|
-
export const model = (input) => ImageModel.fromRoute({
|
|
105
|
-
id: ADAPTER,
|
|
106
|
-
provider: PROVIDER,
|
|
107
|
-
protocol,
|
|
108
|
-
baseURL: FalQueue.DEFAULT_BASE_URL,
|
|
109
|
-
path: ({ request }) => `/${request.model.id}`,
|
|
110
|
-
}, input);
|
|
100
|
+
export const model = (input) => ImageModel.fromRoute({ protocol, baseURL: FalQueue.DEFAULT_BASE_URL, path: ({ request }) => `/${request.model.id}` }, input);
|
|
111
101
|
export const FalImages = {
|
|
112
102
|
protocol,
|
|
113
103
|
model,
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
2
2
|
import { MediaRoute } from "../route/media.js";
|
|
3
|
+
import { type OpenString } from "../schema/index.js";
|
|
3
4
|
import { VideoModel, VideoResponse, type VideoRequestFor } from "../video.js";
|
|
4
|
-
export type FalVideoString<Known extends string> = Known | (string & {});
|
|
5
5
|
/**
|
|
6
6
|
* Provider-native input. fal video endpoints are model-specific: `duration` is a string enum whose values differ per
|
|
7
7
|
* model (`"8s"` for Veo, `"5"` for Kling), and last-frame fields are named per model (`end_image_url`,
|
|
8
8
|
* `last_frame_url`, `tail_image_url`), so those pass through here instead of lowering from common fields.
|
|
9
9
|
*/
|
|
10
10
|
export type FalVideoOptions = {
|
|
11
|
-
readonly duration?:
|
|
11
|
+
readonly duration?: OpenString<"4s" | "6s" | "8s" | "5" | "10">;
|
|
12
12
|
} & Record<string, unknown>;
|
|
13
13
|
export type Request = VideoRequestFor<FalVideoOptions>;
|
|
14
14
|
export declare const protocol: MediaProtocol.Queued<Request, VideoResponse, {
|
|
@@ -2,13 +2,11 @@ import { Effect, Schema } from "effect";
|
|
|
2
2
|
import { Media } from "../media.js";
|
|
3
3
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
4
4
|
import { MediaRoute } from "../route/media.js";
|
|
5
|
-
import {
|
|
5
|
+
import { mergeJsonRecords } from "../schema/index.js";
|
|
6
6
|
import { VideoModel, VideoResponse } from "../video.js";
|
|
7
|
-
import {
|
|
7
|
+
import { optionalNull } from "./shared.js";
|
|
8
8
|
import { FalQueue } from "./utils/fal-queue.js";
|
|
9
|
-
const
|
|
10
|
-
const NAME = "fal Video";
|
|
11
|
-
const PROVIDER = ProviderID.make("fal");
|
|
9
|
+
const route = MediaProtocol.identity({ id: "fal-video", name: "fal Video", provider: "fal" });
|
|
12
10
|
// ---------------------------------------------------------------------------
|
|
13
11
|
// 2. Response schema
|
|
14
12
|
// ---------------------------------------------------------------------------
|
|
@@ -26,14 +24,9 @@ const QueueResult = Schema.StructWithRest(Schema.Struct({
|
|
|
26
24
|
// ---------------------------------------------------------------------------
|
|
27
25
|
const fromRequest = Effect.fn("FalVideo.fromRequest")(function* (request) {
|
|
28
26
|
if (request.frames?.last !== undefined)
|
|
29
|
-
return yield*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
route: ADAPTER,
|
|
33
|
-
message: `${NAME} names the last frame per model; pass it through providerOptions (e.g. end_image_url) instead of frames.last`,
|
|
34
|
-
});
|
|
35
|
-
const imageUrl = request.frames?.first === undefined ? undefined : yield* FalQueue.mediaUrl(request.frames.first, NAME);
|
|
36
|
-
const videoUrl = request.video === undefined ? undefined : yield* FalQueue.mediaUrl(request.video, NAME);
|
|
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);
|
|
37
30
|
return MediaProtocol.json(mergeJsonRecords({
|
|
38
31
|
prompt: request.prompt,
|
|
39
32
|
negative_prompt: request.negativePrompt,
|
|
@@ -48,7 +41,7 @@ const fromRequest = Effect.fn("FalVideo.fromRequest")(function* (request) {
|
|
|
48
41
|
// ---------------------------------------------------------------------------
|
|
49
42
|
// 6. Response decoding
|
|
50
43
|
// ---------------------------------------------------------------------------
|
|
51
|
-
const decodeQueueResult =
|
|
44
|
+
const decodeQueueResult = route.decodeJson(QueueResult);
|
|
52
45
|
const decodeResult = Effect.fn("FalVideo.decodeResult")(function* (response, context) {
|
|
53
46
|
const output = yield* decodeQueueResult(response);
|
|
54
47
|
const { video, seed, ...rest } = output.value;
|
|
@@ -68,20 +61,12 @@ const decodeResult = Effect.fn("FalVideo.decodeResult")(function* (response, con
|
|
|
68
61
|
// ---------------------------------------------------------------------------
|
|
69
62
|
// 7. Protocol and route
|
|
70
63
|
// ---------------------------------------------------------------------------
|
|
71
|
-
export const protocol = FalQueue.protocol({
|
|
72
|
-
id: ADAPTER,
|
|
73
|
-
name: NAME,
|
|
64
|
+
export const protocol = FalQueue.protocol(route, {
|
|
74
65
|
unsupported: ["n", "durationSeconds", "references"],
|
|
75
66
|
from: fromRequest,
|
|
76
67
|
decodeResult,
|
|
77
68
|
});
|
|
78
|
-
export const model = (input) => VideoModel.fromRoute({
|
|
79
|
-
id: ADAPTER,
|
|
80
|
-
provider: PROVIDER,
|
|
81
|
-
protocol,
|
|
82
|
-
baseURL: FalQueue.DEFAULT_BASE_URL,
|
|
83
|
-
path: ({ request }) => `/${request.model.id}`,
|
|
84
|
-
}, input);
|
|
69
|
+
export const model = (input) => VideoModel.fromRoute({ protocol, baseURL: FalQueue.DEFAULT_BASE_URL, path: ({ request }) => `/${request.model.id}` }, input);
|
|
85
70
|
export const FalVideo = {
|
|
86
71
|
protocol,
|
|
87
72
|
model,
|
|
@@ -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<{
|
|
@@ -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
|
}>;
|
|
@@ -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;
|
|
@@ -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
|
@@ -11,10 +11,11 @@ import { Media } from "../media.js";
|
|
|
11
11
|
import { JsonObject, knownString, lenient, optionalArray, optionalNull, ProviderShared } from "./shared.js";
|
|
12
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
|
|
@@ -188,12 +189,11 @@ const GeminiEvent = Schema.Struct({
|
|
|
188
189
|
// =============================================================================
|
|
189
190
|
// Request Lowering
|
|
190
191
|
// =============================================================================
|
|
191
|
-
// Tool schemas go in `parametersJsonSchema`, which accepts standard JSON Schema.
|
|
192
|
-
|
|
193
|
-
const lowerTool = (tool, model) => ({
|
|
192
|
+
// Tool schemas go in `parametersJsonSchema`, which accepts standard JSON Schema.
|
|
193
|
+
const lowerTool = (tool) => ({
|
|
194
194
|
name: tool.name,
|
|
195
195
|
description: tool.description,
|
|
196
|
-
parametersJsonSchema:
|
|
196
|
+
parametersJsonSchema: tool.inputSchema,
|
|
197
197
|
});
|
|
198
198
|
const lowerToolConfig = (toolChoice) => ProviderShared.matchToolChoice("Gemini", toolChoice, {
|
|
199
199
|
auto: () => ({ functionCallingConfig: { mode: "AUTO" } }),
|
|
@@ -366,9 +366,16 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request) {
|
|
|
366
366
|
presencePenalty: generation?.presencePenalty,
|
|
367
367
|
seed: generation?.seed,
|
|
368
368
|
stopSequences: generation?.stop,
|
|
369
|
+
// Gemini accepts a budget above `maxOutputTokens`, but thinking then leaves the answer empty.
|
|
369
370
|
thinkingConfig: options.thinkingConfig === undefined
|
|
370
371
|
? undefined
|
|
371
|
-
: {
|
|
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
|
+
},
|
|
372
379
|
};
|
|
373
380
|
return {
|
|
374
381
|
cachedContent: options.cachedContent,
|
|
@@ -379,7 +386,7 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request) {
|
|
|
379
386
|
tools: hasTools
|
|
380
387
|
? [
|
|
381
388
|
{
|
|
382
|
-
functionDeclarations: flattened.tools.map(
|
|
389
|
+
functionDeclarations: flattened.tools.map(lowerTool),
|
|
383
390
|
},
|
|
384
391
|
]
|
|
385
392
|
: undefined,
|
|
@@ -645,6 +652,8 @@ export const protocol = Protocol.make({
|
|
|
645
652
|
schema: GeminiBody,
|
|
646
653
|
from: fromRequest,
|
|
647
654
|
},
|
|
655
|
+
// Gemini's schema rules are this API's default, including for tuned endpoints whose IDs do not name Gemini.
|
|
656
|
+
sanitizer: "gemini",
|
|
648
657
|
stream: {
|
|
649
658
|
event: Protocol.jsonEvent(GeminiEvent),
|
|
650
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>;
|
|
@@ -2,13 +2,11 @@ import { Effect, Schema } from "effect";
|
|
|
2
2
|
import { ImageModel, ImageResponse } from "../image.js";
|
|
3
3
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
4
4
|
import { MediaRoute } from "../route/media.js";
|
|
5
|
-
import {
|
|
5
|
+
import { mergeJsonRecords } from "../schema/index.js";
|
|
6
6
|
import { ProviderShared } from "./shared.js";
|
|
7
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
|
|
@@ -63,13 +61,8 @@ const generationConfig = (request) => {
|
|
|
63
61
|
};
|
|
64
62
|
const fromRequest = Effect.fn("GoogleImages.fromRequest")(function* (request) {
|
|
65
63
|
if (request.n !== undefined && request.n > 1)
|
|
66
|
-
return yield*
|
|
67
|
-
|
|
68
|
-
provider: PROVIDER,
|
|
69
|
-
route: ADAPTER,
|
|
70
|
-
message: `${NAME} generates one image per request; call it once per image instead of n=${request.n}`,
|
|
71
|
-
});
|
|
72
|
-
const parts = yield* Effect.forEach(request.images ?? [], (image) => GeminiGenerateContent.mediaPart(NAME, image));
|
|
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));
|
|
73
66
|
return MediaProtocol.json(mergeJsonRecords({
|
|
74
67
|
contents: [{ role: "user", parts: [{ text: request.prompt }, ...parts] }],
|
|
75
68
|
generationConfig: generationConfig(request),
|
|
@@ -78,8 +71,9 @@ const fromRequest = Effect.fn("GoogleImages.fromRequest")(function* (request) {
|
|
|
78
71
|
// ---------------------------------------------------------------------------
|
|
79
72
|
// 6. Response decoding
|
|
80
73
|
// ---------------------------------------------------------------------------
|
|
74
|
+
const decodeDocument = route.decodeJson(GoogleImageResponse);
|
|
81
75
|
const decodeResponse = Effect.fn("GoogleImages.decodeResponse")(function* (response) {
|
|
82
|
-
const output = yield*
|
|
76
|
+
const output = yield* decodeDocument(response);
|
|
83
77
|
const decoded = output.value;
|
|
84
78
|
const candidates = decoded.candidates ?? [];
|
|
85
79
|
const candidateMetadata = candidates.map((candidate, candidateIndex) => ({
|
|
@@ -110,7 +104,7 @@ const decodeResponse = Effect.fn("GoogleImages.decodeResponse")(function* (respo
|
|
|
110
104
|
thoughtSignature: part.thoughtSignature,
|
|
111
105
|
},
|
|
112
106
|
]));
|
|
113
|
-
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, {
|
|
114
108
|
providerMetadata: {
|
|
115
109
|
google: {
|
|
116
110
|
candidateIndex: item.candidate.index ?? item.candidateIndex,
|
|
@@ -125,7 +119,7 @@ const decodeResponse = Effect.fn("GoogleImages.decodeResponse")(function* (respo
|
|
|
125
119
|
}));
|
|
126
120
|
if (images.length === 0) {
|
|
127
121
|
const finishReasons = candidates.flatMap((candidate) => candidate.finishReason === undefined ? [] : [candidate.finishReason]);
|
|
128
|
-
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`);
|
|
129
123
|
}
|
|
130
124
|
// Candidates that stopped for a safety or policy reason are partial results, not a silent drop.
|
|
131
125
|
const notices = [
|
|
@@ -134,7 +128,7 @@ const decodeResponse = Effect.fn("GoogleImages.decodeResponse")(function* (respo
|
|
|
134
128
|
: [
|
|
135
129
|
{
|
|
136
130
|
type: "filtered",
|
|
137
|
-
message: `${
|
|
131
|
+
message: `${route.name} reported prompt feedback`,
|
|
138
132
|
providerMetadata: { google: { promptFeedback: decoded.promptFeedback } },
|
|
139
133
|
},
|
|
140
134
|
]),
|
|
@@ -143,7 +137,7 @@ const decodeResponse = Effect.fn("GoogleImages.decodeResponse")(function* (respo
|
|
|
143
137
|
: [
|
|
144
138
|
{
|
|
145
139
|
type: "filtered",
|
|
146
|
-
message: `${
|
|
140
|
+
message: `${route.name} candidate ${candidate.index ?? index} finished with ${candidate.finishReason}${candidate.finishMessage === undefined ? "" : `: ${candidate.finishMessage}`}`,
|
|
147
141
|
providerMetadata: {
|
|
148
142
|
google: {
|
|
149
143
|
candidateIndex: candidate.index ?? index,
|
|
@@ -186,16 +180,12 @@ const decodeResponse = Effect.fn("GoogleImages.decodeResponse")(function* (respo
|
|
|
186
180
|
// ---------------------------------------------------------------------------
|
|
187
181
|
// 7. Protocol and route
|
|
188
182
|
// ---------------------------------------------------------------------------
|
|
189
|
-
export const protocol = MediaProtocol.inline({
|
|
190
|
-
id: ADAPTER,
|
|
191
|
-
name: NAME,
|
|
183
|
+
export const protocol = MediaProtocol.inline(route, {
|
|
192
184
|
unsupported: ["mask", "size", "format"],
|
|
193
185
|
body: { from: fromRequest },
|
|
194
186
|
response: { decode: decodeResponse },
|
|
195
187
|
});
|
|
196
188
|
export const model = (input) => ImageModel.fromRoute({
|
|
197
|
-
id: ADAPTER,
|
|
198
|
-
provider: PROVIDER,
|
|
199
189
|
protocol,
|
|
200
190
|
baseURL: DEFAULT_BASE_URL,
|
|
201
191
|
path: ({ request }) => `/models/${request.model.id}:generateContent`,
|
|
@@ -26,6 +26,14 @@ interface State extends SpeechStream.Audio, GeminiGenerateContent.Metadata {
|
|
|
26
26
|
readonly mimeType?: string;
|
|
27
27
|
}
|
|
28
28
|
export declare const protocol: MediaProtocol.Streamed<Request, {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly type: "generation-queued";
|
|
31
|
+
readonly position?: number | undefined;
|
|
32
|
+
} | {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly type: "generation-progress";
|
|
35
|
+
readonly progress?: number | undefined;
|
|
36
|
+
} | {
|
|
29
37
|
readonly type: "audio-delta";
|
|
30
38
|
readonly chunk: Uint8Array<ArrayBufferLike>;
|
|
31
39
|
} | {
|
|
@@ -77,6 +85,14 @@ export declare const protocol: MediaProtocol.Streamed<Request, {
|
|
|
77
85
|
export declare const model: (input: MediaRoute.ModelInput) => SpeechModel<GoogleSpeechOptions>;
|
|
78
86
|
export declare const GoogleSpeech: {
|
|
79
87
|
readonly protocol: MediaProtocol.Streamed<Request, {
|
|
88
|
+
readonly id: string;
|
|
89
|
+
readonly type: "generation-queued";
|
|
90
|
+
readonly position?: number | undefined;
|
|
91
|
+
} | {
|
|
92
|
+
readonly id: string;
|
|
93
|
+
readonly type: "generation-progress";
|
|
94
|
+
readonly progress?: number | undefined;
|
|
95
|
+
} | {
|
|
80
96
|
readonly type: "audio-delta";
|
|
81
97
|
readonly chunk: Uint8Array<ArrayBufferLike>;
|
|
82
98
|
} | {
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
3
3
|
import { MediaRoute } from "../route/media.js";
|
|
4
|
-
import {
|
|
4
|
+
import { mergeJsonRecords } from "../schema/index.js";
|
|
5
5
|
import { SpeechModel } from "../speech.js";
|
|
6
6
|
import { GeminiGenerateContent } from "./utils/gemini-generate-content.js";
|
|
7
7
|
import { SpeechStream } from "./utils/speech-stream.js";
|
|
8
|
-
const
|
|
9
|
-
const NAME = "Google Speech";
|
|
10
|
-
const PROVIDER = ProviderID.make("google");
|
|
8
|
+
const route = MediaProtocol.identity({ id: "google-speech", name: "Google Speech", provider: "google" });
|
|
11
9
|
export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
12
10
|
const DEFAULT_SAMPLE_RATE = 24000;
|
|
13
11
|
// ---------------------------------------------------------------------------
|
|
@@ -17,13 +15,18 @@ const GenerateContentChunk = GeminiGenerateContent.chunk(Schema.Struct({
|
|
|
17
15
|
text: Schema.optional(Schema.String),
|
|
18
16
|
inlineData: Schema.optional(Schema.Struct({ mimeType: Schema.String, data: Schema.Uint8ArrayFromBase64 })),
|
|
19
17
|
}));
|
|
20
|
-
const decodeChunk =
|
|
18
|
+
const decodeChunk = route.decodeFrame(GenerateContentChunk);
|
|
21
19
|
// ---------------------------------------------------------------------------
|
|
22
20
|
// 5. Request body construction
|
|
23
21
|
// ---------------------------------------------------------------------------
|
|
24
22
|
const fromRequest = Effect.fn("GoogleSpeech.fromRequest")(function* (request) {
|
|
23
|
+
// Not in `unsupported`: that list would also reject `timestamps: false`, which asks for nothing.
|
|
24
|
+
if (request.timestamps === true)
|
|
25
|
+
return yield* route.unsupported("media.timestamps", `${route.name} does not return timestamps`);
|
|
26
|
+
if (request.format === "pcm" && request.mode === "generate" && /^gemini-3\.8-.*-tts(?:-|$)/.test(request.model.id))
|
|
27
|
+
return yield* route.unsupported("media.format", `${route.name} returns WAV by default for Gemini 3.8 TTS unary requests; omit the format to accept it`);
|
|
25
28
|
if (request.format !== undefined && request.format !== "pcm")
|
|
26
|
-
return yield*
|
|
29
|
+
return yield* route.unsupported("media.format", `${route.name} only accepts raw PCM as an explicit format; omit it to accept the provider's default output`);
|
|
27
30
|
const voiceName = SpeechStream.voiceID(request.voice);
|
|
28
31
|
return MediaProtocol.json(mergeJsonRecords({
|
|
29
32
|
contents: [{ role: "user", parts: [{ text: request.text }] }],
|
|
@@ -41,17 +44,22 @@ const fromRequest = Effect.fn("GoogleSpeech.fromRequest")(function* (request) {
|
|
|
41
44
|
// ---------------------------------------------------------------------------
|
|
42
45
|
const step = Effect.fn("GoogleSpeech.step")(function* (state, frame) {
|
|
43
46
|
const chunk = yield* decodeChunk(frame);
|
|
44
|
-
const blocked = GeminiGenerateContent.blocked(
|
|
47
|
+
const blocked = GeminiGenerateContent.blocked(route.name, chunk, frame);
|
|
45
48
|
if (blocked !== undefined)
|
|
46
49
|
return yield* blocked;
|
|
47
50
|
const audio = (chunk.candidates?.[0]?.content?.parts ?? []).flatMap((part) => part.inlineData === undefined ? [] : [part.inlineData]);
|
|
48
51
|
const next = { ...GeminiGenerateContent.track(state, chunk), mimeType: state.mimeType ?? audio[0]?.mimeType };
|
|
49
52
|
return [next, audio.flatMap((part) => SpeechStream.delta(next, part.data)[1])];
|
|
50
53
|
});
|
|
51
|
-
const finish = (state) => {
|
|
54
|
+
const finish = (state, context) => {
|
|
52
55
|
const sampleRate = SpeechStream.sampleRate(state.mimeType) ?? DEFAULT_SAMPLE_RATE;
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
const output = state.mimeType?.split(";")[0]?.toLowerCase() === "audio/wav"
|
|
57
|
+
? SpeechStream.container("wav", sampleRate)
|
|
58
|
+
: SpeechStream.pcm("pcm_s16le", sampleRate, state.mimeType ?? `audio/L16;codec=pcm;rate=${sampleRate}`);
|
|
59
|
+
if (context.request.format === "pcm" && output.info.format !== "pcm")
|
|
60
|
+
return Effect.fail(route.frameError(`Google Speech returned ${output.info.format} instead of the requested raw PCM`));
|
|
61
|
+
return SpeechStream.finish(route, state, {
|
|
62
|
+
...output,
|
|
55
63
|
usage: GeminiGenerateContent.usage(state.usage),
|
|
56
64
|
providerMetadata: GeminiGenerateContent.providerMetadata(state),
|
|
57
65
|
detail: state.finishReason === undefined ? undefined : `finish reason: ${state.finishReason}`,
|
|
@@ -60,10 +68,8 @@ const finish = (state) => {
|
|
|
60
68
|
// ---------------------------------------------------------------------------
|
|
61
69
|
// 7. Protocol and route
|
|
62
70
|
// ---------------------------------------------------------------------------
|
|
63
|
-
export const protocol = MediaProtocol.stream({
|
|
64
|
-
|
|
65
|
-
name: NAME,
|
|
66
|
-
unsupported: ["instructions", "speed", "timestamps"],
|
|
71
|
+
export const protocol = MediaProtocol.stream(route, {
|
|
72
|
+
unsupported: ["instructions", "speed"],
|
|
67
73
|
body: { from: fromRequest },
|
|
68
74
|
frames: (bytes, context) => GeminiGenerateContent.frames(bytes, context.request.mode),
|
|
69
75
|
initial: () => ({ chunks: [] }),
|
|
@@ -71,8 +77,6 @@ export const protocol = MediaProtocol.stream({
|
|
|
71
77
|
finish,
|
|
72
78
|
});
|
|
73
79
|
export const model = (input) => SpeechModel.fromRoute({
|
|
74
|
-
id: ADAPTER,
|
|
75
|
-
provider: PROVIDER,
|
|
76
80
|
protocol,
|
|
77
81
|
baseURL: DEFAULT_BASE_URL,
|
|
78
82
|
// Only `gemini-3.1-flash-tts-preview` and later stream; earlier TTS models reject `streamGenerateContent`.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MediaProtocol } from "../route/media-protocol.js";
|
|
2
2
|
import { MediaRoute } from "../route/media.js";
|
|
3
|
+
import { type OpenString } from "../schema/index.js";
|
|
3
4
|
import { TranscriptionModel, type TranscriptionRequestFor, type TranscriptionSegment, type TranscriptionWord } from "../transcription.js";
|
|
4
5
|
import { GeminiGenerateContent } from "./utils/gemini-generate-content.js";
|
|
5
6
|
export declare const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
@@ -9,7 +10,7 @@ export declare const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.c
|
|
|
9
10
|
*/
|
|
10
11
|
export type GoogleTranscriptionOptions = {
|
|
11
12
|
readonly audioTranscriptionConfig?: {
|
|
12
|
-
readonly mode?: "VERBATIM" | "SMART"
|
|
13
|
+
readonly mode?: OpenString<"VERBATIM" | "SMART">;
|
|
13
14
|
readonly customVocabulary?: ReadonlyArray<string>;
|
|
14
15
|
readonly languageCodes?: ReadonlyArray<string>;
|
|
15
16
|
};
|