@opencode/ai 2.0.18 → 2.0.20

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 (94) hide show
  1. package/README.md +24 -12
  2. package/dist/cache-policy.js +7 -0
  3. package/dist/experimental/evaluation.d.ts +7 -0
  4. package/dist/experimental/evaluation.js +2 -0
  5. package/dist/experimental/system-one.js +10 -7
  6. package/dist/generation.d.ts +1 -1
  7. package/dist/generation.js +23 -18
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/llm.d.ts +1 -1
  11. package/dist/promise.d.ts +3 -3
  12. package/dist/promise.js +4 -3
  13. package/dist/protocols/alibaba-chat.d.ts +8 -2
  14. package/dist/protocols/alibaba-responses.d.ts +14 -14
  15. package/dist/protocols/anthropic-messages.js +2 -15
  16. package/dist/protocols/bedrock-converse.d.ts +1 -1
  17. package/dist/protocols/bedrock-converse.js +38 -12
  18. package/dist/protocols/deepgram-speech.js +9 -7
  19. package/dist/protocols/deepgram-transcription.js +4 -10
  20. package/dist/protocols/elevenlabs-transcription.d.ts +28 -0
  21. package/dist/protocols/elevenlabs-transcription.js +137 -0
  22. package/dist/protocols/gemini.js +1 -10
  23. package/dist/protocols/google-speech.js +9 -2
  24. package/dist/protocols/google-transcription.js +4 -0
  25. package/dist/protocols/google-video.js +11 -2
  26. package/dist/protocols/meta-messages.d.ts +5 -1
  27. package/dist/protocols/meta-messages.js +11 -4
  28. package/dist/protocols/meta-responses.d.ts +16 -16
  29. package/dist/protocols/mistral-chat.d.ts +6 -0
  30. package/dist/protocols/mistral-chat.js +8 -5
  31. package/dist/protocols/open-responses.d.ts +31 -31
  32. package/dist/protocols/openai-chat.d.ts +53 -10
  33. package/dist/protocols/openai-chat.js +21 -8
  34. package/dist/protocols/openai-compatible-chat.d.ts +7 -2
  35. package/dist/protocols/openai-compatible-responses.d.ts +2 -2
  36. package/dist/protocols/openai-responses.d.ts +22 -22
  37. package/dist/protocols/openai-speech.js +6 -1
  38. package/dist/protocols/openai-transcription.js +1 -1
  39. package/dist/protocols/runway-video.js +2 -1
  40. package/dist/protocols/utils/claude-model.d.ts +8 -0
  41. package/dist/protocols/utils/claude-model.js +15 -0
  42. package/dist/protocols/utils/gemini-generate-content.d.ts +6 -0
  43. package/dist/protocols/utils/gemini-generate-content.js +33 -0
  44. package/dist/protocols/utils/media-input.d.ts +4 -3
  45. package/dist/protocols/utils/media-input.js +5 -4
  46. package/dist/protocols/utils/speaker-turns.d.ts +3 -0
  47. package/dist/protocols/utils/speaker-turns.js +9 -0
  48. package/dist/protocols/utils/speech-stream.d.ts +1 -0
  49. package/dist/protocols/utils/speech-stream.js +1 -0
  50. package/dist/protocols/utils/tool-stream.d.ts +4 -3
  51. package/dist/protocols/utils/tool-stream.js +1 -1
  52. package/dist/protocols/xai-responses.d.ts +14 -14
  53. package/dist/protocols/xai-video.js +7 -1
  54. package/dist/protocols/zai-chat.d.ts +8 -2
  55. package/dist/provider-error.d.ts +6 -0
  56. package/dist/provider-error.js +65 -2
  57. package/dist/providers/alibaba.d.ts +9 -4
  58. package/dist/providers/amazon-bedrock-mantle.d.ts +9 -4
  59. package/dist/providers/anthropic-compatible.js +3 -2
  60. package/dist/providers/azure.d.ts +11 -6
  61. package/dist/providers/baseten.d.ts +14 -4
  62. package/dist/providers/cerebras.d.ts +14 -4
  63. package/dist/providers/cloudflare-ai-gateway.d.ts +18 -8
  64. package/dist/providers/cloudflare-workers-ai.d.ts +14 -4
  65. package/dist/providers/deepinfra.d.ts +14 -4
  66. package/dist/providers/deepseek.d.ts +14 -4
  67. package/dist/providers/elevenlabs.d.ts +5 -0
  68. package/dist/providers/elevenlabs.js +4 -0
  69. package/dist/providers/fireworks.d.ts +14 -4
  70. package/dist/providers/google-vertex-chat.d.ts +7 -2
  71. package/dist/providers/google-vertex-responses.d.ts +2 -2
  72. package/dist/providers/groq.d.ts +15 -4
  73. package/dist/providers/meta.d.ts +14 -5
  74. package/dist/providers/minimax.d.ts +9 -4
  75. package/dist/providers/moonshot.d.ts +9 -4
  76. package/dist/providers/openai-compatible-responses.d.ts +2 -2
  77. package/dist/providers/openai-compatible.d.ts +7 -2
  78. package/dist/providers/openai.d.ts +9 -4
  79. package/dist/providers/openrouter.d.ts +27 -6
  80. package/dist/providers/togetherai.d.ts +14 -4
  81. package/dist/providers/xai.d.ts +7 -2
  82. package/dist/providers/xai.js +3 -3
  83. package/dist/providers/zai-coding-plan.d.ts +9 -4
  84. package/dist/providers/zai.d.ts +7 -2
  85. package/dist/route/client.d.ts +1 -1
  86. package/dist/route/executor.js +12 -8
  87. package/dist/route/media-protocol.d.ts +13 -3
  88. package/dist/route/media-protocol.js +16 -6
  89. package/dist/route/media.js +18 -4
  90. package/dist/schema/events.d.ts +30 -30
  91. package/dist/testing.d.ts +8 -8
  92. package/dist/transcription.d.ts +1 -1
  93. package/dist/transcription.js +1 -1
  94. package/package.json +3 -3
package/README.md CHANGED
@@ -129,8 +129,9 @@ VercelAIGateway.configure().experimental.evaluation("typesafe-ai/jev")
129
129
 
130
130
  OpenRouter reads `OPENROUTER_API_KEY`. Vercel reads `AI_GATEWAY_API_KEY`, then `VERCEL_OIDC_TOKEN`.
131
131
  The common API uses `boolean`; System One routes lower it to native `noul`.
132
- Choice and score confidence plus score legends remain available in provider metadata, and the
133
- provider's rounded probabilities are returned unchanged.
132
+ Choice and score answers include `confidence` when the provider returns it, such as
133
+ `response.answers.department.confidence`. Score legends remain available in provider metadata, and
134
+ the provider's rounded probabilities are returned unchanged.
134
135
 
135
136
  ## Alibaba Cloud Model Studio
136
137
 
@@ -752,7 +753,10 @@ const events = Video.stream({ model: Runway.configure({ apiKey }).video("gen4.5"
752
753
 
753
754
  Status polls, result fetches, cancels, and asset downloads all run through the same request executor with the route's
754
755
  auth. `Generation.await` and `Generation.events` fail with a
755
- `Timeout` reason when `poll.timeout` (default 10 minutes) elapses. Failed,
756
+ `Timeout` reason when `poll.timeout` (default 10 minutes) elapses. Status polls and result fetches retry transient
757
+ failures (rate limits, provider 5xx, network errors) with backoff that honors `retry-after`, always within
758
+ `poll.timeout`; submits and cancels never retry. Interrupting a wait (or aborting its `signal`) does not cancel the
759
+ provider job, which keeps running and billing: call `cancel()` to stop it. Failed,
756
760
  cancelled, and expired generations fail typed with the provider's terminal document on `reason.body`; moderation
757
761
  outcomes (Veo `raiMediaFilteredReasons`, xAI `respect_moderation`, Runway `SAFETY.*` codes) surface as `notices` when
758
762
  a video is still returned and as a `ContentPolicy` reason when nothing is.
@@ -773,7 +777,9 @@ Provider notes:
773
777
  The promise client exposes the same surface: `ai.video.start(...)` resolves to a handle with `await`, `events`,
774
778
  `result`, `refresh`, `cancel`, and `token`; `ai.video.generate`, `ai.video.resume(model, token)`, and
775
779
  `ai.video.stream` mirror the Effect API. The handle's `status` and `progress` are a snapshot from when it was
776
- created; `refresh()` resolves to a new handle.
780
+ created; `refresh()` resolves to a new handle. Every promise method and stream accepts `{ signal }`: like `fetch`,
781
+ aborting rejects the Promise or throws from the `for await` loop with `signal.reason` (an `AbortError` `DOMException`
782
+ unless `abort(reason)` passed one), while `break` stops a stream without throwing.
777
783
 
778
784
  ```ts
779
785
  import { ai } from "@opencode/ai/promise"
@@ -871,11 +877,12 @@ for await (const event of ai.speech.stream({ model, text: "Hello from OpenCode."
871
877
  ## Transcription
872
878
 
873
879
  Transcription (speech-to-text) is the one modality whose providers use every route kind: OpenAI and Gemini stream,
874
- Deepgram answers inline, and AssemblyAI is queued. `Transcription.generate` and `Transcription.stream` work on all of
875
- them; `Transcription.start` / `resume` return a `Generation` on queued routes and fail with `UnsupportedOperation`
876
- elsewhere. Models come from `.transcription(...)` selectors on the `OpenAI`, `Google`, `Deepgram`, and `AssemblyAI`
877
- facades. Common fields (`language`, `prompt`, `timestamps: "none" | "segment" | "word"`, `diarize`, `speakers`) lower
878
- natively or fail with a typed `AIError` before any network call; a route may return more than asked.
880
+ Deepgram and ElevenLabs answer inline, and AssemblyAI is queued. `Transcription.generate` and `Transcription.stream`
881
+ work on all of them; `Transcription.start` / `resume` return a `Generation` on queued routes and fail with
882
+ `UnsupportedOperation` elsewhere. Models come from `.transcription(...)` selectors on the `OpenAI`, `Google`,
883
+ `Deepgram`, `ElevenLabs`, and `AssemblyAI` facades. Common fields (`language`, `prompt`,
884
+ `timestamps: "none" | "segment" | "word"`, `diarize`, `speakers`) lower natively or fail with a typed `AIError` before
885
+ any network call; a route may return more than asked.
879
886
 
880
887
  ```ts
881
888
  import { Console, Effect, Stream } from "effect"
@@ -887,7 +894,7 @@ const openai = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY })
887
894
  const program = Effect.gen(function* () {
888
895
  const audio = yield* Media.file("./call.mp3")
889
896
 
890
- // Speaker-labelled segments; labels are provider-native strings ("A", "0", "spk:0").
897
+ // Speaker-labelled segments; labels are provider-native strings ("A", "0", "spk:0", "speaker_0").
891
898
  const response = yield* Transcription.generate({
892
899
  model: Deepgram.configure({ apiKey }).transcription("nova-3"),
893
900
  audio,
@@ -897,7 +904,7 @@ const program = Effect.gen(function* () {
897
904
  response.text // "Hello from OpenCode."
898
905
  response.segments // [{ text, startSeconds, endSeconds, speaker: "0" }]
899
906
  response.words // [{ text, startSeconds, endSeconds, speaker, confidence }]
900
- response.language // the provider's own value, lowercased ("en", "english", "en_us")
907
+ response.language // the provider's own value, lowercased ("en", "eng", "english", "en_us")
901
908
 
902
909
  // Text deltas as the model transcribes, then one finish carrying the whole transcript.
903
910
  yield* Transcription.stream({ model: openai.transcription("gpt-4o-mini-transcribe"), audio }).pipe(
@@ -921,7 +928,12 @@ Provider notes:
921
928
  - **OpenAI** takes inline audio only; `diarize` needs `gpt-4o-transcribe-diarize`, timestamps need `whisper-1`, and `whisper-1` does not stream.
922
929
  - **Gemini** needs a transcribe model (`gemini-3.5-transcribe`); `prompt` and `speakers` fail typed.
923
930
  - **Deepgram** detects the language unless `language` is set; vocabulary goes in `providerOptions.keyterm`.
924
- - **AssemblyAI** uploads inline audio before submitting and is the only route that accepts `speakers`.
931
+ - **ElevenLabs** (`scribe_v2`) uploads inline audio as the multipart `file` and sends a URL as `source_url`. Words
932
+ always carry timestamps, and segments are speaker turns, so `diarize`, `timestamps: "segment"`, or `speakers` turns
933
+ on diarization. `speakers` is an upper bound (`num_speakers`); `prompt` fails typed (vocabulary goes in
934
+ `providerOptions.keyterms`), as do webhook delivery and per-channel output (`use_multi_channel` without
935
+ `multichannel_output_style: "combined"`).
936
+ - **AssemblyAI** uploads inline audio before submitting and treats `speakers` as the exact speaker count.
925
937
 
926
938
  The promise client mirrors the Effect API:
927
939
 
@@ -36,8 +36,15 @@ const resolve = (policy) => {
36
36
  // prefix caching, Gemini's implicit + out-of-band CachedContent). Skip the
37
37
  // whole policy pass for these — emitting hints would be harmless but pointless.
38
38
  const RESPECTS_INLINE_HINTS = new Set([
39
+ "alibaba-messages",
39
40
  "anthropic-messages",
41
+ "anthropic-compatible-messages",
42
+ "cloudflare-ai-gateway-messages",
40
43
  "google-vertex-messages",
44
+ "meta-messages",
45
+ "minimax-messages",
46
+ "moonshot-messages",
47
+ "zai-coding-messages",
41
48
  "bedrock-converse",
42
49
  "openrouter",
43
50
  ]);
@@ -54,12 +54,14 @@ export declare const ChoiceAnswer: Schema.Struct<{
54
54
  readonly type: Schema.Literal<"choice">;
55
55
  readonly choice: Schema.String;
56
56
  readonly probabilities: Schema.optional<Schema.$Record<Schema.String, Schema.Number>>;
57
+ readonly confidence: Schema.optional<Schema.Number>;
57
58
  }>;
58
59
  export type ChoiceAnswer = Schema.Schema.Type<typeof ChoiceAnswer>;
59
60
  export declare const ScoreAnswer: Schema.Struct<{
60
61
  readonly type: Schema.Literal<"score">;
61
62
  readonly score: Schema.Number;
62
63
  readonly probabilities: Schema.optional<Schema.$Record<Schema.String, Schema.Number>>;
64
+ readonly confidence: Schema.optional<Schema.Number>;
63
65
  }>;
64
66
  export type ScoreAnswer = Schema.Schema.Type<typeof ScoreAnswer>;
65
67
  export declare const BooleanAnswer: Schema.Struct<{
@@ -71,10 +73,12 @@ export declare const EvaluationAnswer: Schema.toTaggedUnion<"type", readonly [Sc
71
73
  readonly type: Schema.Literal<"choice">;
72
74
  readonly choice: Schema.String;
73
75
  readonly probabilities: Schema.optional<Schema.$Record<Schema.String, Schema.Number>>;
76
+ readonly confidence: Schema.optional<Schema.Number>;
74
77
  }>, Schema.Struct<{
75
78
  readonly type: Schema.Literal<"score">;
76
79
  readonly score: Schema.Number;
77
80
  readonly probabilities: Schema.optional<Schema.$Record<Schema.String, Schema.Number>>;
81
+ readonly confidence: Schema.optional<Schema.Number>;
78
82
  }>, Schema.Struct<{
79
83
  readonly type: Schema.Literal<"boolean">;
80
84
  readonly probability: Schema.Number;
@@ -87,6 +91,7 @@ export type AnswerFor<Question extends EvaluationQuestion> = Question extends {
87
91
  readonly type: "choice";
88
92
  readonly choice: Extract<keyof Criteria, string>;
89
93
  readonly probabilities?: Readonly<Record<Extract<keyof Criteria, string>, number>>;
94
+ readonly confidence?: number;
90
95
  } : Question extends {
91
96
  readonly type: "score";
92
97
  } ? ScoreAnswer : BooleanAnswer;
@@ -206,10 +211,12 @@ declare const EvaluationResponse_base: Schema.Class<EvaluationResponse, Schema.S
206
211
  readonly type: Schema.Literal<"choice">;
207
212
  readonly choice: Schema.String;
208
213
  readonly probabilities: Schema.optional<Schema.$Record<Schema.String, Schema.Number>>;
214
+ readonly confidence: Schema.optional<Schema.Number>;
209
215
  }>, Schema.Struct<{
210
216
  readonly type: Schema.Literal<"score">;
211
217
  readonly score: Schema.Number;
212
218
  readonly probabilities: Schema.optional<Schema.$Record<Schema.String, Schema.Number>>;
219
+ readonly confidence: Schema.optional<Schema.Number>;
213
220
  }>, Schema.Struct<{
214
221
  readonly type: Schema.Literal<"boolean">;
215
222
  readonly probability: Schema.Number;
@@ -33,11 +33,13 @@ export const ChoiceAnswer = Schema.Struct({
33
33
  type: Schema.Literal("choice"),
34
34
  choice: Schema.String,
35
35
  probabilities: Schema.optional(Schema.Record(Schema.String, Probability)),
36
+ confidence: Schema.optional(Probability),
36
37
  });
37
38
  export const ScoreAnswer = Schema.Struct({
38
39
  type: Schema.Literal("score"),
39
40
  score: Schema.Number,
40
41
  probabilities: Schema.optional(Schema.Record(Schema.String, Probability)),
42
+ confidence: Schema.optional(Probability),
41
43
  });
42
44
  export const BooleanAnswer = Schema.Struct({
43
45
  type: Schema.Literal("boolean"),
@@ -80,34 +80,37 @@ export const model = (cfg) => EvaluationModel.make({
80
80
  const fail = (message, cause, body) => new AIError({ reason: new InvalidProviderOutputError({ route: "system-one", message, body, http, cause }) });
81
81
  const text = yield* res.text.pipe(Effect.mapError((cause) => fail("Failed to read the System One response", cause)));
82
82
  const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Response))(text).pipe(Effect.mapError((cause) => fail("System One returned an invalid response", cause, text)));
83
- const confidence = {};
84
83
  const legend = {};
85
84
  const answers = Object.fromEntries(Object.entries(data.answers).map(([id, answer]) => {
86
85
  if (answer.type === "noul")
87
86
  return [id, { type: "boolean", probability: answer.noul }];
88
87
  if (answer.type === "choice") {
89
- if (answer.confidence !== undefined)
90
- confidence[id] = answer.confidence;
91
88
  return [
92
89
  id,
93
90
  {
94
91
  type: "choice",
95
92
  choice: answer.choice,
96
93
  probabilities: answer.probabilities,
94
+ ...(answer.confidence === undefined ? {} : { confidence: answer.confidence }),
97
95
  },
98
96
  ];
99
97
  }
100
- if (answer.confidence !== undefined)
101
- confidence[id] = answer.confidence;
102
98
  if (answer.legend !== undefined)
103
99
  legend[id] = answer.legend;
104
- return [id, { type: "score", score: answer.score, probabilities: answer.probabilities }];
100
+ return [
101
+ id,
102
+ {
103
+ type: "score",
104
+ score: answer.score,
105
+ probabilities: answer.probabilities,
106
+ ...(answer.confidence === undefined ? {} : { confidence: answer.confidence }),
107
+ },
108
+ ];
105
109
  }));
106
110
  const meta = {
107
111
  ...(data.id === undefined ? {} : { responseId: data.id }),
108
112
  ...(data.provider === undefined ? {} : { provider: data.provider }),
109
113
  ...data.provider_metadata?.[cfg.providerMetadataKey],
110
- ...(Object.keys(confidence).length === 0 ? {} : { confidence }),
111
114
  ...(Object.keys(legend).length === 0 ? {} : { legend }),
112
115
  };
113
116
  return new EvaluationResponse({
@@ -72,8 +72,8 @@ export declare class Generation<Response> {
72
72
  */
73
73
  events(options?: AwaitOptions): Stream.Stream<Event, AIError>;
74
74
  private event;
75
- private timeoutError;
76
75
  private poll;
77
76
  private schedule;
78
77
  }
78
+ /** `events` followed by the expanded result, with the result fetch bounded by the same `poll.timeout` deadline. */
79
79
  export declare const resultEvents: <Response, A>(generation: Generation<Response>, expand: (response: Response) => ReadonlyArray<A>, options?: AwaitOptions) => Stream.Stream<Observation | A, AIError>;
@@ -56,7 +56,7 @@ export class Generation {
56
56
  const settled = this.terminal ? Effect.succeed(this) : this.poll(options?.poll);
57
57
  return settled.pipe(
58
58
  // Non-completed terminal states also go through `result` so the route can surface its provider failure body.
59
- Effect.flatMap((generation) => generation.result()), Effect.timeoutOrElse({ duration: timeout, orElse: () => this.timeoutError(timeout) }));
59
+ Effect.flatMap((generation) => generation.result()), Effect.timeoutOrElse({ duration: timeout, orElse: () => timeoutError(this.id, timeout) }));
60
60
  }
61
61
  cancel() {
62
62
  return this.route.cancel ?? Effect.void;
@@ -73,14 +73,7 @@ export class Generation {
73
73
  const timeout = Duration.fromInputUnsafe(options?.poll?.timeout ?? DEFAULT_POLL_TIMEOUT);
74
74
  return Stream.unwrap(Clock.currentTimeMillis.pipe(Effect.map((start) => {
75
75
  const deadline = start + Duration.toMillis(timeout);
76
- // Fail before polling once the deadline has passed: a fast status request could otherwise win the zero-budget
77
- // race and schedule another zero-delay poll.
78
- const refresh = Clock.currentTimeMillis.pipe(Effect.flatMap((now) => now >= deadline
79
- ? this.timeoutError(timeout)
80
- : this.refresh().pipe(Effect.timeoutOrElse({
81
- duration: Duration.millis(deadline - now),
82
- orElse: () => this.timeoutError(timeout),
83
- }))));
76
+ const refresh = within(this.refresh(), this.id, timeout, deadline);
84
77
  const schedule = this.schedule(options?.poll).pipe(Schedule.modifyDelay((meta) => Effect.succeed(Duration.min(meta.duration, Duration.millis(Math.max(0, deadline - meta.now))))));
85
78
  return Stream.fromEffectSchedule(refresh, schedule).pipe(Stream.takeUntil((generation) => generation.terminal), Stream.map((generation) => generation.event()));
86
79
  })));
@@ -92,14 +85,6 @@ export class Generation {
92
85
  return { type: "generation-queued", id: this.id, position: this.position };
93
86
  return { type: "generation-progress", id: this.id, progress: this.progress };
94
87
  }
95
- timeoutError(timeout) {
96
- return new AIError({
97
- reason: new TimeoutError({
98
- message: `Generation ${this.id} did not finish within ${Duration.format(timeout)}`,
99
- timeoutMs: Duration.toMillis(timeout),
100
- }),
101
- });
102
- }
103
88
  poll(poll) {
104
89
  return this.refresh().pipe(Effect.repeat({ schedule: this.schedule(poll), until: (generation) => generation.terminal }));
105
90
  }
@@ -107,4 +92,24 @@ export class Generation {
107
92
  return Schedule.spaced(poll?.interval ?? DEFAULT_POLL_INTERVAL);
108
93
  }
109
94
  }
110
- export const resultEvents = (generation, expand, options) => generation.events(options).pipe(Stream.filter((event) => event.type !== "generation-finished"), Stream.concat(Stream.fromIterableEffect(Effect.map(generation.result(), expand))));
95
+ /** `events` followed by the expanded result, with the result fetch bounded by the same `poll.timeout` deadline. */
96
+ export const resultEvents = (generation, expand, options) => {
97
+ const timeout = Duration.fromInputUnsafe(options?.poll?.timeout ?? DEFAULT_POLL_TIMEOUT);
98
+ return Stream.unwrap(Clock.currentTimeMillis.pipe(Effect.map((start) => generation.events(options).pipe(Stream.filter((event) => event.type !== "generation-finished"), Stream.concat(Stream.fromIterableEffect(within(generation.result(), generation.id, timeout, start + Duration.toMillis(timeout)).pipe(Effect.map(expand))))))));
99
+ };
100
+ /**
101
+ * Run `effect` within the time left until `deadline`. Fails before starting once the deadline has passed: a fast
102
+ * request could otherwise win the zero-budget race and schedule another zero-delay poll.
103
+ */
104
+ const within = (effect, id, timeout, deadline) => Clock.currentTimeMillis.pipe(Effect.flatMap((now) => now >= deadline
105
+ ? Effect.fail(timeoutError(id, timeout))
106
+ : effect.pipe(Effect.timeoutOrElse({
107
+ duration: Duration.millis(deadline - now),
108
+ orElse: () => Effect.fail(timeoutError(id, timeout)),
109
+ }))));
110
+ const timeoutError = (id, timeout) => new AIError({
111
+ reason: new TimeoutError({
112
+ message: `Generation ${id} did not finish within ${Duration.format(timeout)}`,
113
+ timeoutMs: Duration.toMillis(timeout),
114
+ }),
115
+ });
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { ImageClient } from "./image-client.js";
4
4
  export { Auth } from "./route/auth.js";
5
5
  export { Provider } from "./provider.js";
6
6
  export { ProviderPackage } from "./provider-package.js";
7
- export { isContextOverflow, isContextOverflowFailure } from "./provider-error.js";
7
+ export { isContextOverflow, isContextOverflowFailure, isRetryable } from "./provider-error.js";
8
8
  export type { RouteLanguageModelInput, RouteRoutedLanguageModelInput, Interface as LLMClientShape, LLMClientService, } from "./route/client.js";
9
9
  export * from "./schema/index.js";
10
10
  export { ImageAspectRatio, ImageEvent, ImageModel, ImageModelSchema, ImageRequest, ImageResponse, ImageSize, } from "./image.js";
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ export { ImageClient } from "./image-client.js";
4
4
  export { Auth } from "./route/auth.js";
5
5
  export { Provider } from "./provider.js";
6
6
  export { ProviderPackage } from "./provider-package.js";
7
- export { isContextOverflow, isContextOverflowFailure } from "./provider-error.js";
7
+ export { isContextOverflow, isContextOverflowFailure, isRetryable } from "./provider-error.js";
8
8
  export * from "./schema/index.js";
9
9
  export { ImageAspectRatio, ImageEvent, ImageModel, ImageModelSchema, ImageRequest, ImageResponse, ImageSize, } from "./image.js";
10
10
  export { Image } from "./image.js";
package/dist/llm.d.ts CHANGED
@@ -184,8 +184,8 @@ export declare class GenerateObjectResponse<T> {
184
184
  } | {
185
185
  readonly id: string;
186
186
  readonly type: "tool-error";
187
- readonly name: string;
188
187
  readonly message: string;
188
+ readonly name: string;
189
189
  readonly error?: unknown;
190
190
  readonly providerMetadata?: {
191
191
  readonly [x: string]: {
package/dist/promise.d.ts CHANGED
@@ -30,7 +30,7 @@ export type GenerationHandle<Response> = Snapshot & {
30
30
  /** Serializable JSON; pass it back to `resume` from another process. */
31
31
  readonly token: unknown;
32
32
  readonly await: (options?: AwaitOptions & RunOptions) => Promise<Response>;
33
- /** Status observations until the first terminal one, polling like `await`; abort ends iteration without throwing. */
33
+ /** Status observations until the first terminal one, polling like `await`; abort throws `signal.reason`. */
34
34
  readonly events: (options?: AwaitOptions & RunOptions) => AsyncIterable<Event>;
35
35
  /** The result without polling; fails when the generation has not completed. */
36
36
  readonly result: (options?: RunOptions) => Promise<Response>;
@@ -211,8 +211,8 @@ export declare const make: (options?: Options) => {
211
211
  } | {
212
212
  readonly id: string;
213
213
  readonly type: "tool-error";
214
- readonly name: string;
215
214
  readonly message: string;
215
+ readonly name: string;
216
216
  readonly error?: unknown;
217
217
  readonly providerMetadata?: {
218
218
  readonly [x: string]: {
@@ -698,8 +698,8 @@ export declare const ai: {
698
698
  } | {
699
699
  readonly id: string;
700
700
  readonly type: "tool-error";
701
- readonly name: string;
702
701
  readonly message: string;
702
+ readonly name: string;
703
703
  readonly error?: unknown;
704
704
  readonly providerMetadata?: {
705
705
  readonly [x: string]: {
package/dist/promise.js CHANGED
@@ -10,21 +10,22 @@ import { Speech } from "./speech.js";
10
10
  import { Transcription, } from "./transcription.js";
11
11
  import { fileMediaType } from "./utils/media-type.js";
12
12
  import { Video } from "./video.js";
13
+ // Fails with `signal.reason` so aborted calls reject and aborted streams throw like `fetch`: an `AbortError` by default.
13
14
  const abortEffect = (signal) => signal === undefined
14
15
  ? Effect.never
15
16
  : Effect.callback((resume) => {
16
17
  if (signal.aborted) {
17
- resume(Effect.void);
18
+ resume(Effect.fail(signal.reason));
18
19
  return;
19
20
  }
20
- const onAbort = () => resume(Effect.void);
21
+ const onAbort = () => resume(Effect.fail(signal.reason));
21
22
  signal.addEventListener("abort", onAbort, { once: true });
22
23
  return Effect.sync(() => signal.removeEventListener("abort", onAbort));
23
24
  });
24
25
  export const make = (options = {}) => {
25
26
  const runtime = ManagedRuntime.make(AIClient.layerWith(options.layer ?? RequestExecutor.fetchLayer));
26
27
  /** Run any package Effect (for example `LLMClient.compact(...)`) inside this runtime. */
27
- const run = (effect, options) => runtime.runPromise(effect, { signal: options?.signal });
28
+ const run = (effect, options) => runtime.runPromise(Effect.raceFirst(effect, abortEffect(options?.signal)));
28
29
  const iterate = (stream, options) => Stream.toAsyncIterable(Stream.unwrap(runtime.contextEffect.pipe(Effect.map((context) => stream.pipe(Stream.interruptWhen(abortEffect(options?.signal)), Stream.provideContext(context))))));
29
30
  const handle = (generation) => ({
30
31
  ...generation.snapshot,
@@ -90,12 +90,17 @@ export declare const protocol: Protocol<{
90
90
  readonly ttl?: string | undefined;
91
91
  } | undefined;
92
92
  readonly tool_calls?: readonly {
93
- readonly id: string;
94
- readonly type: "function";
95
93
  readonly function: {
96
94
  readonly name: string;
97
95
  readonly arguments: string;
98
96
  };
97
+ readonly id: string;
98
+ readonly type: "function";
99
+ readonly extra_content?: {
100
+ readonly google: {
101
+ readonly thought_signature: string;
102
+ };
103
+ } | undefined;
99
104
  }[] | undefined;
100
105
  readonly reasoning_details?: unknown;
101
106
  } | {
@@ -208,6 +213,7 @@ export declare const protocol: Protocol<{
208
213
  } | null | undefined;
209
214
  readonly id?: string | null | undefined;
210
215
  readonly index?: number | null | undefined;
216
+ readonly extra_content?: unknown;
211
217
  }[] | null | undefined;
212
218
  readonly reasoning_details?: unknown;
213
219
  } | null | undefined;
@@ -77,8 +77,8 @@ export declare const protocol: Protocol<{
77
77
  } | {
78
78
  readonly type: "input_file";
79
79
  readonly filename: string;
80
- readonly file_data?: string | undefined;
81
80
  readonly detail?: string | undefined;
81
+ readonly file_data?: string | undefined;
82
82
  readonly file_url?: string | undefined;
83
83
  } | {
84
84
  readonly type: "input_text";
@@ -114,8 +114,8 @@ export declare const protocol: Protocol<{
114
114
  } | {
115
115
  readonly type: "input_file";
116
116
  readonly filename: string;
117
- readonly file_data?: string | undefined;
118
117
  readonly detail?: string | undefined;
118
+ readonly file_data?: string | undefined;
119
119
  readonly file_url?: string | undefined;
120
120
  } | {
121
121
  readonly type: "input_text";
@@ -190,21 +190,10 @@ export declare const protocol: Protocol<{
190
190
  readonly [x: string]: unknown;
191
191
  readonly type: string;
192
192
  readonly status?: unknown;
193
- readonly code?: unknown;
194
193
  readonly message?: unknown;
194
+ readonly code?: unknown;
195
195
  readonly text?: string | undefined;
196
196
  readonly error?: unknown;
197
- readonly item?: {
198
- readonly [x: string]: unknown;
199
- readonly type: string;
200
- readonly id?: string | undefined;
201
- readonly name?: string | undefined;
202
- readonly namespace?: string | undefined;
203
- readonly arguments?: string | undefined;
204
- readonly encrypted_content?: string | null | undefined;
205
- readonly call_id?: string | undefined;
206
- } | null | undefined;
207
- readonly delta?: string | undefined;
208
197
  readonly response?: {
209
198
  readonly [x: string]: unknown;
210
199
  readonly id?: string | undefined;
@@ -236,6 +225,17 @@ export declare const protocol: Protocol<{
236
225
  readonly reason?: string | undefined;
237
226
  } | null | undefined;
238
227
  } | undefined;
228
+ readonly item?: {
229
+ readonly [x: string]: unknown;
230
+ readonly type: string;
231
+ readonly id?: string | undefined;
232
+ readonly name?: string | undefined;
233
+ readonly namespace?: string | undefined;
234
+ readonly arguments?: string | undefined;
235
+ readonly encrypted_content?: string | null | undefined;
236
+ readonly call_id?: string | undefined;
237
+ } | null | undefined;
238
+ readonly delta?: string | undefined;
239
239
  readonly arguments?: string | undefined;
240
240
  readonly item_id?: string | undefined;
241
241
  readonly output_index?: number | undefined;
@@ -13,6 +13,7 @@ import { JsonObject, knownString, optionalArray, optionalNull, ProviderShared }
13
13
  import { classifyProviderFailure } from "../provider-error.js";
14
14
  import { effortUpdate, resolveEffortUpdates } from "../effort-updates.js";
15
15
  import * as Cache from "./utils/cache.js";
16
+ import { claudeVersion, supportsThinkingBlockBinding, THINKING_BINDING_BETA } from "./utils/claude-model.js";
16
17
  import { Lifecycle } from "./utils/lifecycle.js";
17
18
  import { ToolStream } from "./utils/tool-stream.js";
18
19
  const ADAPTER = "anthropic-messages";
@@ -850,20 +851,6 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (re
850
851
  }
851
852
  return messages;
852
853
  });
853
- // Accept gateway namespaces and Vertex suffixes without treating a snapshot date as a minor version.
854
- const claudeVersion = (id) => {
855
- const match = /(?:^|[./])claude-(?<family>[a-z]+)-(?<major>\d+)(?:[.-](?<minor>\d{1,2}))?(?:$|[-:@])/.exec(id.toLowerCase())?.groups;
856
- if (!match)
857
- return undefined;
858
- return { family: match.family, major: Number(match.major), minor: Number(match.minor ?? 0) };
859
- };
860
- const supportsThinkingBlockBinding = (model) => {
861
- const override = model.compatibility?.supportsThinkingBlockBinding;
862
- if (override !== undefined)
863
- return override;
864
- const version = claudeVersion(model.id);
865
- return version !== undefined && (version.major > 5 || (version.major === 5 && version.minor >= 1));
866
- };
867
854
  const supportsEffortUpdates = (model) => {
868
855
  const override = model.compatibility?.supportsEffortUpdates;
869
856
  if (override !== undefined)
@@ -1437,7 +1424,7 @@ function requiredBetaHeaders(body) {
1437
1424
  betas.push("mid-conversation-output-config-2026-07-01");
1438
1425
  const thinking = body.thinking;
1439
1426
  if (thinking && thinking.type !== "disabled" && thinking.block_binding)
1440
- betas.push("thinking-binding-controls-2026-08-01");
1427
+ betas.push(THINKING_BINDING_BETA);
1441
1428
  return betas;
1442
1429
  }
1443
1430
  export const route = Route.make({
@@ -141,7 +141,7 @@ interface ParserState {
141
141
  readonly hasToolCalls: boolean;
142
142
  readonly lifecycle: Lifecycle.State;
143
143
  readonly reasoningSignatures: Readonly<Record<number, string>>;
144
- readonly reasoningRedactedContent: Readonly<Record<number, ReadonlyArray<Uint8Array>>>;
144
+ readonly reasoningRedactedContent: Readonly<Record<number, Uint8Array[]>>;
145
145
  }
146
146
  /**
147
147
  * The Bedrock Converse protocol — request body construction, body schema, and
@@ -9,6 +9,7 @@ import { JsonObject, optionalArray, ProviderShared } from "./shared.js";
9
9
  import { BedrockAuth } from "./utils/bedrock-auth.js";
10
10
  import { BedrockCache } from "./utils/bedrock-cache.js";
11
11
  import { BedrockMedia } from "./utils/bedrock-media.js";
12
+ import { supportsThinkingBlockBinding, THINKING_BINDING_BETA } from "./utils/claude-model.js";
12
13
  import { Lifecycle } from "./utils/lifecycle.js";
13
14
  import { MistralToolID } from "./utils/mistral-tool-id.js";
14
15
  import { ToolStream } from "./utils/tool-stream.js";
@@ -351,18 +352,33 @@ const Options = Schema.Struct({
351
352
  const decodeOptions = ProviderShared.validateWith(Schema.decodeUnknownEffect(Options));
352
353
  // Claude on Bedrock requires the thinking budget below `maxTokens`, with a minimum of 1,024.
353
354
  const MIN_THINKING_BUDGET = 1_024;
355
+ const isThinkingDisabled = Schema.is(Schema.Struct({
356
+ additionalModelRequestFields: Schema.Struct({ thinking: Schema.Struct({ type: Schema.Literal("disabled") }) }),
357
+ }));
358
+ // Claude 5.1+ binds each thinking signature to the prefix above it. Ask Bedrock to drop the affected blocks instead of
359
+ // failing when that prefix changes. `http.body` overlays this field by field, so callers can still override it.
360
+ const applyThinkingBindingDefault = (request, thinking) => {
361
+ if (isThinkingDisabled(request.http?.body))
362
+ return thinking;
363
+ if (!supportsThinkingBlockBinding(request.model))
364
+ return thinking;
365
+ return {
366
+ ...(thinking ?? { type: "adaptive" }),
367
+ block_binding: { prefix_mismatch_behavior: "drop_block" },
368
+ };
369
+ };
354
370
  const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request) {
355
371
  const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined;
356
372
  const flattened = ProviderShared.flattenToolRequest(request);
357
373
  const generation = request.generation;
358
374
  const options = yield* decodeOptions(request.providerOptions ?? {});
359
375
  const maxTokens = isNova2(request.model) && isHighReasoningEffort(request.http?.body) ? undefined : generation?.maxTokens;
360
- const thinking = options.thinking === undefined
376
+ const thinking = applyThinkingBindingDefault(request, options.thinking === undefined
361
377
  ? undefined
362
378
  : {
363
379
  type: "enabled",
364
380
  budget_tokens: ProviderShared.fitThinkingBudget(options.thinking.budgetTokens, maxTokens, MIN_THINKING_BUDGET),
365
- };
381
+ });
366
382
  // Bedrock-Claude shares Anthropic's 4-breakpoint cap. Spend the budget in
367
383
  // tools → system → messages order to favour the highest-impact prefixes.
368
384
  const breakpoints = BedrockCache.breakpoints(request.model.id);
@@ -407,6 +423,8 @@ const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request)
407
423
  : {
408
424
  ...(generation?.topK === undefined ? {} : { top_k: generation.topK }),
409
425
  ...(thinking === undefined ? {} : { thinking }),
426
+ // Converse takes Anthropic betas in the body, and Bedrock rejects `block_binding` without this one.
427
+ ...(thinking?.block_binding === undefined ? {} : { anthropic_beta: [THINKING_BINDING_BETA] }),
410
428
  },
411
429
  };
412
430
  });
@@ -478,25 +496,29 @@ const step = (state, event) => Effect.gen(function* () {
478
496
  const index = event.contentBlockDelta.contentBlockIndex;
479
497
  const reasoning = event.contentBlockDelta.delta.reasoningContent;
480
498
  const events = [];
481
- const redactedChunks = yield* (() => {
499
+ const redactedChunk = yield* (() => {
482
500
  if (reasoning.redactedContent === undefined)
483
501
  return Effect.succeed(undefined);
484
- return Effect.fromResult(Encoding.decodeBase64(reasoning.redactedContent)).pipe(Effect.map((chunk) => [...(state.reasoningRedactedContent[index] ?? []), chunk]), Effect.mapError((cause) => ProviderShared.eventError(ADAPTER, "Bedrock Converse reasoningContent.redactedContent contains invalid base64 data", undefined, cause)));
502
+ return Effect.fromResult(Encoding.decodeBase64(reasoning.redactedContent)).pipe(Effect.mapError((cause) => ProviderShared.eventError(ADAPTER, "Bedrock Converse reasoningContent.redactedContent contains invalid base64 data", undefined, cause)));
485
503
  })();
486
- const redactedData = redactedChunks === undefined ? reasoning.data : encodeRedactedContent(redactedChunks);
504
+ const redactedChunks = state.reasoningRedactedContent[index] ?? [];
505
+ if (redactedChunk !== undefined)
506
+ redactedChunks.push(redactedChunk);
487
507
  const metadata = (() => {
488
508
  if (reasoning.signature)
489
509
  return providerMetadata(state.providerMetadataKey, { signature: reasoning.signature });
490
- if (redactedData !== undefined)
491
- return providerMetadata(state.providerMetadataKey, { redactedData });
510
+ if (redactedChunk === undefined && reasoning.data !== undefined)
511
+ return providerMetadata(state.providerMetadataKey, { redactedData: reasoning.data });
492
512
  })();
493
513
  const lifecycle = (() => {
494
- if (reasoning.text === undefined && metadata === undefined)
495
- return state.lifecycle;
496
- return Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${index}`, reasoning.text ?? "", metadata);
514
+ if (reasoning.text !== undefined || metadata !== undefined)
515
+ return Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${index}`, reasoning.text ?? "", metadata);
516
+ if (redactedChunk !== undefined)
517
+ return Lifecycle.reasoningStart(state.lifecycle, events, `reasoning-${index}`);
518
+ return state.lifecycle;
497
519
  })();
498
520
  const reasoningRedactedContent = (() => {
499
- if (redactedChunks !== undefined)
521
+ if (redactedChunk !== undefined)
500
522
  return { ...state.reasoningRedactedContent, [index]: redactedChunks };
501
523
  if (reasoning.data === undefined)
502
524
  return state.reasoningRedactedContent;
@@ -609,7 +631,11 @@ const onHalt = (state) => {
609
631
  return state.finishReason.normalized;
610
632
  })();
611
633
  const events = [];
612
- Lifecycle.finish(state.lifecycle, events, {
634
+ const lifecycle = Object.entries(state.reasoningRedactedContent).reduce((current, [index, chunks]) => {
635
+ const signature = state.reasoningSignatures[Number(index)];
636
+ return Lifecycle.reasoningEnd(current, events, `reasoning-${index}`, providerMetadata(state.providerMetadataKey, signature ? { signature } : { redactedData: encodeRedactedContent(chunks) }));
637
+ }, state.lifecycle);
638
+ Lifecycle.finish(lifecycle, events, {
613
639
  reason: {
614
640
  ...state.finishReason,
615
641
  normalized,