@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
@@ -1,15 +1,14 @@
1
1
  import { Effect, Schema, Stream } from "effect";
2
+ import { classifyProviderFailure } from "../provider-error.js";
2
3
  import { Framing } from "../route/framing.js";
3
4
  import { MediaProtocol } from "../route/media-protocol.js";
4
5
  import { MediaRoute } from "../route/media.js";
5
- import { ProviderID, mergeJsonRecords } from "../schema/index.js";
6
+ import { AIError, mergeJsonRecords } from "../schema/index.js";
6
7
  import { TranscriptionFinishEvent, TranscriptionModel, TranscriptionSegmentEvent, TranscriptionTextDeltaEvent, } from "../transcription.js";
7
8
  import { mediaTypeExtension } from "../utils/media-type.js";
8
9
  import { ProviderShared } from "./shared.js";
9
10
  import { MediaInput } from "./utils/media-input.js";
10
- const ADAPTER = "openai-transcription";
11
- const NAME = "OpenAI Transcription";
12
- const PROVIDER = ProviderID.make("openai");
11
+ const route = MediaProtocol.identity({ id: "openai-transcription", name: "OpenAI Transcription", provider: "openai" });
13
12
  export const DEFAULT_BASE_URL = "https://api.openai.com/v1";
14
13
  export const PATH = "/audio/transcriptions";
15
14
  // ---------------------------------------------------------------------------
@@ -27,6 +26,7 @@ const Usage = Schema.Union([
27
26
  input_tokens: Schema.optional(Schema.Number),
28
27
  output_tokens: Schema.optional(Schema.Number),
29
28
  total_tokens: Schema.optional(Schema.Number),
29
+ input_token_details: Schema.optional(Schema.Struct({ audio_tokens: Schema.optional(Schema.Number), text_tokens: Schema.optional(Schema.Number) })),
30
30
  }),
31
31
  Schema.Struct({ type: Schema.Literal("duration"), seconds: Schema.Number }),
32
32
  ]);
@@ -39,14 +39,23 @@ const transcriptFields = {
39
39
  words: Schema.optional(Schema.Array(Schema.Struct({ word: Schema.String, start: Schema.Number, end: Schema.Number }))),
40
40
  usage: Schema.optional(Usage),
41
41
  };
42
+ /** OpenAI may add stream event types; frames outside `EVENT_TYPES` are ignored. */
43
+ const EventType = Schema.Struct({ type: Schema.String });
42
44
  const Event = Schema.Union([
43
45
  Schema.Struct({ type: Schema.Literal("transcript.text.delta"), delta: Schema.String }),
44
46
  Schema.Struct({ type: Schema.Literal("transcript.text.segment"), ...Segment.fields }),
45
47
  Schema.Struct({ type: Schema.Literal("transcript.text.done"), ...transcriptFields }),
48
+ Schema.Struct({
49
+ type: Schema.Literal("error"),
50
+ message: Schema.optional(Schema.String),
51
+ error: Schema.optional(Schema.Struct({ message: Schema.optional(Schema.String) })),
52
+ }),
46
53
  ]);
54
+ const EVENT_TYPES = new Set(["transcript.text.delta", "transcript.text.segment", "transcript.text.done", "error"]);
47
55
  const Transcript = Schema.Struct(transcriptFields);
48
- const decodeEvent = MediaProtocol.decodeFrame(ADAPTER, NAME, Event);
49
- const decodeTranscript = MediaProtocol.decodeFrame(ADAPTER, NAME, Transcript);
56
+ const decodeEventType = route.decodeFrame(EventType);
57
+ const decodeEvent = route.decodeFrame(Event);
58
+ const decodeTranscript = route.decodeFrame(Transcript);
50
59
  const TRANSCRIBE = { stream: true, timestamps: [], diarize: false, languageField: "language" };
51
60
  const capabilities = (model) => {
52
61
  if (model.startsWith("whisper"))
@@ -58,20 +67,19 @@ const capabilities = (model) => {
58
67
  return { ...TRANSCRIBE, languageField: "languages" };
59
68
  return TRANSCRIBE;
60
69
  };
61
- const unsupported = (operation, message) => Effect.fail(ProviderShared.unsupportedOperation({ operation, provider: PROVIDER, route: ADAPTER, message }));
70
+ /** whisper-1 ignores `stream`, so its `stream` mode sends a plain request and emits only `finish`. */
71
+ const streamsEvents = (request) => request.mode === "stream" && capabilities(request.model.id).stream;
62
72
  const validate = (request, model) => {
63
73
  const id = request.model.id;
64
- if (request.mode === "stream" && !model.stream)
65
- return unsupported("media.stream", `${id} does not stream; use Transcription.generate`);
66
74
  if (request.diarize === true && !model.diarize)
67
- return unsupported("media.diarize", `${id} does not diarize; use gpt-4o-transcribe-diarize`);
75
+ return Effect.fail(route.unsupported("media.diarize", `${id} does not diarize; use gpt-4o-transcribe-diarize`));
68
76
  if (request.prompt !== undefined && model.diarize)
69
- return unsupported("media.prompt", `${id} does not accept a prompt`);
77
+ return Effect.fail(route.unsupported("media.prompt", `${id} does not accept a prompt`));
70
78
  if (request.timestamps === undefined ||
71
79
  request.timestamps === "none" ||
72
80
  model.timestamps.includes(request.timestamps))
73
81
  return Effect.void;
74
- return unsupported("media.timestamps", `${id} does not return ${request.timestamps} timestamps`);
82
+ return Effect.fail(route.unsupported("media.timestamps", `${id} does not return ${request.timestamps} timestamps`));
75
83
  };
76
84
  const RESERVED_FORM_FIELDS = new Set([
77
85
  "file",
@@ -82,26 +90,22 @@ const RESERVED_FORM_FIELDS = new Set([
82
90
  "timestamp_granularities",
83
91
  "stream",
84
92
  ]);
85
- const appendField = (form, key, value) => {
86
- if (Array.isArray(value))
87
- return value.forEach((item) => form.append(`${key}[]`, String(item)));
88
- form.append(key, typeof value === "object" && value !== null ? ProviderShared.encodeJson(value) : String(value));
89
- };
90
93
  const fromRequest = Effect.fn("OpenAITranscription.fromRequest")(function* (request) {
91
94
  const model = capabilities(request.model.id);
92
95
  yield* validate(request, model);
93
96
  // The API detects the audio format from the upload's filename extension.
94
97
  const extension = mediaTypeExtension(request.audio.mediaType);
95
98
  if (extension === undefined)
96
- return yield* ProviderShared.invalidRequest(`${NAME} cannot name a ${request.audio.mediaType} upload; send mp3, mp4, m4a, wav, webm, ogg, or flac audio`);
97
- const audio = yield* MediaInput.inlineBytes(ADAPTER, request.audio);
99
+ return yield* ProviderShared.invalidRequest(`${route.name} cannot name a ${request.audio.mediaType} upload; send mp3, mp4, m4a, wav, webm, ogg, or flac audio`);
100
+ const audio = yield* MediaInput.inlineBytes(route.id, request.audio);
98
101
  const responseFormat = model.diarize
99
102
  ? "diarized_json"
100
103
  : request.timestamps === undefined || request.timestamps === "none"
101
104
  ? undefined
102
105
  : "verbose_json";
103
- const native = Object.entries(mergeJsonRecords(request.providerOptions, request.http?.body) ?? {}).filter(([key]) => !RESERVED_FORM_FIELDS.has(key));
104
- const fields = mergeJsonRecords({
106
+ const form = new FormData();
107
+ form.append("file", MediaInput.blob(audio, request.audio.mediaType), `audio.${extension}`);
108
+ MediaInput.appendFields(form, {
105
109
  model: request.model.id,
106
110
  language: model.languageField === "language" ? request.language : undefined,
107
111
  languages: model.languageField === "languages" && request.language !== undefined ? [request.language] : undefined,
@@ -110,11 +114,12 @@ const fromRequest = Effect.fn("OpenAITranscription.fromRequest")(function* (requ
110
114
  timestamp_granularities: responseFormat === "verbose_json" ? [request.timestamps] : undefined,
111
115
  // Diarizing audio longer than 30 seconds requires a chunking strategy.
112
116
  chunking_strategy: model.diarize ? "auto" : undefined,
113
- stream: request.mode === "stream" ? true : undefined,
114
- }, Object.fromEntries(native));
115
- const form = new FormData();
116
- form.append("file", MediaInput.blob(audio, request.audio.mediaType), `audio.${extension}`);
117
- Object.entries(fields ?? {}).forEach(([key, value]) => appendField(form, key, value));
117
+ stream: streamsEvents(request) ? true : undefined,
118
+ }, {
119
+ overlay: mergeJsonRecords(request.providerOptions, request.http?.body),
120
+ reserved: RESERVED_FORM_FIELDS,
121
+ repeatArrays: true,
122
+ });
118
123
  return MediaProtocol.multipart(form);
119
124
  });
120
125
  // ---------------------------------------------------------------------------
@@ -127,7 +132,16 @@ const segment = (value) => ({
127
132
  speaker: value.speaker,
128
133
  });
129
134
  const onEvent = Effect.fn("OpenAITranscription.onEvent")(function* (state, frame) {
135
+ if (!EVENT_TYPES.has((yield* decodeEventType(frame)).type))
136
+ return [state, []];
130
137
  const event = yield* decodeEvent(frame);
138
+ if (event.type === "error")
139
+ return yield* new AIError({
140
+ reason: classifyProviderFailure({
141
+ message: `${route.name} stream failed: ${event.message ?? event.error?.message ?? "unknown error"}`,
142
+ rawBody: frame,
143
+ }),
144
+ });
131
145
  if (event.type === "transcript.text.done")
132
146
  return [{ ...state, transcript: event }, []];
133
147
  if (event.type === "transcript.text.delta")
@@ -155,7 +169,7 @@ const usage = (value) => {
155
169
  const finish = (state) => {
156
170
  const transcript = state.transcript;
157
171
  if (transcript === undefined)
158
- return Effect.fail(MediaProtocol.incomplete(ADAPTER));
172
+ return Effect.fail(route.incomplete());
159
173
  const segments = transcript.segments?.map(segment) ?? state.segments;
160
174
  return Effect.succeed([
161
175
  TranscriptionFinishEvent.make({
@@ -171,19 +185,17 @@ const finish = (state) => {
171
185
  // ---------------------------------------------------------------------------
172
186
  // 7. Protocol and route
173
187
  // ---------------------------------------------------------------------------
174
- export const protocol = MediaProtocol.stream({
175
- id: ADAPTER,
176
- name: NAME,
188
+ export const protocol = MediaProtocol.stream(route, {
177
189
  unsupported: ["speakers"],
178
190
  body: { from: fromRequest },
179
- frames: (bytes, context) => context.request.mode === "stream"
191
+ frames: (bytes, context) => streamsEvents(context.request)
180
192
  ? Framing.sse.frame(bytes)
181
193
  : Framing.document.frame(bytes).pipe(Stream.map((document) => ({ document }))),
182
194
  initial: () => ({ segments: [] }),
183
195
  step,
184
196
  finish,
185
197
  });
186
- export const model = (input) => TranscriptionModel.fromRoute({ id: ADAPTER, provider: PROVIDER, protocol, baseURL: DEFAULT_BASE_URL, path: PATH }, input);
198
+ export const model = (input) => TranscriptionModel.fromRoute({ protocol, baseURL: DEFAULT_BASE_URL, path: PATH }, input);
187
199
  export const OpenAITranscription = {
188
200
  protocol,
189
201
  model,
@@ -3,11 +3,9 @@ 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
- const ADAPTER = "replicate-images";
9
- const NAME = "Replicate";
10
- const PROVIDER = ProviderID.make("replicate");
8
+ const route = MediaProtocol.identity({ id: "replicate-images", name: "Replicate", provider: "replicate" });
11
9
  export const DEFAULT_BASE_URL = "https://api.replicate.com";
12
10
  const OUTPUT_RETENTION = Duration.hours(1);
13
11
  const MAX_DATA_URL_BYTES = 256 * 1024;
@@ -50,8 +48,8 @@ const inlineSize = (source) => {
50
48
  };
51
49
  const fileInput = (asset) => {
52
50
  if (inlineSize(asset.source) > MAX_DATA_URL_BYTES)
53
- return Effect.fail(ProviderShared.invalidRequest(`${NAME} data URL inputs are limited to 256 KB; pass a larger file by https URL`));
54
- return ProviderShared.mediaReference(asset, undefined, NAME).pipe(Effect.map((reference) => reference.value));
51
+ return Effect.fail(ProviderShared.invalidRequest(`${route.name} data URL inputs are limited to 256 KB; pass a larger file by https URL`));
52
+ return ProviderShared.mediaReference(asset, undefined, route.name).pipe(Effect.map((reference) => reference.value));
55
53
  };
56
54
  const inputValue = (value) => {
57
55
  if (value instanceof Media.Asset)
@@ -69,7 +67,7 @@ const fromRequest = Effect.fn("ReplicateImages.fromRequest")(function* (request)
69
67
  // ---------------------------------------------------------------------------
70
68
  // 6. Response decoding
71
69
  // ---------------------------------------------------------------------------
72
- const decodePrediction = MediaProtocol.decodeJson(ADAPTER, NAME, Prediction);
70
+ const decodePrediction = route.decodeJson(Prediction);
73
71
  const decodeStart = Effect.fn("ReplicateImages.decodeStart")(function* (response) {
74
72
  const output = yield* decodePrediction(response);
75
73
  const prediction = output.value;
@@ -88,16 +86,16 @@ const decodeResult = Effect.fn("ReplicateImages.decodeResult")(function* (respon
88
86
  const prediction = output.value;
89
87
  const status = yield* MediaProtocol.status(STATUS, prediction.status, output);
90
88
  if (status === "failed" || status === "cancelled" || status === "expired")
91
- return yield* output.ended(status, `${NAME} prediction ${context.token.id} ${prediction.status}${typeof prediction.error === "string" ? `: ${prediction.error}` : ""}`);
89
+ return yield* output.ended(status, `${route.name} prediction ${context.token.id} ${prediction.status}${typeof prediction.error === "string" ? `: ${prediction.error}` : ""}`);
92
90
  if (status !== "completed")
93
- return yield* output.invalid(`${NAME} prediction ${context.token.id} has not finished`);
91
+ return yield* output.pending(context.token.id);
94
92
  if (prediction.data_removed === true)
95
- return yield* output.ended("expired", `${NAME} removed the output of prediction ${context.token.id}`);
93
+ return yield* output.ended("expired", `${route.name} removed the output of prediction ${context.token.id}`);
96
94
  if (!isOutput(prediction.output))
97
- return yield* output.invalid(`${NAME} prediction ${context.token.id} returned output that is not image URLs`);
95
+ return yield* output.invalid(`${route.name} prediction ${context.token.id} returned output that is not image URLs`);
98
96
  const urls = typeof prediction.output === "string" ? [prediction.output] : prediction.output;
99
97
  if (urls.length === 0)
100
- return yield* output.invalid(`${NAME} prediction ${context.token.id} returned no images`);
98
+ return yield* output.invalid(`${route.name} prediction ${context.token.id} returned no images`);
101
99
  const predictTime = prediction.metrics?.predict_time ?? undefined;
102
100
  const completedAt = prediction.completed_at ?? undefined;
103
101
  const expiresAt = completedAt === undefined ? undefined : Date.parse(completedAt) + Duration.toMillis(OUTPUT_RETENTION);
@@ -110,9 +108,7 @@ const decodeResult = Effect.fn("ReplicateImages.decodeResult")(function* (respon
110
108
  // ---------------------------------------------------------------------------
111
109
  // 7. Protocol and route
112
110
  // ---------------------------------------------------------------------------
113
- export const protocol = MediaProtocol.queued({
114
- id: ADAPTER,
115
- name: NAME,
111
+ export const protocol = MediaProtocol.queued(route, {
116
112
  token: Token,
117
113
  unsupported: ["images", "mask", "n", "size", "aspectRatio", "seed", "format"],
118
114
  start: { body: { from: fromRequest }, decode: decodeStart },
@@ -121,8 +117,6 @@ export const protocol = MediaProtocol.queued({
121
117
  cancel: { method: "POST", path: (token) => token.cancelURL },
122
118
  });
123
119
  export const model = (input) => ImageModel.fromRoute({
124
- id: ADAPTER,
125
- provider: PROVIDER,
126
120
  protocol,
127
121
  baseURL: DEFAULT_BASE_URL,
128
122
  path: ({ request }) => isOfficial(request.model.id) ? `/v1/models/${request.model.id}/predictions` : "/v1/predictions",
@@ -1,6 +1,7 @@
1
1
  import { Schema } from "effect";
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
  import { VideoModel, VideoResponse, type VideoRequestFor } from "../video.js";
5
6
  export declare const DEFAULT_BASE_URL = "https://api.dev.runwayml.com/v1";
6
7
  /** Every Runway request must pin the API version. */
@@ -9,7 +10,6 @@ export declare const TEXT_TO_VIDEO_PATH = "/text_to_video";
9
10
  export declare const IMAGE_TO_VIDEO_PATH = "/image_to_video";
10
11
  export declare const VIDEO_TO_VIDEO_PATH = "/video_to_video";
11
12
  export declare const TASKS_PATH = "/tasks";
12
- export type RunwayVideoString<Known extends string> = Known | (string & {});
13
13
  /**
14
14
  * Provider-native options. Common fields lower to Runway's names: `aspectRatio` → `ratio` (Runway expects pixel
15
15
  * ratios such as `1280:720` for most models), `durationSeconds` → `duration`, `audio`, `negativePrompt`,
@@ -17,9 +17,9 @@ export type RunwayVideoString<Known extends string> = Known | (string & {});
17
17
  */
18
18
  export type RunwayVideoOptions = {
19
19
  readonly contentModeration?: {
20
- readonly publicFigureThreshold?: RunwayVideoString<"auto" | "low">;
20
+ readonly publicFigureThreshold?: OpenString<"auto" | "low">;
21
21
  };
22
- readonly outputFormat?: RunwayVideoString<"mp4" | "prores" | "png_sequence">;
22
+ readonly outputFormat?: OpenString<"mp4" | "prores" | "png_sequence">;
23
23
  } & Record<string, unknown>;
24
24
  export type Request = VideoRequestFor<RunwayVideoOptions>;
25
25
  export declare const Token: Schema.Struct<{
@@ -2,12 +2,10 @@ import { Duration, 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
7
  import { ProviderShared, optionalArray, optionalNull } from "./shared.js";
8
- const ADAPTER = "runway-video";
9
- const NAME = "Runway";
10
- const PROVIDER = ProviderID.make("runway");
8
+ const route = MediaProtocol.identity({ id: "runway-video", name: "Runway", provider: "runway" });
11
9
  export const DEFAULT_BASE_URL = "https://api.dev.runwayml.com/v1";
12
10
  /** Every Runway request must pin the API version. */
13
11
  export const API_VERSION = "2024-11-06";
@@ -44,7 +42,7 @@ const STATUS = {
44
42
  // 5. Request body construction
45
43
  // ---------------------------------------------------------------------------
46
44
  // Runway accepts HTTPS URLs, `runway://` upload URIs, and data URIs, all as one string.
47
- const mediaUri = (asset) => ProviderShared.mediaReference(asset, PROVIDER, NAME).pipe(Effect.map((reference) => reference.value));
45
+ const mediaUri = (asset) => ProviderShared.mediaReference(asset, route.provider, route.name).pipe(Effect.map((reference) => reference.value));
48
46
  const fromRequest = Effect.fn("RunwayVideo.fromRequest")(function* (request) {
49
47
  const first = request.frames?.first === undefined ? undefined : yield* mediaUri(request.frames.first);
50
48
  const last = request.frames?.last === undefined ? undefined : yield* mediaUri(request.frames.last);
@@ -71,11 +69,11 @@ const fromRequest = Effect.fn("RunwayVideo.fromRequest")(function* (request) {
71
69
  // ---------------------------------------------------------------------------
72
70
  // 6. Response decoding
73
71
  // ---------------------------------------------------------------------------
74
- const decodeStart = MediaProtocol.decodeStarted(ADAPTER, NAME, StartResponse, (value) => ({
72
+ const decodeStart = route.decodeStarted(StartResponse, (value) => ({
75
73
  token: { taskID: value.id },
76
74
  snapshot: { id: value.id, status: "queued" },
77
75
  }));
78
- const decodeTask = MediaProtocol.decodeJson(ADAPTER, NAME, Task);
76
+ const decodeTask = route.decodeJson(Task);
79
77
  const decodeStatus = Effect.fn("RunwayVideo.decodeStatus")(function* (response, context) {
80
78
  const output = yield* decodeTask(response);
81
79
  const status = yield* MediaProtocol.status(STATUS, output.value.status, output);
@@ -87,19 +85,19 @@ const decodeResult = Effect.fn("RunwayVideo.decodeResult")(function* (response,
87
85
  const status = yield* MediaProtocol.status(STATUS, task.status, output);
88
86
  if (status === "failed") {
89
87
  const code = task.failureCode ?? undefined;
90
- const message = `${NAME} task failed${code === undefined ? "" : ` (${code})`}${task.failure ? `: ${task.failure}` : ""}`;
88
+ const message = `${route.name} task failed${code === undefined ? "" : ` (${code})`}${task.failure ? `: ${task.failure}` : ""}`;
91
89
  // Runway failure codes are dotted paths; every moderation outcome carries a SAFETY segment.
92
90
  if (code !== undefined && /(^|\.)SAFETY(\.|$)/.test(code))
93
91
  return yield* output.contentPolicy(message);
94
92
  return yield* output.ended("failed", message);
95
93
  }
96
94
  if (status === "cancelled")
97
- return yield* output.ended("cancelled", `${NAME} task ${context.token.taskID} was cancelled`);
95
+ return yield* output.ended("cancelled", `${route.name} task ${context.token.taskID} was cancelled`);
98
96
  if (status !== "completed")
99
- return yield* output.invalid(`${NAME} task ${context.token.taskID} has not finished`);
97
+ return yield* output.pending(context.token.taskID);
100
98
  const urls = task.output ?? [];
101
99
  if (urls.length === 0)
102
- return yield* output.invalid(`${NAME} task succeeded without any output`);
100
+ return yield* output.invalid(`${route.name} task succeeded without any output`);
103
101
  return new VideoResponse({
104
102
  videos: yield* Effect.forEach(urls, (url) => MediaProtocol.expiringUrl(url, OUTPUT_RETENTION, { mediaType: "video/mp4" })),
105
103
  usage: task.cost === undefined ? undefined : { type: "credits", credits: task.cost.credits },
@@ -115,15 +113,13 @@ const decodeResult = Effect.fn("RunwayVideo.decodeResult")(function* (response,
115
113
  // 7. Protocol and route
116
114
  // ---------------------------------------------------------------------------
117
115
  const taskPath = (token) => `${TASKS_PATH}/${token.taskID}`;
118
- export const protocol = MediaProtocol.queued({
119
- id: ADAPTER,
120
- name: NAME,
116
+ export const protocol = MediaProtocol.queued(route, {
121
117
  token: Token,
122
118
  unsupported: ["n"],
123
119
  start: { body: { from: fromRequest }, decode: decodeStart },
124
120
  status: { path: taskPath, decode: decodeStatus },
125
121
  result: { path: taskPath, decode: decodeResult },
126
- cancel: { method: "DELETE", path: taskPath },
122
+ cancel: { method: "DELETE", path: taskPath, activeOnly: true },
127
123
  });
128
124
  const startPath = (request) => {
129
125
  if (request.video !== undefined)
@@ -133,8 +129,6 @@ const startPath = (request) => {
133
129
  return TEXT_TO_VIDEO_PATH;
134
130
  };
135
131
  export const model = (input) => VideoModel.fromRoute({
136
- id: ADAPTER,
137
- provider: PROVIDER,
138
132
  protocol,
139
133
  baseURL: DEFAULT_BASE_URL,
140
134
  headers: { "X-Runway-Version": API_VERSION },
@@ -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
  /**
@@ -60,6 +58,12 @@ export declare const subtractTokens: (total: number | undefined, subtrahend: num
60
58
  * (e.g. Anthropic, whose `input_tokens` is already non-cached only).
61
59
  */
62
60
  export declare const sumTokens: (...values: ReadonlyArray<number | undefined>) => number | undefined;
61
+ /**
62
+ * Caps an explicit thinking budget at half the output limit. Thinking counts against the output limit, so a budget
63
+ * near it leaves the answer, a tool call, or a summary without room. Smaller budgets, special values such as `-1` and
64
+ * `0`, and requests without an output limit pass through unchanged.
65
+ */
66
+ export declare const fitThinkingBudget: (budget: number, maxTokens: number | undefined, minimum?: number) => number;
63
67
  export declare const eventError: (route: string, message: string, body?: string, cause?: unknown) => AIError;
64
68
  export declare const parseJson: (route: string, input: string, message: string) => Effect.Effect<unknown, AIError, never>;
65
69
  /**
@@ -131,18 +135,8 @@ export declare const mediaReference: (asset: Media.Asset, provider: ProviderID |
131
135
  * `uri`; the declared `mime` wins over any data-URL prefix so tool authors control the type the model sees.
132
136
  */
133
137
  export declare const toolFileMedia: (item: Tool.FileContent) => MediaPart;
134
- export declare const trimBaseUrl: (value: string) => string;
135
138
  export declare const toolResultText: (part: ToolResultPart) => string;
136
139
  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
140
  /**
147
141
  * Canonical invalid-request constructor shared by protocol lowering.
148
142
  */
@@ -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));
@@ -78,6 +75,12 @@ export const sumTokens = (...values) => {
78
75
  return undefined;
79
76
  return values.reduce((acc, value) => acc + (value ?? 0), 0);
80
77
  };
78
+ /**
79
+ * Caps an explicit thinking budget at half the output limit. Thinking counts against the output limit, so a budget
80
+ * near it leaves the answer, a tool call, or a summary without room. Smaller budgets, special values such as `-1` and
81
+ * `0`, and requests without an output limit pass through unchanged.
82
+ */
83
+ export const fitThinkingBudget = (budget, maxTokens, minimum = 1) => maxTokens === undefined || budget <= maxTokens / 2 ? budget : Math.max(minimum, Math.floor(maxTokens / 2));
81
84
  export const eventError = (route, message, body, cause) => new AIError({
82
85
  reason: new InvalidProviderOutputError({ route, message, body, cause }),
83
86
  });
@@ -149,7 +152,8 @@ export const mediaReference = (asset, provider, label) => {
149
152
  if (provider !== undefined && asset.source.type === "ref" && asset.source.provider === provider)
150
153
  return Effect.succeed({ type: "ref", value: asset.source.id });
151
154
  const accepted = provider === undefined ? "" : `, and ${provider} references`;
152
- return Effect.fail(invalidRequest(`${label} accepts inline bytes, data URLs, http(s) URLs${accepted}`));
155
+ const got = asset.source.type === "ref" ? `; got ${asset.source.provider}:${asset.source.id}` : "";
156
+ return Effect.fail(invalidRequest(`${label} accepts inline bytes, data URLs, http(s) URLs${accepted}${got}`));
153
157
  };
154
158
  /**
155
159
  * Lift a tool-result file into a `MediaPart`. Tool files carry either a data URL, an `http(s)` URL, or raw base64 in
@@ -164,7 +168,6 @@ export const toolFileMedia = (item) => {
164
168
  : Media.base64(item.uri, item.mime);
165
169
  return Message.media(asset, { filename: item.name });
166
170
  };
167
- export const trimBaseUrl = (value) => value.replace(/\/+$/, "");
168
171
  export const toolResultText = (part) => {
169
172
  if (part.result.type === "text")
170
173
  return String(part.result.value);
@@ -189,38 +192,6 @@ export const errorText = (error) => {
189
192
  return "undefined";
190
193
  return "Unknown stream error";
191
194
  };
192
- /**
193
- * `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
194
- * decoder, optionally filters named events, and drops empty events and known
195
- * keepalives that proxies send as data. `[DONE]` is dropped by default or
196
- * retained for protocols that use it as their stream boundary. Retry control events are ignored without
197
- * interrupting the stream. Decoder failures become provider output errors so
198
- * the public error channel stays `AIError`.
199
- */
200
- export const sseFraming = (bytes, events, includeDone = false) => bytes.pipe(Stream.decodeText(), Stream.mapAccumEffect(() => {
201
- const output = [];
202
- return {
203
- output,
204
- parser: Sse.makeParser((event) => {
205
- if (event._tag === "Event")
206
- output.push(event);
207
- }),
208
- };
209
- }, (state, chunk) => Effect.gen(function* () {
210
- const error = state.parser.feed(chunk);
211
- if (error)
212
- return yield* eventError("sse", error.message, chunk, error);
213
- return [state, state.output.splice(0)];
214
- })), Stream.filter((event) => (events === undefined || events.has(event.event)) &&
215
- event.data.length > 0 &&
216
- // Some OpenAI-compatible proxies serialize an empty flush as a bare
217
- // `data: null`, between events or after `[DONE]`. No protocol has a
218
- // null event, so it carries nothing and must not abort the stream.
219
- event.data !== "null" &&
220
- // Vertex AI partner models (e.g. `xai/grok-4.6`) send their SSE
221
- // keepalive comment as `data: : keepalive` while reasoning.
222
- event.data !== ": keepalive" &&
223
- (event.data !== "[DONE]" || includeDone || (events !== undefined && event.event !== "message"))), Stream.map((event) => event.data));
224
195
  /**
225
196
  * Canonical invalid-request constructor shared by protocol lowering.
226
197
  */
@@ -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;