@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.
Files changed (224) hide show
  1. package/README.md +441 -99
  2. package/dist/ai-client.d.ts +8 -0
  3. package/dist/ai-client.js +12 -0
  4. package/dist/experimental/evaluation-client.d.ts +3 -3
  5. package/dist/experimental/evaluation-client.js +1 -1
  6. package/dist/experimental/evaluation.js +1 -1
  7. package/dist/generation.d.ts +39 -29
  8. package/dist/generation.js +62 -36
  9. package/dist/image-client.d.ts +59 -17
  10. package/dist/image-client.js +16 -24
  11. package/dist/image.d.ts +394 -55
  12. package/dist/image.js +48 -57
  13. package/dist/index.d.ts +15 -2
  14. package/dist/index.js +10 -0
  15. package/dist/llm.d.ts +7 -5
  16. package/dist/llm.js +10 -4
  17. package/dist/media-client.d.ts +30 -0
  18. package/dist/media-client.js +51 -0
  19. package/dist/media-model.d.ts +43 -0
  20. package/dist/media-model.js +47 -0
  21. package/dist/media.d.ts +10 -9
  22. package/dist/media.js +11 -12
  23. package/dist/promise.d.ts +473 -15
  24. package/dist/promise.js +71 -13
  25. package/dist/protocols/alibaba-chat.d.ts +12 -0
  26. package/dist/protocols/alibaba-chat.js +4 -1
  27. package/dist/protocols/alibaba-messages.d.ts +1 -1
  28. package/dist/protocols/alibaba-messages.js +6 -4
  29. package/dist/protocols/alibaba-responses.d.ts +2 -2
  30. package/dist/protocols/anthropic-messages.d.ts +34 -34
  31. package/dist/protocols/anthropic-messages.js +14 -9
  32. package/dist/protocols/assemblyai-transcription.d.ts +41 -0
  33. package/dist/protocols/assemblyai-transcription.js +136 -0
  34. package/dist/protocols/bedrock-converse.d.ts +7 -0
  35. package/dist/protocols/bedrock-converse.js +39 -19
  36. package/dist/protocols/bfl-images.d.ts +38 -0
  37. package/dist/protocols/bfl-images.js +153 -0
  38. package/dist/protocols/cartesia-speech.d.ts +143 -0
  39. package/dist/protocols/cartesia-speech.js +120 -0
  40. package/dist/protocols/deepgram-speech.d.ts +135 -0
  41. package/dist/protocols/deepgram-speech.js +91 -0
  42. package/dist/protocols/deepgram-transcription.d.ts +26 -0
  43. package/dist/protocols/deepgram-transcription.js +125 -0
  44. package/dist/protocols/elevenlabs-speech.d.ts +138 -0
  45. package/dist/protocols/elevenlabs-speech.js +111 -0
  46. package/dist/protocols/fal-images.d.ts +25 -0
  47. package/dist/protocols/fal-images.js +104 -0
  48. package/dist/protocols/fal-video.d.ts +29 -0
  49. package/dist/protocols/fal-video.js +73 -0
  50. package/dist/protocols/gemini.d.ts +22 -22
  51. package/dist/protocols/gemini.js +19 -36
  52. package/dist/protocols/google-images.d.ts +3 -3
  53. package/dist/protocols/google-images.js +12 -34
  54. package/dist/protocols/google-speech.d.ts +146 -0
  55. package/dist/protocols/google-speech.js +88 -0
  56. package/dist/protocols/google-transcription.d.ts +174 -0
  57. package/dist/protocols/google-transcription.js +126 -0
  58. package/dist/protocols/google-video.d.ts +26 -0
  59. package/dist/protocols/google-video.js +142 -0
  60. package/dist/protocols/meta-images.d.ts +3 -4
  61. package/dist/protocols/meta-images.js +13 -29
  62. package/dist/protocols/meta-messages.d.ts +5 -5
  63. package/dist/protocols/meta-responses.d.ts +4 -4
  64. package/dist/protocols/meta-responses.js +6 -4
  65. package/dist/protocols/open-responses.d.ts +10 -9
  66. package/dist/protocols/open-responses.js +4 -9
  67. package/dist/protocols/openai-chat.d.ts +84 -0
  68. package/dist/protocols/openai-chat.js +28 -17
  69. package/dist/protocols/openai-compatible-chat.d.ts +12 -0
  70. package/dist/protocols/openai-compatible-responses.d.ts +2 -2
  71. package/dist/protocols/openai-images.d.ts +130 -7
  72. package/dist/protocols/openai-images.js +143 -91
  73. package/dist/protocols/openai-responses.d.ts +20 -20
  74. package/dist/protocols/openai-responses.js +31 -31
  75. package/dist/protocols/openai-speech.d.ts +136 -0
  76. package/dist/protocols/openai-speech.js +97 -0
  77. package/dist/protocols/openai-transcription.d.ts +211 -0
  78. package/dist/protocols/openai-transcription.js +202 -0
  79. package/dist/protocols/replicate-images.d.ts +28 -0
  80. package/dist/protocols/replicate-images.js +127 -0
  81. package/dist/protocols/runway-video.d.ts +38 -0
  82. package/dist/protocols/runway-video.js +140 -0
  83. package/dist/protocols/shared.d.ts +21 -17
  84. package/dist/protocols/shared.js +27 -36
  85. package/dist/protocols/stability-images.d.ts +39 -0
  86. package/dist/protocols/stability-images.js +136 -0
  87. package/dist/protocols/utils/fal-queue.d.ts +26 -0
  88. package/dist/protocols/utils/fal-queue.js +67 -0
  89. package/dist/protocols/utils/gemini-generate-content.d.ts +65 -0
  90. package/dist/protocols/utils/gemini-generate-content.js +65 -0
  91. package/dist/protocols/utils/gemini-json-schema.d.ts +3 -0
  92. package/dist/protocols/utils/gemini-json-schema.js +76 -0
  93. package/dist/protocols/utils/media-input.d.ts +23 -1
  94. package/dist/protocols/utils/media-input.js +40 -0
  95. package/dist/protocols/utils/responses-checkpoint.js +3 -7
  96. package/dist/protocols/utils/responses-compaction.d.ts +3 -1
  97. package/dist/protocols/utils/responses-compaction.js +16 -3
  98. package/dist/protocols/utils/speech-stream.d.ts +49 -0
  99. package/dist/protocols/utils/speech-stream.js +64 -0
  100. package/dist/protocols/utils/tool-schema.d.ts +2 -2
  101. package/dist/protocols/utils/tool-schema.js +62 -19
  102. package/dist/protocols/xai-images.d.ts +4 -4
  103. package/dist/protocols/xai-images.js +14 -40
  104. package/dist/protocols/xai-responses.d.ts +2 -2
  105. package/dist/protocols/xai-responses.js +1 -1
  106. package/dist/protocols/xai-video.d.ts +34 -0
  107. package/dist/protocols/xai-video.js +141 -0
  108. package/dist/protocols/zai-chat.d.ts +13 -1
  109. package/dist/protocols/zai-images.d.ts +2 -2
  110. package/dist/protocols/zai-images.js +11 -14
  111. package/dist/protocols/zai-messages.d.ts +1 -1
  112. package/dist/provider-error.js +10 -1
  113. package/dist/providers/alibaba.d.ts +15 -3
  114. package/dist/providers/amazon-bedrock-mantle.d.ts +14 -2
  115. package/dist/providers/amazon-bedrock.d.ts +2 -0
  116. package/dist/providers/amazon-bedrock.js +1 -0
  117. package/dist/providers/anthropic-compatible.d.ts +5 -5
  118. package/dist/providers/anthropic.d.ts +5 -5
  119. package/dist/providers/assemblyai.d.ts +25 -0
  120. package/dist/providers/assemblyai.js +24 -0
  121. package/dist/providers/azure.d.ts +20 -8
  122. package/dist/providers/azure.js +2 -2
  123. package/dist/providers/baseten.d.ts +24 -0
  124. package/dist/providers/black-forest-labs.d.ts +25 -0
  125. package/dist/providers/black-forest-labs.js +23 -0
  126. package/dist/providers/cartesia.d.ts +24 -0
  127. package/dist/providers/cartesia.js +17 -0
  128. package/dist/providers/cerebras.d.ts +24 -0
  129. package/dist/providers/cloudflare-ai-gateway.d.ts +42 -18
  130. package/dist/providers/cloudflare-workers-ai.d.ts +24 -0
  131. package/dist/providers/deepgram.d.ts +29 -0
  132. package/dist/providers/deepgram.js +25 -0
  133. package/dist/providers/deepinfra.d.ts +24 -0
  134. package/dist/providers/deepseek.d.ts +24 -0
  135. package/dist/providers/elevenlabs.d.ts +24 -0
  136. package/dist/providers/elevenlabs.js +23 -0
  137. package/dist/providers/fal.d.ts +29 -0
  138. package/dist/providers/fal.js +26 -0
  139. package/dist/providers/fireworks.d.ts +24 -0
  140. package/dist/providers/google-vertex-chat.d.ts +12 -0
  141. package/dist/providers/google-vertex-messages.d.ts +5 -5
  142. package/dist/providers/google-vertex-responses.d.ts +2 -2
  143. package/dist/providers/google-vertex.d.ts +6 -6
  144. package/dist/providers/google.d.ts +21 -6
  145. package/dist/providers/google.js +13 -9
  146. package/dist/providers/groq.d.ts +24 -0
  147. package/dist/providers/index.d.ts +9 -0
  148. package/dist/providers/index.js +9 -0
  149. package/dist/providers/meta.d.ts +22 -10
  150. package/dist/providers/meta.js +4 -8
  151. package/dist/providers/minimax.d.ts +19 -7
  152. package/dist/providers/moonshot.d.ts +19 -7
  153. package/dist/providers/moonshot.js +3 -3
  154. package/dist/providers/openai-compatible-responses.d.ts +2 -2
  155. package/dist/providers/openai-compatible.d.ts +12 -0
  156. package/dist/providers/openai-options.d.ts +3 -9
  157. package/dist/providers/openai-options.js +4 -7
  158. package/dist/providers/openai.d.ts +33 -12
  159. package/dist/providers/openai.js +17 -9
  160. package/dist/providers/opencode-zen.js +1 -1
  161. package/dist/providers/openrouter.d.ts +54 -7
  162. package/dist/providers/openrouter.js +10 -5
  163. package/dist/providers/replicate.d.ts +25 -0
  164. package/dist/providers/replicate.js +17 -0
  165. package/dist/providers/runway.d.ts +24 -0
  166. package/dist/providers/runway.js +17 -0
  167. package/dist/providers/stability.d.ts +28 -0
  168. package/dist/providers/stability.js +18 -0
  169. package/dist/providers/togetherai.d.ts +24 -0
  170. package/dist/providers/typesafe-ai.js +1 -1
  171. package/dist/providers/vercel-ai-gateway.js +1 -1
  172. package/dist/providers/xai.d.ts +17 -0
  173. package/dist/providers/xai.js +7 -9
  174. package/dist/providers/zai-coding-plan.d.ts +16 -4
  175. package/dist/providers/zai.d.ts +13 -1
  176. package/dist/providers/zai.js +4 -8
  177. package/dist/route/auth.d.ts +5 -2
  178. package/dist/route/auth.js +20 -13
  179. package/dist/route/client.d.ts +9 -7
  180. package/dist/route/client.js +6 -8
  181. package/dist/route/endpoint.d.ts +1 -0
  182. package/dist/route/endpoint.js +2 -2
  183. package/dist/route/executor-service.d.ts +4 -2
  184. package/dist/route/executor-service.js +2 -1
  185. package/dist/route/executor.d.ts +3 -1
  186. package/dist/route/executor.js +7 -0
  187. package/dist/route/framing.d.ts +15 -2
  188. package/dist/route/framing.js +53 -4
  189. package/dist/route/index.d.ts +1 -1
  190. package/dist/route/media-protocol.d.ts +145 -18
  191. package/dist/route/media-protocol.js +96 -28
  192. package/dist/route/media.d.ts +54 -9
  193. package/dist/route/media.js +194 -38
  194. package/dist/route/protocol.d.ts +3 -1
  195. package/dist/schema/events.d.ts +0 -6
  196. package/dist/schema/messages.d.ts +0 -3
  197. package/dist/schema/options.d.ts +10 -6
  198. package/dist/schema/options.js +10 -5
  199. package/dist/speech-client.d.ts +66 -0
  200. package/dist/speech-client.js +21 -0
  201. package/dist/speech.d.ts +1307 -0
  202. package/dist/speech.js +124 -0
  203. package/dist/testing.d.ts +2 -2
  204. package/dist/transcription-client.d.ts +82 -0
  205. package/dist/transcription-client.js +13 -0
  206. package/dist/transcription.d.ts +1492 -0
  207. package/dist/transcription.js +127 -0
  208. package/dist/utils/bytes.d.ts +1 -0
  209. package/dist/utils/bytes.js +10 -0
  210. package/dist/utils/json.d.ts +4 -0
  211. package/dist/utils/json.js +4 -0
  212. package/dist/utils/media-type.d.ts +3 -1
  213. package/dist/utils/media-type.js +25 -2
  214. package/dist/video-client.d.ts +59 -0
  215. package/dist/video-client.js +20 -0
  216. package/dist/video.d.ts +1351 -0
  217. package/dist/video.js +118 -0
  218. package/package.json +3 -3
  219. package/dist/protocols/utils/gemini-tool-schema.d.ts +0 -2
  220. package/dist/protocols/utils/gemini-tool-schema.js +0 -103
  221. package/dist/protocols/utils/meta-image.d.ts +0 -2
  222. package/dist/protocols/utils/meta-image.js +0 -13
  223. package/dist/protocols/utils/openai-image.d.ts +0 -5
  224. 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<(string & {}) | "HARM_CATEGORY_UNSPECIFIED" | "HARM_CATEGORY_HATE_SPEECH" | "HARM_CATEGORY_DANGEROUS_CONTENT" | "HARM_CATEGORY_HARASSMENT" | "HARM_CATEGORY_SEXUALLY_EXPLICIT" | "HARM_CATEGORY_CIVIC_INTEGRITY", (string & {}) | "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<(string & {}) | "HARM_BLOCK_THRESHOLD_UNSPECIFIED" | "BLOCK_LOW_AND_ABOVE" | "BLOCK_MEDIUM_AND_ABOVE" | "BLOCK_ONLY_HIGH" | "BLOCK_NONE" | "OFF", (string & {}) | "HARM_BLOCK_THRESHOLD_UNSPECIFIED" | "BLOCK_LOW_AND_ABOVE" | "BLOCK_MEDIUM_AND_ABOVE" | "BLOCK_ONLY_HIGH" | "BLOCK_NONE" | "OFF">;
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<(string & {}) | "standard" | "flex" | "priority", (string & {}) | "standard" | "flex" | "priority">>, never>>;
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<(string & {}) | "minimal" | "low" | "medium" | "high", (string & {}) | "minimal" | "low" | "medium" | "high">>, never>>;
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<(string & {}) | "HARM_CATEGORY_UNSPECIFIED" | "HARM_CATEGORY_HATE_SPEECH" | "HARM_CATEGORY_DANGEROUS_CONTENT" | "HARM_CATEGORY_HARASSMENT" | "HARM_CATEGORY_SEXUALLY_EXPLICIT" | "HARM_CATEGORY_CIVIC_INTEGRITY", (string & {}) | "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<(string & {}) | "HARM_BLOCK_THRESHOLD_UNSPECIFIED" | "BLOCK_LOW_AND_ABOVE" | "BLOCK_MEDIUM_AND_ABOVE" | "BLOCK_ONLY_HIGH" | "BLOCK_NONE" | "OFF", (string & {}) | "HARM_BLOCK_THRESHOLD_UNSPECIFIED" | "BLOCK_LOW_AND_ABOVE" | "BLOCK_MEDIUM_AND_ABOVE" | "BLOCK_ONLY_HIGH" | "BLOCK_NONE" | "OFF">;
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 parameters: Schema.optional<Schema.$Record<Schema.String, Schema.Unknown>>;
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<(string & {}) | "minimal" | "low" | "medium" | "high", (string & {}) | "minimal" | "low" | "medium" | "high">>;
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 parameters?: {
152
+ readonly description: string;
153
+ readonly parametersJsonSchema: {
154
154
  readonly [x: string]: unknown;
155
- } | undefined;
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: (string & {}) | "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: (string & {}) | "HARM_BLOCK_THRESHOLD_UNSPECIFIED" | "BLOCK_LOW_AND_ABOVE" | "BLOCK_MEDIUM_AND_ABOVE" | "BLOCK_ONLY_HIGH" | "BLOCK_NONE" | "OFF";
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?: (string & {}) | "minimal" | "low" | "medium" | "high" | undefined;
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 parameters?: {
266
+ readonly description: string;
267
+ readonly parametersJsonSchema: {
268
268
  readonly [x: string]: unknown;
269
- } | undefined;
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: (string & {}) | "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: (string & {}) | "HARM_BLOCK_THRESHOLD_UNSPECIFIED" | "BLOCK_LOW_AND_ABOVE" | "BLOCK_MEDIUM_AND_ABOVE" | "BLOCK_ONLY_HIGH" | "BLOCK_NONE" | "OFF";
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?: (string & {}) | "minimal" | "low" | "medium" | "high" | undefined;
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;
@@ -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 { GeminiToolSchema } from "./utils/gemini-tool-schema.js";
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
- parameters: Schema.optional(JsonObject),
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
- const lowerTool = (tool, inputSchema) => ({
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
- parameters: GeminiToolSchema.convert(inputSchema),
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
- const source = part.media.source;
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
- : { ...options.thinkingConfig, includeThoughts: options.thinkingConfig.includeThoughts ?? true },
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((tool) => lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility))),
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?: GoogleImageString<"1K" | "2K" | "4K">;
9
- readonly thinkingLevel?: GoogleImageString<"MINIMAL" | "LOW" | "MEDIUM" | "HIGH">;
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 { ProviderID, mergeJsonRecords } from "../schema/index.js";
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 ADAPTER = "google-images";
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* ProviderShared.unsupportedOperation({
79
- operation: "image.n",
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* MediaProtocol.decodeJson(ADAPTER, NAME, GoogleImageResponse)(response);
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, `${NAME} candidate ${item.candidateIndex} part ${item.partIndex}`, item.inlineData.data, item.inlineData.mimeType, {
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(`${NAME} returned no final images${finishReasons.length === 0 ? "" : ` (finish reasons: ${finishReasons.join(", ")})`}; inspect body for prompt feedback and candidate details`);
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: `${NAME} reported prompt feedback`,
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: `${NAME} candidate ${candidate.index ?? index} finished with ${candidate.finishReason}${candidate.finishMessage === undefined ? "" : `: ${candidate.finishMessage}`}`,
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`,