@opencode/ai 2.0.20 → 2.0.22

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 (59) hide show
  1. package/dist/cache-policy.js +22 -3
  2. package/dist/llm.d.ts +4 -0
  3. package/dist/promise.d.ts +8 -0
  4. package/dist/protocols/alibaba-chat.d.ts +20 -10
  5. package/dist/protocols/alibaba-chat.js +2 -1
  6. package/dist/protocols/anthropic-messages.js +14 -12
  7. package/dist/protocols/bedrock-converse.js +30 -1
  8. package/dist/protocols/open-responses.d.ts +4 -0
  9. package/dist/protocols/openai-chat.d.ts +122 -71
  10. package/dist/protocols/openai-chat.js +40 -37
  11. package/dist/protocols/openai-compatible-chat.d.ts +16 -10
  12. package/dist/protocols/openai-responses.js +4 -3
  13. package/dist/protocols/utils/cache.d.ts +5 -0
  14. package/dist/protocols/utils/cache.js +9 -1
  15. package/dist/protocols/utils/tool-stream.d.ts +12 -0
  16. package/dist/protocols/zai-chat.d.ts +20 -10
  17. package/dist/protocols/zai-chat.js +6 -3
  18. package/dist/provider-error.js +24 -6
  19. package/dist/providers/alibaba.d.ts +16 -10
  20. package/dist/providers/amazon-bedrock-mantle.d.ts +16 -10
  21. package/dist/providers/azure.d.ts +16 -10
  22. package/dist/providers/baseten.d.ts +32 -20
  23. package/dist/providers/cerebras.d.ts +32 -20
  24. package/dist/providers/cloudflare-ai-gateway.d.ts +32 -20
  25. package/dist/providers/cloudflare-ai-gateway.js +43 -35
  26. package/dist/providers/cloudflare-workers-ai.d.ts +32 -20
  27. package/dist/providers/deepinfra.d.ts +32 -20
  28. package/dist/providers/deepseek.d.ts +32 -20
  29. package/dist/providers/digitalocean.d.ts +469 -0
  30. package/dist/providers/digitalocean.js +53 -0
  31. package/dist/providers/fireworks.d.ts +32 -20
  32. package/dist/providers/google-vertex-chat.d.ts +16 -10
  33. package/dist/providers/google-vertex-messages.js +1 -1
  34. package/dist/providers/groq.d.ts +36 -20
  35. package/dist/providers/groq.js +1 -1
  36. package/dist/providers/index.d.ts +1 -0
  37. package/dist/providers/index.js +1 -0
  38. package/dist/providers/meta.d.ts +16 -10
  39. package/dist/providers/minimax.d.ts +16 -10
  40. package/dist/providers/moonshot.d.ts +16 -10
  41. package/dist/providers/openai-compatible.d.ts +16 -10
  42. package/dist/providers/openai.d.ts +16 -10
  43. package/dist/providers/openrouter.d.ts +68 -40
  44. package/dist/providers/openrouter.js +1 -13
  45. package/dist/providers/togetherai.d.ts +32 -20
  46. package/dist/providers/xai.d.ts +16 -10
  47. package/dist/providers/zai-coding-plan.d.ts +16 -10
  48. package/dist/providers/zai.d.ts +16 -10
  49. package/dist/route/client.d.ts +4 -0
  50. package/dist/route/executor.js +13 -6
  51. package/dist/route/transport/http.d.ts +2 -1
  52. package/dist/route/transport/http.js +28 -6
  53. package/dist/schema/events.d.ts +176 -0
  54. package/dist/schema/events.js +4 -0
  55. package/dist/schema/options.d.ts +11 -0
  56. package/dist/schema/options.js +15 -2
  57. package/dist/testing.d.ts +32 -0
  58. package/dist/tool-history.js +17 -3
  59. package/package.json +3 -3
@@ -36,6 +36,7 @@ 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-chat",
39
40
  "alibaba-messages",
40
41
  "anthropic-messages",
41
42
  "anthropic-compatible-messages",
@@ -47,7 +48,21 @@ const RESPECTS_INLINE_HINTS = new Set([
47
48
  "zai-coding-messages",
48
49
  "bedrock-converse",
49
50
  "openrouter",
51
+ "digitalocean",
50
52
  ]);
53
+ // OpenRouter upstreams other than Anthropic and Alibaba Qwen cache without breakpoints. Gemini uses only the last
54
+ // breakpoint, so a conversation-tail breakpoint writes a new cache every step and costs more than none. Qwen ignores
55
+ // breakpoints on tool definitions and caches tools with the system prompt.
56
+ const QWEN = { system: true, messages: { tail: 1 } };
57
+ const openRouterPolicy = (modelID) => {
58
+ // `~anthropic/claude-sonnet-latest` style IDs are OpenRouter aliases for the latest model in a family.
59
+ const id = modelID.replace(/^~/, "");
60
+ if (id.startsWith("anthropic/"))
61
+ return AUTO;
62
+ if (id.startsWith("qwen/"))
63
+ return QWEN;
64
+ return NONE;
65
+ };
51
66
  const makeHint = (ttlSeconds) => ttlSeconds !== undefined ? new CacheHint({ type: "ephemeral", ttlSeconds }) : new CacheHint({ type: "ephemeral" });
52
67
  const markLastTool = (tools, hint, budget) => {
53
68
  const target = tools.at(-1);
@@ -128,9 +143,13 @@ const countHints = (request) => countToolHints(request.tools) +
128
143
  export const applyCachePolicy = (request) => {
129
144
  if (!RESPECTS_INLINE_HINTS.has(request.model.route.id))
130
145
  return request;
131
- if (request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto"))
132
- return request;
133
- const policy = resolve(request.cache);
146
+ const policy = request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto")
147
+ ? openRouterPolicy(request.model.id)
148
+ : request.model.route.id === "alibaba-chat" && (request.cache === undefined || request.cache === "auto")
149
+ ? request.model.id.toLowerCase().startsWith("qwen")
150
+ ? QWEN
151
+ : NONE
152
+ : resolve(request.cache);
134
153
  if (!policy.tools && !policy.system && !policy.messages)
135
154
  return request;
136
155
  const hint = makeHint(policy.ttlSeconds);
package/dist/llm.d.ts CHANGED
@@ -206,6 +206,8 @@ export declare class GenerateObjectResponse<T> {
206
206
  readonly reason: {
207
207
  readonly normalized: "length" | "stop" | "tool-calls" | "content-filter" | "error" | "unknown";
208
208
  readonly raw?: string | undefined;
209
+ readonly category?: string | undefined;
210
+ readonly explanation?: string | undefined;
209
211
  };
210
212
  readonly index: number;
211
213
  readonly providerMetadata?: {
@@ -219,6 +221,8 @@ export declare class GenerateObjectResponse<T> {
219
221
  readonly reason: {
220
222
  readonly normalized: "length" | "stop" | "tool-calls" | "content-filter" | "error" | "unknown";
221
223
  readonly raw?: string | undefined;
224
+ readonly category?: string | undefined;
225
+ readonly explanation?: string | undefined;
222
226
  };
223
227
  readonly providerMetadata?: {
224
228
  readonly [x: string]: {
package/dist/promise.d.ts CHANGED
@@ -233,6 +233,8 @@ export declare const make: (options?: Options) => {
233
233
  readonly reason: {
234
234
  readonly normalized: "length" | "stop" | "tool-calls" | "content-filter" | "error" | "unknown";
235
235
  readonly raw?: string | undefined;
236
+ readonly category?: string | undefined;
237
+ readonly explanation?: string | undefined;
236
238
  };
237
239
  readonly index: number;
238
240
  readonly providerMetadata?: {
@@ -246,6 +248,8 @@ export declare const make: (options?: Options) => {
246
248
  readonly reason: {
247
249
  readonly normalized: "length" | "stop" | "tool-calls" | "content-filter" | "error" | "unknown";
248
250
  readonly raw?: string | undefined;
251
+ readonly category?: string | undefined;
252
+ readonly explanation?: string | undefined;
249
253
  };
250
254
  readonly providerMetadata?: {
251
255
  readonly [x: string]: {
@@ -720,6 +724,8 @@ export declare const ai: {
720
724
  readonly reason: {
721
725
  readonly normalized: "length" | "stop" | "tool-calls" | "content-filter" | "error" | "unknown";
722
726
  readonly raw?: string | undefined;
727
+ readonly category?: string | undefined;
728
+ readonly explanation?: string | undefined;
723
729
  };
724
730
  readonly index: number;
725
731
  readonly providerMetadata?: {
@@ -733,6 +739,8 @@ export declare const ai: {
733
739
  readonly reason: {
734
740
  readonly normalized: "length" | "stop" | "tool-calls" | "content-filter" | "error" | "unknown";
735
741
  readonly raw?: string | undefined;
742
+ readonly category?: string | undefined;
743
+ readonly explanation?: string | undefined;
736
744
  };
737
745
  readonly providerMetadata?: {
738
746
  readonly [x: string]: {
@@ -80,15 +80,18 @@ export declare const protocol: Protocol<{
80
80
  })[];
81
81
  } | {
82
82
  readonly [x: string]: unknown;
83
- readonly content: string | null;
83
+ readonly content: string | readonly {
84
+ readonly type: "text";
85
+ readonly text: string;
86
+ readonly cache_control?: {
87
+ readonly type: "ephemeral";
88
+ readonly ttl?: string | undefined;
89
+ } | undefined;
90
+ }[] | null;
84
91
  readonly role: "assistant";
85
92
  readonly reasoning?: string | undefined;
86
93
  readonly reasoning_content?: string | undefined;
87
94
  readonly reasoning_text?: string | undefined;
88
- readonly cache_control?: {
89
- readonly type: "ephemeral";
90
- readonly ttl?: string | undefined;
91
- } | undefined;
92
95
  readonly tool_calls?: readonly {
93
96
  readonly function: {
94
97
  readonly name: string;
@@ -104,13 +107,16 @@ export declare const protocol: Protocol<{
104
107
  }[] | undefined;
105
108
  readonly reasoning_details?: unknown;
106
109
  } | {
107
- readonly content: string;
108
110
  readonly role: "tool";
109
111
  readonly tool_call_id: string;
110
- readonly cache_control?: {
111
- readonly type: "ephemeral";
112
- readonly ttl?: string | undefined;
113
- } | undefined;
112
+ readonly content: string | readonly {
113
+ readonly type: "text";
114
+ readonly text: string;
115
+ readonly cache_control?: {
116
+ readonly type: "ephemeral";
117
+ readonly ttl?: string | undefined;
118
+ } | undefined;
119
+ }[];
114
120
  })[];
115
121
  readonly stream: true;
116
122
  readonly max_completion_tokens?: number | undefined;
@@ -180,6 +186,7 @@ export declare const protocol: Protocol<{
180
186
  } | null | undefined;
181
187
  readonly usage?: {
182
188
  readonly [x: string]: unknown;
189
+ readonly cache_read_input_tokens?: number | null | undefined;
183
190
  readonly cached_tokens?: number | null | undefined;
184
191
  readonly prompt_tokens?: number | null | undefined;
185
192
  readonly completion_tokens?: number | null | undefined;
@@ -190,6 +197,7 @@ export declare const protocol: Protocol<{
190
197
  readonly cache_write_tokens?: number | null | undefined;
191
198
  } | null | undefined;
192
199
  readonly prompt_cache_hit_tokens?: number | null | undefined;
200
+ readonly cache_created_input_tokens?: number | null | undefined;
193
201
  readonly completion_tokens_details?: {
194
202
  readonly [x: string]: unknown;
195
203
  readonly reasoning_tokens?: number | null | undefined;
@@ -219,6 +227,7 @@ export declare const protocol: Protocol<{
219
227
  } | null | undefined;
220
228
  readonly usage?: {
221
229
  readonly [x: string]: unknown;
230
+ readonly cache_read_input_tokens?: number | null | undefined;
222
231
  readonly cached_tokens?: number | null | undefined;
223
232
  readonly prompt_tokens?: number | null | undefined;
224
233
  readonly completion_tokens?: number | null | undefined;
@@ -229,6 +238,7 @@ export declare const protocol: Protocol<{
229
238
  readonly cache_write_tokens?: number | null | undefined;
230
239
  } | null | undefined;
231
240
  readonly prompt_cache_hit_tokens?: number | null | undefined;
241
+ readonly cache_created_input_tokens?: number | null | undefined;
232
242
  readonly completion_tokens_details?: {
233
243
  readonly [x: string]: unknown;
234
244
  readonly reasoning_tokens?: number | null | undefined;
@@ -2,6 +2,7 @@ import { Effect, Schema } from "effect";
2
2
  import { Protocol } from "../route/protocol.js";
3
3
  import { OpenAIChat } from "./openai-chat.js";
4
4
  import { JsonObject, ProviderShared } from "./shared.js";
5
+ import { cacheControl } from "./utils/cache.js";
5
6
  import { OpenResponsesOptions } from "./utils/open-responses-options.js";
6
7
  const Options = Schema.Struct({
7
8
  reasoningEffort: Schema.optional(OpenResponsesOptions.ReasoningEffort),
@@ -53,7 +54,7 @@ export const protocol = Protocol.make({
53
54
  from: Effect.fn("AlibabaChat.fromRequest")(function* (req) {
54
55
  const opts = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Options))(req.providerOptions ?? {});
55
56
  return {
56
- ...(yield* OpenAIChat.protocol.body.from(req)),
57
+ ...(yield* OpenAIChat.fromRequest(req, { cacheControl: cacheControl() })),
57
58
  enable_thinking: opts.enableThinking,
58
59
  // Alibaba also rejects an explicit budget that is not below `max_completion_tokens`.
59
60
  thinking_budget: opts.thinkingBudget === undefined
@@ -359,6 +359,7 @@ const AnthropicStreamDelta = Schema.Struct({
359
359
  signature: Schema.optional(Schema.String),
360
360
  stop_reason: optionalNull(Schema.String),
361
361
  stop_sequence: optionalNull(Schema.String),
362
+ stop_details: optionalNull(Schema.Struct({ category: optionalNull(Schema.String), explanation: optionalNull(Schema.String) })),
362
363
  });
363
364
  const decodeAnthropicStreamDelta = Schema.decodeUnknownOption(AnthropicStreamDelta);
364
365
  const AnthropicEvent = Schema.Struct({
@@ -677,16 +678,14 @@ const requireThinkingSignature = (request) => {
677
678
  // Mid-conversation system messages became available with Opus 4.8 and version
678
679
  // 5 of the other supported Claude families. Treat later family versions as
679
680
  // compatible without assuming that every Anthropic Messages model is Claude.
681
+ // Opus 4.8 and every Claude 5 model accept mid-conversation system messages; later versions inherit support.
680
682
  const supportsNativeSystemUpdates = (request) => {
681
- const match = /(?:^|[./])claude-(fable|haiku|mythos|opus|sonnet)-(\d+)(?:[.-](\d+))?/.exec(String(request.model.id).toLowerCase());
682
- if (!match)
683
+ const version = claudeVersion(String(request.model.id));
684
+ if (version === undefined)
683
685
  return false;
684
- const major = Number(match[2]);
685
- if (match[1] !== "opus")
686
- return major >= 5;
687
- if (major !== 4)
688
- return major >= 5;
689
- return match[3] !== undefined && match[3].length <= 2 && Number(match[3]) >= 8;
686
+ if (version.family === "opus" && version.major === 4)
687
+ return version.minor >= 8;
688
+ return version.major >= 5;
690
689
  };
691
690
  const endsInServerToolUse = (message) => {
692
691
  const last = message.content.at(-1);
@@ -851,6 +850,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (re
851
850
  }
852
851
  return messages;
853
852
  });
853
+ // Per-turn effort started with Claude Opus 5 and every Claude 5.1 model; later versions of any family inherit it.
854
854
  const supportsEffortUpdates = (model) => {
855
855
  const override = model.compatibility?.supportsEffortUpdates;
856
856
  if (override !== undefined)
@@ -858,10 +858,8 @@ const supportsEffortUpdates = (model) => {
858
858
  const version = claudeVersion(model.id);
859
859
  if (version === undefined)
860
860
  return false;
861
- if (version.family === "opus")
862
- return version.major >= 5;
863
- if (version.family !== "fable" && version.family !== "mythos")
864
- return false;
861
+ if (version.family === "opus" && version.major >= 5)
862
+ return true;
865
863
  return version.major > 5 || (version.major === 5 && version.minor >= 1);
866
864
  };
867
865
  const applyThinkingBindingDefault = (model, thinking) => {
@@ -1227,10 +1225,14 @@ const onMessageDelta = (state, event) => {
1227
1225
  const finishMetadata = stopSequence === null || stopSequence === undefined
1228
1226
  ? state.pendingFinish?.providerMetadata
1229
1227
  : providerMetadata(state.providerMetadataKey, { stopSequence });
1228
+ const category = event.delta?.stop_details?.category;
1229
+ const explanation = event.delta?.stop_details?.explanation;
1230
1230
  return {
1231
1231
  reason: {
1232
1232
  normalized: mapFinishReason(stopReason),
1233
1233
  raw: stopReason,
1234
+ ...(category ? { category } : {}),
1235
+ ...(explanation ? { explanation } : {}),
1234
1236
  },
1235
1237
  providerMetadata: finishMetadata,
1236
1238
  };
@@ -236,13 +236,28 @@ const lowerToolResult = Effect.fn("BedrockConverse.lowerToolResult")(function* (
236
236
  },
237
237
  };
238
238
  });
239
+ // Keep Claude and Nova tool-result images inline; put other models' images beside the result.
240
+ const keepToolImagesInline = (id) => id.includes("anthropic.claude-") || id.includes("amazon.nova-");
239
241
  const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (request, breakpoints) {
240
242
  const messages = [];
241
243
  const documentNames = new Set();
242
244
  // Mistral can reject replay IDs even when they satisfy Converse's broader ID syntax.
243
245
  const normalizeID = request.model.id.includes("mistral.") ? MistralToolID.normalizer(request) : (id) => id;
244
246
  const providerMetadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider);
247
+ const hoistImages = !keepToolImagesInline(request.model.id);
248
+ // Bedrock expects parallel tool results before any images hoisted beside them.
249
+ const pendingImages = [];
250
+ const flushImages = () => {
251
+ if (pendingImages.length === 0)
252
+ return;
253
+ const previous = messages.at(-1);
254
+ if (previous?.role === "user")
255
+ messages[messages.length - 1] = { role: "user", content: [...previous.content, ...pendingImages] };
256
+ pendingImages.length = 0;
257
+ };
245
258
  for (const message of request.messages) {
259
+ if (message.role !== "tool")
260
+ flushImages();
246
261
  if (message.role === "system") {
247
262
  const part = yield* ProviderShared.wrappedSystemUpdate("Bedrock Converse", message);
248
263
  const content = textWithCache(breakpoints, part.text, part.cache);
@@ -317,7 +332,20 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (requ
317
332
  for (const part of message.content) {
318
333
  if (!ProviderShared.supportsContent(part, ["tool-result"]))
319
334
  return yield* ProviderShared.unsupportedContent("Bedrock Converse", "tool", ["tool-result"]);
320
- content.push(yield* lowerToolResult(part, documentNames, normalizeID));
335
+ const result = yield* lowerToolResult(part, documentNames, normalizeID);
336
+ const images = hoistImages
337
+ ? result.toolResult.content.filter((item) => "image" in item)
338
+ : [];
339
+ const nonImageContent = result.toolResult.content.filter((item) => !("image" in item));
340
+ content.push(images.length === 0
341
+ ? result
342
+ : {
343
+ toolResult: {
344
+ ...result.toolResult,
345
+ content: nonImageContent.length > 0 ? nonImageContent : [{ text: "See attached image." }],
346
+ },
347
+ });
348
+ pendingImages.push(...images);
321
349
  const cachePoint = BedrockCache.block(breakpoints, part.cache);
322
350
  if (cachePoint)
323
351
  content.push(cachePoint);
@@ -328,6 +356,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (requ
328
356
  else
329
357
  messages.push({ role: "user", content });
330
358
  }
359
+ flushImages();
331
360
  return messages;
332
361
  });
333
362
  // System prompts share the cache-point convention: emit the text block, then
@@ -1191,6 +1191,8 @@ export declare const step: (state: ParserState, event: NormalizedEvent) => AIErr
1191
1191
  readonly reason: {
1192
1192
  readonly normalized: "length" | "stop" | "tool-calls" | "content-filter" | "error" | "unknown";
1193
1193
  readonly raw?: string | undefined;
1194
+ readonly category?: string | undefined;
1195
+ readonly explanation?: string | undefined;
1194
1196
  };
1195
1197
  readonly index: number;
1196
1198
  readonly providerMetadata?: {
@@ -1204,6 +1206,8 @@ export declare const step: (state: ParserState, event: NormalizedEvent) => AIErr
1204
1206
  readonly reason: {
1205
1207
  readonly normalized: "length" | "stop" | "tool-calls" | "content-filter" | "error" | "unknown";
1206
1208
  readonly raw?: string | undefined;
1209
+ readonly category?: string | undefined;
1210
+ readonly explanation?: string | undefined;
1207
1211
  };
1208
1212
  readonly providerMetadata?: {
1209
1213
  readonly [x: string]: {