@sayknow-cli/ai 0.3.11 → 0.3.13

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 (197) hide show
  1. package/package.json +24 -25
  2. package/src/model-cache.ts +1 -1
  3. package/src/model-manager.ts +14 -3
  4. package/src/model-retirements.ts +13 -0
  5. package/src/model-thinking.ts +34 -2
  6. package/src/models.ts +26 -5
  7. package/src/provider-models/google.ts +1 -0
  8. package/src/providers/amazon-bedrock.ts +37 -33
  9. package/src/providers/anthropic.ts +94 -65
  10. package/src/providers/aws-credential-config.ts +180 -0
  11. package/src/providers/aws-credentials.ts +20 -63
  12. package/src/providers/google-gemini-cli.ts +27 -3
  13. package/src/providers/google-gemini-headers.ts +6 -1
  14. package/src/providers/google-shared.ts +55 -3
  15. package/src/types.ts +2 -0
  16. package/src/utils/discovery/antigravity.ts +16 -2
  17. package/dist/types/api-registry.d.ts +0 -30
  18. package/dist/types/auth-broker/client.d.ts +0 -67
  19. package/dist/types/auth-broker/index.d.ts +0 -5
  20. package/dist/types/auth-broker/refresher.d.ts +0 -25
  21. package/dist/types/auth-broker/remote-store.d.ts +0 -99
  22. package/dist/types/auth-broker/server.d.ts +0 -32
  23. package/dist/types/auth-broker/types.d.ts +0 -110
  24. package/dist/types/auth-broker/wire-schemas.d.ts +0 -443
  25. package/dist/types/auth-gateway/http.d.ts +0 -40
  26. package/dist/types/auth-gateway/index.d.ts +0 -3
  27. package/dist/types/auth-gateway/server.d.ts +0 -17
  28. package/dist/types/auth-gateway/types.d.ts +0 -115
  29. package/dist/types/auth-storage.d.ts +0 -695
  30. package/dist/types/cli.d.ts +0 -2
  31. package/dist/types/index.d.ts +0 -51
  32. package/dist/types/model-cache.d.ts +0 -17
  33. package/dist/types/model-manager.d.ts +0 -62
  34. package/dist/types/model-thinking.d.ts +0 -74
  35. package/dist/types/models.d.ts +0 -12
  36. package/dist/types/provider-details.d.ts +0 -24
  37. package/dist/types/provider-models/bundled-references.d.ts +0 -4
  38. package/dist/types/provider-models/descriptors.d.ts +0 -48
  39. package/dist/types/provider-models/google.d.ts +0 -20
  40. package/dist/types/provider-models/index.d.ts +0 -5
  41. package/dist/types/provider-models/ollama.d.ts +0 -7
  42. package/dist/types/provider-models/openai-compat.d.ts +0 -256
  43. package/dist/types/provider-models/special.d.ts +0 -19
  44. package/dist/types/providers/amazon-bedrock.d.ts +0 -60
  45. package/dist/types/providers/anthropic-messages-server-schema.d.ts +0 -450
  46. package/dist/types/providers/anthropic-messages-server.d.ts +0 -17
  47. package/dist/types/providers/anthropic.d.ts +0 -208
  48. package/dist/types/providers/aws-credentials.d.ts +0 -43
  49. package/dist/types/providers/aws-eventstream.d.ts +0 -38
  50. package/dist/types/providers/aws-sigv4.d.ts +0 -55
  51. package/dist/types/providers/azure-openai-responses.d.ts +0 -15
  52. package/dist/types/providers/composer-discipline.d.ts +0 -26
  53. package/dist/types/providers/cursor/client-version.d.ts +0 -10
  54. package/dist/types/providers/cursor/gen/agent_pb.d.ts +0 -13022
  55. package/dist/types/providers/cursor.d.ts +0 -45
  56. package/dist/types/providers/error-message.d.ts +0 -27
  57. package/dist/types/providers/github-copilot-headers.d.ts +0 -40
  58. package/dist/types/providers/gitlab-duo.d.ts +0 -27
  59. package/dist/types/providers/google-auth.d.ts +0 -24
  60. package/dist/types/providers/google-gemini-cli.d.ts +0 -75
  61. package/dist/types/providers/google-gemini-headers.d.ts +0 -40
  62. package/dist/types/providers/google-shared.d.ts +0 -173
  63. package/dist/types/providers/google-types.d.ts +0 -138
  64. package/dist/types/providers/google-vertex.d.ts +0 -7
  65. package/dist/types/providers/google.d.ts +0 -4
  66. package/dist/types/providers/grammar.d.ts +0 -1
  67. package/dist/types/providers/kimi.d.ts +0 -27
  68. package/dist/types/providers/mock.d.ts +0 -177
  69. package/dist/types/providers/ollama.d.ts +0 -41
  70. package/dist/types/providers/openai-anthropic-shim.d.ts +0 -31
  71. package/dist/types/providers/openai-bounded-rate-limits.d.ts +0 -3
  72. package/dist/types/providers/openai-chat-server-schema.d.ts +0 -815
  73. package/dist/types/providers/openai-chat-server.d.ts +0 -16
  74. package/dist/types/providers/openai-codex/constants.d.ts +0 -26
  75. package/dist/types/providers/openai-codex/request-transformer.d.ts +0 -50
  76. package/dist/types/providers/openai-codex/response-handler.d.ts +0 -17
  77. package/dist/types/providers/openai-codex-responses.d.ts +0 -67
  78. package/dist/types/providers/openai-completions-compat.d.ts +0 -27
  79. package/dist/types/providers/openai-completions.d.ts +0 -33
  80. package/dist/types/providers/openai-request-transform.d.ts +0 -4
  81. package/dist/types/providers/openai-responses-server-schema.d.ts +0 -392
  82. package/dist/types/providers/openai-responses-server.d.ts +0 -17
  83. package/dist/types/providers/openai-responses-shared.d.ts +0 -104
  84. package/dist/types/providers/openai-responses.d.ts +0 -32
  85. package/dist/types/providers/pi-native-client.d.ts +0 -13
  86. package/dist/types/providers/pi-native-server.d.ts +0 -68
  87. package/dist/types/providers/register-builtins.d.ts +0 -31
  88. package/dist/types/providers/synthetic.d.ts +0 -26
  89. package/dist/types/providers/transform-messages.d.ts +0 -14
  90. package/dist/types/providers/vision-guard.d.ts +0 -8
  91. package/dist/types/rate-limit-utils.d.ts +0 -19
  92. package/dist/types/stream.d.ts +0 -43
  93. package/dist/types/types.d.ts +0 -829
  94. package/dist/types/usage/claude.d.ts +0 -3
  95. package/dist/types/usage/gemini.d.ts +0 -2
  96. package/dist/types/usage/github-copilot.d.ts +0 -7
  97. package/dist/types/usage/google-antigravity.d.ts +0 -2
  98. package/dist/types/usage/grok-cli.d.ts +0 -10
  99. package/dist/types/usage/kimi.d.ts +0 -2
  100. package/dist/types/usage/minimax-code.d.ts +0 -2
  101. package/dist/types/usage/openai-codex.d.ts +0 -3
  102. package/dist/types/usage/shared.d.ts +0 -1
  103. package/dist/types/usage/zai.d.ts +0 -2
  104. package/dist/types/usage.d.ts +0 -258
  105. package/dist/types/utils/abort.d.ts +0 -19
  106. package/dist/types/utils/anthropic-auth.d.ts +0 -31
  107. package/dist/types/utils/discovery/antigravity.d.ts +0 -61
  108. package/dist/types/utils/discovery/codex.d.ts +0 -38
  109. package/dist/types/utils/discovery/cursor.d.ts +0 -23
  110. package/dist/types/utils/discovery/gemini.d.ts +0 -25
  111. package/dist/types/utils/discovery/index.d.ts +0 -4
  112. package/dist/types/utils/discovery/openai-compatible.d.ts +0 -74
  113. package/dist/types/utils/event-stream.d.ts +0 -33
  114. package/dist/types/utils/fireworks-model-id.d.ts +0 -10
  115. package/dist/types/utils/foundry.d.ts +0 -1
  116. package/dist/types/utils/h2-fetch.d.ts +0 -22
  117. package/dist/types/utils/http-inspector.d.ts +0 -35
  118. package/dist/types/utils/idle-iterator.d.ts +0 -67
  119. package/dist/types/utils/json-parse.d.ts +0 -18
  120. package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +0 -18
  121. package/dist/types/utils/oauth/anthropic.d.ts +0 -22
  122. package/dist/types/utils/oauth/api-key-login.d.ts +0 -35
  123. package/dist/types/utils/oauth/api-key-validation.d.ts +0 -27
  124. package/dist/types/utils/oauth/callback-server.d.ts +0 -60
  125. package/dist/types/utils/oauth/cerebras.d.ts +0 -1
  126. package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +0 -18
  127. package/dist/types/utils/oauth/cursor.d.ts +0 -15
  128. package/dist/types/utils/oauth/deepinfra.d.ts +0 -1
  129. package/dist/types/utils/oauth/deepseek.d.ts +0 -10
  130. package/dist/types/utils/oauth/firepass.d.ts +0 -1
  131. package/dist/types/utils/oauth/fireworks.d.ts +0 -1
  132. package/dist/types/utils/oauth/fugu.d.ts +0 -1
  133. package/dist/types/utils/oauth/github-copilot.d.ts +0 -38
  134. package/dist/types/utils/oauth/gitlab-duo.d.ts +0 -3
  135. package/dist/types/utils/oauth/glm-zcode.d.ts +0 -71
  136. package/dist/types/utils/oauth/google-antigravity.d.ts +0 -11
  137. package/dist/types/utils/oauth/google-gemini-cli.d.ts +0 -10
  138. package/dist/types/utils/oauth/google-oauth-shared.d.ts +0 -28
  139. package/dist/types/utils/oauth/huggingface.d.ts +0 -19
  140. package/dist/types/utils/oauth/index.d.ts +0 -39
  141. package/dist/types/utils/oauth/kagi.d.ts +0 -17
  142. package/dist/types/utils/oauth/kilo.d.ts +0 -5
  143. package/dist/types/utils/oauth/kimi.d.ts +0 -21
  144. package/dist/types/utils/oauth/litellm.d.ts +0 -18
  145. package/dist/types/utils/oauth/lm-studio.d.ts +0 -17
  146. package/dist/types/utils/oauth/minimax-code.d.ts +0 -28
  147. package/dist/types/utils/oauth/moonshot.d.ts +0 -1
  148. package/dist/types/utils/oauth/nanogpt.d.ts +0 -1
  149. package/dist/types/utils/oauth/nvidia.d.ts +0 -18
  150. package/dist/types/utils/oauth/ollama-cloud.d.ts +0 -2
  151. package/dist/types/utils/oauth/ollama.d.ts +0 -18
  152. package/dist/types/utils/oauth/openai-codex.d.ts +0 -21
  153. package/dist/types/utils/oauth/opencode.d.ts +0 -18
  154. package/dist/types/utils/oauth/parallel.d.ts +0 -17
  155. package/dist/types/utils/oauth/perplexity.d.ts +0 -9
  156. package/dist/types/utils/oauth/pkce.d.ts +0 -8
  157. package/dist/types/utils/oauth/qianfan.d.ts +0 -17
  158. package/dist/types/utils/oauth/qwen-portal.d.ts +0 -19
  159. package/dist/types/utils/oauth/synthetic.d.ts +0 -1
  160. package/dist/types/utils/oauth/tavily.d.ts +0 -17
  161. package/dist/types/utils/oauth/together.d.ts +0 -1
  162. package/dist/types/utils/oauth/types.d.ts +0 -45
  163. package/dist/types/utils/oauth/venice.d.ts +0 -18
  164. package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +0 -18
  165. package/dist/types/utils/oauth/vllm.d.ts +0 -16
  166. package/dist/types/utils/oauth/xai.d.ts +0 -30
  167. package/dist/types/utils/oauth/xiaomi.d.ts +0 -25
  168. package/dist/types/utils/oauth/zai.d.ts +0 -18
  169. package/dist/types/utils/oauth/zenmux.d.ts +0 -1
  170. package/dist/types/utils/overflow.d.ts +0 -64
  171. package/dist/types/utils/parse-bind.d.ts +0 -23
  172. package/dist/types/utils/provider-response.d.ts +0 -3
  173. package/dist/types/utils/retry-after.d.ts +0 -3
  174. package/dist/types/utils/retry-budget.d.ts +0 -1
  175. package/dist/types/utils/retry.d.ts +0 -26
  176. package/dist/types/utils/schema/adapt.d.ts +0 -24
  177. package/dist/types/utils/schema/compatibility.d.ts +0 -30
  178. package/dist/types/utils/schema/dereference.d.ts +0 -11
  179. package/dist/types/utils/schema/draft.d.ts +0 -10
  180. package/dist/types/utils/schema/equality.d.ts +0 -4
  181. package/dist/types/utils/schema/fields.d.ts +0 -49
  182. package/dist/types/utils/schema/index.d.ts +0 -14
  183. package/dist/types/utils/schema/json-schema-validator.d.ts +0 -12
  184. package/dist/types/utils/schema/meta-validator.d.ts +0 -2
  185. package/dist/types/utils/schema/normalize.d.ts +0 -93
  186. package/dist/types/utils/schema/root-combinator.d.ts +0 -12
  187. package/dist/types/utils/schema/spill.d.ts +0 -8
  188. package/dist/types/utils/schema/stamps.d.ts +0 -25
  189. package/dist/types/utils/schema/types.d.ts +0 -4
  190. package/dist/types/utils/schema/wire.d.ts +0 -54
  191. package/dist/types/utils/schema/zod-decontaminate.d.ts +0 -31
  192. package/dist/types/utils/sse-debug.d.ts +0 -10
  193. package/dist/types/utils/tool-call-healing.d.ts +0 -71
  194. package/dist/types/utils/tool-choice-capability.d.ts +0 -41
  195. package/dist/types/utils/tool-choice.d.ts +0 -50
  196. package/dist/types/utils/validation.d.ts +0 -17
  197. package/dist/types/utils.d.ts +0 -35
@@ -586,15 +586,12 @@ const ANTHROPIC_BUILTIN_TOOL_NAMES = new Set(["web_search", "code_execution", "t
586
586
  export const applyClaudeToolPrefix = (name: string, prefixOverride: string = claudeToolPrefix) => {
587
587
  if (!prefixOverride) return name;
588
588
  if (ANTHROPIC_BUILTIN_TOOL_NAMES.has(name.toLowerCase())) return name;
589
- const prefix = prefixOverride.toLowerCase();
590
- if (name.toLowerCase().startsWith(prefix)) return name;
591
589
  return `${prefixOverride}${name}`;
592
590
  };
593
591
 
594
592
  export const stripClaudeToolPrefix = (name: string, prefixOverride: string = claudeToolPrefix) => {
595
593
  if (!prefixOverride) return name;
596
- const prefix = prefixOverride.toLowerCase();
597
- if (!name.toLowerCase().startsWith(prefix)) return name;
594
+ if (!name.startsWith(prefixOverride)) return name;
598
595
  return name.slice(prefixOverride.length);
599
596
  };
600
597
 
@@ -1325,6 +1322,36 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1325
1322
  | (ToolCall & { partialJson: string })
1326
1323
  ) & { index: number };
1327
1324
  const blocks = output.content as Block[];
1325
+ const blocksByAnthropicIndex = new Map<number, Block>();
1326
+ const getBlockByAnthropicIndex = (anthropicIndex: number) => {
1327
+ const block = blocksByAnthropicIndex.get(anthropicIndex);
1328
+ if (!block) return { block: undefined, contentIndex: -1 };
1329
+ return { block, contentIndex: blocks.indexOf(block) };
1330
+ };
1331
+ const trackBlockByAnthropicIndex = (anthropicIndex: number, block: Block) => {
1332
+ // A duplicate start for an active index is a provider-envelope violation;
1333
+ // finalize the orphaned block so no internal stream fields leak into output.
1334
+ const orphaned = blocksByAnthropicIndex.get(anthropicIndex);
1335
+ if (orphaned) {
1336
+ if (orphaned.type === "toolCall" && orphaned.partialJson.trim()) {
1337
+ orphaned.arguments = parseStreamingJson(orphaned.partialJson);
1338
+ }
1339
+ delete (orphaned as { index?: number }).index;
1340
+ delete (orphaned as { partialJson?: string }).partialJson;
1341
+ }
1342
+ blocksByAnthropicIndex.set(anthropicIndex, block);
1343
+ };
1344
+ const resetOutputForRetry = () => {
1345
+ output.content.length = 0;
1346
+ output.responseId = undefined;
1347
+ output.errorKind = undefined;
1348
+ output.errorStatus = undefined;
1349
+ output.errorMessage = strictFallbackErrorMessage;
1350
+ output.providerPayload = undefined;
1351
+ output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
1352
+ output.stopReason = "stop";
1353
+ firstTokenTime = undefined;
1354
+ };
1328
1355
  const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs();
1329
1356
  const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs);
1330
1357
  stream.push({ type: "start", partial: output });
@@ -1334,6 +1361,8 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1334
1361
  let providerRetryAttempt = 0;
1335
1362
  let thinkingRepairAttempted = false;
1336
1363
  while (true) {
1364
+ // Retries reset output.content; drop stale block correlations from the aborted attempt.
1365
+ blocksByAnthropicIndex.clear();
1337
1366
  activeAbortTracker = createAbortSourceTracker(options?.signal);
1338
1367
  const firstEventTimeoutAbortError = new Error(
1339
1368
  "Anthropic stream timed out while waiting for the first event",
@@ -1342,6 +1371,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1342
1371
  const { requestSignal } = activeAbortTracker;
1343
1372
  const anthropicRequest = client.messages.create({ ...params, stream: true }, { signal: requestSignal });
1344
1373
  let streamedReplayUnsafeContent = false;
1374
+ let sawProviderSafetyStop = false;
1345
1375
 
1346
1376
  try {
1347
1377
  const {
@@ -1368,6 +1398,12 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1368
1398
  abortSignal: options?.signal,
1369
1399
  })) {
1370
1400
  sawEvent = true;
1401
+ if (sawProviderSafetyStop) {
1402
+ if (event.type === "message_stop") {
1403
+ sawTerminalEnvelope = true;
1404
+ }
1405
+ continue;
1406
+ }
1371
1407
 
1372
1408
  if (event.type === "message_start") {
1373
1409
  if (sawMessageStart) {
@@ -1403,6 +1439,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1403
1439
  index: event.index,
1404
1440
  };
1405
1441
  output.content.push(block);
1442
+ trackBlockByAnthropicIndex(event.index, block);
1406
1443
  stream.push({
1407
1444
  type: "text_start",
1408
1445
  contentIndex: output.content.length - 1,
@@ -1416,6 +1453,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1416
1453
  index: event.index,
1417
1454
  };
1418
1455
  output.content.push(block);
1456
+ trackBlockByAnthropicIndex(event.index, block);
1419
1457
  stream.push({
1420
1458
  type: "thinking_start",
1421
1459
  contentIndex: output.content.length - 1,
@@ -1428,6 +1466,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1428
1466
  index: event.index,
1429
1467
  };
1430
1468
  output.content.push(block);
1469
+ trackBlockByAnthropicIndex(event.index, block);
1431
1470
  } else if (event.content_block.type === "tool_use") {
1432
1471
  streamedReplayUnsafeContent = true;
1433
1472
  const block: Block = {
@@ -1441,6 +1480,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1441
1480
  index: event.index,
1442
1481
  };
1443
1482
  output.content.push(block);
1483
+ trackBlockByAnthropicIndex(event.index, block);
1444
1484
  stream.push({
1445
1485
  type: "toolcall_start",
1446
1486
  contentIndex: output.content.length - 1,
@@ -1449,8 +1489,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1449
1489
  }
1450
1490
  } else if (event.type === "content_block_delta") {
1451
1491
  if (event.delta.type === "text_delta") {
1452
- const index = blocks.findIndex(b => b.index === event.index);
1453
- const block = blocks[index];
1492
+ const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
1454
1493
  if (block && block.type === "text") {
1455
1494
  block.text += event.delta.text;
1456
1495
  stream.push({
@@ -1461,8 +1500,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1461
1500
  });
1462
1501
  }
1463
1502
  } else if (event.delta.type === "thinking_delta") {
1464
- const index = blocks.findIndex(b => b.index === event.index);
1465
- const block = blocks[index];
1503
+ const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
1466
1504
  if (block && block.type === "thinking") {
1467
1505
  block.thinking += event.delta.thinking;
1468
1506
  stream.push({
@@ -1473,8 +1511,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1473
1511
  });
1474
1512
  }
1475
1513
  } else if (event.delta.type === "input_json_delta") {
1476
- const index = blocks.findIndex(b => b.index === event.index);
1477
- const block = blocks[index];
1514
+ const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
1478
1515
  if (block && block.type === "toolCall") {
1479
1516
  block.partialJson += event.delta.partial_json;
1480
1517
  block.arguments = parseStreamingJson(block.partialJson);
@@ -1486,17 +1523,16 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1486
1523
  });
1487
1524
  }
1488
1525
  } else if (event.delta.type === "signature_delta") {
1489
- const index = blocks.findIndex(b => b.index === event.index);
1490
- const block = blocks[index];
1526
+ const { block } = getBlockByAnthropicIndex(event.index);
1491
1527
  if (block && block.type === "thinking") {
1492
1528
  block.thinkingSignature = block.thinkingSignature || "";
1493
1529
  block.thinkingSignature += event.delta.signature;
1494
1530
  }
1495
1531
  }
1496
1532
  } else if (event.type === "content_block_stop") {
1497
- const index = blocks.findIndex(b => b.index === event.index);
1498
- const block = blocks[index];
1533
+ const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
1499
1534
  if (block) {
1535
+ blocksByAnthropicIndex.delete(event.index);
1500
1536
  delete (block as { index?: number }).index;
1501
1537
  if (block.type === "text") {
1502
1538
  stream.push({
@@ -1513,7 +1549,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1513
1549
  partial: output,
1514
1550
  });
1515
1551
  } else if (block.type === "toolCall") {
1516
- block.arguments = parseStreamingJson(block.partialJson);
1552
+ if (block.partialJson.trim()) {
1553
+ block.arguments = parseStreamingJson(block.partialJson);
1554
+ }
1517
1555
  delete (block as { partialJson?: string }).partialJson;
1518
1556
  stream.push({
1519
1557
  type: "toolcall_end",
@@ -1525,26 +1563,34 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1525
1563
  }
1526
1564
  } else if (event.type === "message_delta") {
1527
1565
  const rawStopReason = event.delta.stop_reason as string | null | undefined;
1566
+ const stopDetails = event.delta.stop_details;
1567
+ const isProviderSafetyStop =
1568
+ rawStopReason === "refusal" || rawStopReason === "sensitive" || stopDetails?.type === "refusal";
1528
1569
  if (rawStopReason) {
1529
- output.stopReason = mapStopReason(rawStopReason);
1570
+ output.stopReason = isProviderSafetyStop ? "error" : mapStopReason(rawStopReason);
1530
1571
  sawTerminalEnvelope = true;
1531
1572
  }
1532
- const stopDetails = event.delta.stop_details;
1533
- if (stopDetails && stopDetails.type === "refusal") {
1534
- const explanation = stopDetails.explanation?.trim();
1535
- const category = stopDetails.category;
1536
- const label = category ? `Refusal (${category})` : "Refusal";
1537
- output.errorMessage = explanation ? `${label}: ${explanation}` : label;
1573
+ if (isProviderSafetyStop) {
1574
+ sawProviderSafetyStop = true;
1575
+ sawTerminalEnvelope = true;
1576
+ output.stopReason = "error";
1577
+ output.errorKind = "provider_safety_stop";
1578
+ if (stopDetails?.type === "refusal") {
1579
+ const explanation = stopDetails.explanation?.trim();
1580
+ const category = stopDetails.category;
1581
+ const label = category ? `Refusal (${category})` : "Refusal";
1582
+ output.errorMessage = explanation ? `${label}: ${explanation}` : label;
1583
+ } else if (!output.errorMessage) {
1584
+ output.errorMessage =
1585
+ rawStopReason === "refusal"
1586
+ ? "Refusal (no details provided)"
1587
+ : "Content flagged by safety filters";
1588
+ }
1538
1589
  } else if (output.stopReason === "error" && !output.errorMessage) {
1539
- // Anthropic flagged an error-class stop (refusal / sensitive) without
1540
- // populating stop_details. Surface the raw reason instead of falling
1541
- // through to the generic "unknown error" string when we throw below.
1542
- output.errorMessage =
1543
- rawStopReason === "refusal"
1544
- ? "Refusal (no details provided)"
1545
- : rawStopReason === "sensitive"
1546
- ? "Content flagged by safety filters"
1547
- : `Anthropic stream ended with stop_reason: ${rawStopReason ?? "unknown"}`;
1590
+ // Anthropic flagged an error-class stop without populating stop_details.
1591
+ // Surface the raw reason instead of falling through to the generic
1592
+ // "unknown error" string when we throw below.
1593
+ output.errorMessage = `Anthropic stream ended with stop_reason: ${rawStopReason ?? "unknown"}`;
1548
1594
  }
1549
1595
  if (event.usage.input_tokens != null) {
1550
1596
  output.usage.input = event.usage.input_tokens;
@@ -1587,6 +1633,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1587
1633
  break;
1588
1634
  } catch (streamError) {
1589
1635
  const streamFailure = activeAbortTracker.getLocalAbortReason() ?? streamError;
1636
+ if (sawProviderSafetyStop) {
1637
+ throw streamFailure;
1638
+ }
1590
1639
  if (
1591
1640
  !disableStrictTools &&
1592
1641
  firstTokenTime === undefined &&
@@ -1594,19 +1643,13 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1594
1643
  isAnthropicStrictGrammarTooLargeError(streamFailure)
1595
1644
  ) {
1596
1645
  strictFallbackErrorMessage = await finalizeErrorMessage(streamFailure, rawRequestDump);
1597
- output.errorMessage = strictFallbackErrorMessage;
1598
1646
  if (providerSessionState) {
1599
1647
  providerSessionState.strictToolsDisabled = true;
1600
1648
  }
1601
1649
  disableStrictTools = true;
1602
1650
  params = await prepareParams();
1603
1651
  providerRetryAttempt = 0;
1604
- output.content.length = 0;
1605
- output.responseId = undefined;
1606
- output.providerPayload = undefined;
1607
- output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
1608
- output.stopReason = "stop";
1609
- firstTokenTime = undefined;
1652
+ resetOutputForRetry();
1610
1653
  continue;
1611
1654
  }
1612
1655
  if (
@@ -1634,12 +1677,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1634
1677
  droppedForcedToolChoice = true;
1635
1678
  params = await prepareParams({ dropForcedToolChoice: true });
1636
1679
  providerRetryAttempt = 0;
1637
- output.content.length = 0;
1638
- output.responseId = undefined;
1639
- output.providerPayload = undefined;
1640
- output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
1641
- output.stopReason = "stop";
1642
- firstTokenTime = undefined;
1680
+ resetOutputForRetry();
1643
1681
  continue;
1644
1682
  }
1645
1683
  if (
@@ -1654,12 +1692,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1654
1692
  thinkingRepairAttempted = true;
1655
1693
  params = await prepareParams({ repairLatestAssistantThinking: true });
1656
1694
  providerRetryAttempt = 0;
1657
- output.content.length = 0;
1658
- output.responseId = undefined;
1659
- output.providerPayload = undefined;
1660
- output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
1661
- output.stopReason = "stop";
1662
- firstTokenTime = undefined;
1695
+ resetOutputForRetry();
1663
1696
  continue;
1664
1697
  }
1665
1698
  if (
@@ -1678,12 +1711,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1678
1711
  dropFastMode = true;
1679
1712
  params = await prepareParams();
1680
1713
  providerRetryAttempt = 0;
1681
- output.content.length = 0;
1682
- output.responseId = undefined;
1683
- output.providerPayload = undefined;
1684
- output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
1685
- output.stopReason = "stop";
1686
- firstTokenTime = undefined;
1714
+ resetOutputForRetry();
1687
1715
  continue;
1688
1716
  }
1689
1717
  const isTransientEnvelopeFailure =
@@ -1705,13 +1733,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1705
1733
  } else {
1706
1734
  await scheduler.wait(delayMs, { signal: options?.signal });
1707
1735
  }
1708
- output.content.length = 0;
1709
- output.responseId = undefined;
1710
- output.errorMessage = strictFallbackErrorMessage;
1711
- output.providerPayload = undefined;
1712
- output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
1713
- output.stopReason = "stop";
1714
- firstTokenTime = undefined;
1736
+ resetOutputForRetry();
1715
1737
  }
1716
1738
  }
1717
1739
 
@@ -1730,7 +1752,10 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1730
1752
  const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
1731
1753
  output.stopReason = activeAbortTracker.wasCallerAbort() ? "aborted" : "error";
1732
1754
  output.errorStatus = extractHttpStatusFromError(error);
1733
- output.errorMessage = firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
1755
+ if (output.errorKind !== "provider_safety_stop" || !output.errorMessage) {
1756
+ output.errorMessage =
1757
+ firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
1758
+ }
1734
1759
  output.errorMessage = rewriteCopilotError(output.errorMessage, error, model.provider);
1735
1760
  output.duration = Date.now() - startTime;
1736
1761
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
@@ -2243,7 +2268,11 @@ function buildParams(
2243
2268
  params.tools = convertTools(
2244
2269
  context.tools,
2245
2270
  isOAuthToken,
2246
- disableStrictTools || model.provider === "github-copilot",
2271
+ // The Claude Code OAuth surface mishandles `strict: true` tools:
2272
+ // streamed tool_use blocks arrive with empty/undefined arguments and
2273
+ // occasionally corrupted names (works with PI_NO_STRICT=1). Never
2274
+ // request strict tool use on OAuth requests.
2275
+ disableStrictTools || isOAuthToken || model.provider === "github-copilot",
2247
2276
  getAnthropicCompat(model).supportsEagerToolInputStreaming,
2248
2277
  );
2249
2278
  }
@@ -0,0 +1,180 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { $credentialEnv } from "@sayknow-cli/utils";
5
+ import type { AwsCredentials } from "./aws-sigv4";
6
+
7
+ export type AwsIniFile = Record<string, Record<string, string>>;
8
+
9
+ export interface AwsCredentialSourceOptions {
10
+ profile?: string;
11
+ }
12
+
13
+ export interface AwsCredentialSource {
14
+ profile: string;
15
+ credentialsPath: string;
16
+ configPath: string;
17
+ }
18
+
19
+ export type AwsProfileCapability = "static" | "process" | "sso" | undefined;
20
+
21
+ const AVAILABILITY_CACHE_MAX_AGE_MS = 1_000;
22
+ const MAX_AWS_INI_FILE_BYTES = 1024 * 1024;
23
+
24
+ interface FileFingerprint {
25
+ exists: boolean;
26
+ size?: number;
27
+ mtimeMs?: number;
28
+ ctimeMs?: number;
29
+ ino?: number;
30
+ }
31
+
32
+ interface AvailabilityCacheEntry {
33
+ source: AwsCredentialSource;
34
+ credentials: FileFingerprint;
35
+ config: FileFingerprint;
36
+ value: boolean;
37
+ checkedAt: number;
38
+ }
39
+
40
+ let availabilityCache: AvailabilityCacheEntry | undefined;
41
+
42
+ export function parseAwsIni(text: string): AwsIniFile {
43
+ const out: AwsIniFile = {};
44
+ let current: Record<string, string> | undefined;
45
+ for (const rawLine of text.split(/\r?\n/)) {
46
+ const line = rawLine.trim();
47
+ if (!line || line.startsWith("#") || line.startsWith(";")) continue;
48
+ if (line.startsWith("[") && line.endsWith("]")) {
49
+ let name = line.slice(1, -1).trim();
50
+ if (name.startsWith("profile ")) name = name.slice(8).trim();
51
+ if (name.startsWith("sso-session ")) name = `sso-session:${name.slice(12).trim()}`;
52
+ current = out[name] ??= {};
53
+ continue;
54
+ }
55
+ if (!current) continue;
56
+ const equals = line.indexOf("=");
57
+ if (equals === -1) continue;
58
+ const key = line.slice(0, equals).trim();
59
+ const value = line.slice(equals + 1).trim();
60
+ if (key) current[key] = value;
61
+ }
62
+ return out;
63
+ }
64
+
65
+ export function resolveAwsCredentialSource(options: AwsCredentialSourceOptions = {}): AwsCredentialSource {
66
+ const profile = options.profile || $credentialEnv("AWS_PROFILE") || "default";
67
+ const home = os.homedir();
68
+ return {
69
+ profile,
70
+ credentialsPath: path.resolve(
71
+ $credentialEnv("AWS_SHARED_CREDENTIALS_FILE") || path.join(home, ".aws", "credentials"),
72
+ ),
73
+ configPath: path.resolve($credentialEnv("AWS_CONFIG_FILE") || path.join(home, ".aws", "config")),
74
+ };
75
+ }
76
+
77
+ export function readAwsStaticEnvironmentCredentials(): AwsCredentials | undefined {
78
+ const accessKeyId = $credentialEnv("AWS_ACCESS_KEY_ID");
79
+ const secretAccessKey = $credentialEnv("AWS_SECRET_ACCESS_KEY");
80
+ if (!accessKeyId || !secretAccessKey) return undefined;
81
+ const sessionToken = $credentialEnv("AWS_SESSION_TOKEN");
82
+ return sessionToken ? { accessKeyId, secretAccessKey, sessionToken } : { accessKeyId, secretAccessKey };
83
+ }
84
+
85
+ export function classifyAwsProfileCapability(
86
+ profile: string,
87
+ credentialsIni: AwsIniFile | undefined,
88
+ configIni: AwsIniFile | undefined,
89
+ ): AwsProfileCapability {
90
+ const merged = { ...(configIni?.[profile] ?? {}), ...(credentialsIni?.[profile] ?? {}) };
91
+ if (merged.aws_access_key_id && merged.aws_secret_access_key) return "static";
92
+ if (merged.sso_account_id && merged.sso_role_name) {
93
+ if (merged.sso_start_url && merged.sso_region) return "sso";
94
+ const session = merged.sso_session ? configIni?.[`sso-session:${merged.sso_session}`] : undefined;
95
+ if (session?.sso_start_url && session.sso_region) return "sso";
96
+ }
97
+ if (merged.credential_process) return "process";
98
+ return undefined;
99
+ }
100
+
101
+ export function hasResolvableAwsProfileSource(
102
+ options: AwsCredentialSourceOptions & { /** @internal Test-only cache scan observer. */ onScan?: () => void } = {},
103
+ now = Date.now(),
104
+ ): boolean {
105
+ const source = resolveAwsCredentialSource(options);
106
+ const credentials = fingerprint(source.credentialsPath);
107
+ const config = fingerprint(source.configPath);
108
+ if (
109
+ availabilityCache &&
110
+ now >= availabilityCache.checkedAt &&
111
+ now - availabilityCache.checkedAt < AVAILABILITY_CACHE_MAX_AGE_MS &&
112
+ sameSource(availabilityCache.source, source) &&
113
+ sameFingerprint(availabilityCache.credentials, credentials) &&
114
+ sameFingerprint(availabilityCache.config, config)
115
+ ) {
116
+ return availabilityCache.value;
117
+ }
118
+ options.onScan?.();
119
+ const credentialsIni = readAwsIniSync(source.credentialsPath);
120
+ const configIni = readAwsIniSync(source.configPath);
121
+ const value = classifyAwsProfileCapability(source.profile, credentialsIni, configIni) !== undefined;
122
+ availabilityCache = { source, credentials, config, value, checkedAt: now };
123
+ return value;
124
+ }
125
+
126
+ export function isValidBedrockBearerToken(token: string | undefined): token is string {
127
+ if (!token) return false;
128
+ return !/[\x00-\x1f\x7f]/.test(token);
129
+ }
130
+
131
+ function readAwsIniSync(filePath: string): AwsIniFile | undefined {
132
+ let fd: number | undefined;
133
+ try {
134
+ fd = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK);
135
+ const stat = fs.fstatSync(fd);
136
+ if (!stat.isFile() || stat.size > MAX_AWS_INI_FILE_BYTES) return undefined;
137
+ const contents = Buffer.allocUnsafe(MAX_AWS_INI_FILE_BYTES + 1);
138
+ let bytesRead = 0;
139
+ while (bytesRead < contents.length) {
140
+ const count = fs.readSync(fd, contents, bytesRead, contents.length - bytesRead, bytesRead);
141
+ if (count === 0) break;
142
+ bytesRead += count;
143
+ }
144
+ if (bytesRead > MAX_AWS_INI_FILE_BYTES) return undefined;
145
+ return parseAwsIni(contents.toString("utf8", 0, bytesRead));
146
+ } catch {
147
+ return undefined;
148
+ } finally {
149
+ if (fd !== undefined) {
150
+ try {
151
+ fs.closeSync(fd);
152
+ } catch {
153
+ // Ignore close errors because file availability has already been determined.
154
+ }
155
+ }
156
+ }
157
+ }
158
+
159
+ function fingerprint(filePath: string): FileFingerprint {
160
+ try {
161
+ const stat = fs.statSync(filePath);
162
+ return { exists: true, size: stat.size, mtimeMs: stat.mtimeMs, ctimeMs: stat.ctimeMs, ino: stat.ino };
163
+ } catch {
164
+ return { exists: false };
165
+ }
166
+ }
167
+
168
+ function sameSource(a: AwsCredentialSource, b: AwsCredentialSource): boolean {
169
+ return a.profile === b.profile && a.credentialsPath === b.credentialsPath && a.configPath === b.configPath;
170
+ }
171
+
172
+ function sameFingerprint(a: FileFingerprint, b: FileFingerprint): boolean {
173
+ return (
174
+ a.exists === b.exists &&
175
+ a.size === b.size &&
176
+ a.mtimeMs === b.mtimeMs &&
177
+ a.ctimeMs === b.ctimeMs &&
178
+ a.ino === b.ino
179
+ );
180
+ }
@@ -23,6 +23,13 @@ import * as fs from "node:fs";
23
23
  import * as os from "node:os";
24
24
  import * as path from "node:path";
25
25
  import { $env, isEnoent, logger } from "@sayknow-cli/utils";
26
+ import {
27
+ type AwsIniFile,
28
+ classifyAwsProfileCapability,
29
+ parseAwsIni,
30
+ readAwsStaticEnvironmentCredentials,
31
+ resolveAwsCredentialSource,
32
+ } from "./aws-credential-config";
26
33
  import type { AwsCredentials } from "./aws-sigv4";
27
34
 
28
35
  export interface ResolvedCredentials extends AwsCredentials {
@@ -48,7 +55,7 @@ interface CacheEntry {
48
55
  const cache: Map<string, CacheEntry> = new Map();
49
56
 
50
57
  export async function resolveAwsCredentials(opts: CredentialResolveOptions = {}): Promise<ResolvedCredentials> {
51
- const profile = opts.profile || $env.AWS_PROFILE || "default";
58
+ const profile = resolveAwsCredentialSource({ profile: opts.profile }).profile;
52
59
  const region = opts.region || $env.AWS_REGION || $env.AWS_DEFAULT_REGION || "us-east-1";
53
60
  const cacheKey = `${profile}\x00${region}`;
54
61
 
@@ -62,7 +69,7 @@ export async function resolveAwsCredentials(opts: CredentialResolveOptions = {})
62
69
 
63
70
  async function resolveFresh(profile: string, region: string, signal?: AbortSignal): Promise<ResolvedCredentials> {
64
71
  // 1. Environment first — matches the AWS SDK chain order.
65
- const envCreds = readEnvCredentials();
72
+ const envCreds = readAwsStaticEnvironmentCredentials();
66
73
  if (envCreds) return envCreds;
67
74
 
68
75
  // 2. Profile (static or SSO).
@@ -81,52 +88,10 @@ async function resolveFresh(profile: string, region: string, signal?: AbortSigna
81
88
  );
82
89
  }
83
90
 
84
- function readEnvCredentials(): ResolvedCredentials | undefined {
85
- const ak = $env.AWS_ACCESS_KEY_ID;
86
- const sk = $env.AWS_SECRET_ACCESS_KEY;
87
- if (!ak || !sk) return undefined;
88
- const token = $env.AWS_SESSION_TOKEN;
89
- return token
90
- ? { accessKeyId: ak, secretAccessKey: sk, sessionToken: token }
91
- : { accessKeyId: ak, secretAccessKey: sk };
92
- }
93
-
94
- // ---------- INI parsing ----------
95
-
96
- /** Map of section name -> map of key -> value. Section names are stripped of
97
- * any leading `profile ` (so `~/.aws/config` aligns with `~/.aws/credentials`). */
98
- type IniFile = Record<string, Record<string, string>>;
99
-
100
- function parseIni(text: string): IniFile {
101
- const out: IniFile = {};
102
- let current: Record<string, string> | null = null;
103
- for (const rawLine of text.split(/\r?\n/)) {
104
- const line = rawLine.trim();
105
- if (!line || line.startsWith("#") || line.startsWith(";")) continue;
106
- if (line.startsWith("[") && line.endsWith("]")) {
107
- let name = line.slice(1, -1).trim();
108
- if (name.startsWith("profile ")) name = name.slice(8).trim();
109
- if (name.startsWith("sso-session ")) name = `sso-session:${name.slice(12).trim()}`;
110
- let section = out[name];
111
- if (!section) {
112
- section = {};
113
- out[name] = section;
114
- }
115
- current = section;
116
- continue;
117
- }
118
- if (!current) continue;
119
- const eq = line.indexOf("=");
120
- if (eq === -1) continue;
121
- current[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
122
- }
123
- return out;
124
- }
125
-
126
- async function readIniFile(p: string): Promise<IniFile | undefined> {
91
+ async function readIniFile(p: string): Promise<AwsIniFile | undefined> {
127
92
  try {
128
93
  const text = await fs.promises.readFile(p, "utf8");
129
- return parseIni(text);
94
+ return parseAwsIni(text);
130
95
  } catch (err) {
131
96
  if (isEnoent(err)) return undefined;
132
97
  throw err;
@@ -140,9 +105,7 @@ async function readProfileCredentials(
140
105
  region: string,
141
106
  signal: AbortSignal | undefined,
142
107
  ): Promise<ResolvedCredentials | undefined> {
143
- const home = os.homedir();
144
- const credentialsPath = $env.AWS_SHARED_CREDENTIALS_FILE || path.join(home, ".aws", "credentials");
145
- const configPath = $env.AWS_CONFIG_FILE || path.join(home, ".aws", "config");
108
+ const { credentialsPath, configPath } = resolveAwsCredentialSource({ profile });
146
109
 
147
110
  const credentialsIni = await readIniFile(credentialsPath);
148
111
  const configIni = await readIniFile(configPath);
@@ -152,22 +115,16 @@ async function readProfileCredentials(
152
115
  const merged: Record<string, string> = { ...(configIni?.[profile] ?? {}), ...(credentialsIni?.[profile] ?? {}) };
153
116
  if (Object.keys(merged).length === 0) return undefined;
154
117
 
155
- if (merged.aws_access_key_id && merged.aws_secret_access_key) {
156
- const out: ResolvedCredentials = {
157
- accessKeyId: merged.aws_access_key_id,
158
- secretAccessKey: merged.aws_secret_access_key,
159
- };
118
+ const capability = classifyAwsProfileCapability(profile, credentialsIni, configIni);
119
+ if (capability === "static") {
120
+ const { aws_access_key_id: accessKeyId, aws_secret_access_key: secretAccessKey } = merged;
121
+ if (!accessKeyId || !secretAccessKey) return undefined;
122
+ const out: ResolvedCredentials = { accessKeyId, secretAccessKey };
160
123
  if (merged.aws_session_token) out.sessionToken = merged.aws_session_token;
161
124
  return out;
162
125
  }
163
-
164
- if (merged.sso_account_id && merged.sso_role_name) {
165
- return readSsoCredentials(merged, configIni, region, signal);
166
- }
167
-
168
- if (merged.credential_process) {
169
- return readCredentialProcess(profile, merged.credential_process, signal);
170
- }
126
+ if (capability === "sso") return readSsoCredentials(merged, configIni, region, signal);
127
+ if (capability === "process") return readCredentialProcess(profile, merged.credential_process, signal);
171
128
 
172
129
  return undefined;
173
130
  }
@@ -181,7 +138,7 @@ interface SsoCachedToken {
181
138
 
182
139
  async function readSsoCredentials(
183
140
  profileCfg: Record<string, string>,
184
- configIni: IniFile | undefined,
141
+ configIni: AwsIniFile | undefined,
185
142
  defaultRegion: string,
186
143
  signal: AbortSignal | undefined,
187
144
  ): Promise<ResolvedCredentials | undefined> {