@opencode/ai 2.0.16 → 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 (185) hide show
  1. package/README.md +187 -129
  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 +6 -10
  8. package/dist/generation.js +17 -20
  9. package/dist/image-client.d.ts +59 -24
  10. package/dist/image-client.js +16 -40
  11. package/dist/image.d.ts +15 -27
  12. package/dist/image.js +4 -15
  13. package/dist/index.d.ts +2 -1
  14. package/dist/index.js +1 -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 +9 -10
  20. package/dist/media-model.js +10 -12
  21. package/dist/media.js +2 -2
  22. package/dist/promise.d.ts +68 -30
  23. package/dist/promise.js +52 -31
  24. package/dist/protocols/alibaba-chat.js +4 -1
  25. package/dist/protocols/alibaba-messages.d.ts +1 -1
  26. package/dist/protocols/alibaba-messages.js +6 -4
  27. package/dist/protocols/anthropic-messages.d.ts +34 -34
  28. package/dist/protocols/anthropic-messages.js +14 -8
  29. package/dist/protocols/assemblyai-transcription.d.ts +2 -1
  30. package/dist/protocols/assemblyai-transcription.js +14 -16
  31. package/dist/protocols/bedrock-converse.d.ts +7 -0
  32. package/dist/protocols/bedrock-converse.js +37 -11
  33. package/dist/protocols/bfl-images.d.ts +7 -1
  34. package/dist/protocols/bfl-images.js +36 -36
  35. package/dist/protocols/cartesia-speech.d.ts +18 -2
  36. package/dist/protocols/cartesia-speech.js +10 -16
  37. package/dist/protocols/deepgram-speech.d.ts +19 -3
  38. package/dist/protocols/deepgram-speech.js +11 -12
  39. package/dist/protocols/deepgram-transcription.d.ts +2 -1
  40. package/dist/protocols/deepgram-transcription.js +9 -13
  41. package/dist/protocols/elevenlabs-speech.d.ts +19 -3
  42. package/dist/protocols/elevenlabs-speech.js +9 -13
  43. package/dist/protocols/fal-images.d.ts +2 -1
  44. package/dist/protocols/fal-images.js +30 -40
  45. package/dist/protocols/fal-video.d.ts +2 -2
  46. package/dist/protocols/fal-video.js +9 -24
  47. package/dist/protocols/gemini.d.ts +13 -13
  48. package/dist/protocols/gemini.js +16 -7
  49. package/dist/protocols/google-images.d.ts +3 -3
  50. package/dist/protocols/google-images.js +11 -21
  51. package/dist/protocols/google-speech.d.ts +16 -0
  52. package/dist/protocols/google-speech.js +20 -16
  53. package/dist/protocols/google-transcription.d.ts +2 -1
  54. package/dist/protocols/google-transcription.js +8 -20
  55. package/dist/protocols/google-video.d.ts +2 -2
  56. package/dist/protocols/google-video.js +14 -30
  57. package/dist/protocols/meta-images.d.ts +3 -4
  58. package/dist/protocols/meta-images.js +12 -21
  59. package/dist/protocols/meta-messages.d.ts +5 -5
  60. package/dist/protocols/meta-responses.js +6 -4
  61. package/dist/protocols/open-responses.d.ts +4 -3
  62. package/dist/protocols/open-responses.js +4 -8
  63. package/dist/protocols/openai-chat.js +3 -4
  64. package/dist/protocols/openai-images.d.ts +7 -5
  65. package/dist/protocols/openai-images.js +65 -66
  66. package/dist/protocols/openai-responses.d.ts +10 -10
  67. package/dist/protocols/openai-responses.js +31 -30
  68. package/dist/protocols/openai-speech.d.ts +21 -1
  69. package/dist/protocols/openai-speech.js +11 -12
  70. package/dist/protocols/openai-transcription.d.ts +4 -0
  71. package/dist/protocols/openai-transcription.js +44 -32
  72. package/dist/protocols/replicate-images.js +11 -17
  73. package/dist/protocols/runway-video.d.ts +3 -3
  74. package/dist/protocols/runway-video.js +11 -17
  75. package/dist/protocols/shared.d.ts +11 -17
  76. package/dist/protocols/shared.js +11 -40
  77. package/dist/protocols/stability-images.d.ts +2 -1
  78. package/dist/protocols/stability-images.js +24 -36
  79. package/dist/protocols/utils/fal-queue.d.ts +1 -3
  80. package/dist/protocols/utils/fal-queue.js +4 -6
  81. package/dist/protocols/utils/media-input.d.ts +15 -1
  82. package/dist/protocols/utils/media-input.js +22 -0
  83. package/dist/protocols/utils/responses-checkpoint.js +3 -7
  84. package/dist/protocols/utils/responses-compaction.d.ts +3 -1
  85. package/dist/protocols/utils/responses-compaction.js +16 -3
  86. package/dist/protocols/utils/speech-stream.d.ts +3 -3
  87. package/dist/protocols/utils/speech-stream.js +1 -4
  88. package/dist/protocols/utils/tool-schema.d.ts +2 -2
  89. package/dist/protocols/utils/tool-schema.js +29 -9
  90. package/dist/protocols/xai-images.d.ts +4 -4
  91. package/dist/protocols/xai-images.js +14 -29
  92. package/dist/protocols/xai-responses.js +1 -1
  93. package/dist/protocols/xai-video.js +12 -18
  94. package/dist/protocols/zai-images.d.ts +2 -2
  95. package/dist/protocols/zai-images.js +11 -14
  96. package/dist/protocols/zai-messages.d.ts +1 -1
  97. package/dist/provider-error.js +7 -1
  98. package/dist/providers/alibaba.d.ts +1 -1
  99. package/dist/providers/amazon-bedrock.d.ts +2 -0
  100. package/dist/providers/amazon-bedrock.js +1 -0
  101. package/dist/providers/anthropic-compatible.d.ts +5 -5
  102. package/dist/providers/anthropic.d.ts +5 -5
  103. package/dist/providers/assemblyai.d.ts +1 -1
  104. package/dist/providers/assemblyai.js +5 -10
  105. package/dist/providers/azure.d.ts +4 -4
  106. package/dist/providers/azure.js +2 -2
  107. package/dist/providers/black-forest-labs.d.ts +1 -1
  108. package/dist/providers/black-forest-labs.js +5 -10
  109. package/dist/providers/cartesia.d.ts +1 -1
  110. package/dist/providers/cartesia.js +5 -10
  111. package/dist/providers/cloudflare-ai-gateway.d.ts +14 -14
  112. package/dist/providers/deepgram.d.ts +1 -1
  113. package/dist/providers/deepgram.js +6 -12
  114. package/dist/providers/elevenlabs.d.ts +1 -1
  115. package/dist/providers/elevenlabs.js +5 -10
  116. package/dist/providers/fal.d.ts +1 -1
  117. package/dist/providers/fal.js +5 -12
  118. package/dist/providers/google-vertex-messages.d.ts +5 -5
  119. package/dist/providers/google-vertex.d.ts +3 -3
  120. package/dist/providers/google.d.ts +3 -3
  121. package/dist/providers/google.js +7 -12
  122. package/dist/providers/meta.d.ts +8 -8
  123. package/dist/providers/meta.js +4 -8
  124. package/dist/providers/minimax.d.ts +5 -5
  125. package/dist/providers/moonshot.d.ts +5 -5
  126. package/dist/providers/openai-options.d.ts +3 -9
  127. package/dist/providers/openai-options.js +4 -7
  128. package/dist/providers/openai.d.ts +9 -10
  129. package/dist/providers/openai.js +11 -12
  130. package/dist/providers/opencode-zen.js +1 -1
  131. package/dist/providers/openrouter.d.ts +6 -7
  132. package/dist/providers/openrouter.js +10 -5
  133. package/dist/providers/replicate.d.ts +1 -1
  134. package/dist/providers/replicate.js +5 -10
  135. package/dist/providers/runway.d.ts +1 -1
  136. package/dist/providers/runway.js +5 -10
  137. package/dist/providers/stability.d.ts +1 -1
  138. package/dist/providers/stability.js +6 -11
  139. package/dist/providers/typesafe-ai.js +1 -1
  140. package/dist/providers/vercel-ai-gateway.js +1 -1
  141. package/dist/providers/xai.js +5 -10
  142. package/dist/providers/zai-coding-plan.d.ts +1 -1
  143. package/dist/providers/zai.js +4 -8
  144. package/dist/route/auth.d.ts +1 -1
  145. package/dist/route/auth.js +14 -13
  146. package/dist/route/client.d.ts +9 -7
  147. package/dist/route/client.js +6 -8
  148. package/dist/route/endpoint.d.ts +1 -0
  149. package/dist/route/endpoint.js +2 -2
  150. package/dist/route/executor-service.d.ts +4 -2
  151. package/dist/route/executor-service.js +2 -1
  152. package/dist/route/executor.d.ts +3 -1
  153. package/dist/route/executor.js +7 -0
  154. package/dist/route/framing.d.ts +10 -1
  155. package/dist/route/framing.js +45 -5
  156. package/dist/route/index.d.ts +1 -1
  157. package/dist/route/media-protocol.d.ts +49 -35
  158. package/dist/route/media-protocol.js +77 -65
  159. package/dist/route/media.d.ts +10 -16
  160. package/dist/route/media.js +46 -62
  161. package/dist/route/protocol.d.ts +3 -1
  162. package/dist/schema/options.d.ts +6 -3
  163. package/dist/schema/options.js +7 -3
  164. package/dist/speech-client.d.ts +62 -17
  165. package/dist/speech-client.js +17 -21
  166. package/dist/speech.d.ts +178 -21
  167. package/dist/speech.js +13 -8
  168. package/dist/testing.d.ts +2 -2
  169. package/dist/transcription-client.d.ts +78 -24
  170. package/dist/transcription-client.js +9 -40
  171. package/dist/transcription.d.ts +15 -27
  172. package/dist/transcription.js +4 -10
  173. package/dist/utils/json.d.ts +4 -0
  174. package/dist/utils/json.js +4 -0
  175. package/dist/utils/media-type.d.ts +2 -1
  176. package/dist/utils/media-type.js +3 -1
  177. package/dist/video-client.d.ts +55 -24
  178. package/dist/video-client.js +16 -36
  179. package/dist/video.d.ts +18 -26
  180. package/dist/video.js +15 -16
  181. package/package.json +3 -3
  182. package/dist/protocols/utils/meta-image.d.ts +0 -2
  183. package/dist/protocols/utils/meta-image.js +0 -13
  184. package/dist/protocols/utils/openai-image.d.ts +0 -5
  185. 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 { ProviderID, mergeJsonRecords } from "../schema/index.js";
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 ADAPTER = "fal-images";
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(`${NAME} accepts either size or aspectRatio, not both`));
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(id, "size", "sizes by aspectRatio"));
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(id, "aspectRatio", "sizes by size (image_size)"));
51
- if ((request.images?.length ?? 0) > 1 && !isEdit(id))
52
- return Effect.fail(unsupported(id, "images", "takes one image_url; use an /edit endpoint for several images"));
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 `image_url` (beside `mask_url`).
56
- const isEdit = (model) => model.endsWith("/edit");
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, NAME));
60
- const edit = isEdit(request.model.id);
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: edit && images.length > 0 ? images : undefined,
69
- image_url: edit ? undefined : images[0],
70
- mask_url: request.mask === undefined ? undefined : yield* FalQueue.mediaUrl(request.mask, NAME),
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 = MediaProtocol.decodeJson(ADAPTER, NAME, QueueResult);
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(`${NAME} returned no images`);
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) => Media.url(image.url, {
86
- mediaType: image.content_type ?? undefined,
87
- info: { width: image.width ?? undefined, height: image.height ?? undefined },
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) => ({ type: "moderated", message: `${NAME} flagged image ${index} as NSFW` })),
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?: FalVideoString<"4s" | "6s" | "8s" | "5" | "10">;
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 { ProviderID, mergeJsonRecords } from "../schema/index.js";
5
+ import { mergeJsonRecords } from "../schema/index.js";
6
6
  import { VideoModel, VideoResponse } from "../video.js";
7
- import { ProviderShared, optionalNull } from "./shared.js";
7
+ import { optionalNull } from "./shared.js";
8
8
  import { FalQueue } from "./utils/fal-queue.js";
9
- const ADAPTER = "fal-video";
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* ProviderShared.unsupportedOperation({
30
- operation: "video.frames.last",
31
- provider: PROVIDER,
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 = MediaProtocol.decodeJson(ADAPTER, NAME, QueueResult);
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<(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<{
@@ -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
  }>;
@@ -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;
@@ -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;
@@ -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. Gemini's schema
192
- // rules are this API's default, including for tuned endpoints whose IDs do not name Gemini.
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: ToolSchemaProjection.modelCompatibility(tool.inputSchema, model, "gemini"),
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
- : { ...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
+ },
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((tool) => lowerTool(tool, request.model)),
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?: 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>;
@@ -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 { ProviderID, mergeJsonRecords } from "../schema/index.js";
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 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
@@ -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* ProviderShared.unsupportedOperation({
67
- operation: "image.n",
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* MediaProtocol.decodeJson(ADAPTER, NAME, GoogleImageResponse)(response);
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, `${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, {
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(`${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`);
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: `${NAME} reported prompt feedback`,
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: `${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}`}`,
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 { ProviderID, mergeJsonRecords } from "../schema/index.js";
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 ADAPTER = "google-speech";
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 = MediaProtocol.decodeFrame(ADAPTER, NAME, GenerateContentChunk);
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* SpeechStream.unsupportedFormat(PROVIDER, ADAPTER, `${NAME} only returns raw PCM; request format "pcm" or omit it, then wrap the samples yourself`);
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(NAME, chunk, frame);
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
- return SpeechStream.finish(ADAPTER, state, {
54
- ...SpeechStream.pcm("pcm_s16le", sampleRate, state.mimeType ?? `audio/L16;codec=pcm;rate=${sampleRate}`),
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
- id: ADAPTER,
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" | (string & {});
13
+ readonly mode?: OpenString<"VERBATIM" | "SMART">;
13
14
  readonly customVocabulary?: ReadonlyArray<string>;
14
15
  readonly languageCodes?: ReadonlyArray<string>;
15
16
  };