@opencode/ai 0.0.0-dev-20120 → 0.0.0-dev-20123

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 (128) hide show
  1. package/dist/experimental/evaluation.js +1 -1
  2. package/dist/generation.d.ts +1 -7
  3. package/dist/generation.js +1 -11
  4. package/dist/image.d.ts +2 -2
  5. package/dist/image.js +2 -2
  6. package/dist/llm.js +1 -1
  7. package/dist/media-model.d.ts +0 -2
  8. package/dist/media-model.js +0 -2
  9. package/dist/protocols/alibaba-messages.d.ts +1 -1
  10. package/dist/protocols/anthropic-messages.d.ts +34 -34
  11. package/dist/protocols/assemblyai-transcription.d.ts +2 -1
  12. package/dist/protocols/assemblyai-transcription.js +10 -14
  13. package/dist/protocols/bfl-images.js +15 -31
  14. package/dist/protocols/cartesia-speech.d.ts +2 -2
  15. package/dist/protocols/cartesia-speech.js +10 -16
  16. package/dist/protocols/deepgram-speech.d.ts +3 -3
  17. package/dist/protocols/deepgram-speech.js +7 -11
  18. package/dist/protocols/deepgram-transcription.d.ts +2 -1
  19. package/dist/protocols/deepgram-transcription.js +9 -13
  20. package/dist/protocols/elevenlabs-speech.d.ts +3 -3
  21. package/dist/protocols/elevenlabs-speech.js +9 -13
  22. package/dist/protocols/fal-images.d.ts +2 -1
  23. package/dist/protocols/fal-images.js +16 -29
  24. package/dist/protocols/fal-video.d.ts +2 -2
  25. package/dist/protocols/fal-video.js +9 -24
  26. package/dist/protocols/gemini.d.ts +13 -13
  27. package/dist/protocols/google-images.d.ts +3 -3
  28. package/dist/protocols/google-images.js +11 -21
  29. package/dist/protocols/google-speech.js +7 -13
  30. package/dist/protocols/google-transcription.d.ts +2 -1
  31. package/dist/protocols/google-transcription.js +8 -20
  32. package/dist/protocols/google-video.d.ts +2 -2
  33. package/dist/protocols/google-video.js +14 -30
  34. package/dist/protocols/meta-images.d.ts +3 -4
  35. package/dist/protocols/meta-images.js +12 -21
  36. package/dist/protocols/meta-messages.d.ts +5 -5
  37. package/dist/protocols/meta-responses.js +5 -2
  38. package/dist/protocols/mistral-chat.js +6 -3
  39. package/dist/protocols/openai-images.d.ts +4 -5
  40. package/dist/protocols/openai-images.js +24 -55
  41. package/dist/protocols/openai-responses.js +8 -2
  42. package/dist/protocols/openai-speech.js +7 -11
  43. package/dist/protocols/openai-transcription.js +21 -29
  44. package/dist/protocols/replicate-images.js +11 -17
  45. package/dist/protocols/runway-video.d.ts +3 -3
  46. package/dist/protocols/runway-video.js +10 -16
  47. package/dist/protocols/shared.d.ts +5 -17
  48. package/dist/protocols/shared.js +3 -39
  49. package/dist/protocols/stability-images.d.ts +2 -1
  50. package/dist/protocols/stability-images.js +24 -36
  51. package/dist/protocols/utils/fal-queue.d.ts +1 -3
  52. package/dist/protocols/utils/fal-queue.js +4 -6
  53. package/dist/protocols/utils/media-input.d.ts +15 -1
  54. package/dist/protocols/utils/media-input.js +22 -0
  55. package/dist/protocols/utils/speech-stream.d.ts +3 -3
  56. package/dist/protocols/utils/speech-stream.js +1 -4
  57. package/dist/protocols/utils/tool-schema.js +19 -6
  58. package/dist/protocols/xai-images.d.ts +4 -4
  59. package/dist/protocols/xai-images.js +12 -28
  60. package/dist/protocols/xai-video.js +12 -18
  61. package/dist/protocols/zai-images.d.ts +2 -2
  62. package/dist/protocols/zai-images.js +8 -11
  63. package/dist/protocols/zai-messages.d.ts +1 -1
  64. package/dist/providers/alibaba.d.ts +1 -1
  65. package/dist/providers/anthropic-compatible.d.ts +5 -5
  66. package/dist/providers/anthropic.d.ts +5 -5
  67. package/dist/providers/assemblyai.d.ts +1 -1
  68. package/dist/providers/assemblyai.js +5 -10
  69. package/dist/providers/azure.js +2 -2
  70. package/dist/providers/black-forest-labs.d.ts +1 -1
  71. package/dist/providers/black-forest-labs.js +5 -10
  72. package/dist/providers/cartesia.d.ts +1 -1
  73. package/dist/providers/cartesia.js +5 -10
  74. package/dist/providers/cloudflare-ai-gateway.d.ts +10 -10
  75. package/dist/providers/deepgram.d.ts +1 -1
  76. package/dist/providers/deepgram.js +6 -12
  77. package/dist/providers/elevenlabs.d.ts +1 -1
  78. package/dist/providers/elevenlabs.js +5 -10
  79. package/dist/providers/fal.d.ts +1 -1
  80. package/dist/providers/fal.js +5 -12
  81. package/dist/providers/google-vertex-messages.d.ts +5 -5
  82. package/dist/providers/google-vertex.d.ts +3 -3
  83. package/dist/providers/google.d.ts +3 -3
  84. package/dist/providers/google.js +7 -12
  85. package/dist/providers/meta.d.ts +8 -8
  86. package/dist/providers/meta.js +4 -8
  87. package/dist/providers/minimax.d.ts +5 -5
  88. package/dist/providers/moonshot.d.ts +5 -5
  89. package/dist/providers/openai.d.ts +7 -8
  90. package/dist/providers/openai.js +10 -11
  91. package/dist/providers/opencode-zen.js +1 -1
  92. package/dist/providers/openrouter.d.ts +6 -7
  93. package/dist/providers/openrouter.js +1 -1
  94. package/dist/providers/replicate.d.ts +1 -1
  95. package/dist/providers/replicate.js +5 -10
  96. package/dist/providers/runway.d.ts +1 -1
  97. package/dist/providers/runway.js +5 -10
  98. package/dist/providers/stability.d.ts +1 -1
  99. package/dist/providers/stability.js +6 -11
  100. package/dist/providers/typesafe-ai.js +1 -1
  101. package/dist/providers/vercel-ai-gateway.js +1 -1
  102. package/dist/providers/xai.js +5 -10
  103. package/dist/providers/zai-coding-plan.d.ts +1 -1
  104. package/dist/providers/zai.js +4 -8
  105. package/dist/route/client.d.ts +0 -1
  106. package/dist/route/client.js +1 -6
  107. package/dist/route/endpoint.d.ts +1 -0
  108. package/dist/route/endpoint.js +2 -2
  109. package/dist/route/framing.d.ts +10 -1
  110. package/dist/route/framing.js +45 -5
  111. package/dist/route/media-protocol.d.ts +42 -35
  112. package/dist/route/media-protocol.js +67 -62
  113. package/dist/route/media.d.ts +6 -2
  114. package/dist/route/media.js +40 -29
  115. package/dist/schema/options.d.ts +6 -3
  116. package/dist/schema/options.js +7 -3
  117. package/dist/speech.d.ts +2 -2
  118. package/dist/speech.js +2 -2
  119. package/dist/transcription.js +2 -2
  120. package/dist/utils/json.d.ts +4 -0
  121. package/dist/utils/json.js +4 -0
  122. package/dist/video.d.ts +2 -2
  123. package/dist/video.js +2 -2
  124. package/package.json +3 -3
  125. package/dist/protocols/utils/meta-image.d.ts +0 -2
  126. package/dist/protocols/utils/meta-image.js +0 -13
  127. package/dist/protocols/utils/openai-image.d.ts +0 -5
  128. package/dist/protocols/utils/openai-image.js +0 -18
@@ -1,20 +1,18 @@
1
1
  import { Tool } from "@opencode/schema/tool";
2
- import { Effect, Schema, Stream } from "effect";
2
+ import { Effect, Schema } from "effect";
3
3
  import { Headers, HttpClientRequest } from "effect/unstable/http";
4
4
  import { Media } from "../media.js";
5
- import { AIError, LLMRequest, Message, ToolDefinition, type ContentPart, type MediaPart, type ProviderID, type ToolEntry, type ToolResultPart } from "../schema/index.js";
5
+ import { AIError, LLMRequest, Message, ToolDefinition, type ContentPart, type MediaPart, type OpenString, type ProviderID, type ToolEntry, type ToolResultPart } from "../schema/index.js";
6
+ import { Json, decodeJson, encodeJson } from "../utils/json.js";
6
7
  import { isRecord } from "../utils/record.js";
7
- export { isRecord };
8
- export declare const Json: Schema.fromJsonString<Schema.Unknown>;
9
- export declare const decodeJson: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => unknown;
10
- export declare const encodeJson: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => string;
8
+ export { Json, decodeJson, encodeJson, isRecord };
11
9
  export declare const JsonObject: Schema.$Record<Schema.String, Schema.Unknown>;
12
10
  export declare const optionalArray: <const S extends Schema.Top>(schema: S) => Schema.optional<Schema.$Array<S>>;
13
11
  export declare const optionalNull: <const S extends Schema.Top>(schema: S) => Schema.optional<Schema.NullOr<S>>;
14
12
  /** Optional field whose malformed value decodes to `undefined` instead of failing the enclosing struct. */
15
13
  export declare const lenient: <const S extends Schema.Top>(schema: S) => Schema.optionalKey<Schema.middlewareDecoding<Schema.UndefinedOr<S>, S["DecodingServices"]>>;
16
14
  /** Provider-defined string enum: known values for autocomplete, any string accepted at runtime. */
17
- export declare const knownString: <Known extends string>() => Schema.declare<(string & {}) | Known, (string & {}) | Known>;
15
+ export declare const knownString: <Known extends string>() => Schema.declare<OpenString<Known>, OpenString<Known>>;
18
16
  export declare const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
19
17
  export declare const promptCacheKey: (request: LLMRequest) => string | undefined;
20
18
  /**
@@ -131,18 +129,8 @@ export declare const mediaReference: (asset: Media.Asset, provider: ProviderID |
131
129
  * `uri`; the declared `mime` wins over any data-URL prefix so tool authors control the type the model sees.
132
130
  */
133
131
  export declare const toolFileMedia: (item: Tool.FileContent) => MediaPart;
134
- export declare const trimBaseUrl: (value: string) => string;
135
132
  export declare const toolResultText: (part: ToolResultPart) => string;
136
133
  export declare const errorText: (error: unknown) => string;
137
- /**
138
- * `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
139
- * decoder, optionally filters named events, and drops empty events and known
140
- * keepalives that proxies send as data. `[DONE]` is dropped by default or
141
- * retained for protocols that use it as their stream boundary. Retry control events are ignored without
142
- * interrupting the stream. Decoder failures become provider output errors so
143
- * the public error channel stays `AIError`.
144
- */
145
- export declare const sseFraming: (bytes: Stream.Stream<Uint8Array, AIError>, events?: ReadonlySet<string>, includeDone?: boolean) => Stream.Stream<string, AIError>;
146
134
  /**
147
135
  * Canonical invalid-request constructor shared by protocol lowering.
148
136
  */
@@ -1,14 +1,11 @@
1
1
  import { Tool } from "@opencode/schema/tool";
2
- import { Effect, Option, Schema, Stream } from "effect";
3
- import * as Sse from "effect/unstable/encoding/Sse";
2
+ import { Effect, Option, Schema } from "effect";
4
3
  import { Headers, HttpClientRequest } from "effect/unstable/http";
5
4
  import { Media } from "../media.js";
6
5
  import { InvalidProviderOutputError, InvalidRequestError, UnsupportedOperationError, AIError, LLMRequest, Message, ToolDefinition, } from "../schema/index.js";
6
+ import { Json, decodeJson, encodeJson } from "../utils/json.js";
7
7
  import { isRecord } from "../utils/record.js";
8
- export { isRecord };
9
- export const Json = Schema.fromJsonString(Schema.Unknown);
10
- export const decodeJson = Schema.decodeUnknownSync(Json);
11
- export const encodeJson = Schema.encodeSync(Json);
8
+ export { Json, decodeJson, encodeJson, isRecord };
12
9
  const isJson = Schema.is(Schema.Json);
13
10
  export const JsonObject = Schema.Record(Schema.String, Schema.Unknown);
14
11
  export const optionalArray = (schema) => Schema.optional(Schema.Array(schema));
@@ -165,7 +162,6 @@ export const toolFileMedia = (item) => {
165
162
  : Media.base64(item.uri, item.mime);
166
163
  return Message.media(asset, { filename: item.name });
167
164
  };
168
- export const trimBaseUrl = (value) => value.replace(/\/+$/, "");
169
165
  export const toolResultText = (part) => {
170
166
  if (part.result.type === "text")
171
167
  return String(part.result.value);
@@ -190,38 +186,6 @@ export const errorText = (error) => {
190
186
  return "undefined";
191
187
  return "Unknown stream error";
192
188
  };
193
- /**
194
- * `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
195
- * decoder, optionally filters named events, and drops empty events and known
196
- * keepalives that proxies send as data. `[DONE]` is dropped by default or
197
- * retained for protocols that use it as their stream boundary. Retry control events are ignored without
198
- * interrupting the stream. Decoder failures become provider output errors so
199
- * the public error channel stays `AIError`.
200
- */
201
- export const sseFraming = (bytes, events, includeDone = false) => bytes.pipe(Stream.decodeText(), Stream.mapAccumEffect(() => {
202
- const output = [];
203
- return {
204
- output,
205
- parser: Sse.makeParser((event) => {
206
- if (event._tag === "Event")
207
- output.push(event);
208
- }),
209
- };
210
- }, (state, chunk) => Effect.gen(function* () {
211
- const error = state.parser.feed(chunk);
212
- if (error)
213
- return yield* eventError("sse", error.message, chunk, error);
214
- return [state, state.output.splice(0)];
215
- })), Stream.filter((event) => (events === undefined || events.has(event.event)) &&
216
- event.data.length > 0 &&
217
- // Some OpenAI-compatible proxies serialize an empty flush as a bare
218
- // `data: null`, between events or after `[DONE]`. No protocol has a
219
- // null event, so it carries nothing and must not abort the stream.
220
- event.data !== "null" &&
221
- // Vertex AI partner models (e.g. `xai/grok-4.6`) send their SSE
222
- // keepalive comment as `data: : keepalive` while reasoning.
223
- event.data !== ": keepalive" &&
224
- (event.data !== "[DONE]" || includeDone || (events !== undefined && event.event !== "message"))), Stream.map((event) => event.data));
225
189
  /**
226
190
  * Canonical invalid-request constructor shared by protocol lowering.
227
191
  */
@@ -2,8 +2,9 @@ import { Schema } from "effect";
2
2
  import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js";
3
3
  import { MediaProtocol } from "../route/media-protocol.js";
4
4
  import { MediaRoute } from "../route/media.js";
5
+ import { type OpenString } from "../schema/index.js";
5
6
  export declare const DEFAULT_BASE_URL = "https://api.stability.ai";
6
- export type StabilityStylePreset = "enhance" | "anime" | "photographic" | "digital-art" | "comic-book" | "fantasy-art" | "line-art" | "analog-film" | "neon-punk" | "isometric" | "low-poly" | "origami" | "modeling-compound" | "cinematic" | "3d-model" | "pixel-art" | "tile-texture" | (string & {});
7
+ export type StabilityStylePreset = OpenString<"enhance" | "anime" | "photographic" | "digital-art" | "comic-book" | "fantasy-art" | "line-art" | "analog-film" | "neon-punk" | "isometric" | "low-poly" | "origami" | "modeling-compound" | "cinematic" | "3d-model" | "pixel-art" | "tile-texture">;
7
8
  export type StabilityImageOptions = {
8
9
  readonly negative_prompt?: string;
9
10
  readonly style_preset?: StabilityStylePreset;
@@ -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 { MediaInput } from "./utils/media-input.js";
8
- const ADAPTER = "stability-images";
9
- const UPSCALE_ADAPTER = "stability-upscale";
10
- const NAME = "Stability AI";
11
- const PROVIDER = ProviderID.make("stability");
8
+ const route = MediaProtocol.identity({ id: "stability-images", name: "Stability AI", provider: "stability" });
9
+ const upscaleRoute = MediaProtocol.identity({ id: "stability-upscale", name: "Stability AI", provider: "stability" });
12
10
  export const DEFAULT_BASE_URL = "https://api.stability.ai";
13
11
  const RESULTS_PATH = "/v2beta/results";
14
12
  const UPSCALE_MODEL = "creative";
@@ -32,26 +30,24 @@ const Started = Schema.Struct({ id: Schema.String });
32
30
  /** `sd3.5-*` ids share the `sd3` endpoint and select the model with a form field. */
33
31
  const endpoint = (model) => (model.startsWith("sd3") ? "sd3" : model);
34
32
  const RESERVED_FORM_FIELDS = new Set(["image", "prompt", "mode", "model"]);
35
- const unsupported = (route, operation, message) => ProviderShared.unsupportedOperation({ operation, provider: PROVIDER, route, message });
36
- const form = Effect.fn("StabilityImages.form")(function* (route, fields, native, source) {
33
+ const form = Effect.fn("StabilityImages.form")(function* (identity, fields, native, source) {
37
34
  if ((source?.length ?? 0) > 1)
38
- return yield* unsupported(route, "media.images", `${NAME} takes one source image`);
35
+ return yield* identity.unsupported("media.images", `${identity.name} takes one source image`);
39
36
  const body = new FormData();
40
- const overlay = Object.entries(native ?? {}).filter(([key]) => !RESERVED_FORM_FIELDS.has(key));
41
- Object.entries(mergeJsonRecords(fields, Object.fromEntries(overlay)) ?? {}).forEach(([key, value]) => body.append(key, typeof value === "string" ? value : ProviderShared.encodeJson(value)));
37
+ MediaInput.appendFields(body, fields, { overlay: native, reserved: RESERVED_FORM_FIELDS });
42
38
  const image = source?.[0];
43
39
  if (image !== undefined)
44
- body.append("image", MediaInput.blob(yield* MediaInput.inlineBytes(route, image), image.mediaType), "image");
40
+ body.append("image", MediaInput.blob(yield* MediaInput.inlineBytes(identity.id, image), image.mediaType), "image");
45
41
  return MediaProtocol.multipart(body);
46
42
  });
47
43
  const fromRequest = Effect.fn("StabilityImages.fromRequest")(function* (request) {
48
44
  if (request.n !== undefined && request.n > 1)
49
- return yield* unsupported(ADAPTER, "media.n", `${NAME} generates one image per request; call it once per image`);
45
+ return yield* route.unsupported("media.n", `${route.name} generates one image per request; call it once per image`);
50
46
  const target = endpoint(request.model.id);
51
47
  const edit = (request.images?.length ?? 0) > 0;
52
48
  if (edit && target === "core")
53
- return yield* unsupported(ADAPTER, "media.images", `${NAME} core is text-to-image only; use ultra or sd3.5-*`);
54
- return yield* form(ADAPTER, {
49
+ return yield* route.unsupported("media.images", `${route.name} core is text-to-image only; use ultra or sd3.5-*`);
50
+ return yield* form(route, {
55
51
  prompt: request.prompt,
56
52
  aspect_ratio: request.aspectRatio,
57
53
  seed: request.seed,
@@ -62,33 +58,33 @@ const fromRequest = Effect.fn("StabilityImages.fromRequest")(function* (request)
62
58
  });
63
59
  const fromUpscaleRequest = Effect.fn("StabilityImages.fromUpscaleRequest")(function* (request) {
64
60
  if ((request.images?.length ?? 0) === 0)
65
- return yield* ProviderShared.invalidRequest(`${NAME} upscale requires the source image in images`);
66
- return yield* form(UPSCALE_ADAPTER, { prompt: request.prompt, seed: request.seed, output_format: request.format }, mergeJsonRecords(request.providerOptions, request.http?.body), request.images);
61
+ return yield* ProviderShared.invalidRequest(`${upscaleRoute.name} upscale requires the source image in images`);
62
+ return yield* form(upscaleRoute, { prompt: request.prompt, seed: request.seed, output_format: request.format }, mergeJsonRecords(request.providerOptions, request.http?.body), request.images);
67
63
  });
68
64
  // ---------------------------------------------------------------------------
69
65
  // 6. Response decoding
70
66
  // ---------------------------------------------------------------------------
71
- const decodeImageDocument = (route) => {
72
- const decode = MediaProtocol.decodeJson(route, NAME, ImageDocument);
67
+ const decodeImageDocument = (identity) => {
68
+ const decode = identity.decodeJson(ImageDocument);
73
69
  return Effect.fn("StabilityImages.decodeImage")(function* (response) {
74
70
  const output = yield* decode(response);
75
71
  const document = output.value;
76
72
  const data = document.image ?? document.result;
77
73
  if (data === undefined)
78
- return yield* output.invalid(`${NAME} returned no image`);
79
- const image = yield* MediaInput.decodedAsset(output.invalid, `${NAME} result`, data, undefined);
74
+ return yield* output.invalid(`${identity.name} returned no image`);
75
+ const image = yield* MediaInput.decodedAsset(output.invalid, `${identity.name} result`, data, undefined);
80
76
  return new ImageResponse({
81
77
  images: [image],
82
78
  notices: document.finish_reason === "CONTENT_FILTERED"
83
- ? [{ type: "moderated", message: `${NAME} blurred the image for violating its content policy` }]
79
+ ? [{ type: "moderated", message: `${identity.name} blurred the image for violating its content policy` }]
84
80
  : undefined,
85
81
  providerMetadata: { stability: { seed: document.seed, finishReason: document.finish_reason } },
86
82
  });
87
83
  });
88
84
  };
89
- const decodeResponse = decodeImageDocument(ADAPTER);
90
- const decodeUpscaleImage = decodeImageDocument(UPSCALE_ADAPTER);
91
- const decodeStart = MediaProtocol.decodeStarted(UPSCALE_ADAPTER, NAME, Started, (value) => ({
85
+ const decodeResponse = decodeImageDocument(route);
86
+ const decodeUpscaleImage = decodeImageDocument(upscaleRoute);
87
+ const decodeStart = upscaleRoute.decodeStarted(Started, (value) => ({
92
88
  token: { id: value.id },
93
89
  snapshot: { id: value.id, status: "queued" },
94
90
  }));
@@ -99,24 +95,20 @@ const decodeStatus = (response, context) => Effect.succeed({
99
95
  });
100
96
  const decodeUpscaleResult = Effect.fn("StabilityImages.decodeUpscaleResult")(function* (response, context) {
101
97
  if (response.status === 202) {
102
- const output = yield* MediaProtocol.text(UPSCALE_ADAPTER, NAME, response);
103
- return yield* output.invalid(`${NAME} upscale ${context.token.id} has not finished`);
98
+ const output = yield* upscaleRoute.text(response);
99
+ return yield* output.invalid(`${upscaleRoute.name} upscale ${context.token.id} has not finished`);
104
100
  }
105
101
  return yield* decodeUpscaleImage(response);
106
102
  });
107
103
  // ---------------------------------------------------------------------------
108
104
  // 7. Protocol and route
109
105
  // ---------------------------------------------------------------------------
110
- export const protocol = MediaProtocol.inline({
111
- id: ADAPTER,
112
- name: NAME,
106
+ export const protocol = MediaProtocol.inline(route, {
113
107
  unsupported: ["size", "mask"],
114
108
  body: { from: fromRequest },
115
109
  response: { decode: decodeResponse },
116
110
  });
117
- export const upscaleProtocol = MediaProtocol.queued({
118
- id: UPSCALE_ADAPTER,
119
- name: NAME,
111
+ export const upscaleProtocol = MediaProtocol.queued(upscaleRoute, {
120
112
  token: Token,
121
113
  unsupported: ["n", "size", "aspectRatio", "mask"],
122
114
  start: { body: { from: fromUpscaleRequest }, decode: decodeStart },
@@ -124,16 +116,12 @@ export const upscaleProtocol = MediaProtocol.queued({
124
116
  result: { path: (token) => `${RESULTS_PATH}/${token.id}`, decode: decodeUpscaleResult },
125
117
  });
126
118
  export const model = (input) => ImageModel.fromRoute({
127
- id: ADAPTER,
128
- provider: PROVIDER,
129
119
  protocol,
130
120
  baseURL: DEFAULT_BASE_URL,
131
121
  headers: HEADERS,
132
122
  path: ({ request }) => `/v2beta/stable-image/generate/${endpoint(request.model.id)}`,
133
123
  }, input);
134
124
  export const upscaleModel = (input) => ImageModel.fromRoute({
135
- id: UPSCALE_ADAPTER,
136
- provider: PROVIDER,
137
125
  protocol: upscaleProtocol,
138
126
  baseURL: DEFAULT_BASE_URL,
139
127
  headers: HEADERS,
@@ -13,9 +13,7 @@ export declare const Token: Schema.Struct<{
13
13
  }>;
14
14
  export type Token = Schema.Schema.Type<typeof Token>;
15
15
  export declare const mediaUrl: (asset: Media.Asset, name: string) => Effect.Effect<string, AIError, never>;
16
- export declare const protocol: <Request, Response>(input: {
17
- readonly id: string;
18
- readonly name: string;
16
+ export declare const protocol: <Request, Response>(route: MediaProtocol.Identity, input: {
19
17
  readonly unsupported?: ReadonlyArray<keyof Request & string>;
20
18
  readonly from: (request: Request) => Effect.Effect<MediaProtocol.Body, AIError>;
21
19
  readonly decodeResult: (response: HttpClientResponse.HttpClientResponse, context: MediaProtocol.PollContext<Token>) => Effect.Effect<Response, AIError>;
@@ -28,16 +28,14 @@ const STATUS = {
28
28
  };
29
29
  // fal accepts public URLs and data URIs; there is no provider file handle to forward.
30
30
  export const mediaUrl = (asset, name) => ProviderShared.mediaReference(asset, undefined, name).pipe(Effect.map((reference) => reference.value));
31
- export const protocol = (input) => {
32
- const decodeQueueStatus = MediaProtocol.decodeJson(input.id, input.name, QueueStatus);
33
- return MediaProtocol.queued({
34
- id: input.id,
35
- name: input.name,
31
+ export const protocol = (route, input) => {
32
+ const decodeQueueStatus = route.decodeJson(QueueStatus);
33
+ return MediaProtocol.queued(route, {
36
34
  token: Token,
37
35
  unsupported: input.unsupported,
38
36
  start: {
39
37
  body: { from: input.from },
40
- decode: MediaProtocol.decodeStarted(input.id, input.name, StartResponse, (value) => ({
38
+ decode: route.decodeStarted(StartResponse, (value) => ({
41
39
  token: {
42
40
  requestID: value.request_id,
43
41
  statusURL: value.status_url,
@@ -1,7 +1,7 @@
1
1
  import { Effect } from "effect";
2
2
  import { Media } from "../../media.js";
3
3
  import type { MediaProtocol } from "../../route/media-protocol.js";
4
- import type { AIError, ProviderID } from "../../schema/index.js";
4
+ import { type AIError, type ProviderID } from "../../schema/index.js";
5
5
  /** Owned bytes for multipart uploads; decodes `base64` sources and rejects remote sources. */
6
6
  export declare const inlineBytes: (route: string, asset: Media.Asset) => Effect.Effect<Uint8Array, AIError>;
7
7
  /** Copied because `BlobPart` requires a plain `ArrayBuffer`. */
@@ -15,4 +15,18 @@ export declare const dimensions: (size: string) => {
15
15
  export declare const refID: (asset: Media.Asset, provider: ProviderID) => string | undefined;
16
16
  /** Decode a provider's base64 output once into an owned `bytes` asset, sniffing the type when it is not declared. */
17
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;
18
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) => {
@@ -32,4 +34,24 @@ export const dimensions = (size) => {
32
34
  export const refID = (asset, provider) => asset.source.type === "ref" && asset.source.provider === provider ? asset.source.id : undefined;
33
35
  /** Decode a provider's base64 output once into an owned `bytes` asset, sniffing the type when it is not declared. */
34
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
+ };
35
57
  export * as MediaInput from "./media-input.js";
@@ -1,6 +1,7 @@
1
1
  import { Effect } from "effect";
2
2
  import { Media } from "../../media.js";
3
- import type { AIError, MediaUsage, ProviderID, ProviderMetadata } from "../../schema/index.js";
3
+ import type { MediaProtocol } from "../../route/media-protocol.js";
4
+ import type { AIError, MediaUsage, ProviderMetadata } from "../../schema/index.js";
4
5
  import { type SpeechEvent, type SpeechVoice } from "../../speech.js";
5
6
  export interface Audio {
6
7
  /** Appended in place: the route creates fresh state for each response through `initial`. */
@@ -36,9 +37,8 @@ export declare const pcm: (encoding: PcmEncoding, sampleRate: number | undefined
36
37
  };
37
38
  };
38
39
  export declare const sampleRate: (mediaType: string | undefined) => number | undefined;
39
- export declare const unsupportedFormat: (provider: ProviderID, route: string, message: string) => AIError;
40
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: string, state: Audio, output: {
41
+ export declare const finish: (route: MediaProtocol.Identity, state: Audio, output: {
42
42
  readonly mediaType: string | undefined;
43
43
  readonly info?: Media.Info;
44
44
  readonly usage?: MediaUsage;
@@ -1,9 +1,7 @@
1
1
  import { Effect } from "effect";
2
2
  import { Media } from "../../media.js";
3
- import { MediaProtocol } from "../../route/media-protocol.js";
4
3
  import { SpeechAudioDeltaEvent, SpeechFinishEvent, SpeechTimestampsEvent, } from "../../speech.js";
5
4
  import { concatBytes } from "../../utils/bytes.js";
6
- import { ProviderShared } from "../shared.js";
7
5
  /** Empty chunks (keep-alive records) emit nothing. */
8
6
  export const delta = (state, chunk) => {
9
7
  if (chunk.length === 0)
@@ -45,11 +43,10 @@ export const sampleRate = (mediaType) => {
45
43
  const rate = /rate=(\d+)/i.exec(mediaType ?? "")?.[1];
46
44
  return rate === undefined ? undefined : Number(rate);
47
45
  };
48
- export const unsupportedFormat = (provider, route, message) => ProviderShared.unsupportedOperation({ operation: "media.format", provider, route, message });
49
46
  /** A declared `mediaType` wins over sniffing: headerless PCM can start with bytes that look like an MPEG frame sync. */
50
47
  export const finish = (route, state, output) => {
51
48
  if (state.chunks.length === 0)
52
- return Effect.fail(MediaProtocol.frameError(route, `The provider returned no audio${output.detail === undefined ? "" : ` (${output.detail})`}`));
49
+ return Effect.fail(route.frameError(`The provider returned no audio${output.detail === undefined ? "" : ` (${output.detail})`}`));
53
50
  return Effect.succeed([
54
51
  SpeechFinishEvent.make({
55
52
  audio: Media.bytes(concatBytes(state.chunks), output.mediaType, { info: output.info }),
@@ -54,18 +54,31 @@ const MODEL_NAMES = [
54
54
  [/gemini/i, "gemini"],
55
55
  [/kimi/i, "moonshot"],
56
56
  ];
57
- // An explicit `sanitizer` wins, and `none` opts out. Otherwise the protocol's own default
58
- // applies (the Gemini API always uses Gemini's rules), then the model name selects the family's rules
59
- // so models reached through gateways and OpenAI-compatible endpoints get the same handling.
57
+ // Tool arguments are always a JSON object, and most providers reject a tool schema whose root does not
58
+ // declare `type: "object"`, such as `{}` or a bare `properties` map. Effect encodes an empty struct as
59
+ // `anyOf` object or array; every object matches its bare object branch, so that `anyOf` is dropped.
60
+ const objectRoot = (schema) => {
61
+ if (schema.type !== undefined)
62
+ return schema;
63
+ if (Array.isArray(schema.anyOf) &&
64
+ schema.anyOf.some((branch) => isRecord(branch) && branch.type === "object" && Object.keys(branch).length === 1))
65
+ return { type: "object", ...Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "anyOf")) };
66
+ return { type: "object", ...schema };
67
+ };
68
+ // Every tool schema gets an object root. Then an explicit `sanitizer` wins, and `none` opts out.
69
+ // Otherwise the protocol's own default applies (the Gemini API always uses Gemini's rules), then the
70
+ // model name selects the family's rules so models reached through gateways and OpenAI-compatible
71
+ // endpoints get the same handling.
60
72
  const modelCompatibility = (schema, model, protocolDefault) => {
73
+ const root = objectRoot(schema);
61
74
  switch (model.compatibility?.sanitizer ?? protocolDefault ?? MODEL_NAMES.find(([name]) => name.test(model.id))?.[1]) {
62
75
  case "gemini":
63
- return gemini(schema);
76
+ return gemini(root);
64
77
  case "moonshot":
65
- return moonshot(schema);
78
+ return moonshot(root);
66
79
  case "none":
67
80
  case undefined:
68
- return schema;
81
+ return root;
69
82
  }
70
83
  };
71
84
  export const ToolSchemaProjection = {
@@ -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>;
@@ -3,12 +3,10 @@ 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 { MediaInput } from "./utils/media-input.js";
9
- const ADAPTER = "xai-images";
10
- const NAME = "xAI Images";
11
- const PROVIDER = ProviderID.make("xai");
9
+ const route = MediaProtocol.identity({ id: "xai-images", name: "xAI Images", provider: "xai" });
12
10
  export const DEFAULT_BASE_URL = "https://api.x.ai/v1";
13
11
  export const PATH = "/images/generations";
14
12
  export const EDIT_PATH = "/images/edits";
@@ -34,7 +32,7 @@ const nativeOptions = (options) => {
34
32
  return { response_format: responseFormat, ...native };
35
33
  };
36
34
  const isEdit = (request) => (request.images?.length ?? 0) > 0;
37
- const reference = (asset) => ProviderShared.mediaReference(asset, PROVIDER, NAME).pipe(Effect.map((item) => item.type === "ref" ? { file_id: item.value } : { url: item.value, type: "image_url" }));
35
+ const reference = (asset) => ProviderShared.mediaReference(asset, route.provider, route.name).pipe(Effect.map((item) => item.type === "ref" ? { file_id: item.value } : { url: item.value, type: "image_url" }));
38
36
  const fromRequest = Effect.fn("XAIImages.fromRequest")(function* (request) {
39
37
  const references = yield* Effect.forEach(request.images ?? [], reference);
40
38
  return MediaProtocol.json(mergeJsonRecords({
@@ -50,23 +48,17 @@ const fromRequest = Effect.fn("XAIImages.fromRequest")(function* (request) {
50
48
  // ---------------------------------------------------------------------------
51
49
  // 6. Response decoding
52
50
  // ---------------------------------------------------------------------------
51
+ const decodeDocument = route.decodeJson(XAIImageResponse);
53
52
  const decodeResponse = Effect.fn("XAIImages.decodeResponse")(function* (response) {
54
- const output = yield* MediaProtocol.decodeJson(ADAPTER, NAME, XAIImageResponse)(response);
53
+ const output = yield* decodeDocument(response);
55
54
  const decoded = output.value;
56
- const images = yield* Effect.forEach(decoded.data, (item, index) => {
57
- const providerMetadata = item.revised_prompt === undefined || item.revised_prompt === null
55
+ const images = yield* Effect.forEach(decoded.data, (item, index) => MediaInput.imageOutput(output.invalid, `${route.name} result ${index}`, item, item.mime_type ?? undefined, {
56
+ providerMetadata: item.revised_prompt === undefined || item.revised_prompt === null
58
57
  ? undefined
59
- : { xai: { revisedPrompt: item.revised_prompt } };
60
- if (item.b64_json)
61
- return MediaInput.decodedAsset(output.invalid, `${NAME} result ${index}`, item.b64_json, item.mime_type ?? undefined, {
62
- providerMetadata,
63
- });
64
- if (item.url)
65
- return Effect.succeed(Media.url(item.url, { mediaType: item.mime_type ?? undefined, providerMetadata }));
66
- return Effect.fail(output.invalid(`${NAME} result ${index} has neither image data nor a URL`));
67
- });
58
+ : { xai: { revisedPrompt: item.revised_prompt } },
59
+ }));
68
60
  if (images.length === 0)
69
- return yield* output.invalid(`${NAME} returned no images`);
61
+ return yield* output.invalid(`${route.name} returned no images`);
70
62
  const usage = ProviderShared.isRecord(decoded.usage) ? decoded.usage : undefined;
71
63
  // xAI reports image counts rather than tokens, seconds, or credits; the raw record stays in provider metadata.
72
64
  return new ImageResponse({
@@ -77,20 +69,12 @@ const decodeResponse = Effect.fn("XAIImages.decodeResponse")(function* (response
77
69
  // ---------------------------------------------------------------------------
78
70
  // 7. Protocol and route
79
71
  // ---------------------------------------------------------------------------
80
- export const protocol = MediaProtocol.inline({
81
- id: ADAPTER,
82
- name: NAME,
72
+ export const protocol = MediaProtocol.inline(route, {
83
73
  unsupported: ["mask", "size", "seed", "format"],
84
74
  body: { from: fromRequest },
85
75
  response: { decode: decodeResponse },
86
76
  });
87
- export const model = (input) => ImageModel.fromRoute({
88
- id: ADAPTER,
89
- provider: PROVIDER,
90
- protocol,
91
- baseURL: DEFAULT_BASE_URL,
92
- path: ({ request }) => (isEdit(request) ? EDIT_PATH : PATH),
93
- }, input);
77
+ export const model = (input) => ImageModel.fromRoute({ protocol, baseURL: DEFAULT_BASE_URL, path: ({ request }) => (isEdit(request) ? EDIT_PATH : PATH) }, input);
94
78
  export const XAIImages = {
95
79
  protocol,
96
80
  model,