@opencode/ai 0.0.0-dev-20386 → 0.0.0-dev-20389

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.
@@ -47,6 +47,7 @@ const RESPECTS_INLINE_HINTS = new Set([
47
47
  "zai-coding-messages",
48
48
  "bedrock-converse",
49
49
  "openrouter",
50
+ "digitalocean",
50
51
  ]);
51
52
  // OpenRouter upstreams other than Anthropic and Alibaba Qwen cache without breakpoints. Gemini uses only the last
52
53
  // breakpoint, so a conversation-tail breakpoint writes a new cache every step and costs more than none. Qwen ignores
@@ -186,6 +186,7 @@ export declare const protocol: Protocol<{
186
186
  } | null | undefined;
187
187
  readonly usage?: {
188
188
  readonly [x: string]: unknown;
189
+ readonly cache_read_input_tokens?: number | null | undefined;
189
190
  readonly cached_tokens?: number | null | undefined;
190
191
  readonly prompt_tokens?: number | null | undefined;
191
192
  readonly completion_tokens?: number | null | undefined;
@@ -196,6 +197,7 @@ export declare const protocol: Protocol<{
196
197
  readonly cache_write_tokens?: number | null | undefined;
197
198
  } | null | undefined;
198
199
  readonly prompt_cache_hit_tokens?: number | null | undefined;
200
+ readonly cache_created_input_tokens?: number | null | undefined;
199
201
  readonly completion_tokens_details?: {
200
202
  readonly [x: string]: unknown;
201
203
  readonly reasoning_tokens?: number | null | undefined;
@@ -225,6 +227,7 @@ export declare const protocol: Protocol<{
225
227
  } | null | undefined;
226
228
  readonly usage?: {
227
229
  readonly [x: string]: unknown;
230
+ readonly cache_read_input_tokens?: number | null | undefined;
228
231
  readonly cached_tokens?: number | null | undefined;
229
232
  readonly prompt_tokens?: number | null | undefined;
230
233
  readonly completion_tokens?: number | null | undefined;
@@ -235,6 +238,7 @@ export declare const protocol: Protocol<{
235
238
  readonly cache_write_tokens?: number | null | undefined;
236
239
  } | null | undefined;
237
240
  readonly prompt_cache_hit_tokens?: number | null | undefined;
241
+ readonly cache_created_input_tokens?: number | null | undefined;
238
242
  readonly completion_tokens_details?: {
239
243
  readonly [x: string]: unknown;
240
244
  readonly reasoning_tokens?: number | null | undefined;
@@ -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
@@ -315,6 +315,8 @@ export declare const OpenAIChatEvent: Schema.StructWithRest<Schema.Struct<{
315
315
  readonly total_tokens: Schema.optional<Schema.NullOr<Schema.Number>>;
316
316
  readonly cached_tokens: Schema.optional<Schema.NullOr<Schema.Number>>;
317
317
  readonly prompt_cache_hit_tokens: Schema.optional<Schema.NullOr<Schema.Number>>;
318
+ readonly cache_read_input_tokens: Schema.optional<Schema.NullOr<Schema.Number>>;
319
+ readonly cache_created_input_tokens: Schema.optional<Schema.NullOr<Schema.Number>>;
318
320
  readonly prompt_tokens_details: Schema.optional<Schema.NullOr<Schema.StructWithRest<Schema.Struct<{
319
321
  readonly cached_tokens: Schema.optional<Schema.NullOr<Schema.Number>>;
320
322
  readonly cache_write_tokens: Schema.optional<Schema.NullOr<Schema.Number>>;
@@ -332,6 +334,8 @@ export declare const OpenAIChatEvent: Schema.StructWithRest<Schema.Struct<{
332
334
  readonly total_tokens: Schema.optional<Schema.NullOr<Schema.Number>>;
333
335
  readonly cached_tokens: Schema.optional<Schema.NullOr<Schema.Number>>;
334
336
  readonly prompt_cache_hit_tokens: Schema.optional<Schema.NullOr<Schema.Number>>;
337
+ readonly cache_read_input_tokens: Schema.optional<Schema.NullOr<Schema.Number>>;
338
+ readonly cache_created_input_tokens: Schema.optional<Schema.NullOr<Schema.Number>>;
335
339
  readonly prompt_tokens_details: Schema.optional<Schema.NullOr<Schema.StructWithRest<Schema.Struct<{
336
340
  readonly cached_tokens: Schema.optional<Schema.NullOr<Schema.Number>>;
337
341
  readonly cache_write_tokens: Schema.optional<Schema.NullOr<Schema.Number>>;
@@ -755,6 +759,7 @@ export declare const protocol: Protocol<{
755
759
  } | null | undefined;
756
760
  readonly usage?: {
757
761
  readonly [x: string]: unknown;
762
+ readonly cache_read_input_tokens?: number | null | undefined;
758
763
  readonly cached_tokens?: number | null | undefined;
759
764
  readonly prompt_tokens?: number | null | undefined;
760
765
  readonly completion_tokens?: number | null | undefined;
@@ -765,6 +770,7 @@ export declare const protocol: Protocol<{
765
770
  readonly cache_write_tokens?: number | null | undefined;
766
771
  } | null | undefined;
767
772
  readonly prompt_cache_hit_tokens?: number | null | undefined;
773
+ readonly cache_created_input_tokens?: number | null | undefined;
768
774
  readonly completion_tokens_details?: {
769
775
  readonly [x: string]: unknown;
770
776
  readonly reasoning_tokens?: number | null | undefined;
@@ -794,6 +800,7 @@ export declare const protocol: Protocol<{
794
800
  } | null | undefined;
795
801
  readonly usage?: {
796
802
  readonly [x: string]: unknown;
803
+ readonly cache_read_input_tokens?: number | null | undefined;
797
804
  readonly cached_tokens?: number | null | undefined;
798
805
  readonly prompt_tokens?: number | null | undefined;
799
806
  readonly completion_tokens?: number | null | undefined;
@@ -804,6 +811,7 @@ export declare const protocol: Protocol<{
804
811
  readonly cache_write_tokens?: number | null | undefined;
805
812
  } | null | undefined;
806
813
  readonly prompt_cache_hit_tokens?: number | null | undefined;
814
+ readonly cache_created_input_tokens?: number | null | undefined;
807
815
  readonly completion_tokens_details?: {
808
816
  readonly [x: string]: unknown;
809
817
  readonly reasoning_tokens?: number | null | undefined;
@@ -161,9 +161,11 @@ const OpenAIChatUsage = Schema.StructWithRest(Schema.Struct({
161
161
  prompt_tokens: optionalNull(Schema.Number),
162
162
  completion_tokens: optionalNull(Schema.Number),
163
163
  total_tokens: optionalNull(Schema.Number),
164
- // Zai reports cache hits as top-level `cached_tokens`; DeepSeek uses `prompt_cache_hit_tokens`.
164
+ // Provider-specific cache accounting fields.
165
165
  cached_tokens: optionalNull(Schema.Number),
166
166
  prompt_cache_hit_tokens: optionalNull(Schema.Number),
167
+ cache_read_input_tokens: optionalNull(Schema.Number),
168
+ cache_created_input_tokens: optionalNull(Schema.Number),
167
169
  prompt_tokens_details: optionalNull(Schema.StructWithRest(Schema.Struct({
168
170
  cached_tokens: optionalNull(Schema.Number),
169
171
  cache_write_tokens: optionalNull(Schema.Number),
@@ -719,17 +721,19 @@ const mapFinishReason = Effect.fn("OpenAIChat.mapFinishReason")(function* (event
719
721
  // satisfied on both sides.
720
722
  // Providers differ on cache-hit location: OpenAI uses
721
723
  // `prompt_tokens_details.cached_tokens`, DeepSeek uses
722
- // `prompt_cache_hit_tokens`, and Zai uses top-level `cached_tokens`.
724
+ // `prompt_cache_hit_tokens`, Zai uses top-level `cached_tokens`, and
725
+ // DigitalOcean uses top-level `cache_read_input_tokens` / `cache_created_input_tokens`.
723
726
  const mapUsage = (usage, providerMetadataKey) => {
724
727
  if (!usage)
725
728
  return undefined;
726
729
  const input = usage.prompt_tokens ?? undefined;
727
730
  const output = usage.completion_tokens ?? undefined;
728
- const cached = (usage.prompt_tokens_details?.cached_tokens ??
731
+ const cached = usage.prompt_tokens_details?.cached_tokens ??
729
732
  usage.prompt_cache_hit_tokens ??
730
733
  usage.cached_tokens ??
731
- undefined);
732
- const cacheWrite = usage.prompt_tokens_details?.cache_write_tokens ?? undefined;
734
+ usage.cache_read_input_tokens ??
735
+ undefined;
736
+ const cacheWrite = usage.prompt_tokens_details?.cache_write_tokens ?? usage.cache_created_input_tokens ?? undefined;
733
737
  const reasoning = usage.completion_tokens_details?.reasoning_tokens ?? undefined;
734
738
  const nonCached = ProviderShared.subtractTokens(input, ProviderShared.sumTokens(cached, cacheWrite));
735
739
  return new Usage({
@@ -1,6 +1,11 @@
1
+ import type { CacheHint } from "../../schema/index.js";
1
2
  export interface Breakpoints {
2
3
  remaining: number;
3
4
  dropped: number;
4
5
  }
5
6
  export declare const newBreakpoints: (cap: number) => Breakpoints;
6
7
  export declare const ttlBucket: (ttlSeconds: number | undefined) => "1h" | undefined;
8
+ export declare const cacheControl: () => (cache: CacheHint | undefined) => {
9
+ type: "ephemeral";
10
+ ttl: "1h" | undefined;
11
+ } | undefined;
@@ -1,5 +1,13 @@
1
- // Shared counter and TTL mapping for provider cache-marker lowering.
2
1
  export const newBreakpoints = (cap) => ({ remaining: cap, dropped: 0 });
3
2
  // Requests of at least one hour use the explicit `"1h"` bucket; shorter
4
3
  // requests omit the wire TTL and use the provider default.
5
4
  export const ttlBucket = (ttlSeconds) => ttlSeconds !== undefined && ttlSeconds >= 3600 ? "1h" : undefined;
5
+ export const cacheControl = () => {
6
+ const breakpoints = newBreakpoints(4);
7
+ return (cache) => {
8
+ if (cache === undefined || breakpoints.remaining === 0)
9
+ return undefined;
10
+ breakpoints.remaining -= 1;
11
+ return { type: "ephemeral", ttl: ttlBucket(cache.ttlSeconds) };
12
+ };
13
+ };
@@ -163,6 +163,7 @@ export declare const protocol: Protocol<{
163
163
  } | null | undefined;
164
164
  readonly usage?: {
165
165
  readonly [x: string]: unknown;
166
+ readonly cache_read_input_tokens?: number | null | undefined;
166
167
  readonly cached_tokens?: number | null | undefined;
167
168
  readonly prompt_tokens?: number | null | undefined;
168
169
  readonly completion_tokens?: number | null | undefined;
@@ -173,6 +174,7 @@ export declare const protocol: Protocol<{
173
174
  readonly cache_write_tokens?: number | null | undefined;
174
175
  } | null | undefined;
175
176
  readonly prompt_cache_hit_tokens?: number | null | undefined;
177
+ readonly cache_created_input_tokens?: number | null | undefined;
176
178
  readonly completion_tokens_details?: {
177
179
  readonly [x: string]: unknown;
178
180
  readonly reasoning_tokens?: number | null | undefined;
@@ -202,6 +204,7 @@ export declare const protocol: Protocol<{
202
204
  } | null | undefined;
203
205
  readonly usage?: {
204
206
  readonly [x: string]: unknown;
207
+ readonly cache_read_input_tokens?: number | null | undefined;
205
208
  readonly cached_tokens?: number | null | undefined;
206
209
  readonly prompt_tokens?: number | null | undefined;
207
210
  readonly completion_tokens?: number | null | undefined;
@@ -212,6 +215,7 @@ export declare const protocol: Protocol<{
212
215
  readonly cache_write_tokens?: number | null | undefined;
213
216
  } | null | undefined;
214
217
  readonly prompt_cache_hit_tokens?: number | null | undefined;
218
+ readonly cache_created_input_tokens?: number | null | undefined;
215
219
  readonly completion_tokens_details?: {
216
220
  readonly [x: string]: unknown;
217
221
  readonly reasoning_tokens?: number | null | undefined;
@@ -0,0 +1,469 @@
1
+ import type { ProviderPackage } from "../provider-package.js";
2
+ import { OpenAIChat } from "../protocols/openai-chat.js";
3
+ import { type ProviderAuthOption } from "../route/auth-options.js";
4
+ import { Route, type RouteDefaultsInput } from "../route/client.js";
5
+ import { Protocol } from "../route/protocol.js";
6
+ import { type ModelID } from "../schema/index.js";
7
+ import type { OpenAIProviderOptionsInput } from "./openai-options.js";
8
+ export declare const id: string & import("effect/Brand").Brand<"AI.ProviderID">;
9
+ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> & ProviderAuthOption<"optional"> & {
10
+ readonly baseURL?: string;
11
+ readonly providerOptions?: OpenAIProviderOptionsInput;
12
+ };
13
+ export type Settings = ProviderPackage.Settings & OpenAIProviderOptionsInput & {
14
+ readonly apiKey?: string;
15
+ };
16
+ export declare const protocol: Protocol<{
17
+ readonly model: string;
18
+ readonly messages: readonly ({
19
+ readonly role: "system";
20
+ readonly content: string | readonly ({
21
+ readonly type: "text";
22
+ readonly text: string;
23
+ readonly cache_control?: {
24
+ readonly type: "ephemeral";
25
+ readonly ttl?: string | undefined;
26
+ } | undefined;
27
+ } | {
28
+ readonly type: "image_url";
29
+ readonly image_url: {
30
+ readonly url: string;
31
+ };
32
+ } | {
33
+ readonly type: "file";
34
+ readonly file: {
35
+ readonly filename: string;
36
+ readonly file_data: string;
37
+ };
38
+ })[];
39
+ } | {
40
+ readonly role: "user";
41
+ readonly content: string | readonly ({
42
+ readonly type: "text";
43
+ readonly text: string;
44
+ readonly cache_control?: {
45
+ readonly type: "ephemeral";
46
+ readonly ttl?: string | undefined;
47
+ } | undefined;
48
+ } | {
49
+ readonly type: "image_url";
50
+ readonly image_url: {
51
+ readonly url: string;
52
+ };
53
+ } | {
54
+ readonly type: "file";
55
+ readonly file: {
56
+ readonly filename: string;
57
+ readonly file_data: string;
58
+ };
59
+ })[];
60
+ } | {
61
+ readonly [x: string]: unknown;
62
+ readonly content: string | readonly {
63
+ readonly type: "text";
64
+ readonly text: string;
65
+ readonly cache_control?: {
66
+ readonly type: "ephemeral";
67
+ readonly ttl?: string | undefined;
68
+ } | undefined;
69
+ }[] | null;
70
+ readonly role: "assistant";
71
+ readonly reasoning?: string | undefined;
72
+ readonly reasoning_content?: string | undefined;
73
+ readonly reasoning_text?: string | undefined;
74
+ readonly tool_calls?: readonly {
75
+ readonly function: {
76
+ readonly name: string;
77
+ readonly arguments: string;
78
+ };
79
+ readonly id: string;
80
+ readonly type: "function";
81
+ readonly extra_content?: {
82
+ readonly google: {
83
+ readonly thought_signature: string;
84
+ };
85
+ } | undefined;
86
+ }[] | undefined;
87
+ readonly reasoning_details?: unknown;
88
+ } | {
89
+ readonly role: "tool";
90
+ readonly tool_call_id: string;
91
+ readonly content: string | readonly {
92
+ readonly type: "text";
93
+ readonly text: string;
94
+ readonly cache_control?: {
95
+ readonly type: "ephemeral";
96
+ readonly ttl?: string | undefined;
97
+ } | undefined;
98
+ }[];
99
+ })[];
100
+ readonly stream: true;
101
+ readonly max_completion_tokens?: number | undefined;
102
+ readonly max_tokens?: number | undefined;
103
+ readonly tools?: readonly {
104
+ readonly function: {
105
+ readonly description: string;
106
+ readonly name: string;
107
+ readonly parameters: {
108
+ readonly [x: string]: unknown;
109
+ };
110
+ readonly strict?: boolean | undefined;
111
+ };
112
+ readonly type: "function";
113
+ readonly cache_control?: {
114
+ readonly type: "ephemeral";
115
+ readonly ttl?: string | undefined;
116
+ } | undefined;
117
+ }[] | undefined;
118
+ readonly stop?: readonly string[] | undefined;
119
+ readonly temperature?: number | undefined;
120
+ readonly seed?: number | undefined;
121
+ readonly tool_choice?: "required" | "auto" | "none" | {
122
+ readonly type: "function";
123
+ readonly function: {
124
+ readonly name: string;
125
+ };
126
+ } | undefined;
127
+ readonly top_p?: number | undefined;
128
+ readonly frequency_penalty?: number | undefined;
129
+ readonly presence_penalty?: number | undefined;
130
+ readonly prompt_cache_key?: string | undefined;
131
+ readonly reasoning_effort?: import("../schema/options.js").ReasoningEffort | undefined;
132
+ readonly store?: boolean | undefined;
133
+ readonly stream_options?: {
134
+ readonly include_usage: boolean;
135
+ } | undefined;
136
+ readonly tool_stream?: boolean | undefined;
137
+ }, string, "[DONE]" | {
138
+ readonly [x: string]: unknown;
139
+ readonly error?: {
140
+ readonly [x: string]: unknown;
141
+ readonly message: string;
142
+ readonly code?: string | number | null | undefined;
143
+ } | null | undefined;
144
+ readonly usage?: {
145
+ readonly [x: string]: unknown;
146
+ readonly cache_read_input_tokens?: number | null | undefined;
147
+ readonly cached_tokens?: number | null | undefined;
148
+ readonly prompt_tokens?: number | null | undefined;
149
+ readonly completion_tokens?: number | null | undefined;
150
+ readonly total_tokens?: number | null | undefined;
151
+ readonly prompt_tokens_details?: {
152
+ readonly [x: string]: unknown;
153
+ readonly cached_tokens?: number | null | undefined;
154
+ readonly cache_write_tokens?: number | null | undefined;
155
+ } | null | undefined;
156
+ readonly prompt_cache_hit_tokens?: number | null | undefined;
157
+ readonly cache_created_input_tokens?: number | null | undefined;
158
+ readonly completion_tokens_details?: {
159
+ readonly [x: string]: unknown;
160
+ readonly reasoning_tokens?: number | null | undefined;
161
+ readonly accepted_prediction_tokens?: number | null | undefined;
162
+ readonly rejected_prediction_tokens?: number | null | undefined;
163
+ } | null | undefined;
164
+ } | null | undefined;
165
+ readonly choices?: readonly {
166
+ readonly [x: string]: unknown;
167
+ readonly delta?: {
168
+ readonly [x: string]: unknown;
169
+ readonly reasoning?: string | null | undefined;
170
+ readonly reasoning_content?: string | null | undefined;
171
+ readonly reasoning_text?: string | null | undefined;
172
+ readonly content?: string | null | undefined;
173
+ readonly refusal?: string | null | undefined;
174
+ readonly tool_calls?: readonly {
175
+ readonly function?: {
176
+ readonly name?: string | null | undefined;
177
+ readonly arguments?: string | null | undefined;
178
+ } | null | undefined;
179
+ readonly id?: string | null | undefined;
180
+ readonly index?: number | null | undefined;
181
+ readonly extra_content?: unknown;
182
+ }[] | null | undefined;
183
+ readonly reasoning_details?: unknown;
184
+ } | null | undefined;
185
+ readonly usage?: {
186
+ readonly [x: string]: unknown;
187
+ readonly cache_read_input_tokens?: number | null | undefined;
188
+ readonly cached_tokens?: number | null | undefined;
189
+ readonly prompt_tokens?: number | null | undefined;
190
+ readonly completion_tokens?: number | null | undefined;
191
+ readonly total_tokens?: number | null | undefined;
192
+ readonly prompt_tokens_details?: {
193
+ readonly [x: string]: unknown;
194
+ readonly cached_tokens?: number | null | undefined;
195
+ readonly cache_write_tokens?: number | null | undefined;
196
+ } | null | undefined;
197
+ readonly prompt_cache_hit_tokens?: number | null | undefined;
198
+ readonly cache_created_input_tokens?: number | null | undefined;
199
+ readonly completion_tokens_details?: {
200
+ readonly [x: string]: unknown;
201
+ readonly reasoning_tokens?: number | null | undefined;
202
+ readonly accepted_prediction_tokens?: number | null | undefined;
203
+ readonly rejected_prediction_tokens?: number | null | undefined;
204
+ } | null | undefined;
205
+ } | null | undefined;
206
+ readonly finish_reason?: string | null | undefined;
207
+ readonly native_finish_reason?: string | null | undefined;
208
+ }[] | null | undefined;
209
+ }, OpenAIChat.ParserState>;
210
+ export declare const route: Route<{
211
+ readonly model: string;
212
+ readonly messages: readonly ({
213
+ readonly role: "system";
214
+ readonly content: string | readonly ({
215
+ readonly type: "text";
216
+ readonly text: string;
217
+ readonly cache_control?: {
218
+ readonly type: "ephemeral";
219
+ readonly ttl?: string | undefined;
220
+ } | undefined;
221
+ } | {
222
+ readonly type: "image_url";
223
+ readonly image_url: {
224
+ readonly url: string;
225
+ };
226
+ } | {
227
+ readonly type: "file";
228
+ readonly file: {
229
+ readonly filename: string;
230
+ readonly file_data: string;
231
+ };
232
+ })[];
233
+ } | {
234
+ readonly role: "user";
235
+ readonly content: string | readonly ({
236
+ readonly type: "text";
237
+ readonly text: string;
238
+ readonly cache_control?: {
239
+ readonly type: "ephemeral";
240
+ readonly ttl?: string | undefined;
241
+ } | undefined;
242
+ } | {
243
+ readonly type: "image_url";
244
+ readonly image_url: {
245
+ readonly url: string;
246
+ };
247
+ } | {
248
+ readonly type: "file";
249
+ readonly file: {
250
+ readonly filename: string;
251
+ readonly file_data: string;
252
+ };
253
+ })[];
254
+ } | {
255
+ readonly [x: string]: unknown;
256
+ readonly content: string | readonly {
257
+ readonly type: "text";
258
+ readonly text: string;
259
+ readonly cache_control?: {
260
+ readonly type: "ephemeral";
261
+ readonly ttl?: string | undefined;
262
+ } | undefined;
263
+ }[] | null;
264
+ readonly role: "assistant";
265
+ readonly reasoning?: string | undefined;
266
+ readonly reasoning_content?: string | undefined;
267
+ readonly reasoning_text?: string | undefined;
268
+ readonly tool_calls?: readonly {
269
+ readonly function: {
270
+ readonly name: string;
271
+ readonly arguments: string;
272
+ };
273
+ readonly id: string;
274
+ readonly type: "function";
275
+ readonly extra_content?: {
276
+ readonly google: {
277
+ readonly thought_signature: string;
278
+ };
279
+ } | undefined;
280
+ }[] | undefined;
281
+ readonly reasoning_details?: unknown;
282
+ } | {
283
+ readonly role: "tool";
284
+ readonly tool_call_id: string;
285
+ readonly content: string | readonly {
286
+ readonly type: "text";
287
+ readonly text: string;
288
+ readonly cache_control?: {
289
+ readonly type: "ephemeral";
290
+ readonly ttl?: string | undefined;
291
+ } | undefined;
292
+ }[];
293
+ })[];
294
+ readonly stream: true;
295
+ readonly max_completion_tokens?: number | undefined;
296
+ readonly max_tokens?: number | undefined;
297
+ readonly tools?: readonly {
298
+ readonly function: {
299
+ readonly description: string;
300
+ readonly name: string;
301
+ readonly parameters: {
302
+ readonly [x: string]: unknown;
303
+ };
304
+ readonly strict?: boolean | undefined;
305
+ };
306
+ readonly type: "function";
307
+ readonly cache_control?: {
308
+ readonly type: "ephemeral";
309
+ readonly ttl?: string | undefined;
310
+ } | undefined;
311
+ }[] | undefined;
312
+ readonly stop?: readonly string[] | undefined;
313
+ readonly temperature?: number | undefined;
314
+ readonly seed?: number | undefined;
315
+ readonly tool_choice?: "required" | "auto" | "none" | {
316
+ readonly type: "function";
317
+ readonly function: {
318
+ readonly name: string;
319
+ };
320
+ } | undefined;
321
+ readonly top_p?: number | undefined;
322
+ readonly frequency_penalty?: number | undefined;
323
+ readonly presence_penalty?: number | undefined;
324
+ readonly prompt_cache_key?: string | undefined;
325
+ readonly reasoning_effort?: import("../schema/options.js").ReasoningEffort | undefined;
326
+ readonly store?: boolean | undefined;
327
+ readonly stream_options?: {
328
+ readonly include_usage: boolean;
329
+ } | undefined;
330
+ readonly tool_stream?: boolean | undefined;
331
+ }, import("../route/transport/http.js").HttpPrepared<string>, import("../route/client.js").CompactionOperations | undefined>;
332
+ export declare const routes: Route<{
333
+ readonly model: string;
334
+ readonly messages: readonly ({
335
+ readonly role: "system";
336
+ readonly content: string | readonly ({
337
+ readonly type: "text";
338
+ readonly text: string;
339
+ readonly cache_control?: {
340
+ readonly type: "ephemeral";
341
+ readonly ttl?: string | undefined;
342
+ } | undefined;
343
+ } | {
344
+ readonly type: "image_url";
345
+ readonly image_url: {
346
+ readonly url: string;
347
+ };
348
+ } | {
349
+ readonly type: "file";
350
+ readonly file: {
351
+ readonly filename: string;
352
+ readonly file_data: string;
353
+ };
354
+ })[];
355
+ } | {
356
+ readonly role: "user";
357
+ readonly content: string | readonly ({
358
+ readonly type: "text";
359
+ readonly text: string;
360
+ readonly cache_control?: {
361
+ readonly type: "ephemeral";
362
+ readonly ttl?: string | undefined;
363
+ } | undefined;
364
+ } | {
365
+ readonly type: "image_url";
366
+ readonly image_url: {
367
+ readonly url: string;
368
+ };
369
+ } | {
370
+ readonly type: "file";
371
+ readonly file: {
372
+ readonly filename: string;
373
+ readonly file_data: string;
374
+ };
375
+ })[];
376
+ } | {
377
+ readonly [x: string]: unknown;
378
+ readonly content: string | readonly {
379
+ readonly type: "text";
380
+ readonly text: string;
381
+ readonly cache_control?: {
382
+ readonly type: "ephemeral";
383
+ readonly ttl?: string | undefined;
384
+ } | undefined;
385
+ }[] | null;
386
+ readonly role: "assistant";
387
+ readonly reasoning?: string | undefined;
388
+ readonly reasoning_content?: string | undefined;
389
+ readonly reasoning_text?: string | undefined;
390
+ readonly tool_calls?: readonly {
391
+ readonly function: {
392
+ readonly name: string;
393
+ readonly arguments: string;
394
+ };
395
+ readonly id: string;
396
+ readonly type: "function";
397
+ readonly extra_content?: {
398
+ readonly google: {
399
+ readonly thought_signature: string;
400
+ };
401
+ } | undefined;
402
+ }[] | undefined;
403
+ readonly reasoning_details?: unknown;
404
+ } | {
405
+ readonly role: "tool";
406
+ readonly tool_call_id: string;
407
+ readonly content: string | readonly {
408
+ readonly type: "text";
409
+ readonly text: string;
410
+ readonly cache_control?: {
411
+ readonly type: "ephemeral";
412
+ readonly ttl?: string | undefined;
413
+ } | undefined;
414
+ }[];
415
+ })[];
416
+ readonly stream: true;
417
+ readonly max_completion_tokens?: number | undefined;
418
+ readonly max_tokens?: number | undefined;
419
+ readonly tools?: readonly {
420
+ readonly function: {
421
+ readonly description: string;
422
+ readonly name: string;
423
+ readonly parameters: {
424
+ readonly [x: string]: unknown;
425
+ };
426
+ readonly strict?: boolean | undefined;
427
+ };
428
+ readonly type: "function";
429
+ readonly cache_control?: {
430
+ readonly type: "ephemeral";
431
+ readonly ttl?: string | undefined;
432
+ } | undefined;
433
+ }[] | undefined;
434
+ readonly stop?: readonly string[] | undefined;
435
+ readonly temperature?: number | undefined;
436
+ readonly seed?: number | undefined;
437
+ readonly tool_choice?: "required" | "auto" | "none" | {
438
+ readonly type: "function";
439
+ readonly function: {
440
+ readonly name: string;
441
+ };
442
+ } | undefined;
443
+ readonly top_p?: number | undefined;
444
+ readonly frequency_penalty?: number | undefined;
445
+ readonly presence_penalty?: number | undefined;
446
+ readonly prompt_cache_key?: string | undefined;
447
+ readonly reasoning_effort?: import("../schema/options.js").ReasoningEffort | undefined;
448
+ readonly store?: boolean | undefined;
449
+ readonly stream_options?: {
450
+ readonly include_usage: boolean;
451
+ } | undefined;
452
+ readonly tool_stream?: boolean | undefined;
453
+ }, import("../route/transport/http.js").HttpPrepared<string>, import("../route/client.js").CompactionOperations | undefined>[];
454
+ export declare const configure: (input?: LanguageModelOptions) => {
455
+ id: string & import("effect/Brand").Brand<"AI.ProviderID">;
456
+ model: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<import("./openai-options.js").OpenAIOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
457
+ configure: (input?: LanguageModelOptions) => /*elided*/ any;
458
+ };
459
+ export declare const provider: {
460
+ id: string & import("effect/Brand").Brand<"AI.ProviderID">;
461
+ model: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<import("./openai-options.js").OpenAIOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
462
+ configure: (input?: LanguageModelOptions) => {
463
+ id: string & import("effect/Brand").Brand<"AI.ProviderID">;
464
+ model: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<import("./openai-options.js").OpenAIOptionsInput, import("../route/client.js").CompactionOperations | undefined>;
465
+ configure: /*elided*/ any;
466
+ };
467
+ };
468
+ export declare const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"];
469
+ export * as DigitalOcean from "./digitalocean.js";
@@ -0,0 +1,53 @@
1
+ import { OpenAIChat } from "../protocols/openai-chat.js";
2
+ import { cacheControl } from "../protocols/utils/cache.js";
3
+ import { AuthOptions } from "../route/auth-options.js";
4
+ import { Route } from "../route/client.js";
5
+ import { Endpoint } from "../route/endpoint.js";
6
+ import { Protocol } from "../route/protocol.js";
7
+ import { ProviderID } from "../schema/index.js";
8
+ export const id = ProviderID.make("digitalocean");
9
+ const baseURL = "https://inference.do-ai.run/v1";
10
+ export const protocol = Protocol.make({
11
+ id: "digitalocean-chat",
12
+ body: {
13
+ schema: OpenAIChat.protocol.body.schema,
14
+ from: (request) => OpenAIChat.fromRequest(request, { cacheControl: cacheControl() }),
15
+ },
16
+ stream: OpenAIChat.protocol.stream,
17
+ });
18
+ export const route = Route.make({
19
+ id: "digitalocean",
20
+ provider: id,
21
+ providerMetadataKey: "digitalocean",
22
+ protocol,
23
+ endpoint: Endpoint.path("/chat/completions", { baseURL }),
24
+ framing: OpenAIChat.framing,
25
+ });
26
+ export const routes = [route];
27
+ export const configure = (input = {}) => {
28
+ const { apiKey: _apiKey, auth: _auth, baseURL: endpoint, ...defaults } = input;
29
+ const configured = route.with({
30
+ ...defaults,
31
+ endpoint: { baseURL: endpoint ?? baseURL },
32
+ auth: AuthOptions.bearer(input, ["DIGITALOCEAN_ACCESS_TOKEN", "DIGITALOCEAN_API_KEY", "DO_INFERENCE_API_KEY"]),
33
+ });
34
+ return {
35
+ id,
36
+ model: (modelID) => configured.model({
37
+ id: modelID,
38
+ compatibility: {
39
+ supportsPromptCacheKey: true,
40
+ },
41
+ }),
42
+ configure,
43
+ };
44
+ };
45
+ export const provider = configure();
46
+ export const model = (modelID, { apiKey, baseURL, body, headers, ...providerOptions }) => configure({
47
+ apiKey,
48
+ baseURL,
49
+ headers,
50
+ http: { body },
51
+ providerOptions,
52
+ }).model(modelID);
53
+ export * as DigitalOcean from "./digitalocean.js";
@@ -156,6 +156,7 @@ export declare const protocol: Protocol<{
156
156
  } | null | undefined;
157
157
  readonly usage?: {
158
158
  readonly [x: string]: unknown;
159
+ readonly cache_read_input_tokens?: number | null | undefined;
159
160
  readonly cached_tokens?: number | null | undefined;
160
161
  readonly prompt_tokens?: number | null | undefined;
161
162
  readonly completion_tokens?: number | null | undefined;
@@ -166,6 +167,7 @@ export declare const protocol: Protocol<{
166
167
  readonly cache_write_tokens?: number | null | undefined;
167
168
  } | null | undefined;
168
169
  readonly prompt_cache_hit_tokens?: number | null | undefined;
170
+ readonly cache_created_input_tokens?: number | null | undefined;
169
171
  readonly completion_tokens_details?: {
170
172
  readonly [x: string]: unknown;
171
173
  readonly reasoning_tokens?: number | null | undefined;
@@ -195,6 +197,7 @@ export declare const protocol: Protocol<{
195
197
  } | null | undefined;
196
198
  readonly usage?: {
197
199
  readonly [x: string]: unknown;
200
+ readonly cache_read_input_tokens?: number | null | undefined;
198
201
  readonly cached_tokens?: number | null | undefined;
199
202
  readonly prompt_tokens?: number | null | undefined;
200
203
  readonly completion_tokens?: number | null | undefined;
@@ -205,6 +208,7 @@ export declare const protocol: Protocol<{
205
208
  readonly cache_write_tokens?: number | null | undefined;
206
209
  } | null | undefined;
207
210
  readonly prompt_cache_hit_tokens?: number | null | undefined;
211
+ readonly cache_created_input_tokens?: number | null | undefined;
208
212
  readonly completion_tokens_details?: {
209
213
  readonly [x: string]: unknown;
210
214
  readonly reasoning_tokens?: number | null | undefined;
@@ -14,6 +14,7 @@ export * as CloudflareWorkersAI from "./cloudflare-workers-ai.js";
14
14
  export * as DeepInfra from "./deepinfra.js";
15
15
  export * as Deepgram from "./deepgram.js";
16
16
  export * as DeepSeek from "./deepseek.js";
17
+ export * as DigitalOcean from "./digitalocean.js";
17
18
  export * as ElevenLabs from "./elevenlabs.js";
18
19
  export * as Fal from "./fal.js";
19
20
  export * as Fireworks from "./fireworks.js";
@@ -14,6 +14,7 @@ export * as CloudflareWorkersAI from "./cloudflare-workers-ai.js";
14
14
  export * as DeepInfra from "./deepinfra.js";
15
15
  export * as Deepgram from "./deepgram.js";
16
16
  export * as DeepSeek from "./deepseek.js";
17
+ export * as DigitalOcean from "./digitalocean.js";
17
18
  export * as ElevenLabs from "./elevenlabs.js";
18
19
  export * as Fal from "./fal.js";
19
20
  export * as Fireworks from "./fireworks.js";
@@ -338,6 +338,7 @@ export declare const protocol: Protocol<{
338
338
  } | null | undefined;
339
339
  readonly usage?: {
340
340
  readonly [x: string]: unknown;
341
+ readonly cache_read_input_tokens?: number | null | undefined;
341
342
  readonly cached_tokens?: number | null | undefined;
342
343
  readonly prompt_tokens?: number | null | undefined;
343
344
  readonly completion_tokens?: number | null | undefined;
@@ -348,6 +349,7 @@ export declare const protocol: Protocol<{
348
349
  readonly cache_write_tokens?: number | null | undefined;
349
350
  } | null | undefined;
350
351
  readonly prompt_cache_hit_tokens?: number | null | undefined;
352
+ readonly cache_created_input_tokens?: number | null | undefined;
351
353
  readonly completion_tokens_details?: {
352
354
  readonly [x: string]: unknown;
353
355
  readonly reasoning_tokens?: number | null | undefined;
@@ -377,6 +379,7 @@ export declare const protocol: Protocol<{
377
379
  } | null | undefined;
378
380
  readonly usage?: {
379
381
  readonly [x: string]: unknown;
382
+ readonly cache_read_input_tokens?: number | null | undefined;
380
383
  readonly cached_tokens?: number | null | undefined;
381
384
  readonly prompt_tokens?: number | null | undefined;
382
385
  readonly completion_tokens?: number | null | undefined;
@@ -387,6 +390,7 @@ export declare const protocol: Protocol<{
387
390
  readonly cache_write_tokens?: number | null | undefined;
388
391
  } | null | undefined;
389
392
  readonly prompt_cache_hit_tokens?: number | null | undefined;
393
+ readonly cache_created_input_tokens?: number | null | undefined;
390
394
  readonly completion_tokens_details?: {
391
395
  readonly [x: string]: unknown;
392
396
  readonly reasoning_tokens?: number | null | undefined;
@@ -6,7 +6,7 @@ import { AuthOptions } from "../route/auth-options.js";
6
6
  import { HttpOptions, ProviderID } from "../schema/index.js";
7
7
  import { SystemOne } from "../experimental/system-one.js";
8
8
  import { OpenAIChat } from "../protocols/openai-chat.js";
9
- import { newBreakpoints, ttlBucket } from "../protocols/utils/cache.js";
9
+ import { cacheControl } from "../protocols/utils/cache.js";
10
10
  import { isRecord, ProviderShared } from "../protocols/shared.js";
11
11
  export const id = ProviderID.make("openrouter");
12
12
  const baseURL = "https://openrouter.ai/api/v1";
@@ -47,18 +47,6 @@ export const protocol = Protocol.make({
47
47
  },
48
48
  stream: OpenAIChat.protocol.stream,
49
49
  });
50
- const cacheControl = () => {
51
- const breakpoints = newBreakpoints(4);
52
- return (cache) => {
53
- if (cache === undefined || breakpoints.remaining === 0)
54
- return undefined;
55
- breakpoints.remaining -= 1;
56
- return {
57
- type: "ephemeral",
58
- ...(ttlBucket(cache.ttlSeconds) === "1h" ? { ttl: "1h" } : {}),
59
- };
60
- };
61
- };
62
50
  // OpenRouter forwards `reasoning.max_tokens` as the upstream thinking budget. Upstreams such as Anthropic and Alibaba
63
51
  // reject one that is not below the output limit; 1,024 is Anthropic's minimum budget.
64
52
  const fitReasoning = (reasoning, maxTokens) => typeof reasoning.max_tokens === "number"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "0.0.0-dev-20386",
3
+ "version": "0.0.0-dev-20389",
4
4
  "name": "@opencode/ai",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@
34
34
  "devDependencies": {
35
35
  "@clack/prompts": "1.0.0-alpha.1",
36
36
  "@effect/platform-node": "4.0.0-rc.112",
37
- "@opencode/http-recorder": "0.0.0-dev-20386",
37
+ "@opencode/http-recorder": "0.0.0-dev-20389",
38
38
  "@tsconfig/bun": "1.0.9",
39
39
  "@types/bun": "1.4.0",
40
40
  "@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -44,7 +44,7 @@
44
44
  "@aws-sdk/credential-providers": "3.1057.0",
45
45
  "@smithy/eventstream-codec": "4.2.14",
46
46
  "@smithy/util-utf8": "4.2.2",
47
- "@opencode/schema": "0.0.0-dev-20386",
47
+ "@opencode/schema": "0.0.0-dev-20389",
48
48
  "aws4fetch": "1.0.20",
49
49
  "effect": "4.0.0-rc.112",
50
50
  "google-auth-library": "10.5.0"