@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,65 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { Framing } from "../../route/framing.js";
3
+ import { AIError, ContentPolicyError, ProviderID } from "../../schema/index.js";
4
+ import { ProviderShared } from "../shared.js";
5
+ import { MediaInput } from "./media-input.js";
6
+ const PROVIDER = ProviderID.make("google");
7
+ const UsageMetadata = Schema.Struct({
8
+ promptTokenCount: Schema.optional(Schema.Number),
9
+ candidatesTokenCount: Schema.optional(Schema.Number),
10
+ totalTokenCount: Schema.optional(Schema.Number),
11
+ });
12
+ export const chunk = (part) => Schema.Struct({
13
+ candidates: Schema.optional(Schema.Array(Schema.Struct({
14
+ content: Schema.optional(Schema.Struct({ parts: Schema.optional(Schema.Array(part)) })),
15
+ finishReason: Schema.optional(Schema.String),
16
+ }))),
17
+ promptFeedback: Schema.optional(Schema.Struct({
18
+ blockReason: Schema.optional(Schema.String),
19
+ blockReasonMessage: Schema.optional(Schema.String),
20
+ })),
21
+ usageMetadata: Schema.optional(UsageMetadata),
22
+ modelVersion: Schema.optional(Schema.String),
23
+ responseId: Schema.optional(Schema.String),
24
+ });
25
+ export const track = (state, chunk) => ({
26
+ ...state,
27
+ usage: chunk.usageMetadata ?? state.usage,
28
+ finishReason: chunk.candidates?.[0]?.finishReason ?? state.finishReason,
29
+ modelVersion: chunk.modelVersion ?? state.modelVersion,
30
+ responseId: chunk.responseId ?? state.responseId,
31
+ });
32
+ export const blocked = (name, chunk, frame) => {
33
+ const feedback = chunk.promptFeedback;
34
+ if (feedback?.blockReason === undefined)
35
+ return undefined;
36
+ return new AIError({
37
+ reason: new ContentPolicyError({
38
+ message: `${name} blocked the request (${feedback.blockReason})${feedback.blockReasonMessage === undefined ? "" : `: ${feedback.blockReasonMessage}`}`,
39
+ body: frame,
40
+ }),
41
+ });
42
+ };
43
+ export const usage = (usage) => usage === undefined
44
+ ? undefined
45
+ : {
46
+ type: "tokens",
47
+ input: usage.promptTokenCount,
48
+ output: usage.candidatesTokenCount,
49
+ total: ProviderShared.totalTokens(usage.promptTokenCount, usage.candidatesTokenCount, usage.totalTokenCount),
50
+ details: { google: usage },
51
+ };
52
+ export const providerMetadata = (state) => ({
53
+ google: { finishReason: state.finishReason, modelVersion: state.modelVersion, responseId: state.responseId },
54
+ });
55
+ export const path = (model, mode) => mode === "stream" ? `/models/${model}:streamGenerateContent?alt=sse` : `/models/${model}:generateContent`;
56
+ // `generateContent` answers with one document shaped exactly like a streamed chunk, so it is a single frame.
57
+ export const frames = (bytes, mode) => mode === "stream" ? Framing.sse.frame(bytes) : Framing.document.frame(bytes);
58
+ // Gemini does not fetch public URLs; inline payloads and Gemini Files references are the accepted inputs.
59
+ export const mediaPart = (route, asset) => {
60
+ const fileUri = MediaInput.refID(asset, PROVIDER);
61
+ if (fileUri !== undefined)
62
+ return Effect.succeed({ fileData: { mimeType: asset.mediaType, fileUri } });
63
+ return ProviderShared.requireInlineMedia(route, asset).pipe(Effect.map((media) => ({ inlineData: { mimeType: media.mime, data: media.base64 } })));
64
+ };
65
+ export * as GeminiGenerateContent from "./gemini-generate-content.js";
@@ -0,0 +1,3 @@
1
+ import type { JsonSchema } from "../../schema/index.js";
2
+ export declare const normalize: (schema: JsonSchema) => JsonSchema;
3
+ export * as GeminiJsonSchema from "./gemini-json-schema.js";
@@ -0,0 +1,76 @@
1
+ import { isRecord } from "../../utils/record.js";
2
+ // Gemini's `parametersJsonSchema` accepts standard JSON Schema, but rejects a few shapes that
3
+ // published tool schemas commonly contain. Rewrite only those and send everything else unchanged.
4
+ const SCHEMA_MAPS = new Set([
5
+ "properties",
6
+ "patternProperties",
7
+ "$defs",
8
+ "definitions",
9
+ "dependentSchemas",
10
+ "dependencies",
11
+ ]);
12
+ const VALUES = new Set(["const", "default", "enum", "examples", "dependentRequired"]);
13
+ const mapValues = (record, map) => Object.fromEntries(Object.entries(record).map(([key, value]) => [key, map(value, key)]));
14
+ const normalizeNode = (schema) => {
15
+ if (Array.isArray(schema))
16
+ return schema.map(normalizeNode);
17
+ if (!isRecord(schema))
18
+ return schema;
19
+ const properties = isRecord(schema.properties) ? schema.properties : undefined;
20
+ return Object.fromEntries(Object.entries(schema).flatMap(([key, value]) => {
21
+ if (VALUES.has(key))
22
+ return [[key, value]];
23
+ if (SCHEMA_MAPS.has(key) && isRecord(value))
24
+ return [[key, mapValues(value, normalizeNode)]];
25
+ // `required` may only name declared properties.
26
+ if (key === "required" && properties && Array.isArray(value))
27
+ return [[key, value.filter((name) => typeof name === "string" && Object.hasOwn(properties, name))]];
28
+ // Draft-04 boolean exclusive bounds become the numeric form.
29
+ if (key === "exclusiveMinimum" && typeof value === "boolean")
30
+ return value && typeof schema.minimum === "number" ? [[key, schema.minimum]] : [];
31
+ if (key === "exclusiveMaximum" && typeof value === "boolean")
32
+ return value && typeof schema.maximum === "number" ? [[key, schema.maximum]] : [];
33
+ if (key === "minimum" && schema.exclusiveMinimum === true)
34
+ return [];
35
+ if (key === "maximum" && schema.exclusiveMaximum === true)
36
+ return [];
37
+ // Draft-07 tuples (`items` array plus `additionalItems`) are `prefixItems` plus `items` in 2020-12.
38
+ if (key === "items" && Array.isArray(value))
39
+ return [["prefixItems", value.map(normalizeNode)]];
40
+ if (key === "additionalItems" && Array.isArray(schema.items))
41
+ return [["items", normalizeNode(value)]];
42
+ return [[key, normalizeNode(value)]];
43
+ }));
44
+ };
45
+ // Gemini accepts a recursive `$ref` only when the loop passes through an optional property or
46
+ // potentially empty array `items`. Replace other self-references with an unconstrained schema.
47
+ const cutLoops = (schema, target, safe) => {
48
+ if (Array.isArray(schema))
49
+ return schema.map((item) => cutLoops(item, target, safe));
50
+ if (!isRecord(schema))
51
+ return schema;
52
+ if (schema.$ref === target && !safe)
53
+ return {};
54
+ const required = Array.isArray(schema.required) ? schema.required : [];
55
+ return mapValues(schema, (value, key) => {
56
+ if (VALUES.has(key))
57
+ return value;
58
+ if (key === "items")
59
+ return cutLoops(value, target, safe || !(Number(schema.minItems) > 0));
60
+ if (key === "properties" && isRecord(value))
61
+ return mapValues(value, (child, name) => cutLoops(child, target, safe || !required.includes(name)));
62
+ if (SCHEMA_MAPS.has(key) && isRecord(value))
63
+ return mapValues(value, (child) => cutLoops(child, target, safe));
64
+ return cutLoops(value, target, safe);
65
+ });
66
+ };
67
+ export const normalize = (schema) => {
68
+ const normalized = normalizeNode(schema);
69
+ if (!isRecord(normalized))
70
+ return {};
71
+ const result = cutLoops(mapValues(normalized, (value, key) => (key === "$defs" || key === "definitions") && isRecord(value)
72
+ ? mapValues(value, (def, name) => cutLoops(def, `#/${key}/${name}`, false))
73
+ : value), "#", false);
74
+ return isRecord(result) ? result : {};
75
+ };
76
+ export * as GeminiJsonSchema from "./gemini-json-schema.js";
@@ -1,10 +1,32 @@
1
1
  import { Effect } from "effect";
2
2
  import { Media } from "../../media.js";
3
- import type { AIError, ProviderID } from "../../schema/index.js";
3
+ import type { MediaProtocol } from "../../route/media-protocol.js";
4
+ import { type AIError, type ProviderID } from "../../schema/index.js";
4
5
  /** Owned bytes for multipart uploads; decodes `base64` sources and rejects remote sources. */
5
6
  export declare const inlineBytes: (route: string, asset: Media.Asset) => Effect.Effect<Uint8Array, AIError>;
7
+ /** Copied because `BlobPart` requires a plain `ArrayBuffer`. */
8
+ export declare const blob: (data: Uint8Array, mediaType: string) => Blob;
9
+ export declare const query: (route: string, values: Record<string, unknown>) => Effect.Effect<MediaProtocol.Query, AIError>;
10
+ export declare const dimensions: (size: string) => {
11
+ width: number;
12
+ height: number;
13
+ };
6
14
  /** Provider file handle when the ref belongs to this provider; refs from other providers are never forwarded. */
7
15
  export declare const refID: (asset: Media.Asset, provider: ProviderID) => string | undefined;
8
16
  /** Decode a provider's base64 output once into an owned `bytes` asset, sniffing the type when it is not declared. */
9
17
  export declare const decodedAsset: (invalid: (message: string, cause?: unknown) => AIError, label: string, data: string, mediaType: string | undefined, options?: Media.AssetOptions) => Effect.Effect<Media.Asset, AIError, never>;
18
+ /** One image of an OpenAI-shaped `data` array, which carries either `b64_json` or a `url`. */
19
+ export declare const imageOutput: (invalid: (message: string, cause?: unknown) => AIError, label: string, item: {
20
+ readonly b64_json?: string | null;
21
+ readonly url?: string | null;
22
+ }, mediaType: string | undefined, options?: Media.AssetOptions) => Effect.Effect<Media.Asset, AIError, never>;
23
+ /**
24
+ * Append multipart text fields: strings as-is, other values as JSON, or arrays as repeated `key[]` parts with
25
+ * `repeatArrays`. `overlay` keys in `reserved` are dropped so `http.body` cannot replace route-owned fields.
26
+ */
27
+ export declare const appendFields: (form: FormData, fields: Record<string, unknown>, options: {
28
+ readonly overlay?: Record<string, unknown>;
29
+ readonly reserved: ReadonlySet<string>;
30
+ readonly repeatArrays?: true;
31
+ }) => void;
10
32
  export * as MediaInput from "./media-input.js";
@@ -1,5 +1,7 @@
1
1
  import { Effect, Encoding } from "effect";
2
2
  import { Media } from "../../media.js";
3
+ import { mergeJsonRecords } from "../../schema/index.js";
4
+ import { encodeJson } from "../../utils/json.js";
3
5
  import { ProviderShared } from "../shared.js";
4
6
  /** Owned bytes for multipart uploads; decodes `base64` sources and rejects remote sources. */
5
7
  export const inlineBytes = (route, asset) => {
@@ -10,8 +12,46 @@ export const inlineBytes = (route, asset) => {
10
12
  return Effect.fail(ProviderShared.inlineRequired(route, asset));
11
13
  return Effect.fromResult(Encoding.decodeBase64(inline.base64)).pipe(Effect.mapError((cause) => ProviderShared.invalidRequest(`${route} media contains invalid base64 data`, cause)));
12
14
  };
15
+ /** Copied because `BlobPart` requires a plain `ArrayBuffer`. */
16
+ export const blob = (data, mediaType) => {
17
+ const buffer = new ArrayBuffer(data.byteLength);
18
+ new Uint8Array(buffer).set(data);
19
+ return new Blob([buffer], { type: mediaType });
20
+ };
21
+ const isScalar = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
22
+ export const query = (route, values) => {
23
+ const entries = Object.entries(values).filter(([, value]) => value !== undefined);
24
+ const invalid = entries.find(([, value]) => !isScalar(value) && !(Array.isArray(value) && value.every(isScalar)));
25
+ if (invalid !== undefined)
26
+ return Effect.fail(ProviderShared.invalidRequest(`${route} cannot send "${invalid[0]}" as a query parameter`));
27
+ return Effect.succeed(Object.fromEntries(entries.map(([key, value]) => [key, Array.isArray(value) ? value.map(String) : String(value)])));
28
+ };
29
+ export const dimensions = (size) => {
30
+ const [width, height] = size.split("x").map(Number);
31
+ return { width, height };
32
+ };
13
33
  /** Provider file handle when the ref belongs to this provider; refs from other providers are never forwarded. */
14
34
  export const refID = (asset, provider) => asset.source.type === "ref" && asset.source.provider === provider ? asset.source.id : undefined;
15
35
  /** Decode a provider's base64 output once into an owned `bytes` asset, sniffing the type when it is not declared. */
16
36
  export const decodedAsset = (invalid, label, data, mediaType, options) => Effect.fromResult(Encoding.decodeBase64(data)).pipe(Effect.mapError((cause) => invalid(`${label} contains invalid base64 data`, cause)), Effect.map((bytes) => Media.bytes(bytes, mediaType, options)));
37
+ /** One image of an OpenAI-shaped `data` array, which carries either `b64_json` or a `url`. */
38
+ export const imageOutput = (invalid, label, item, mediaType, options) => {
39
+ if (item.b64_json)
40
+ return decodedAsset(invalid, label, item.b64_json, mediaType, options);
41
+ if (item.url)
42
+ return Effect.succeed(Media.url(item.url, { ...options, mediaType }));
43
+ return Effect.fail(invalid(`${label} has neither image data nor a URL`));
44
+ };
45
+ /**
46
+ * Append multipart text fields: strings as-is, other values as JSON, or arrays as repeated `key[]` parts with
47
+ * `repeatArrays`. `overlay` keys in `reserved` are dropped so `http.body` cannot replace route-owned fields.
48
+ */
49
+ export const appendFields = (form, fields, options) => {
50
+ const overlay = Object.entries(options.overlay ?? {}).filter(([key]) => !options.reserved.has(key));
51
+ Object.entries(mergeJsonRecords(fields, Object.fromEntries(overlay)) ?? {}).forEach(([key, value]) => {
52
+ if (Array.isArray(value) && options.repeatArrays)
53
+ return value.forEach((item) => form.append(`${key}[]`, String(item)));
54
+ form.append(key, typeof value === "string" ? value : encodeJson(value));
55
+ });
56
+ };
17
57
  export * as MediaInput from "./media-input.js";
@@ -1,7 +1,7 @@
1
1
  import { Effect, Schema, Stream } from "effect";
2
2
  import { Route } from "../../route/client.js";
3
3
  import { Protocol } from "../../route/protocol.js";
4
- import { CompactionCheckpointResponse, HttpOptions, LLMEvent, LLMRequest } from "../../schema/index.js";
4
+ import { CompactionCheckpointResponse, LLMEvent, LLMRequest } from "../../schema/index.js";
5
5
  import { OpenResponses } from "../open-responses.js";
6
6
  import { ProviderShared } from "../shared.js";
7
7
  const onOutputItem = Effect.fn("ResponsesCheckpoint.onOutputItem")(function* (state, input) {
@@ -94,12 +94,8 @@ export const make = (body) => Effect.fn("ResponsesCheckpoint.execute")(function*
94
94
  transport: source.transport,
95
95
  });
96
96
  const native = yield* body.from(request);
97
- // The body builder already applied and validated overlays. Do not let transport reapply them.
98
- const preparedRequest = LLMRequest.update(request, {
99
- http: request.http === undefined ? undefined : new HttpOptions({ ...request.http, body: undefined }),
100
- });
101
- const prepared = yield* route.prepareTransport(native, preparedRequest, options);
102
- yield* route.streamPrepared(prepared, preparedRequest, { http: executor }, options).pipe(Stream.runDrain);
97
+ const prepared = yield* route.prepareTransport(native, request, options);
98
+ yield* route.streamPrepared(prepared, request, { http: executor }, options).pipe(Stream.runDrain);
103
99
  if (!result)
104
100
  return yield* ProviderShared.eventError(source.id, "Compaction response ended without a checkpoint");
105
101
  return result;
@@ -1,4 +1,6 @@
1
+ import { Effect } from "effect";
2
+ import { AIError, LLMRequest } from "../../schema/index.js";
1
3
  import type { CompactOperation } from "../../route/client.js";
2
4
  import { OpenResponses } from "../open-responses.js";
3
- export declare const make: (adapter: OpenResponses.ProviderAdapter) => CompactOperation;
5
+ export declare const make: (adapter: OpenResponses.ProviderAdapter, lowerTools: (request: LLMRequest) => Effect.Effect<ReadonlyArray<Record<string, unknown>>, AIError>) => CompactOperation;
4
6
  export * as ResponsesCompaction from "./responses-compaction.js";
@@ -7,12 +7,18 @@ import { HttpTransport } from "../../route/transport/index.js";
7
7
  import { OpenResponses } from "../open-responses.js";
8
8
  import { JsonObject, optionalNull, ProviderShared } from "../shared.js";
9
9
  import { Media } from "../../media.js";
10
+ // /compact has a smaller wire contract than /responses; keep the request controls it accepts.
10
11
  const Body = Schema.Struct({
11
12
  model: Schema.String,
12
13
  input: Schema.Array(Schema.Unknown),
13
14
  instructions: optionalNull(Schema.String),
14
15
  previous_response_id: optionalNull(Schema.String),
15
16
  service_tier: optionalNull(Schema.String),
17
+ reasoning: Schema.optional(JsonObject),
18
+ text: Schema.optional(JsonObject),
19
+ include: OpenResponses.coreFields.include,
20
+ parallel_tool_calls: OpenResponses.coreFields.parallel_tool_calls,
21
+ tools: Schema.optional(Schema.Array(JsonObject)),
16
22
  prompt_cache_key: optionalNull(Schema.String),
17
23
  prompt_cache_retention: optionalNull(Schema.String),
18
24
  prompt_cache_options: optionalNull(Schema.Struct({ mode: Schema.optional(Schema.String), ttl: Schema.optional(Schema.String) })),
@@ -54,14 +60,21 @@ const Response = Schema.Struct({
54
60
  ])),
55
61
  usage: Schema.optional(Schema.StructWithRest(OpenResponses.OpenResponsesUsage, [JsonObject])),
56
62
  });
57
- export const make = (adapter) => Effect.fn("ResponsesCompaction.execute")(function* (request, executor, options) {
63
+ export const make = (adapter, lowerTools) => Effect.fn("ResponsesCompaction.execute")(function* (request, executor, options) {
58
64
  const route = request.model.route;
59
65
  // The standalone compaction endpoint rejects histories containing configuration updates.
60
66
  const native = yield* OpenResponses.lowerConversation(stripEffortUpdates(request), adapter);
67
+ const generation = OpenResponses.lowerGeneration(request);
68
+ const tools = request.tools.length === 0 ? undefined : yield* lowerTools(request);
61
69
  const body = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Body))(mergeJsonRecords({
62
70
  ...native,
63
- service_tier: request.providerOptions?.serviceTier,
64
- prompt_cache_key: ProviderShared.promptCacheKey(request),
71
+ service_tier: generation.service_tier,
72
+ reasoning: generation.reasoning,
73
+ text: generation.text,
74
+ include: generation.include,
75
+ parallel_tool_calls: generation.parallel_tool_calls,
76
+ tools,
77
+ prompt_cache_key: generation.prompt_cache_key,
65
78
  }, request.http?.body));
66
79
  const url = Endpoint.render(route.endpoint, { request, body: native });
67
80
  url.pathname = `${url.pathname.replace(/\/$/, "")}/compact`;
@@ -0,0 +1,49 @@
1
+ import { Effect } from "effect";
2
+ import { Media } from "../../media.js";
3
+ import type { MediaProtocol } from "../../route/media-protocol.js";
4
+ import type { AIError, MediaUsage, ProviderMetadata } from "../../schema/index.js";
5
+ import { type SpeechEvent, type SpeechVoice } from "../../speech.js";
6
+ export interface Audio {
7
+ /** Appended in place: the route creates fresh state for each response through `initial`. */
8
+ readonly chunks: Array<Uint8Array>;
9
+ }
10
+ export type StepResult<State> = readonly [State, ReadonlyArray<SpeechEvent>];
11
+ /** Empty chunks (keep-alive records) emit nothing. */
12
+ export declare const delta: <State extends Audio>(state: State, chunk: Uint8Array) => StepResult<State>;
13
+ export declare const step: <State extends Audio>(onRecord: (state: State, frame: string) => Effect.Effect<StepResult<State>, AIError>) => (state: State, frame: string | Uint8Array) => Effect.Effect<StepResult<State>, AIError, never>;
14
+ export declare const timestamps: (texts: ReadonlyArray<string>, starts: ReadonlyArray<number>, ends: ReadonlyArray<number>) => ReadonlyArray<SpeechEvent>;
15
+ export declare const voiceID: (voice: SpeechVoice | undefined) => string | undefined;
16
+ export declare const container: (format: string, sampleRate?: number) => {
17
+ mediaType: string;
18
+ info: {
19
+ format: string;
20
+ sampleRate: number | undefined;
21
+ };
22
+ };
23
+ declare const PCM_MEDIA_TYPES: {
24
+ readonly pcm_s16le: "audio/pcm";
25
+ readonly pcm_f32le: "audio/pcm";
26
+ readonly pcm_mulaw: "audio/mulaw";
27
+ readonly pcm_alaw: "audio/alaw";
28
+ };
29
+ export type PcmEncoding = keyof typeof PCM_MEDIA_TYPES;
30
+ export declare const pcm: (encoding: PcmEncoding, sampleRate: number | undefined, mediaType?: string) => {
31
+ mediaType: string;
32
+ info: {
33
+ format: string;
34
+ encoding: "pcm_s16le" | "pcm_f32le" | "pcm_mulaw" | "pcm_alaw";
35
+ sampleRate: number | undefined;
36
+ channels: number;
37
+ };
38
+ };
39
+ export declare const sampleRate: (mediaType: string | undefined) => number | undefined;
40
+ /** A declared `mediaType` wins over sniffing: headerless PCM can start with bytes that look like an MPEG frame sync. */
41
+ export declare const finish: (route: MediaProtocol.Identity, state: Audio, output: {
42
+ readonly mediaType: string | undefined;
43
+ readonly info?: Media.Info;
44
+ readonly usage?: MediaUsage;
45
+ readonly providerMetadata?: ProviderMetadata;
46
+ readonly detail?: string;
47
+ }) => Effect.Effect<ReadonlyArray<SpeechEvent>, AIError>;
48
+ export declare const headerUsage: (type: "characters" | "credits", value: string | undefined) => MediaUsage | undefined;
49
+ export * as SpeechStream from "./speech-stream.js";
@@ -0,0 +1,64 @@
1
+ import { Effect } from "effect";
2
+ import { Media } from "../../media.js";
3
+ import { SpeechAudioDeltaEvent, SpeechFinishEvent, SpeechTimestampsEvent, } from "../../speech.js";
4
+ import { concatBytes } from "../../utils/bytes.js";
5
+ /** Empty chunks (keep-alive records) emit nothing. */
6
+ export const delta = (state, chunk) => {
7
+ if (chunk.length === 0)
8
+ return [state, []];
9
+ state.chunks.push(chunk);
10
+ return [state, [SpeechAudioDeltaEvent.make({ chunk })]];
11
+ };
12
+ export const step = (onRecord) => (state, frame) => typeof frame === "string" ? onRecord(state, frame) : Effect.succeed(delta(state, frame));
13
+ export const timestamps = (texts, starts, ends) => texts.length === 0
14
+ ? []
15
+ : [
16
+ SpeechTimestampsEvent.make({
17
+ items: texts.map((text, index) => ({ text, startSeconds: starts[index] ?? 0, endSeconds: ends[index] ?? 0 })),
18
+ }),
19
+ ];
20
+ export const voiceID = (voice) => (typeof voice === "object" ? voice.id : voice);
21
+ const CONTAINER_MEDIA_TYPES = {
22
+ mp3: "audio/mpeg",
23
+ wav: "audio/wav",
24
+ opus: "audio/ogg",
25
+ aac: "audio/aac",
26
+ flac: "audio/flac",
27
+ };
28
+ export const container = (format, sampleRate) => ({
29
+ mediaType: CONTAINER_MEDIA_TYPES[format],
30
+ info: { format, sampleRate },
31
+ });
32
+ const PCM_MEDIA_TYPES = {
33
+ pcm_s16le: "audio/pcm",
34
+ pcm_f32le: "audio/pcm",
35
+ pcm_mulaw: "audio/mulaw",
36
+ pcm_alaw: "audio/alaw",
37
+ };
38
+ export const pcm = (encoding, sampleRate, mediaType) => ({
39
+ mediaType: mediaType ?? PCM_MEDIA_TYPES[encoding],
40
+ info: { format: "pcm", encoding, sampleRate, channels: 1 },
41
+ });
42
+ export const sampleRate = (mediaType) => {
43
+ const rate = /rate=(\d+)/i.exec(mediaType ?? "")?.[1];
44
+ return rate === undefined ? undefined : Number(rate);
45
+ };
46
+ /** A declared `mediaType` wins over sniffing: headerless PCM can start with bytes that look like an MPEG frame sync. */
47
+ export const finish = (route, state, output) => {
48
+ if (state.chunks.length === 0)
49
+ return Effect.fail(route.frameError(`The provider returned no audio${output.detail === undefined ? "" : ` (${output.detail})`}`));
50
+ return Effect.succeed([
51
+ SpeechFinishEvent.make({
52
+ audio: Media.bytes(concatBytes(state.chunks), output.mediaType, { info: output.info }),
53
+ usage: output.usage,
54
+ providerMetadata: output.providerMetadata,
55
+ }),
56
+ ]);
57
+ };
58
+ export const headerUsage = (type, value) => {
59
+ const amount = Number(value);
60
+ if (!Number.isFinite(amount))
61
+ return undefined;
62
+ return type === "credits" ? { type, credits: amount } : { type, characters: amount };
63
+ };
64
+ export * as SpeechStream from "./speech-stream.js";
@@ -1,8 +1,8 @@
1
- import type { JsonSchema, LanguageModelToolSchemaCompatibility } from "../../schema/index.js";
1
+ import { type JsonSchema, type LanguageModel, type LLMRequest } from "../../schema/index.js";
2
2
  export declare const ToolSchemaProjection: {
3
3
  readonly gemini: (schema: JsonSchema) => JsonSchema;
4
- readonly modelCompatibility: (schema: JsonSchema, compatibility: LanguageModelToolSchemaCompatibility | undefined) => JsonSchema;
5
4
  readonly moonshot: (schema: JsonSchema) => JsonSchema;
6
5
  readonly openAI: (schema: JsonSchema) => JsonSchema;
7
6
  readonly responses: (schema: JsonSchema) => JsonSchema;
7
+ readonly tools: (entries: LLMRequest["tools"], model: LanguageModel) => LLMRequest["tools"];
8
8
  };
@@ -1,5 +1,6 @@
1
+ import { ToolDefinition } from "../../schema/index.js";
1
2
  import { isRecord } from "../../utils/record.js";
2
- import { GeminiToolSchema } from "./gemini-tool-schema.js";
3
+ import { GeminiJsonSchema } from "./gemini-json-schema.js";
3
4
  const tupleItemsSchema = (items) => {
4
5
  const projected = items.map(moonshotNode);
5
6
  if (projected.length === 0)
@@ -8,6 +9,17 @@ const tupleItemsSchema = (items) => {
8
9
  return projected[0];
9
10
  return { anyOf: projected };
10
11
  };
12
+ // Moonshot rejects an `enum` without a sibling `type`. Its `type` may be one type, or one type plus "null".
13
+ const enumType = (values) => {
14
+ const types = [
15
+ ...new Set(values.map((value) => (value === null ? "null" : Array.isArray(value) ? "array" : typeof value))),
16
+ ];
17
+ if (types.length === 1)
18
+ return types[0];
19
+ if (types.length === 2 && types.includes("null"))
20
+ return [...types.filter((type) => type !== "null"), "null"];
21
+ return undefined;
22
+ };
11
23
  const moonshotNode = (schema) => {
12
24
  if (Array.isArray(schema))
13
25
  return schema.map(moonshotNode);
@@ -15,18 +27,22 @@ const moonshotNode = (schema) => {
15
27
  return schema;
16
28
  if (typeof schema.$ref === "string")
17
29
  return { $ref: schema.$ref };
18
- return Object.fromEntries(Object.entries(schema).flatMap(([key, value]) => {
19
- if (key === "items" && Array.isArray(value))
20
- return [[key, tupleItemsSchema(value)]];
21
- if (key === "prefixItems") {
22
- if ("items" in schema)
30
+ const type = schema.type === undefined && Array.isArray(schema.enum) ? enumType(schema.enum) : undefined;
31
+ return Object.fromEntries([
32
+ ...(type === undefined ? [] : [["type", type]]),
33
+ ...Object.entries(schema).flatMap(([key, value]) => {
34
+ if (key === "items" && Array.isArray(value))
35
+ return [[key, tupleItemsSchema(value)]];
36
+ if (key === "prefixItems") {
37
+ if ("items" in schema)
38
+ return [];
39
+ return [["items", tupleItemsSchema(Array.isArray(value) ? value : [])]];
40
+ }
41
+ if (key === "unevaluatedItems")
23
42
  return [];
24
- return [["items", tupleItemsSchema(Array.isArray(value) ? value : [])]];
25
- }
26
- if (key === "unevaluatedItems")
27
- return [];
28
- return [[key, moonshotNode(value)]];
29
- }));
43
+ return [[key, moonshotNode(value)]];
44
+ }),
45
+ ]);
30
46
  };
31
47
  const moonshot = (schema) => {
32
48
  const projected = moonshotNode(schema);
@@ -34,21 +50,48 @@ const moonshot = (schema) => {
34
50
  };
35
51
  const openAI = (schema) => schema;
36
52
  const responses = openAI;
37
- const gemini = (schema) => GeminiToolSchema.convert(schema) ?? {};
38
- const modelCompatibility = (schema, compatibility) => {
39
- if (compatibility === undefined)
53
+ const gemini = GeminiJsonSchema.normalize;
54
+ const MODEL_NAMES = [
55
+ [/gemini/i, "gemini"],
56
+ [/kimi/i, "moonshot"],
57
+ ];
58
+ // Tool arguments are always a JSON object, and most providers reject a tool schema whose root does not
59
+ // declare `type: "object"`, such as `{}` or a bare `properties` map. Effect encodes an empty struct as
60
+ // `anyOf` object or array; every object matches its bare object branch, so that `anyOf` is dropped.
61
+ const objectRoot = (schema) => {
62
+ if (schema.type !== undefined)
40
63
  return schema;
41
- switch (compatibility) {
64
+ if (Array.isArray(schema.anyOf) &&
65
+ schema.anyOf.some((branch) => isRecord(branch) && branch.type === "object" && Object.keys(branch).length === 1))
66
+ return { type: "object", ...Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "anyOf")) };
67
+ return { type: "object", ...schema };
68
+ };
69
+ // Every tool schema gets an object root. Then an explicit `sanitizer` wins, and `none` opts out.
70
+ // Otherwise the protocol's own default applies (the Gemini API always uses Gemini's rules), then the
71
+ // model name selects the family's rules so models reached through gateways and OpenAI-compatible
72
+ // endpoints get the same handling.
73
+ const modelCompatibility = (schema, model) => {
74
+ const root = objectRoot(schema);
75
+ switch (model.compatibility?.sanitizer ??
76
+ model.route.sanitizer ??
77
+ MODEL_NAMES.find(([name]) => name.test(model.id))?.[1]) {
42
78
  case "gemini":
43
- return gemini(schema);
79
+ return gemini(root);
44
80
  case "moonshot":
45
- return moonshot(schema);
81
+ return moonshot(root);
82
+ case "none":
83
+ case undefined:
84
+ return root;
46
85
  }
47
86
  };
87
+ // Applied once to every request before any protocol builds its body, including tools in namespaces.
88
+ const tools = (entries, model) => entries.map((tool) => tool.type === "tool"
89
+ ? new ToolDefinition({ ...tool, inputSchema: modelCompatibility(tool.inputSchema, model) })
90
+ : { ...tool, tools: tools(tool.tools, model) });
48
91
  export const ToolSchemaProjection = {
49
92
  gemini,
50
- modelCompatibility,
51
93
  moonshot,
52
94
  openAI,
53
95
  responses,
96
+ tools,
54
97
  };
@@ -1,15 +1,15 @@
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://api.x.ai/v1";
5
6
  export declare const PATH = "/images/generations";
6
7
  export declare const EDIT_PATH = "/images/edits";
7
- export type XAIImageString<Known extends string> = Known | (string & {});
8
8
  /** Provider-native options. Common fields (`n`, `aspectRatio`, `images`) live on the request. */
9
9
  export type XAIImageOptions = {
10
- readonly resolution?: XAIImageString<"1k" | "2k">;
11
- readonly responseFormat?: XAIImageString<"url" | "b64_json">;
12
- readonly response_format?: XAIImageString<"url" | "b64_json">;
10
+ readonly resolution?: OpenString<"1k" | "2k">;
11
+ readonly responseFormat?: OpenString<"url" | "b64_json">;
12
+ readonly response_format?: OpenString<"url" | "b64_json">;
13
13
  } & Record<string, unknown>;
14
14
  export type Request = ImageRequestFor<XAIImageOptions>;
15
15
  export declare const protocol: MediaProtocol.Inline<Request, ImageResponse>;