@sayknow-cli/ai 0.3.16 → 0.4.0

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 (214) hide show
  1. package/package.json +23 -24
  2. package/src/auth-gateway/server.ts +164 -22
  3. package/src/auth-storage.ts +76 -43
  4. package/src/context-cap-policy.ts +59 -0
  5. package/src/index.ts +2 -0
  6. package/src/model-cache.ts +10 -0
  7. package/src/model-manager.ts +30 -17
  8. package/src/model-thinking.ts +14 -8
  9. package/src/providers/amazon-bedrock.ts +9 -1
  10. package/src/providers/anthropic-messages-server.ts +178 -17
  11. package/src/providers/anthropic.ts +199 -213
  12. package/src/providers/azure-openai-responses.ts +6 -1
  13. package/src/providers/google-gemini-cli.ts +26 -13
  14. package/src/providers/google-shared.ts +7 -1
  15. package/src/providers/ollama.ts +7 -1
  16. package/src/providers/openai-chat-server.ts +93 -5
  17. package/src/providers/openai-codex/response-handler.ts +11 -3
  18. package/src/providers/openai-codex-responses.ts +107 -20
  19. package/src/providers/openai-responses-server.ts +111 -36
  20. package/src/providers/openai-responses-shared.ts +71 -12
  21. package/src/providers/openai-responses.ts +26 -3
  22. package/src/providers/pi-native-client.ts +24 -12
  23. package/src/providers/pi-native-server.ts +275 -21
  24. package/src/types.ts +53 -4
  25. package/src/utils/discovery/codex.ts +3 -12
  26. package/src/utils/event-stream.ts +146 -58
  27. package/src/utils/fallback-transport.ts +269 -0
  28. package/src/utils/overflow.ts +72 -31
  29. package/src/utils/retry.ts +2 -2
  30. package/src/utils/validation.ts +6 -1
  31. package/src/utils.ts +120 -12
  32. package/dist/types/api-registry.d.ts +0 -30
  33. package/dist/types/auth-broker/client.d.ts +0 -67
  34. package/dist/types/auth-broker/index.d.ts +0 -5
  35. package/dist/types/auth-broker/refresher.d.ts +0 -25
  36. package/dist/types/auth-broker/remote-store.d.ts +0 -99
  37. package/dist/types/auth-broker/server.d.ts +0 -32
  38. package/dist/types/auth-broker/types.d.ts +0 -110
  39. package/dist/types/auth-broker/wire-schemas.d.ts +0 -443
  40. package/dist/types/auth-gateway/http.d.ts +0 -40
  41. package/dist/types/auth-gateway/index.d.ts +0 -3
  42. package/dist/types/auth-gateway/server.d.ts +0 -17
  43. package/dist/types/auth-gateway/types.d.ts +0 -115
  44. package/dist/types/auth-storage.d.ts +0 -695
  45. package/dist/types/cli.d.ts +0 -2
  46. package/dist/types/index.d.ts +0 -51
  47. package/dist/types/model-cache.d.ts +0 -17
  48. package/dist/types/model-manager.d.ts +0 -62
  49. package/dist/types/model-retirements.d.ts +0 -6
  50. package/dist/types/model-thinking.d.ts +0 -74
  51. package/dist/types/models.d.ts +0 -21
  52. package/dist/types/provider-details.d.ts +0 -24
  53. package/dist/types/provider-models/bundled-references.d.ts +0 -4
  54. package/dist/types/provider-models/descriptors.d.ts +0 -48
  55. package/dist/types/provider-models/google.d.ts +0 -20
  56. package/dist/types/provider-models/index.d.ts +0 -5
  57. package/dist/types/provider-models/ollama.d.ts +0 -7
  58. package/dist/types/provider-models/openai-compat.d.ts +0 -256
  59. package/dist/types/provider-models/special.d.ts +0 -19
  60. package/dist/types/providers/amazon-bedrock.d.ts +0 -60
  61. package/dist/types/providers/anthropic-messages-server-schema.d.ts +0 -450
  62. package/dist/types/providers/anthropic-messages-server.d.ts +0 -17
  63. package/dist/types/providers/anthropic.d.ts +0 -208
  64. package/dist/types/providers/aws-credential-config.d.ts +0 -19
  65. package/dist/types/providers/aws-credentials.d.ts +0 -43
  66. package/dist/types/providers/aws-eventstream.d.ts +0 -38
  67. package/dist/types/providers/aws-sigv4.d.ts +0 -55
  68. package/dist/types/providers/azure-openai-responses.d.ts +0 -15
  69. package/dist/types/providers/composer-discipline.d.ts +0 -26
  70. package/dist/types/providers/cursor/client-version.d.ts +0 -10
  71. package/dist/types/providers/cursor/gen/agent_pb.d.ts +0 -13022
  72. package/dist/types/providers/cursor.d.ts +0 -45
  73. package/dist/types/providers/error-message.d.ts +0 -27
  74. package/dist/types/providers/github-copilot-headers.d.ts +0 -40
  75. package/dist/types/providers/gitlab-duo.d.ts +0 -27
  76. package/dist/types/providers/google-auth.d.ts +0 -24
  77. package/dist/types/providers/google-gemini-cli.d.ts +0 -75
  78. package/dist/types/providers/google-gemini-headers.d.ts +0 -43
  79. package/dist/types/providers/google-shared.d.ts +0 -179
  80. package/dist/types/providers/google-types.d.ts +0 -138
  81. package/dist/types/providers/google-vertex.d.ts +0 -7
  82. package/dist/types/providers/google.d.ts +0 -4
  83. package/dist/types/providers/grammar.d.ts +0 -1
  84. package/dist/types/providers/kimi.d.ts +0 -27
  85. package/dist/types/providers/mock.d.ts +0 -177
  86. package/dist/types/providers/ollama.d.ts +0 -41
  87. package/dist/types/providers/openai-anthropic-shim.d.ts +0 -31
  88. package/dist/types/providers/openai-bounded-rate-limits.d.ts +0 -3
  89. package/dist/types/providers/openai-chat-server-schema.d.ts +0 -815
  90. package/dist/types/providers/openai-chat-server.d.ts +0 -16
  91. package/dist/types/providers/openai-codex/constants.d.ts +0 -26
  92. package/dist/types/providers/openai-codex/request-transformer.d.ts +0 -50
  93. package/dist/types/providers/openai-codex/response-handler.d.ts +0 -17
  94. package/dist/types/providers/openai-codex-responses.d.ts +0 -67
  95. package/dist/types/providers/openai-completions-compat.d.ts +0 -27
  96. package/dist/types/providers/openai-completions.d.ts +0 -33
  97. package/dist/types/providers/openai-request-transform.d.ts +0 -4
  98. package/dist/types/providers/openai-responses-server-schema.d.ts +0 -392
  99. package/dist/types/providers/openai-responses-server.d.ts +0 -17
  100. package/dist/types/providers/openai-responses-shared.d.ts +0 -104
  101. package/dist/types/providers/openai-responses.d.ts +0 -32
  102. package/dist/types/providers/pi-native-client.d.ts +0 -13
  103. package/dist/types/providers/pi-native-server.d.ts +0 -68
  104. package/dist/types/providers/register-builtins.d.ts +0 -31
  105. package/dist/types/providers/synthetic.d.ts +0 -26
  106. package/dist/types/providers/transform-messages.d.ts +0 -14
  107. package/dist/types/providers/vision-guard.d.ts +0 -8
  108. package/dist/types/rate-limit-utils.d.ts +0 -19
  109. package/dist/types/stream.d.ts +0 -43
  110. package/dist/types/types.d.ts +0 -831
  111. package/dist/types/usage/claude.d.ts +0 -3
  112. package/dist/types/usage/gemini.d.ts +0 -2
  113. package/dist/types/usage/github-copilot.d.ts +0 -7
  114. package/dist/types/usage/google-antigravity.d.ts +0 -2
  115. package/dist/types/usage/grok-cli.d.ts +0 -10
  116. package/dist/types/usage/kimi.d.ts +0 -2
  117. package/dist/types/usage/minimax-code.d.ts +0 -2
  118. package/dist/types/usage/openai-codex.d.ts +0 -3
  119. package/dist/types/usage/shared.d.ts +0 -1
  120. package/dist/types/usage/zai.d.ts +0 -2
  121. package/dist/types/usage.d.ts +0 -258
  122. package/dist/types/utils/abort.d.ts +0 -19
  123. package/dist/types/utils/anthropic-auth.d.ts +0 -31
  124. package/dist/types/utils/discovery/antigravity.d.ts +0 -67
  125. package/dist/types/utils/discovery/codex.d.ts +0 -38
  126. package/dist/types/utils/discovery/cursor.d.ts +0 -23
  127. package/dist/types/utils/discovery/gemini.d.ts +0 -25
  128. package/dist/types/utils/discovery/index.d.ts +0 -4
  129. package/dist/types/utils/discovery/openai-compatible.d.ts +0 -74
  130. package/dist/types/utils/event-stream.d.ts +0 -33
  131. package/dist/types/utils/fireworks-model-id.d.ts +0 -10
  132. package/dist/types/utils/foundry.d.ts +0 -1
  133. package/dist/types/utils/h2-fetch.d.ts +0 -22
  134. package/dist/types/utils/http-inspector.d.ts +0 -35
  135. package/dist/types/utils/idle-iterator.d.ts +0 -67
  136. package/dist/types/utils/json-parse.d.ts +0 -18
  137. package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +0 -18
  138. package/dist/types/utils/oauth/anthropic.d.ts +0 -22
  139. package/dist/types/utils/oauth/api-key-login.d.ts +0 -35
  140. package/dist/types/utils/oauth/api-key-validation.d.ts +0 -27
  141. package/dist/types/utils/oauth/callback-server.d.ts +0 -60
  142. package/dist/types/utils/oauth/cerebras.d.ts +0 -1
  143. package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +0 -18
  144. package/dist/types/utils/oauth/cursor.d.ts +0 -15
  145. package/dist/types/utils/oauth/deepinfra.d.ts +0 -1
  146. package/dist/types/utils/oauth/deepseek.d.ts +0 -10
  147. package/dist/types/utils/oauth/firepass.d.ts +0 -1
  148. package/dist/types/utils/oauth/fireworks.d.ts +0 -1
  149. package/dist/types/utils/oauth/fugu.d.ts +0 -1
  150. package/dist/types/utils/oauth/github-copilot.d.ts +0 -38
  151. package/dist/types/utils/oauth/gitlab-duo.d.ts +0 -3
  152. package/dist/types/utils/oauth/glm-zcode.d.ts +0 -71
  153. package/dist/types/utils/oauth/google-antigravity.d.ts +0 -11
  154. package/dist/types/utils/oauth/google-gemini-cli.d.ts +0 -10
  155. package/dist/types/utils/oauth/google-oauth-shared.d.ts +0 -28
  156. package/dist/types/utils/oauth/huggingface.d.ts +0 -19
  157. package/dist/types/utils/oauth/index.d.ts +0 -39
  158. package/dist/types/utils/oauth/kagi.d.ts +0 -17
  159. package/dist/types/utils/oauth/kilo.d.ts +0 -5
  160. package/dist/types/utils/oauth/kimi.d.ts +0 -21
  161. package/dist/types/utils/oauth/litellm.d.ts +0 -18
  162. package/dist/types/utils/oauth/lm-studio.d.ts +0 -17
  163. package/dist/types/utils/oauth/minimax-code.d.ts +0 -28
  164. package/dist/types/utils/oauth/moonshot.d.ts +0 -1
  165. package/dist/types/utils/oauth/nanogpt.d.ts +0 -1
  166. package/dist/types/utils/oauth/nvidia.d.ts +0 -18
  167. package/dist/types/utils/oauth/ollama-cloud.d.ts +0 -2
  168. package/dist/types/utils/oauth/ollama.d.ts +0 -18
  169. package/dist/types/utils/oauth/openai-codex.d.ts +0 -21
  170. package/dist/types/utils/oauth/opencode.d.ts +0 -18
  171. package/dist/types/utils/oauth/parallel.d.ts +0 -17
  172. package/dist/types/utils/oauth/perplexity.d.ts +0 -9
  173. package/dist/types/utils/oauth/pkce.d.ts +0 -8
  174. package/dist/types/utils/oauth/qianfan.d.ts +0 -17
  175. package/dist/types/utils/oauth/qwen-portal.d.ts +0 -19
  176. package/dist/types/utils/oauth/synthetic.d.ts +0 -1
  177. package/dist/types/utils/oauth/tavily.d.ts +0 -17
  178. package/dist/types/utils/oauth/together.d.ts +0 -1
  179. package/dist/types/utils/oauth/types.d.ts +0 -45
  180. package/dist/types/utils/oauth/venice.d.ts +0 -18
  181. package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +0 -18
  182. package/dist/types/utils/oauth/vllm.d.ts +0 -16
  183. package/dist/types/utils/oauth/xai.d.ts +0 -30
  184. package/dist/types/utils/oauth/xiaomi.d.ts +0 -25
  185. package/dist/types/utils/oauth/zai.d.ts +0 -18
  186. package/dist/types/utils/oauth/zenmux.d.ts +0 -1
  187. package/dist/types/utils/overflow.d.ts +0 -64
  188. package/dist/types/utils/parse-bind.d.ts +0 -23
  189. package/dist/types/utils/provider-response.d.ts +0 -3
  190. package/dist/types/utils/retry-after.d.ts +0 -3
  191. package/dist/types/utils/retry-budget.d.ts +0 -1
  192. package/dist/types/utils/retry.d.ts +0 -26
  193. package/dist/types/utils/schema/adapt.d.ts +0 -24
  194. package/dist/types/utils/schema/compatibility.d.ts +0 -30
  195. package/dist/types/utils/schema/dereference.d.ts +0 -11
  196. package/dist/types/utils/schema/draft.d.ts +0 -10
  197. package/dist/types/utils/schema/equality.d.ts +0 -4
  198. package/dist/types/utils/schema/fields.d.ts +0 -49
  199. package/dist/types/utils/schema/index.d.ts +0 -14
  200. package/dist/types/utils/schema/json-schema-validator.d.ts +0 -12
  201. package/dist/types/utils/schema/meta-validator.d.ts +0 -2
  202. package/dist/types/utils/schema/normalize.d.ts +0 -93
  203. package/dist/types/utils/schema/root-combinator.d.ts +0 -12
  204. package/dist/types/utils/schema/spill.d.ts +0 -8
  205. package/dist/types/utils/schema/stamps.d.ts +0 -25
  206. package/dist/types/utils/schema/types.d.ts +0 -4
  207. package/dist/types/utils/schema/wire.d.ts +0 -54
  208. package/dist/types/utils/schema/zod-decontaminate.d.ts +0 -31
  209. package/dist/types/utils/sse-debug.d.ts +0 -10
  210. package/dist/types/utils/tool-call-healing.d.ts +0 -71
  211. package/dist/types/utils/tool-choice-capability.d.ts +0 -41
  212. package/dist/types/utils/tool-choice.d.ts +0 -50
  213. package/dist/types/utils/validation.d.ts +0 -17
  214. package/dist/types/utils.d.ts +0 -35
@@ -57,6 +57,7 @@ import {
57
57
  } from "../utils";
58
58
  import { createAbortSourceTracker } from "../utils/abort";
59
59
  import { AssistantMessageEventStream } from "../utils/event-stream";
60
+ import { transportFailureFacts } from "../utils/fallback-transport";
60
61
  import { isFoundryEnabled } from "../utils/foundry";
61
62
  import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotError } from "../utils/http-inspector";
62
63
  import { getStreamFirstEventTimeoutMs, getStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
@@ -434,27 +435,31 @@ function getCacheControl(
434
435
  model: Model<"anthropic-messages">,
435
436
  baseUrl: string,
436
437
  cacheRetention?: CacheRetention,
437
- ): { retention: CacheRetention; cacheControl?: AnthropicCacheControl } {
438
- // Default Anthropic prompt caching to long (1h) retention. The provider
439
- // default of ~5m is too fragile for long-running Codex/Sayknow-CLI subagent
440
- // workflows, where the prefix is frequently evicted between turns. Explicit
441
- // request/model `cacheRetention` and the SKC_CACHE_RETENTION /
442
- // PI_CACHE_RETENTION env overrides still win.
438
+ ): { mode: AnthropicCacheMode; cacheControl?: AnthropicCacheControl } {
443
439
  const retention = resolveCacheRetention(cacheRetention, "long");
444
- if (retention === "none") {
445
- return { retention };
446
- }
447
- // `ttl: "1h"` is only honoured on the canonical Anthropic API for models
448
- // that advertise long-cache support. Everywhere else (proxies, gateways,
449
- // models without the capability) we fall back to the default ephemeral
450
- // breakpoint, which Anthropic services at the standard ~5m TTL.
451
- const ttl =
452
- retention === "long" && isAnthropicApiBaseUrl(baseUrl) && getAnthropicCompat(model).supportsLongCacheRetention
453
- ? "1h"
454
- : undefined;
440
+ if (retention === "none") return { mode: "none" };
441
+
442
+ const isCanonicalApi = isAnthropicApiBaseUrl(baseUrl);
443
+ const promptCacheMode = model.compat?.promptCacheMode;
444
+ const mode: AnthropicCacheMode =
445
+ promptCacheMode === "none"
446
+ ? "none"
447
+ : promptCacheMode === "explicit"
448
+ ? "explicit"
449
+ : isCanonicalApi
450
+ ? "automatic"
451
+ : "none";
452
+ if (mode === "none") return { mode };
453
+
454
+ const supportsLongCacheRetention = isCanonicalApi
455
+ ? getAnthropicCompat(model).supportsLongCacheRetention
456
+ : model.compat?.supportsLongCacheRetention === true;
455
457
  return {
456
- retention,
457
- cacheControl: { type: "ephemeral", ...(ttl && { ttl }) },
458
+ mode,
459
+ cacheControl: {
460
+ type: "ephemeral",
461
+ ...(retention === "long" && supportsLongCacheRetention ? { ttl: "1h" } : {}),
462
+ },
458
463
  };
459
464
  }
460
465
 
@@ -1080,6 +1085,7 @@ function getAnthropicCompat(
1080
1085
  supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,
1081
1086
  supportsToolChoice: model.compat?.supportsToolChoice ?? true,
1082
1087
  supportsForcedToolChoice: model.compat?.supportsForcedToolChoice ?? true,
1088
+ promptCacheMode: model.compat?.promptCacheMode ?? "none",
1083
1089
  toolChoiceSupport: model.compat?.toolChoiceSupport,
1084
1090
  };
1085
1091
  }
@@ -1303,6 +1309,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1303
1309
  if (replacementPayload !== undefined) {
1304
1310
  nextParams = replacementPayload as typeof nextParams;
1305
1311
  }
1312
+ validateCacheControls(nextParams as AnthropicCacheParams);
1306
1313
  rawRequestDump = {
1307
1314
  provider: model.provider,
1308
1315
  api: output.api,
@@ -1323,6 +1330,13 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1323
1330
  ) & { index: number };
1324
1331
  const blocks = output.content as Block[];
1325
1332
  const blocksByAnthropicIndex = new Map<number, Block>();
1333
+ // Derive from the ACTUAL request shape, not the option default: the request
1334
+ // only sends `display: "summarized"` on specific paths (adaptive display is
1335
+ // omitted for models where supportsAdaptiveThinkingDisplay is false). Defaulting
1336
+ // to summarized would mislabel raw thinking as a provider-displayable summary.
1337
+ const summarizedThinking =
1338
+ (params.thinking as { display?: AnthropicThinkingDisplay } | undefined)?.display === "summarized";
1339
+ const reasoningBuffers = new WeakMap<object, string>();
1326
1340
  const getBlockByAnthropicIndex = (anthropicIndex: number) => {
1327
1341
  const block = blocksByAnthropicIndex.get(anthropicIndex);
1328
1342
  if (!block) return { block: undefined, contentIndex: -1 };
@@ -1454,11 +1468,24 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1454
1468
  };
1455
1469
  output.content.push(block);
1456
1470
  trackBlockByAnthropicIndex(event.index, block);
1471
+ // Emit thinking_start FIRST so a reasoning item is open before any
1472
+ // summary-start: the Responses SSE encoder only accepts a summary
1473
+ // start when state.open.kind === "reasoning", otherwise the
1474
+ // reasoning_summary_part.added frame is dropped and deltas arrive
1475
+ // out of order.
1457
1476
  stream.push({
1458
1477
  type: "thinking_start",
1459
1478
  contentIndex: output.content.length - 1,
1460
1479
  partial: output,
1461
1480
  });
1481
+ if (summarizedThinking) {
1482
+ reasoningBuffers.set(block, "");
1483
+ stream.push({
1484
+ type: "reasoning_summary_start",
1485
+ contentIndex: output.content.length - 1,
1486
+ partial: output,
1487
+ });
1488
+ }
1462
1489
  } else if (event.content_block.type === "redacted_thinking") {
1463
1490
  const block: Block = {
1464
1491
  type: "redactedThinking",
@@ -1503,12 +1530,23 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1503
1530
  const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
1504
1531
  if (block && block.type === "thinking") {
1505
1532
  block.thinking += event.delta.thinking;
1506
- stream.push({
1507
- type: "thinking_delta",
1508
- contentIndex: index,
1509
- delta: event.delta.thinking,
1510
- partial: output,
1511
- });
1533
+ if (summarizedThinking) {
1534
+ const summary = (reasoningBuffers.get(block) ?? "") + event.delta.thinking;
1535
+ reasoningBuffers.set(block, summary);
1536
+ stream.push({
1537
+ type: "reasoning_summary_delta",
1538
+ contentIndex: index,
1539
+ delta: event.delta.thinking,
1540
+ partial: output,
1541
+ });
1542
+ } else {
1543
+ stream.push({
1544
+ type: "thinking_delta",
1545
+ contentIndex: index,
1546
+ delta: event.delta.thinking,
1547
+ partial: output,
1548
+ });
1549
+ }
1512
1550
  }
1513
1551
  } else if (event.delta.type === "input_json_delta") {
1514
1552
  const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
@@ -1542,6 +1580,21 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1542
1580
  partial: output,
1543
1581
  });
1544
1582
  } else if (block.type === "thinking") {
1583
+ if (summarizedThinking) {
1584
+ const summaryText = reasoningBuffers.get(block) ?? "";
1585
+ const mutable = block as {
1586
+ provenance?: "summary" | "raw" | "mixed";
1587
+ summaryText?: string;
1588
+ };
1589
+ if (mutable.summaryText === undefined) mutable.summaryText = summaryText;
1590
+ if (mutable.provenance === undefined) mutable.provenance = "summary";
1591
+ stream.push({
1592
+ type: "reasoning_summary_end",
1593
+ contentIndex: index,
1594
+ content: summaryText,
1595
+ partial: output,
1596
+ });
1597
+ }
1545
1598
  stream.push({
1546
1599
  type: "thinking_end",
1547
1600
  contentIndex: index,
@@ -1637,6 +1690,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1637
1690
  throw streamFailure;
1638
1691
  }
1639
1692
  if (
1693
+ !options?.fallbackManaged &&
1640
1694
  !disableStrictTools &&
1641
1695
  firstTokenTime === undefined &&
1642
1696
  hasStrictAnthropicTools(params) &&
@@ -1655,6 +1709,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1655
1709
  if (
1656
1710
  !droppedForcedToolChoice &&
1657
1711
  firstTokenTime === undefined &&
1712
+ !options?.fallbackManaged &&
1658
1713
  isSentForcedAnthropicToolChoice(params.tool_choice) &&
1659
1714
  isForcedToolChoiceUnsupportedError(streamFailure, true)
1660
1715
  ) {
@@ -1681,6 +1736,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1681
1736
  continue;
1682
1737
  }
1683
1738
  if (
1739
+ !options?.fallbackManaged &&
1684
1740
  !thinkingRepairAttempted &&
1685
1741
  firstTokenTime === undefined &&
1686
1742
  isAnthropicThinkingBlockMutationError(streamFailure)
@@ -1696,6 +1752,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1696
1752
  continue;
1697
1753
  }
1698
1754
  if (
1755
+ !options?.fallbackManaged &&
1699
1756
  !dropFastMode &&
1700
1757
  resolveServiceTier(options?.serviceTier, model.provider) === "priority" &&
1701
1758
  firstTokenTime === undefined &&
@@ -1752,6 +1809,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1752
1809
  const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
1753
1810
  output.stopReason = activeAbortTracker.wasCallerAbort() ? "aborted" : "error";
1754
1811
  output.errorStatus = extractHttpStatusFromError(error);
1812
+ output.transportFailure = transportFailureFacts(error);
1755
1813
  if (output.errorKind !== "provider_safety_stop" || !output.errorMessage) {
1756
1814
  output.errorMessage =
1757
1815
  firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
@@ -1991,232 +2049,159 @@ type CacheControlBlock = {
1991
2049
  cache_control?: AnthropicCacheControl | null;
1992
2050
  };
1993
2051
 
1994
- function applyCacheControlToLastBlock<T extends CacheControlBlock>(
1995
- blocks: T[],
1996
- cacheControl: AnthropicCacheControl,
1997
- ): void {
1998
- if (blocks.length === 0) return;
1999
- const lastIndex = blocks.length - 1;
2000
- blocks[lastIndex] = { ...blocks[lastIndex], cache_control: cacheControl };
2001
- }
2002
-
2003
- function applyCacheControlToLastTextBlock(
2004
- blocks: Array<ContentBlockParam & CacheControlBlock>,
2005
- cacheControl: AnthropicCacheControl,
2006
- ): void {
2007
- if (blocks.length === 0) return;
2008
- for (let i = blocks.length - 1; i >= 0; i--) {
2009
- if (blocks[i].type === "text") {
2010
- blocks[i] = { ...blocks[i], cache_control: cacheControl };
2011
- return;
2012
- }
2013
- }
2014
- applyCacheControlToLastBlock(blocks, cacheControl);
2015
- }
2016
-
2017
- function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?: AnthropicCacheControl): void {
2018
- if (!cacheControl) return;
2019
-
2020
- // Skip if cache_control breakpoints were already placed externally on messages.
2021
- for (const message of params.messages) {
2022
- if (Array.isArray(message.content)) {
2023
- if ((message.content as Array<ContentBlockParam & CacheControlBlock>).some(b => b.cache_control != null))
2024
- return;
2025
- }
2026
- }
2052
+ type AnthropicCacheParams = MessageCreateParamsStreaming & {
2053
+ cache_control?: AnthropicCacheControl;
2054
+ };
2027
2055
 
2028
- const MAX_CACHE_BREAKPOINTS = 4;
2029
- let cacheBreakpointsUsed = 0;
2056
+ type AnthropicCacheMode = "automatic" | "explicit" | "none";
2030
2057
 
2031
- if (params.tools && params.tools.length > 0) {
2032
- applyCacheControlToLastBlock(params.tools as Array<CacheControlBlock>, cacheControl);
2033
- cacheBreakpointsUsed++;
2034
- }
2058
+ function isCacheableContentBlock(block: ContentBlockParam): boolean {
2059
+ if (block.type === "thinking" || block.type === "redacted_thinking") return false;
2060
+ return block.type !== "text" || block.text.trim().length > 0;
2061
+ }
2035
2062
 
2036
- if (cacheBreakpointsUsed >= MAX_CACHE_BREAKPOINTS) return;
2063
+ function cacheControlError(path: string, reason: string): Error {
2064
+ return new Error(`Invalid Anthropic cache_control at ${path}: ${reason}`);
2065
+ }
2037
2066
 
2038
- if (params.system && Array.isArray(params.system) && params.system.length > 0) {
2039
- applyCacheControlToLastBlock(params.system, cacheControl);
2040
- cacheBreakpointsUsed++;
2067
+ function validateCacheControl(control: unknown, path: string, seenFiveMinute: { value: boolean }): void {
2068
+ if (!isRecord(control) || control.type !== "ephemeral") {
2069
+ throw cacheControlError(path, 'expected { type: "ephemeral" }');
2041
2070
  }
2042
-
2043
- if (cacheBreakpointsUsed >= MAX_CACHE_BREAKPOINTS) return;
2044
-
2045
- const userIndexes = params.messages
2046
- .map((message, index) => (message.role === "user" ? index : -1))
2047
- .filter(index => index >= 0);
2048
-
2049
- if (userIndexes.length >= 2) {
2050
- const penultimateUserIndex = userIndexes[userIndexes.length - 2];
2051
- const penultimateUser = params.messages[penultimateUserIndex];
2052
- if (penultimateUser) {
2053
- if (typeof penultimateUser.content === "string") {
2054
- const contentBlock: ContentBlockParam & CacheControlBlock = {
2055
- type: "text",
2056
- text: penultimateUser.content,
2057
- cache_control: cacheControl,
2058
- };
2059
- penultimateUser.content = [contentBlock];
2060
- cacheBreakpointsUsed++;
2061
- } else if (Array.isArray(penultimateUser.content) && penultimateUser.content.length > 0) {
2062
- applyCacheControlToLastTextBlock(
2063
- penultimateUser.content as Array<ContentBlockParam & CacheControlBlock>,
2064
- cacheControl,
2065
- );
2066
- cacheBreakpointsUsed++;
2067
- }
2068
- }
2071
+ if (control.ttl !== undefined && control.ttl !== "5m" && control.ttl !== "1h") {
2072
+ throw cacheControlError(path, 'ttl must be "5m" or "1h"');
2069
2073
  }
2070
-
2071
- if (cacheBreakpointsUsed >= MAX_CACHE_BREAKPOINTS) return;
2072
-
2073
- if (userIndexes.length >= 1) {
2074
- const lastUserIndex = userIndexes[userIndexes.length - 1];
2075
- const lastUser = params.messages[lastUserIndex];
2076
- if (lastUser) {
2077
- if (typeof lastUser.content === "string") {
2078
- const contentBlock: ContentBlockParam & CacheControlBlock = {
2079
- type: "text",
2080
- text: lastUser.content,
2081
- cache_control: cacheControl,
2082
- };
2083
- lastUser.content = [contentBlock];
2084
- } else if (Array.isArray(lastUser.content) && lastUser.content.length > 0) {
2085
- applyCacheControlToLastTextBlock(
2086
- lastUser.content as Array<ContentBlockParam & CacheControlBlock>,
2087
- cacheControl,
2088
- );
2089
- }
2090
- }
2091
- }
2092
- }
2093
-
2094
- function normalizeCacheControlBlockTtl(block: CacheControlBlock, seenFiveMinute: { value: boolean }): void {
2095
- const cacheControl = block.cache_control;
2096
- if (!cacheControl) return;
2097
- if (cacheControl.ttl !== "1h") {
2098
- seenFiveMinute.value = true;
2074
+ if (control.ttl === "1h") {
2075
+ if (seenFiveMinute.value) throw cacheControlError(path, "1h TTL must precede 5m TTL");
2099
2076
  return;
2100
2077
  }
2101
- if (seenFiveMinute.value) {
2102
- delete cacheControl.ttl;
2103
- }
2078
+ seenFiveMinute.value = true;
2104
2079
  }
2105
2080
 
2106
- function normalizeCacheControlTtlOrdering(params: MessageCreateParamsStreaming): void {
2081
+ function validateCacheControls(params: AnthropicCacheParams): void {
2107
2082
  const seenFiveMinute = { value: false };
2108
- if (params.tools) {
2109
- for (const tool of params.tools as Array<Anthropic.Messages.Tool & CacheControlBlock>) {
2110
- normalizeCacheControlBlockTtl(tool, seenFiveMinute);
2111
- }
2083
+ let count = 0;
2084
+ const validate = (control: unknown, path: string): void => {
2085
+ if (control == null) return;
2086
+ count++;
2087
+ validateCacheControl(control, path, seenFiveMinute);
2088
+ };
2089
+ if (!Array.isArray(params.messages)) throw cacheControlError("messages", "must be an array");
2090
+
2091
+ validate(params.cache_control, "cache_control");
2092
+ for (const [index, tool] of (params.tools ?? []).entries()) {
2093
+ validate((tool as CacheControlBlock).cache_control, `tools[${index}].cache_control`);
2112
2094
  }
2113
- if (params.system && Array.isArray(params.system)) {
2114
- for (const block of params.system as Array<AnthropicSystemBlock & CacheControlBlock>) {
2115
- normalizeCacheControlBlockTtl(block, seenFiveMinute);
2095
+ if (Array.isArray(params.system)) {
2096
+ for (const [index, block] of params.system.entries()) {
2097
+ validate((block as CacheControlBlock).cache_control, `system[${index}].cache_control`);
2116
2098
  }
2117
2099
  }
2118
- for (const message of params.messages) {
2100
+ for (const [messageIndex, message] of params.messages.entries()) {
2119
2101
  if (!Array.isArray(message.content)) continue;
2120
- for (const block of message.content as Array<ContentBlockParam & CacheControlBlock>) {
2121
- normalizeCacheControlBlockTtl(block, seenFiveMinute);
2102
+ for (const [blockIndex, block] of message.content.entries()) {
2103
+ const control = (block as CacheControlBlock).cache_control;
2104
+ if (control != null && !isCacheableContentBlock(block)) {
2105
+ throw cacheControlError(
2106
+ `messages[${messageIndex}].content[${blockIndex}].cache_control`,
2107
+ "block is not cacheable",
2108
+ );
2109
+ }
2110
+ validate(control, `messages[${messageIndex}].content[${blockIndex}].cache_control`);
2122
2111
  }
2123
2112
  }
2113
+ if (count > 4) throw cacheControlError("cache_control", "at most four total breakpoints are allowed");
2124
2114
  }
2125
2115
 
2126
- function findLastCacheControlIndex<T extends CacheControlBlock>(blocks: T[]): number {
2116
+ function applyCacheControlToLastCacheableBlock(
2117
+ blocks: Array<ContentBlockParam & CacheControlBlock>,
2118
+ cacheControl: AnthropicCacheControl,
2119
+ ): boolean {
2127
2120
  for (let index = blocks.length - 1; index >= 0; index--) {
2128
- if (blocks[index]?.cache_control != null) return index;
2121
+ const block = blocks[index];
2122
+ if (!isCacheableContentBlock(block)) continue;
2123
+ blocks[index] = { ...block, cache_control: { ...cacheControl } };
2124
+ return true;
2129
2125
  }
2130
- return -1;
2126
+ return false;
2131
2127
  }
2132
2128
 
2133
- function stripCacheControlExceptIndex<T extends CacheControlBlock>(
2134
- blocks: T[],
2135
- preserveIndex: number,
2136
- excessCounter: { value: number },
2137
- ): void {
2138
- for (let index = 0; index < blocks.length && excessCounter.value > 0; index++) {
2139
- if (index === preserveIndex) continue;
2140
- if (!blocks[index]?.cache_control) continue;
2141
- delete blocks[index].cache_control;
2142
- excessCounter.value--;
2143
- }
2129
+ function isHumanUserMessage(message: MessageCreateParamsStreaming["messages"][number]): boolean {
2130
+ if (message.role !== "user") return false;
2131
+ if (typeof message.content === "string") return true;
2132
+ return message.content.some(block => block.type !== "tool_result");
2144
2133
  }
2145
2134
 
2146
- function stripAllCacheControl<T extends CacheControlBlock>(blocks: T[], excessCounter: { value: number }): void {
2147
- for (const block of blocks) {
2148
- if (excessCounter.value <= 0) return;
2149
- if (!block.cache_control) continue;
2150
- delete block.cache_control;
2151
- excessCounter.value--;
2135
+ function applyExplicitPromptCaching(params: AnthropicCacheParams, cacheControl: AnthropicCacheControl): void {
2136
+ if (countCacheControlBreakpoints(params) >= 4) return;
2137
+
2138
+ const currentUserIndex = params.messages.findLastIndex(isHumanUserMessage);
2139
+ if (currentUserIndex < 0) return;
2140
+ const currentUser = params.messages[currentUserIndex];
2141
+ if (!currentUser) return;
2142
+
2143
+ // A tool result is encoded as role "user" on the wire, but belongs to the
2144
+ // preceding assistant turn. Anchor that assistant turn, not the tool result,
2145
+ // so changing tool output does not invalidate the reusable conversation prefix.
2146
+ for (let index = currentUserIndex - 1; index >= 0; index--) {
2147
+ const message = params.messages[index];
2148
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
2149
+ if (
2150
+ applyCacheControlToLastCacheableBlock(
2151
+ message.content as Array<ContentBlockParam & CacheControlBlock>,
2152
+ cacheControl,
2153
+ )
2154
+ ) {
2155
+ break;
2156
+ }
2157
+ }
2158
+
2159
+ if (countCacheControlBreakpoints(params) >= 4) return;
2160
+ if (typeof currentUser.content === "string" && currentUser.content.trim()) {
2161
+ currentUser.content = [{ type: "text", text: currentUser.content, cache_control: { ...cacheControl } }];
2162
+ } else if (Array.isArray(currentUser.content)) {
2163
+ applyCacheControlToLastCacheableBlock(
2164
+ currentUser.content as Array<ContentBlockParam & CacheControlBlock>,
2165
+ cacheControl,
2166
+ );
2152
2167
  }
2153
2168
  }
2154
2169
 
2155
- function stripMessageCacheControl(
2156
- messages: MessageCreateParamsStreaming["messages"],
2157
- excessCounter: { value: number },
2170
+ function applyPromptCaching(
2171
+ params: AnthropicCacheParams,
2172
+ cacheMode: AnthropicCacheMode,
2173
+ cacheControl?: AnthropicCacheControl,
2158
2174
  ): void {
2159
- for (const message of messages) {
2160
- if (excessCounter.value <= 0) return;
2161
- if (!Array.isArray(message.content)) continue;
2162
- for (const block of message.content as Array<ContentBlockParam & CacheControlBlock>) {
2163
- if (excessCounter.value <= 0) return;
2164
- if (!block.cache_control) continue;
2165
- delete block.cache_control;
2166
- excessCounter.value--;
2167
- }
2175
+ if (!cacheControl || cacheMode === "none") return;
2176
+ validateCacheControls(params);
2177
+ if (cacheMode === "automatic") {
2178
+ params.cache_control = { ...cacheControl };
2179
+ return;
2168
2180
  }
2181
+ applyExplicitPromptCaching(params, cacheControl);
2182
+ validateCacheControls(params);
2169
2183
  }
2170
2184
 
2171
- function countCacheControlBreakpoints(params: MessageCreateParamsStreaming): number {
2172
- let total = 0;
2173
- if (params.tools) {
2174
- for (const tool of params.tools as Array<Anthropic.Messages.Tool & CacheControlBlock>) {
2175
- if (tool.cache_control) total++;
2176
- }
2177
- }
2178
- if (params.system && Array.isArray(params.system)) {
2179
- for (const block of params.system as Array<AnthropicSystemBlock & CacheControlBlock>) {
2180
- if (block.cache_control) total++;
2181
- }
2185
+ export function normalizeCacheControlTtlOrdering(params: MessageCreateParamsStreaming): void {
2186
+ validateCacheControls(params as AnthropicCacheParams);
2187
+ }
2188
+
2189
+ function countCacheControlBreakpoints(params: AnthropicCacheParams): number {
2190
+ let total = params.cache_control ? 1 : 0;
2191
+ for (const tool of params.tools ?? []) if ((tool as CacheControlBlock).cache_control) total++;
2192
+ if (Array.isArray(params.system)) {
2193
+ for (const block of params.system) if ((block as CacheControlBlock).cache_control) total++;
2182
2194
  }
2183
2195
  for (const message of params.messages) {
2184
2196
  if (!Array.isArray(message.content)) continue;
2185
- for (const block of message.content as Array<ContentBlockParam & CacheControlBlock>) {
2186
- if (block.cache_control) total++;
2187
- }
2197
+ for (const block of message.content) if ((block as CacheControlBlock).cache_control) total++;
2188
2198
  }
2189
2199
  return total;
2190
2200
  }
2191
2201
 
2192
2202
  function enforceCacheControlLimit(params: MessageCreateParamsStreaming, maxBreakpoints: number): void {
2193
- const total = countCacheControlBreakpoints(params);
2194
- if (total <= maxBreakpoints) return;
2195
- const excessCounter = { value: total - maxBreakpoints };
2196
- const systemBlocks =
2197
- params.system && Array.isArray(params.system)
2198
- ? (params.system as Array<AnthropicSystemBlock & CacheControlBlock>)
2199
- : [];
2200
- const toolBlocks = (params.tools ?? []) as Array<Anthropic.Messages.Tool & CacheControlBlock>;
2201
- const lastSystemIndex = findLastCacheControlIndex(systemBlocks);
2202
- const lastToolIndex = findLastCacheControlIndex(toolBlocks);
2203
- if (systemBlocks.length > 0) {
2204
- stripCacheControlExceptIndex(systemBlocks, lastSystemIndex, excessCounter);
2205
- }
2206
- if (excessCounter.value <= 0) return;
2207
- if (toolBlocks.length > 0) {
2208
- stripCacheControlExceptIndex(toolBlocks, lastToolIndex, excessCounter);
2209
- }
2210
- if (excessCounter.value <= 0) return;
2211
- stripMessageCacheControl(params.messages, excessCounter);
2212
- if (excessCounter.value <= 0) return;
2213
- if (systemBlocks.length > 0) {
2214
- stripAllCacheControl(systemBlocks, excessCounter);
2215
- }
2216
- if (excessCounter.value <= 0) return;
2217
- if (toolBlocks.length > 0) {
2218
- stripAllCacheControl(toolBlocks, excessCounter);
2219
- }
2203
+ if (maxBreakpoints !== 4) throw new Error("Anthropic supports exactly four cache breakpoints");
2204
+ validateCacheControls(params as AnthropicCacheParams);
2220
2205
  }
2221
2206
  function buildParams(
2222
2207
  model: Model<"anthropic-messages">,
@@ -2227,7 +2212,8 @@ function buildParams(
2227
2212
  disableStrictTools = false,
2228
2213
  repairLatestAssistantThinking = false,
2229
2214
  ): MessageCreateParamsStreaming {
2230
- const { cacheControl } = getCacheControl(model, baseUrl, options?.cacheRetention);
2215
+ const { mode: cacheMode, cacheControl } = getCacheControl(model, baseUrl, options?.cacheRetention);
2216
+
2231
2217
  const params: AnthropicSamplingParams = {
2232
2218
  model: model.id,
2233
2219
  messages: convertAnthropicMessages(context.messages, model, isOAuthToken, { repairLatestAssistantThinking }),
@@ -2358,7 +2344,7 @@ function buildParams(
2358
2344
  }
2359
2345
  disableThinkingIfToolChoiceForced(params);
2360
2346
  ensureMaxTokensForThinking(params, model);
2361
- applyPromptCaching(params, cacheControl);
2347
+ applyPromptCaching(params as AnthropicCacheParams, cacheMode, cacheControl);
2362
2348
  enforceCacheControlLimit(params, 4);
2363
2349
  normalizeCacheControlTtlOrdering(params);
2364
2350
 
@@ -19,6 +19,7 @@ import type {
19
19
  import { normalizeSystemPrompts } from "../utils";
20
20
  import { createAbortSourceTracker } from "../utils/abort";
21
21
  import { AssistantMessageEventStream } from "../utils/event-stream";
22
+ import { transportFailureFacts } from "../utils/fallback-transport";
22
23
  import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
23
24
  import {
24
25
  createWatchdog,
@@ -140,7 +141,10 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
140
141
  try {
141
142
  openaiStream = await client.responses.create(params, { signal: requestSignal });
142
143
  } catch (error) {
143
- if (!isForcedToolChoiceUnsupportedError(error, isForcedAzureResponsesToolChoice(params.tool_choice))) {
144
+ if (
145
+ !isForcedToolChoiceUnsupportedError(error, isForcedAzureResponsesToolChoice(params.tool_choice)) ||
146
+ options?.fallbackManaged
147
+ ) {
144
148
  throw error;
145
149
  }
146
150
  const reason = await finalizeErrorMessage(error, rawRequestDump);
@@ -204,6 +208,7 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
204
208
  const firstEventTimeoutError = abortTracker.getLocalAbortReason();
205
209
  output.stopReason = abortTracker.wasCallerAbort() ? "aborted" : "error";
206
210
  output.errorStatus = extractHttpStatusFromError(error);
211
+ output.transportFailure = transportFailureFacts(error);
207
212
  output.errorMessage = firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
208
213
  output.duration = Date.now() - startTime;
209
214
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;