@opencode/ai 2.0.20 → 2.0.21

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.
@@ -48,6 +48,18 @@ const RESPECTS_INLINE_HINTS = new Set([
48
48
  "bedrock-converse",
49
49
  "openrouter",
50
50
  ]);
51
+ // OpenRouter upstreams other than Anthropic and Alibaba Qwen cache without breakpoints. Gemini uses only the last
52
+ // breakpoint, so a conversation-tail breakpoint writes a new cache every step and costs more than none. Qwen ignores
53
+ // breakpoints on tool definitions and caches tools with the system prompt.
54
+ const openRouterPolicy = (modelID) => {
55
+ // `~anthropic/claude-sonnet-latest` style IDs are OpenRouter aliases for the latest model in a family.
56
+ const id = modelID.replace(/^~/, "");
57
+ if (id.startsWith("anthropic/"))
58
+ return AUTO;
59
+ if (id.startsWith("qwen/"))
60
+ return { system: true, messages: { tail: 1 } };
61
+ return NONE;
62
+ };
51
63
  const makeHint = (ttlSeconds) => ttlSeconds !== undefined ? new CacheHint({ type: "ephemeral", ttlSeconds }) : new CacheHint({ type: "ephemeral" });
52
64
  const markLastTool = (tools, hint, budget) => {
53
65
  const target = tools.at(-1);
@@ -128,9 +140,9 @@ const countHints = (request) => countToolHints(request.tools) +
128
140
  export const applyCachePolicy = (request) => {
129
141
  if (!RESPECTS_INLINE_HINTS.has(request.model.route.id))
130
142
  return request;
131
- if (request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto"))
132
- return request;
133
- const policy = resolve(request.cache);
143
+ const policy = request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto")
144
+ ? openRouterPolicy(request.model.id)
145
+ : resolve(request.cache);
134
146
  if (!policy.tools && !policy.system && !policy.messages)
135
147
  return request;
136
148
  const hint = makeHint(policy.ttlSeconds);
@@ -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;
@@ -12,6 +12,7 @@ declare const OpenAIChatCacheControl: Schema.Struct<{
12
12
  readonly type: Schema.Literal<"ephemeral">;
13
13
  readonly ttl: Schema.optional<Schema.String>;
14
14
  }>;
15
+ type OpenAIChatCacheControl = Schema.Schema.Type<typeof OpenAIChatCacheControl>;
15
16
  declare const ExtraContent: Schema.Struct<{
16
17
  readonly google: Schema.Struct<{
17
18
  readonly thought_signature: Schema.String;
@@ -94,7 +95,14 @@ export declare const bodyFields: {
94
95
  }>]>>]>;
95
96
  }>, Schema.StructWithRest<Schema.Struct<{
96
97
  readonly role: Schema.Literal<"assistant">;
97
- readonly content: Schema.NullOr<Schema.String>;
98
+ readonly content: Schema.NullOr<Schema.Union<readonly [Schema.String, Schema.$Array<Schema.Struct<{
99
+ readonly type: Schema.Literal<"text">;
100
+ readonly text: Schema.String;
101
+ readonly cache_control: Schema.optional<Schema.Struct<{
102
+ readonly type: Schema.Literal<"ephemeral">;
103
+ readonly ttl: Schema.optional<Schema.String>;
104
+ }>>;
105
+ }>>]>>;
98
106
  readonly tool_calls: Schema.optional<Schema.$Array<Schema.Struct<{
99
107
  readonly id: Schema.String;
100
108
  readonly type: Schema.tag<"function">;
@@ -112,18 +120,17 @@ export declare const bodyFields: {
112
120
  readonly reasoning: Schema.optional<Schema.String>;
113
121
  readonly reasoning_text: Schema.optional<Schema.String>;
114
122
  readonly reasoning_details: Schema.optional<Schema.Unknown>;
115
- readonly cache_control: Schema.optional<Schema.Struct<{
116
- readonly type: Schema.Literal<"ephemeral">;
117
- readonly ttl: Schema.optional<Schema.String>;
118
- }>>;
119
123
  }>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>, Schema.Struct<{
120
124
  readonly role: Schema.Literal<"tool">;
121
125
  readonly tool_call_id: Schema.String;
122
- readonly content: Schema.String;
123
- readonly cache_control: Schema.optional<Schema.Struct<{
124
- readonly type: Schema.Literal<"ephemeral">;
125
- readonly ttl: Schema.optional<Schema.String>;
126
- }>>;
126
+ readonly content: Schema.Union<readonly [Schema.String, Schema.$Array<Schema.Struct<{
127
+ readonly type: Schema.Literal<"text">;
128
+ readonly text: Schema.String;
129
+ readonly cache_control: Schema.optional<Schema.Struct<{
130
+ readonly type: Schema.Literal<"ephemeral">;
131
+ readonly ttl: Schema.optional<Schema.String>;
132
+ }>>;
133
+ }>>]>;
127
134
  }>]>>;
128
135
  tools: Schema.optional<Schema.$Array<Schema.Struct<{
129
136
  readonly type: Schema.tag<"function">;
@@ -207,7 +214,14 @@ declare const OpenAIChatBody: Schema.Struct<{
207
214
  }>]>>]>;
208
215
  }>, Schema.StructWithRest<Schema.Struct<{
209
216
  readonly role: Schema.Literal<"assistant">;
210
- readonly content: Schema.NullOr<Schema.String>;
217
+ readonly content: Schema.NullOr<Schema.Union<readonly [Schema.String, Schema.$Array<Schema.Struct<{
218
+ readonly type: Schema.Literal<"text">;
219
+ readonly text: Schema.String;
220
+ readonly cache_control: Schema.optional<Schema.Struct<{
221
+ readonly type: Schema.Literal<"ephemeral">;
222
+ readonly ttl: Schema.optional<Schema.String>;
223
+ }>>;
224
+ }>>]>>;
211
225
  readonly tool_calls: Schema.optional<Schema.$Array<Schema.Struct<{
212
226
  readonly id: Schema.String;
213
227
  readonly type: Schema.tag<"function">;
@@ -225,18 +239,17 @@ declare const OpenAIChatBody: Schema.Struct<{
225
239
  readonly reasoning: Schema.optional<Schema.String>;
226
240
  readonly reasoning_text: Schema.optional<Schema.String>;
227
241
  readonly reasoning_details: Schema.optional<Schema.Unknown>;
228
- readonly cache_control: Schema.optional<Schema.Struct<{
229
- readonly type: Schema.Literal<"ephemeral">;
230
- readonly ttl: Schema.optional<Schema.String>;
231
- }>>;
232
242
  }>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>, Schema.Struct<{
233
243
  readonly role: Schema.Literal<"tool">;
234
244
  readonly tool_call_id: Schema.String;
235
- readonly content: Schema.String;
236
- readonly cache_control: Schema.optional<Schema.Struct<{
237
- readonly type: Schema.Literal<"ephemeral">;
238
- readonly ttl: Schema.optional<Schema.String>;
239
- }>>;
245
+ readonly content: Schema.Union<readonly [Schema.String, Schema.$Array<Schema.Struct<{
246
+ readonly type: Schema.Literal<"text">;
247
+ readonly text: Schema.String;
248
+ readonly cache_control: Schema.optional<Schema.Struct<{
249
+ readonly type: Schema.Literal<"ephemeral">;
250
+ readonly ttl: Schema.optional<Schema.String>;
251
+ }>>;
252
+ }>>]>;
240
253
  }>]>>;
241
254
  tools: Schema.optional<Schema.$Array<Schema.Struct<{
242
255
  readonly type: Schema.tag<"function">;
@@ -360,7 +373,7 @@ export interface ParserState {
360
373
  readonly requireFinishReason: boolean;
361
374
  }
362
375
  interface LoweringOptions {
363
- readonly cacheControl?: (cache: CacheHint | undefined) => Schema.Schema.Type<typeof OpenAIChatCacheControl> | undefined;
376
+ readonly cacheControl?: (cache: CacheHint | undefined) => OpenAIChatCacheControl | undefined;
364
377
  readonly toolCallID?: (id: string) => string;
365
378
  }
366
379
  export declare const fromRequest: (request: LLMRequest<import("../schema/options.js").LanguageModel<{
@@ -425,15 +438,18 @@ export declare const fromRequest: (request: LLMRequest<import("../schema/options
425
438
  })[];
426
439
  } | {
427
440
  readonly [x: string]: unknown;
428
- readonly content: string | null;
441
+ readonly content: string | readonly {
442
+ readonly type: "text";
443
+ readonly text: string;
444
+ readonly cache_control?: {
445
+ readonly type: "ephemeral";
446
+ readonly ttl?: string | undefined;
447
+ } | undefined;
448
+ }[] | null;
429
449
  readonly role: "assistant";
430
450
  readonly reasoning?: string | undefined;
431
451
  readonly reasoning_content?: string | undefined;
432
452
  readonly reasoning_text?: string | undefined;
433
- readonly cache_control?: {
434
- readonly type: "ephemeral";
435
- readonly ttl?: string | undefined;
436
- } | undefined;
437
453
  readonly tool_calls?: readonly {
438
454
  readonly function: {
439
455
  readonly name: string;
@@ -449,13 +465,16 @@ export declare const fromRequest: (request: LLMRequest<import("../schema/options
449
465
  }[] | undefined;
450
466
  readonly reasoning_details?: unknown;
451
467
  } | {
452
- readonly content: string;
453
468
  readonly role: "tool";
454
469
  readonly tool_call_id: string;
455
- readonly cache_control?: {
456
- readonly type: "ephemeral";
457
- readonly ttl?: string | undefined;
458
- } | undefined;
470
+ readonly content: string | readonly {
471
+ readonly type: "text";
472
+ readonly text: string;
473
+ readonly cache_control?: {
474
+ readonly type: "ephemeral";
475
+ readonly ttl?: string | undefined;
476
+ } | undefined;
477
+ }[];
459
478
  })[];
460
479
  tools: {
461
480
  readonly function: {
@@ -539,15 +558,18 @@ export declare const fromRequest: (request: LLMRequest<import("../schema/options
539
558
  })[];
540
559
  } | {
541
560
  readonly [x: string]: unknown;
542
- readonly content: string | null;
561
+ readonly content: string | readonly {
562
+ readonly type: "text";
563
+ readonly text: string;
564
+ readonly cache_control?: {
565
+ readonly type: "ephemeral";
566
+ readonly ttl?: string | undefined;
567
+ } | undefined;
568
+ }[] | null;
543
569
  readonly role: "assistant";
544
570
  readonly reasoning?: string | undefined;
545
571
  readonly reasoning_content?: string | undefined;
546
572
  readonly reasoning_text?: string | undefined;
547
- readonly cache_control?: {
548
- readonly type: "ephemeral";
549
- readonly ttl?: string | undefined;
550
- } | undefined;
551
573
  readonly tool_calls?: readonly {
552
574
  readonly function: {
553
575
  readonly name: string;
@@ -563,13 +585,16 @@ export declare const fromRequest: (request: LLMRequest<import("../schema/options
563
585
  }[] | undefined;
564
586
  readonly reasoning_details?: unknown;
565
587
  } | {
566
- readonly content: string;
567
588
  readonly role: "tool";
568
589
  readonly tool_call_id: string;
569
- readonly cache_control?: {
570
- readonly type: "ephemeral";
571
- readonly ttl?: string | undefined;
572
- } | undefined;
590
+ readonly content: string | readonly {
591
+ readonly type: "text";
592
+ readonly text: string;
593
+ readonly cache_control?: {
594
+ readonly type: "ephemeral";
595
+ readonly ttl?: string | undefined;
596
+ } | undefined;
597
+ }[];
573
598
  })[];
574
599
  tools: {
575
600
  readonly function: {
@@ -646,15 +671,18 @@ export declare const protocol: Protocol<{
646
671
  })[];
647
672
  } | {
648
673
  readonly [x: string]: unknown;
649
- readonly content: string | null;
674
+ readonly content: string | readonly {
675
+ readonly type: "text";
676
+ readonly text: string;
677
+ readonly cache_control?: {
678
+ readonly type: "ephemeral";
679
+ readonly ttl?: string | undefined;
680
+ } | undefined;
681
+ }[] | null;
650
682
  readonly role: "assistant";
651
683
  readonly reasoning?: string | undefined;
652
684
  readonly reasoning_content?: string | undefined;
653
685
  readonly reasoning_text?: string | undefined;
654
- readonly cache_control?: {
655
- readonly type: "ephemeral";
656
- readonly ttl?: string | undefined;
657
- } | undefined;
658
686
  readonly tool_calls?: readonly {
659
687
  readonly function: {
660
688
  readonly name: string;
@@ -670,13 +698,16 @@ export declare const protocol: Protocol<{
670
698
  }[] | undefined;
671
699
  readonly reasoning_details?: unknown;
672
700
  } | {
673
- readonly content: string;
674
701
  readonly role: "tool";
675
702
  readonly tool_call_id: string;
676
- readonly cache_control?: {
677
- readonly type: "ephemeral";
678
- readonly ttl?: string | undefined;
679
- } | undefined;
703
+ readonly content: string | readonly {
704
+ readonly type: "text";
705
+ readonly text: string;
706
+ readonly cache_control?: {
707
+ readonly type: "ephemeral";
708
+ readonly ttl?: string | undefined;
709
+ } | undefined;
710
+ }[];
680
711
  })[];
681
712
  readonly stream: true;
682
713
  readonly max_completion_tokens?: number | undefined;
@@ -831,15 +862,18 @@ export declare const httpTransport: HttpTransport.HttpJsonTransport<{
831
862
  })[];
832
863
  } | {
833
864
  readonly [x: string]: unknown;
834
- readonly content: string | null;
865
+ readonly content: string | readonly {
866
+ readonly type: "text";
867
+ readonly text: string;
868
+ readonly cache_control?: {
869
+ readonly type: "ephemeral";
870
+ readonly ttl?: string | undefined;
871
+ } | undefined;
872
+ }[] | null;
835
873
  readonly role: "assistant";
836
874
  readonly reasoning?: string | undefined;
837
875
  readonly reasoning_content?: string | undefined;
838
876
  readonly reasoning_text?: string | undefined;
839
- readonly cache_control?: {
840
- readonly type: "ephemeral";
841
- readonly ttl?: string | undefined;
842
- } | undefined;
843
877
  readonly tool_calls?: readonly {
844
878
  readonly function: {
845
879
  readonly name: string;
@@ -855,13 +889,16 @@ export declare const httpTransport: HttpTransport.HttpJsonTransport<{
855
889
  }[] | undefined;
856
890
  readonly reasoning_details?: unknown;
857
891
  } | {
858
- readonly content: string;
859
892
  readonly role: "tool";
860
893
  readonly tool_call_id: string;
861
- readonly cache_control?: {
862
- readonly type: "ephemeral";
863
- readonly ttl?: string | undefined;
864
- } | undefined;
894
+ readonly content: string | readonly {
895
+ readonly type: "text";
896
+ readonly text: string;
897
+ readonly cache_control?: {
898
+ readonly type: "ephemeral";
899
+ readonly ttl?: string | undefined;
900
+ } | undefined;
901
+ }[];
865
902
  })[];
866
903
  readonly stream: true;
867
904
  readonly max_completion_tokens?: number | undefined;
@@ -947,15 +984,18 @@ export declare const route: Route<{
947
984
  })[];
948
985
  } | {
949
986
  readonly [x: string]: unknown;
950
- readonly content: string | null;
987
+ readonly content: string | readonly {
988
+ readonly type: "text";
989
+ readonly text: string;
990
+ readonly cache_control?: {
991
+ readonly type: "ephemeral";
992
+ readonly ttl?: string | undefined;
993
+ } | undefined;
994
+ }[] | null;
951
995
  readonly role: "assistant";
952
996
  readonly reasoning?: string | undefined;
953
997
  readonly reasoning_content?: string | undefined;
954
998
  readonly reasoning_text?: string | undefined;
955
- readonly cache_control?: {
956
- readonly type: "ephemeral";
957
- readonly ttl?: string | undefined;
958
- } | undefined;
959
999
  readonly tool_calls?: readonly {
960
1000
  readonly function: {
961
1001
  readonly name: string;
@@ -971,13 +1011,16 @@ export declare const route: Route<{
971
1011
  }[] | undefined;
972
1012
  readonly reasoning_details?: unknown;
973
1013
  } | {
974
- readonly content: string;
975
1014
  readonly role: "tool";
976
1015
  readonly tool_call_id: string;
977
- readonly cache_control?: {
978
- readonly type: "ephemeral";
979
- readonly ttl?: string | undefined;
980
- } | undefined;
1016
+ readonly content: string | readonly {
1017
+ readonly type: "text";
1018
+ readonly text: string;
1019
+ readonly cache_control?: {
1020
+ readonly type: "ephemeral";
1021
+ readonly ttl?: string | undefined;
1022
+ } | undefined;
1023
+ }[];
981
1024
  })[];
982
1025
  readonly stream: true;
983
1026
  readonly max_completion_tokens?: number | undefined;
@@ -83,12 +83,13 @@ const ReasoningDetail = Schema.Union([
83
83
  ]);
84
84
  const decodeReasoningDetail = Schema.decodeUnknownOption(ReasoningDetail);
85
85
  const knownReasoningDetails = (details) => details.flatMap((detail) => Option.toArray(decodeReasoningDetail(detail)));
86
+ const OpenAIChatTextContent = Schema.Struct({
87
+ type: Schema.Literal("text"),
88
+ text: Schema.String,
89
+ cache_control: Schema.optional(OpenAIChatCacheControl),
90
+ });
86
91
  const OpenAIChatUserContent = Schema.Union([
87
- Schema.Struct({
88
- type: Schema.Literal("text"),
89
- text: Schema.String,
90
- cache_control: Schema.optional(OpenAIChatCacheControl),
91
- }),
92
+ OpenAIChatTextContent,
92
93
  Schema.Struct({
93
94
  type: Schema.Literal("image_url"),
94
95
  image_url: Schema.Struct({ url: Schema.String }),
@@ -109,19 +110,17 @@ const OpenAIChatMessage = Schema.Union([
109
110
  }),
110
111
  Schema.StructWithRest(Schema.Struct({
111
112
  role: Schema.Literal("assistant"),
112
- content: Schema.NullOr(Schema.String),
113
+ content: Schema.NullOr(Schema.Union([Schema.String, Schema.Array(OpenAIChatTextContent)])),
113
114
  tool_calls: optionalArray(OpenAIChatAssistantToolCall),
114
115
  reasoning_content: Schema.optional(Schema.String),
115
116
  reasoning: Schema.optional(Schema.String),
116
117
  reasoning_text: Schema.optional(Schema.String),
117
118
  reasoning_details: Schema.optional(Schema.Unknown),
118
- cache_control: Schema.optional(OpenAIChatCacheControl),
119
119
  }), [Schema.Record(Schema.String, Schema.Unknown)]),
120
120
  Schema.Struct({
121
121
  role: Schema.Literal("tool"),
122
122
  tool_call_id: Schema.String,
123
- content: Schema.String,
124
- cache_control: Schema.optional(OpenAIChatCacheControl),
123
+ content: Schema.Union([Schema.String, Schema.Array(OpenAIChatTextContent)]),
125
124
  }),
126
125
  ]).pipe(Schema.toTaggedUnion("role"));
127
126
  const OpenAIChatToolChoice = Schema.Union([
@@ -305,7 +304,7 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
305
304
  if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
306
305
  return yield* ProviderShared.unsupportedContent("OpenAI Chat", "assistant", ["text", "reasoning", "tool-call"]);
307
306
  if (part.type === "text") {
308
- content.push(part);
307
+ content.push({ type: "text", text: part.text, cache_control: options.cacheControl?.(part.cache) });
309
308
  continue;
310
309
  }
311
310
  if (part.type === "reasoning") {
@@ -347,14 +346,17 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
347
346
  return nativeReasoning ?? (requireReasoning ? "" : undefined);
348
347
  return text;
349
348
  })();
350
- const cached = message.content.findLast((part) => "cache" in part && part.cache !== undefined);
351
- const cacheControl = options.cacheControl?.(cached && "cache" in cached ? cached.cache : undefined);
352
349
  const result = {
353
350
  role: "assistant",
354
- content: content.length > 0 ? content.map((part) => part.text).join("") : toolCalls.length > 0 ? null : "",
351
+ content: (() => {
352
+ if (content.some((part) => part.cache_control !== undefined))
353
+ return content;
354
+ if (content.length === 0 && toolCalls.length > 0)
355
+ return null;
356
+ return content.map((part) => part.text).join("");
357
+ })(),
355
358
  ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
356
359
  ...(details !== undefined ? { reasoning_details: details } : {}),
357
- ...(cacheControl !== undefined ? { cache_control: cacheControl } : {}),
358
360
  };
359
361
  if (field === undefined || reasoningText === undefined)
360
362
  return result;
@@ -367,27 +369,23 @@ const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (m
367
369
  if (!ProviderShared.supportsContent(part, ["tool-result"]))
368
370
  return yield* ProviderShared.unsupportedContent("OpenAI Chat", "tool", ["tool-result"]);
369
371
  if (part.result.type !== "content") {
370
- messages.push({
371
- role: "tool",
372
- tool_call_id: options.toolCallID?.(part.id) ?? part.id,
373
- content: ProviderShared.toolResultText(part),
374
- cache_control: options.cacheControl?.(part.cache),
375
- });
372
+ messages.push(toolMessage(options.toolCallID?.(part.id) ?? part.id, ProviderShared.toolResultText(part), options.cacheControl?.(part.cache)));
376
373
  continue;
377
374
  }
378
375
  const content = part.result.value;
379
376
  const text = content.filter((item) => item.type === "text").map((item) => item.text);
380
- messages.push({
381
- role: "tool",
382
- tool_call_id: options.toolCallID?.(part.id) ?? part.id,
383
- content: text.join("\n"),
384
- cache_control: options.cacheControl?.(part.cache),
385
- });
377
+ messages.push(toolMessage(options.toolCallID?.(part.id) ?? part.id, text.join("\n"), options.cacheControl?.(part.cache)));
386
378
  const files = content.filter((item) => item.type === "file");
387
379
  attachments.push(...(yield* Effect.forEach(files, (item) => lowerMedia(ProviderShared.toolFileMedia(item)))));
388
380
  }
389
381
  return { messages, attachments };
390
382
  });
383
+ // Chat cache breakpoints belong on text content parts, not on the message itself.
384
+ const toolMessage = (toolCallID, text, cacheControl) => ({
385
+ role: "tool",
386
+ tool_call_id: toolCallID,
387
+ content: cacheControl === undefined ? text : [{ type: "text", text, cache_control: cacheControl }],
388
+ });
391
389
  const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (message, reasoningField, requireReasoning, options) {
392
390
  if (message.role === "user")
393
391
  return [yield* lowerUserMessage(message, options)];
@@ -452,24 +450,25 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request,
452
450
  bridgeTools();
453
451
  if (message.role === "system") {
454
452
  const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Chat", message);
453
+ const cacheControl = options.cacheControl?.(part.cache);
455
454
  if (pendingAttachments.length > 0) {
456
455
  messages.push({
457
456
  role: "user",
458
457
  content: [
459
458
  ...pendingAttachments.splice(0),
460
- { type: "text", text: part.text, cache_control: options.cacheControl?.(part.cache) },
459
+ { type: "text", text: part.text, cache_control: cacheControl },
461
460
  ],
462
461
  });
463
462
  continue;
464
463
  }
465
464
  const previous = messages.at(-1);
466
465
  if (previous?.role === "user" && typeof previous.content === "string")
467
- messages[messages.length - 1] = options.cacheControl?.(part.cache)
466
+ messages[messages.length - 1] = cacheControl
468
467
  ? {
469
468
  role: "user",
470
469
  content: [
471
470
  { type: "text", text: previous.content },
472
- { type: "text", text: part.text, cache_control: options.cacheControl(part.cache) },
471
+ { type: "text", text: part.text, cache_control: cacheControl },
473
472
  ],
474
473
  }
475
474
  : { role: "user", content: `${previous.content}\n${part.text}` };
@@ -478,14 +477,14 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request,
478
477
  role: "user",
479
478
  content: [
480
479
  ...previous.content,
481
- { type: "text", text: part.text, cache_control: options.cacheControl?.(part.cache) },
480
+ { type: "text", text: part.text, cache_control: cacheControl },
482
481
  ],
483
482
  };
484
483
  else
485
- messages.push(options.cacheControl?.(part.cache)
484
+ messages.push(cacheControl
486
485
  ? {
487
486
  role: "user",
488
- content: [{ type: "text", text: part.text, cache_control: options.cacheControl(part.cache) }],
487
+ content: [{ type: "text", text: part.text, cache_control: cacheControl }],
489
488
  }
490
489
  : { role: "user", content: part.text });
491
490
  continue;
@@ -53,15 +53,18 @@ export declare const route: Route<{
53
53
  })[];
54
54
  } | {
55
55
  readonly [x: string]: unknown;
56
- readonly content: string | null;
56
+ readonly content: string | readonly {
57
+ readonly type: "text";
58
+ readonly text: string;
59
+ readonly cache_control?: {
60
+ readonly type: "ephemeral";
61
+ readonly ttl?: string | undefined;
62
+ } | undefined;
63
+ }[] | null;
57
64
  readonly role: "assistant";
58
65
  readonly reasoning?: string | undefined;
59
66
  readonly reasoning_content?: string | undefined;
60
67
  readonly reasoning_text?: string | undefined;
61
- readonly cache_control?: {
62
- readonly type: "ephemeral";
63
- readonly ttl?: string | undefined;
64
- } | undefined;
65
68
  readonly tool_calls?: readonly {
66
69
  readonly function: {
67
70
  readonly name: string;
@@ -77,13 +80,16 @@ export declare const route: Route<{
77
80
  }[] | undefined;
78
81
  readonly reasoning_details?: unknown;
79
82
  } | {
80
- readonly content: string;
81
83
  readonly role: "tool";
82
84
  readonly tool_call_id: string;
83
- readonly cache_control?: {
84
- readonly type: "ephemeral";
85
- readonly ttl?: string | undefined;
86
- } | undefined;
85
+ readonly content: string | readonly {
86
+ readonly type: "text";
87
+ readonly text: string;
88
+ readonly cache_control?: {
89
+ readonly type: "ephemeral";
90
+ readonly ttl?: string | undefined;
91
+ } | undefined;
92
+ }[];
87
93
  })[];
88
94
  readonly stream: true;
89
95
  readonly max_completion_tokens?: number | undefined;
@@ -69,15 +69,18 @@ export declare const protocol: Protocol<{
69
69
  })[];
70
70
  } | {
71
71
  readonly [x: string]: unknown;
72
- readonly content: string | null;
72
+ readonly content: string | readonly {
73
+ readonly type: "text";
74
+ readonly text: string;
75
+ readonly cache_control?: {
76
+ readonly type: "ephemeral";
77
+ readonly ttl?: string | undefined;
78
+ } | undefined;
79
+ }[] | null;
73
80
  readonly role: "assistant";
74
81
  readonly reasoning?: string | undefined;
75
82
  readonly reasoning_content?: string | undefined;
76
83
  readonly reasoning_text?: string | undefined;
77
- readonly cache_control?: {
78
- readonly type: "ephemeral";
79
- readonly ttl?: string | undefined;
80
- } | undefined;
81
84
  readonly tool_calls?: readonly {
82
85
  readonly function: {
83
86
  readonly name: string;
@@ -93,13 +96,16 @@ export declare const protocol: Protocol<{
93
96
  }[] | undefined;
94
97
  readonly reasoning_details?: unknown;
95
98
  } | {
96
- readonly content: string;
97
99
  readonly role: "tool";
98
100
  readonly tool_call_id: string;
99
- readonly cache_control?: {
100
- readonly type: "ephemeral";
101
- readonly ttl?: string | undefined;
102
- } | undefined;
101
+ readonly content: string | readonly {
102
+ readonly type: "text";
103
+ readonly text: string;
104
+ readonly cache_control?: {
105
+ readonly type: "ephemeral";
106
+ readonly ttl?: string | undefined;
107
+ } | undefined;
108
+ }[];
103
109
  })[];
104
110
  readonly stream: true;
105
111
  readonly max_completion_tokens?: number | undefined;
@@ -3,9 +3,13 @@ import { AuthenticationError, ContentPolicyError, InvalidRequestError, AIError,
3
3
  const patterns = [
4
4
  /prompt is too long/i,
5
5
  /input is too long for requested model/i,
6
+ // Cloudflare Workers AI reports this as HTTP 413.
7
+ /exceeded this model context window limit/i,
6
8
  /exceeds the context window/i,
7
9
  /exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i,
8
10
  /input token count.*exceeds the maximum/i,
11
+ // Amazon Nova on Bedrock reports this as a mid-stream validationException.
12
+ /number of input tokens exceeds maximum length/i,
9
13
  /tokens in request more than max tokens allowed/i,
10
14
  /maximum prompt length is \d+/i,
11
15
  /reduce the length of the messages/i,