@sayknow-cli/ai 0.3.12 → 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 (190) hide show
  1. package/package.json +24 -25
  2. package/src/providers/amazon-bedrock.ts +37 -33
  3. package/src/providers/anthropic.ts +54 -49
  4. package/src/providers/aws-credential-config.ts +180 -0
  5. package/src/providers/aws-credentials.ts +20 -63
  6. package/src/providers/google-gemini-cli.ts +27 -3
  7. package/src/providers/google-shared.ts +55 -3
  8. package/src/types.ts +2 -0
  9. package/dist/types/api-registry.d.ts +0 -30
  10. package/dist/types/auth-broker/client.d.ts +0 -67
  11. package/dist/types/auth-broker/index.d.ts +0 -5
  12. package/dist/types/auth-broker/refresher.d.ts +0 -25
  13. package/dist/types/auth-broker/remote-store.d.ts +0 -99
  14. package/dist/types/auth-broker/server.d.ts +0 -32
  15. package/dist/types/auth-broker/types.d.ts +0 -110
  16. package/dist/types/auth-broker/wire-schemas.d.ts +0 -443
  17. package/dist/types/auth-gateway/http.d.ts +0 -40
  18. package/dist/types/auth-gateway/index.d.ts +0 -3
  19. package/dist/types/auth-gateway/server.d.ts +0 -17
  20. package/dist/types/auth-gateway/types.d.ts +0 -115
  21. package/dist/types/auth-storage.d.ts +0 -695
  22. package/dist/types/cli.d.ts +0 -2
  23. package/dist/types/index.d.ts +0 -51
  24. package/dist/types/model-cache.d.ts +0 -17
  25. package/dist/types/model-manager.d.ts +0 -62
  26. package/dist/types/model-retirements.d.ts +0 -6
  27. package/dist/types/model-thinking.d.ts +0 -74
  28. package/dist/types/models.d.ts +0 -21
  29. package/dist/types/provider-details.d.ts +0 -24
  30. package/dist/types/provider-models/bundled-references.d.ts +0 -4
  31. package/dist/types/provider-models/descriptors.d.ts +0 -48
  32. package/dist/types/provider-models/google.d.ts +0 -20
  33. package/dist/types/provider-models/index.d.ts +0 -5
  34. package/dist/types/provider-models/ollama.d.ts +0 -7
  35. package/dist/types/provider-models/openai-compat.d.ts +0 -256
  36. package/dist/types/provider-models/special.d.ts +0 -19
  37. package/dist/types/providers/amazon-bedrock.d.ts +0 -60
  38. package/dist/types/providers/anthropic-messages-server-schema.d.ts +0 -450
  39. package/dist/types/providers/anthropic-messages-server.d.ts +0 -17
  40. package/dist/types/providers/anthropic.d.ts +0 -208
  41. package/dist/types/providers/aws-credentials.d.ts +0 -43
  42. package/dist/types/providers/aws-eventstream.d.ts +0 -38
  43. package/dist/types/providers/aws-sigv4.d.ts +0 -55
  44. package/dist/types/providers/azure-openai-responses.d.ts +0 -15
  45. package/dist/types/providers/composer-discipline.d.ts +0 -26
  46. package/dist/types/providers/cursor/client-version.d.ts +0 -10
  47. package/dist/types/providers/cursor/gen/agent_pb.d.ts +0 -13022
  48. package/dist/types/providers/cursor.d.ts +0 -45
  49. package/dist/types/providers/error-message.d.ts +0 -27
  50. package/dist/types/providers/github-copilot-headers.d.ts +0 -40
  51. package/dist/types/providers/gitlab-duo.d.ts +0 -27
  52. package/dist/types/providers/google-auth.d.ts +0 -24
  53. package/dist/types/providers/google-gemini-cli.d.ts +0 -75
  54. package/dist/types/providers/google-gemini-headers.d.ts +0 -43
  55. package/dist/types/providers/google-shared.d.ts +0 -173
  56. package/dist/types/providers/google-types.d.ts +0 -138
  57. package/dist/types/providers/google-vertex.d.ts +0 -7
  58. package/dist/types/providers/google.d.ts +0 -4
  59. package/dist/types/providers/grammar.d.ts +0 -1
  60. package/dist/types/providers/kimi.d.ts +0 -27
  61. package/dist/types/providers/mock.d.ts +0 -177
  62. package/dist/types/providers/ollama.d.ts +0 -41
  63. package/dist/types/providers/openai-anthropic-shim.d.ts +0 -31
  64. package/dist/types/providers/openai-bounded-rate-limits.d.ts +0 -3
  65. package/dist/types/providers/openai-chat-server-schema.d.ts +0 -815
  66. package/dist/types/providers/openai-chat-server.d.ts +0 -16
  67. package/dist/types/providers/openai-codex/constants.d.ts +0 -26
  68. package/dist/types/providers/openai-codex/request-transformer.d.ts +0 -50
  69. package/dist/types/providers/openai-codex/response-handler.d.ts +0 -17
  70. package/dist/types/providers/openai-codex-responses.d.ts +0 -67
  71. package/dist/types/providers/openai-completions-compat.d.ts +0 -27
  72. package/dist/types/providers/openai-completions.d.ts +0 -33
  73. package/dist/types/providers/openai-request-transform.d.ts +0 -4
  74. package/dist/types/providers/openai-responses-server-schema.d.ts +0 -392
  75. package/dist/types/providers/openai-responses-server.d.ts +0 -17
  76. package/dist/types/providers/openai-responses-shared.d.ts +0 -104
  77. package/dist/types/providers/openai-responses.d.ts +0 -32
  78. package/dist/types/providers/pi-native-client.d.ts +0 -13
  79. package/dist/types/providers/pi-native-server.d.ts +0 -68
  80. package/dist/types/providers/register-builtins.d.ts +0 -31
  81. package/dist/types/providers/synthetic.d.ts +0 -26
  82. package/dist/types/providers/transform-messages.d.ts +0 -14
  83. package/dist/types/providers/vision-guard.d.ts +0 -8
  84. package/dist/types/rate-limit-utils.d.ts +0 -19
  85. package/dist/types/stream.d.ts +0 -43
  86. package/dist/types/types.d.ts +0 -829
  87. package/dist/types/usage/claude.d.ts +0 -3
  88. package/dist/types/usage/gemini.d.ts +0 -2
  89. package/dist/types/usage/github-copilot.d.ts +0 -7
  90. package/dist/types/usage/google-antigravity.d.ts +0 -2
  91. package/dist/types/usage/grok-cli.d.ts +0 -10
  92. package/dist/types/usage/kimi.d.ts +0 -2
  93. package/dist/types/usage/minimax-code.d.ts +0 -2
  94. package/dist/types/usage/openai-codex.d.ts +0 -3
  95. package/dist/types/usage/shared.d.ts +0 -1
  96. package/dist/types/usage/zai.d.ts +0 -2
  97. package/dist/types/usage.d.ts +0 -258
  98. package/dist/types/utils/abort.d.ts +0 -19
  99. package/dist/types/utils/anthropic-auth.d.ts +0 -31
  100. package/dist/types/utils/discovery/antigravity.d.ts +0 -67
  101. package/dist/types/utils/discovery/codex.d.ts +0 -38
  102. package/dist/types/utils/discovery/cursor.d.ts +0 -23
  103. package/dist/types/utils/discovery/gemini.d.ts +0 -25
  104. package/dist/types/utils/discovery/index.d.ts +0 -4
  105. package/dist/types/utils/discovery/openai-compatible.d.ts +0 -74
  106. package/dist/types/utils/event-stream.d.ts +0 -33
  107. package/dist/types/utils/fireworks-model-id.d.ts +0 -10
  108. package/dist/types/utils/foundry.d.ts +0 -1
  109. package/dist/types/utils/h2-fetch.d.ts +0 -22
  110. package/dist/types/utils/http-inspector.d.ts +0 -35
  111. package/dist/types/utils/idle-iterator.d.ts +0 -67
  112. package/dist/types/utils/json-parse.d.ts +0 -18
  113. package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +0 -18
  114. package/dist/types/utils/oauth/anthropic.d.ts +0 -22
  115. package/dist/types/utils/oauth/api-key-login.d.ts +0 -35
  116. package/dist/types/utils/oauth/api-key-validation.d.ts +0 -27
  117. package/dist/types/utils/oauth/callback-server.d.ts +0 -60
  118. package/dist/types/utils/oauth/cerebras.d.ts +0 -1
  119. package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +0 -18
  120. package/dist/types/utils/oauth/cursor.d.ts +0 -15
  121. package/dist/types/utils/oauth/deepinfra.d.ts +0 -1
  122. package/dist/types/utils/oauth/deepseek.d.ts +0 -10
  123. package/dist/types/utils/oauth/firepass.d.ts +0 -1
  124. package/dist/types/utils/oauth/fireworks.d.ts +0 -1
  125. package/dist/types/utils/oauth/fugu.d.ts +0 -1
  126. package/dist/types/utils/oauth/github-copilot.d.ts +0 -38
  127. package/dist/types/utils/oauth/gitlab-duo.d.ts +0 -3
  128. package/dist/types/utils/oauth/glm-zcode.d.ts +0 -71
  129. package/dist/types/utils/oauth/google-antigravity.d.ts +0 -11
  130. package/dist/types/utils/oauth/google-gemini-cli.d.ts +0 -10
  131. package/dist/types/utils/oauth/google-oauth-shared.d.ts +0 -28
  132. package/dist/types/utils/oauth/huggingface.d.ts +0 -19
  133. package/dist/types/utils/oauth/index.d.ts +0 -39
  134. package/dist/types/utils/oauth/kagi.d.ts +0 -17
  135. package/dist/types/utils/oauth/kilo.d.ts +0 -5
  136. package/dist/types/utils/oauth/kimi.d.ts +0 -21
  137. package/dist/types/utils/oauth/litellm.d.ts +0 -18
  138. package/dist/types/utils/oauth/lm-studio.d.ts +0 -17
  139. package/dist/types/utils/oauth/minimax-code.d.ts +0 -28
  140. package/dist/types/utils/oauth/moonshot.d.ts +0 -1
  141. package/dist/types/utils/oauth/nanogpt.d.ts +0 -1
  142. package/dist/types/utils/oauth/nvidia.d.ts +0 -18
  143. package/dist/types/utils/oauth/ollama-cloud.d.ts +0 -2
  144. package/dist/types/utils/oauth/ollama.d.ts +0 -18
  145. package/dist/types/utils/oauth/openai-codex.d.ts +0 -21
  146. package/dist/types/utils/oauth/opencode.d.ts +0 -18
  147. package/dist/types/utils/oauth/parallel.d.ts +0 -17
  148. package/dist/types/utils/oauth/perplexity.d.ts +0 -9
  149. package/dist/types/utils/oauth/pkce.d.ts +0 -8
  150. package/dist/types/utils/oauth/qianfan.d.ts +0 -17
  151. package/dist/types/utils/oauth/qwen-portal.d.ts +0 -19
  152. package/dist/types/utils/oauth/synthetic.d.ts +0 -1
  153. package/dist/types/utils/oauth/tavily.d.ts +0 -17
  154. package/dist/types/utils/oauth/together.d.ts +0 -1
  155. package/dist/types/utils/oauth/types.d.ts +0 -45
  156. package/dist/types/utils/oauth/venice.d.ts +0 -18
  157. package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +0 -18
  158. package/dist/types/utils/oauth/vllm.d.ts +0 -16
  159. package/dist/types/utils/oauth/xai.d.ts +0 -30
  160. package/dist/types/utils/oauth/xiaomi.d.ts +0 -25
  161. package/dist/types/utils/oauth/zai.d.ts +0 -18
  162. package/dist/types/utils/oauth/zenmux.d.ts +0 -1
  163. package/dist/types/utils/overflow.d.ts +0 -64
  164. package/dist/types/utils/parse-bind.d.ts +0 -23
  165. package/dist/types/utils/provider-response.d.ts +0 -3
  166. package/dist/types/utils/retry-after.d.ts +0 -3
  167. package/dist/types/utils/retry-budget.d.ts +0 -1
  168. package/dist/types/utils/retry.d.ts +0 -26
  169. package/dist/types/utils/schema/adapt.d.ts +0 -24
  170. package/dist/types/utils/schema/compatibility.d.ts +0 -30
  171. package/dist/types/utils/schema/dereference.d.ts +0 -11
  172. package/dist/types/utils/schema/draft.d.ts +0 -10
  173. package/dist/types/utils/schema/equality.d.ts +0 -4
  174. package/dist/types/utils/schema/fields.d.ts +0 -49
  175. package/dist/types/utils/schema/index.d.ts +0 -14
  176. package/dist/types/utils/schema/json-schema-validator.d.ts +0 -12
  177. package/dist/types/utils/schema/meta-validator.d.ts +0 -2
  178. package/dist/types/utils/schema/normalize.d.ts +0 -93
  179. package/dist/types/utils/schema/root-combinator.d.ts +0 -12
  180. package/dist/types/utils/schema/spill.d.ts +0 -8
  181. package/dist/types/utils/schema/stamps.d.ts +0 -25
  182. package/dist/types/utils/schema/types.d.ts +0 -4
  183. package/dist/types/utils/schema/wire.d.ts +0 -54
  184. package/dist/types/utils/schema/zod-decontaminate.d.ts +0 -31
  185. package/dist/types/utils/sse-debug.d.ts +0 -10
  186. package/dist/types/utils/tool-call-healing.d.ts +0 -71
  187. package/dist/types/utils/tool-choice-capability.d.ts +0 -41
  188. package/dist/types/utils/tool-choice.d.ts +0 -50
  189. package/dist/types/utils/validation.d.ts +0 -17
  190. package/dist/types/utils.d.ts +0 -35
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sayknow-cli/ai",
4
- "version": "0.3.12",
4
+ "version": "0.3.13",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://sayknow-cli.com",
7
7
  "author": "jaybeyond",
@@ -27,7 +27,7 @@
27
27
  "api"
28
28
  ],
29
29
  "main": "./src/index.ts",
30
- "types": "./dist/types/index.d.ts",
30
+ "types": "./src/index.ts",
31
31
  "bin": {
32
32
  "pi-ai": "./src/cli.ts"
33
33
  },
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@anthropic-ai/sdk": "^0.94.0",
45
45
  "@bufbuild/protobuf": "^2.12.0",
46
- "@sayknow-cli/utils": "0.3.12",
46
+ "@sayknow-cli/utils": "0.3.13",
47
47
  "openai": "^6.36.0",
48
48
  "partial-json": "^0.1.7",
49
49
  "zod": "4.4.3"
@@ -57,88 +57,87 @@
57
57
  "files": [
58
58
  "src",
59
59
  "README.md",
60
- "CHANGELOG.md",
61
- "dist/types"
60
+ "CHANGELOG.md"
62
61
  ],
63
62
  "exports": {
64
63
  ".": {
65
- "types": "./dist/types/index.d.ts",
64
+ "types": "./src/index.ts",
66
65
  "import": "./src/index.ts"
67
66
  },
68
67
  "./*": {
69
- "types": "./dist/types/*.d.ts",
68
+ "types": "./src/*.ts",
70
69
  "import": "./src/*.ts"
71
70
  },
72
71
  "./auth-broker": {
73
- "types": "./dist/types/auth-broker/index.d.ts",
72
+ "types": "./src/auth-broker/index.ts",
74
73
  "import": "./src/auth-broker/index.ts"
75
74
  },
76
75
  "./auth-broker/*": {
77
- "types": "./dist/types/auth-broker/*.d.ts",
76
+ "types": "./src/auth-broker/*.ts",
78
77
  "import": "./src/auth-broker/*.ts"
79
78
  },
80
79
  "./auth-gateway": {
81
- "types": "./dist/types/auth-gateway/index.d.ts",
80
+ "types": "./src/auth-gateway/index.ts",
82
81
  "import": "./src/auth-gateway/index.ts"
83
82
  },
84
83
  "./auth-gateway/*": {
85
- "types": "./dist/types/auth-gateway/*.d.ts",
84
+ "types": "./src/auth-gateway/*.ts",
86
85
  "import": "./src/auth-gateway/*.ts"
87
86
  },
88
87
  "./models.json": {
89
- "types": "./dist/types/models.json.d.d.ts",
88
+ "types": "./src/models.json.d.ts",
90
89
  "import": "./src/models.json"
91
90
  },
92
91
  "./provider-models": {
93
- "types": "./dist/types/provider-models/index.d.ts",
92
+ "types": "./src/provider-models/index.ts",
94
93
  "import": "./src/provider-models/index.ts"
95
94
  },
96
95
  "./provider-models/*": {
97
- "types": "./dist/types/provider-models/*.d.ts",
96
+ "types": "./src/provider-models/*.ts",
98
97
  "import": "./src/provider-models/*.ts"
99
98
  },
100
99
  "./providers/*": {
101
- "types": "./dist/types/providers/*.d.ts",
100
+ "types": "./src/providers/*.ts",
102
101
  "import": "./src/providers/*.ts"
103
102
  },
104
103
  "./providers/cursor/gen/*": {
105
- "types": "./dist/types/providers/cursor/gen/*.d.ts",
104
+ "types": "./src/providers/cursor/gen/*.ts",
106
105
  "import": "./src/providers/cursor/gen/*.ts"
107
106
  },
108
107
  "./providers/openai-codex/*": {
109
- "types": "./dist/types/providers/openai-codex/*.d.ts",
108
+ "types": "./src/providers/openai-codex/*.ts",
110
109
  "import": "./src/providers/openai-codex/*.ts"
111
110
  },
112
111
  "./usage/*": {
113
- "types": "./dist/types/usage/*.d.ts",
112
+ "types": "./src/usage/*.ts",
114
113
  "import": "./src/usage/*.ts"
115
114
  },
116
115
  "./utils/*": {
117
- "types": "./dist/types/utils/*.d.ts",
116
+ "types": "./src/utils/*.ts",
118
117
  "import": "./src/utils/*.ts"
119
118
  },
120
119
  "./utils/discovery": {
121
- "types": "./dist/types/utils/discovery/index.d.ts",
120
+ "types": "./src/utils/discovery/index.ts",
122
121
  "import": "./src/utils/discovery/index.ts"
123
122
  },
124
123
  "./utils/discovery/*": {
125
- "types": "./dist/types/utils/discovery/*.d.ts",
124
+ "types": "./src/utils/discovery/*.ts",
126
125
  "import": "./src/utils/discovery/*.ts"
127
126
  },
128
127
  "./utils/oauth": {
129
- "types": "./dist/types/utils/oauth/index.d.ts",
128
+ "types": "./src/utils/oauth/index.ts",
130
129
  "import": "./src/utils/oauth/index.ts"
131
130
  },
132
131
  "./utils/oauth/*": {
133
- "types": "./dist/types/utils/oauth/*.d.ts",
132
+ "types": "./src/utils/oauth/*.ts",
134
133
  "import": "./src/utils/oauth/*.ts"
135
134
  },
136
135
  "./utils/schema": {
137
- "types": "./dist/types/utils/schema/index.d.ts",
136
+ "types": "./src/utils/schema/index.ts",
138
137
  "import": "./src/utils/schema/index.ts"
139
138
  },
140
139
  "./utils/schema/*": {
141
- "types": "./dist/types/utils/schema/*.d.ts",
140
+ "types": "./src/utils/schema/*.ts",
142
141
  "import": "./src/utils/schema/*.ts"
143
142
  },
144
143
  "./*.js": "./src/*.ts"
@@ -7,7 +7,7 @@
7
7
  * Bun's native `HTTPS_PROXY` support.
8
8
  */
9
9
 
10
- import { $env, $flag, extractHttpStatusFromError, fetchWithRetry } from "@sayknow-cli/utils";
10
+ import { $credentialEnv, $env, $flag, extractHttpStatusFromError, fetchWithRetry } from "@sayknow-cli/utils";
11
11
  import type { Effort } from "../model-thinking";
12
12
  import { mapEffortToAnthropicAdaptiveEffort, requireSupportedEffort } from "../model-thinking";
13
13
  import { calculateCost } from "../models";
@@ -39,8 +39,10 @@ import {
39
39
  markToolChoiceIncapability,
40
40
  resolveToolChoice,
41
41
  } from "../utils/tool-choice-capability";
42
+ import { isValidBedrockBearerToken } from "./aws-credential-config";
42
43
  import { resolveAwsCredentials } from "./aws-credentials";
43
44
  import { decodeEventStream } from "./aws-eventstream";
45
+ import type { AwsCredentials } from "./aws-sigv4";
44
46
  import { signRequest } from "./aws-sigv4";
45
47
  import { transformMessages } from "./transform-messages";
46
48
 
@@ -162,6 +164,8 @@ interface MetadataEvent {
162
164
  };
163
165
  }
164
166
 
167
+ type BedrockAuthMode = { kind: "bearer"; token: string } | { kind: "sigv4"; credentials: AwsCredentials };
168
+
165
169
  export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
166
170
  model: Model<"bedrock-converse-stream">,
167
171
  context: Context,
@@ -233,34 +237,44 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
233
237
  body: commandInput,
234
238
  };
235
239
 
236
- let credentials: { accessKeyId: string; secretAccessKey: string; sessionToken?: string };
237
- if ($flag("AWS_BEDROCK_SKIP_AUTH")) {
238
- credentials = { accessKeyId: "dummy-access-key", secretAccessKey: "dummy-secret-key" };
239
- } else {
240
- credentials = await resolveAwsCredentials({
241
- profile: options.profile,
242
- region,
243
- signal: options.signal,
244
- });
240
+ const bearerToken = $credentialEnv("AWS_BEARER_TOKEN_BEDROCK");
241
+ if (bearerToken && !isValidBedrockBearerToken(bearerToken)) {
242
+ throw new Error("AWS_BEARER_TOKEN_BEDROCK contains unsafe control characters.");
245
243
  }
246
-
244
+ const authMode: BedrockAuthMode = bearerToken
245
+ ? { kind: "bearer", token: bearerToken }
246
+ : {
247
+ kind: "sigv4",
248
+ credentials: $flag("AWS_BEDROCK_SKIP_AUTH")
249
+ ? { accessKeyId: "dummy-access-key", secretAccessKey: "dummy-secret-key" }
250
+ : await resolveAwsCredentials({ profile: options.profile, region, signal: options.signal }),
251
+ };
247
252
  const bodyText = JSON.stringify(commandInput);
248
253
  const body = new TextEncoder().encode(bodyText);
249
254
  const baseHeaders: Record<string, string> = {
250
255
  "content-type": "application/json",
251
256
  accept: "application/vnd.amazon.eventstream",
252
257
  };
253
- const signed = await signRequest({
254
- method: "POST",
255
- host,
256
- path: urlPath,
257
- body,
258
- region,
259
- service: "bedrock",
260
- credentials,
261
- headers: baseHeaders,
262
- });
263
- const requestHeaders: Record<string, string> = { ...baseHeaders, ...signed };
258
+ const buildRequestHeaders = async (requestBody: Uint8Array): Promise<Record<string, string>> => {
259
+ const headers = new Headers(baseHeaders);
260
+ if (authMode.kind === "bearer") {
261
+ headers.set("authorization", `Bearer ${authMode.token}`);
262
+ return Object.fromEntries(headers);
263
+ }
264
+ const signed = await signRequest({
265
+ method: "POST",
266
+ host,
267
+ path: urlPath,
268
+ body: requestBody,
269
+ region,
270
+ service: "bedrock",
271
+ credentials: authMode.credentials,
272
+ headers: baseHeaders,
273
+ });
274
+ for (const [name, value] of Object.entries(signed)) headers.set(name, value);
275
+ return Object.fromEntries(headers);
276
+ };
277
+ const requestHeaders = await buildRequestHeaders(body);
264
278
  const sentForcedToolChoice = Boolean(toolConfig?.toolChoice?.any || toolConfig?.toolChoice?.tool);
265
279
  let fallbackRan = false;
266
280
  const retryWithoutForcedToolChoice = async (reason: string) => {
@@ -279,20 +293,10 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
279
293
  stripBedrockForcedToolChoiceForRetry(commandInput);
280
294
  const retryBodyText = JSON.stringify(commandInput);
281
295
  const retryBody = new TextEncoder().encode(retryBodyText);
282
- const retrySigned = await signRequest({
283
- method: "POST",
284
- host,
285
- path: urlPath,
286
- body: retryBody,
287
- region,
288
- service: "bedrock",
289
- credentials,
290
- headers: baseHeaders,
291
- });
292
296
  if (rawRequestDump) rawRequestDump.body = commandInput;
293
297
  return fetchWithRetry(url, {
294
298
  method: "POST",
295
- headers: { ...baseHeaders, ...retrySigned },
299
+ headers: await buildRequestHeaders(retryBody),
296
300
  body: retryBody,
297
301
  signal: options.signal,
298
302
  maxAttempts: 1,
@@ -1341,6 +1341,17 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1341
1341
  }
1342
1342
  blocksByAnthropicIndex.set(anthropicIndex, block);
1343
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
+ };
1344
1355
  const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs();
1345
1356
  const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs);
1346
1357
  stream.push({ type: "start", partial: output });
@@ -1360,6 +1371,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1360
1371
  const { requestSignal } = activeAbortTracker;
1361
1372
  const anthropicRequest = client.messages.create({ ...params, stream: true }, { signal: requestSignal });
1362
1373
  let streamedReplayUnsafeContent = false;
1374
+ let sawProviderSafetyStop = false;
1363
1375
 
1364
1376
  try {
1365
1377
  const {
@@ -1386,6 +1398,12 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1386
1398
  abortSignal: options?.signal,
1387
1399
  })) {
1388
1400
  sawEvent = true;
1401
+ if (sawProviderSafetyStop) {
1402
+ if (event.type === "message_stop") {
1403
+ sawTerminalEnvelope = true;
1404
+ }
1405
+ continue;
1406
+ }
1389
1407
 
1390
1408
  if (event.type === "message_start") {
1391
1409
  if (sawMessageStart) {
@@ -1545,26 +1563,34 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1545
1563
  }
1546
1564
  } else if (event.type === "message_delta") {
1547
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";
1548
1569
  if (rawStopReason) {
1549
- output.stopReason = mapStopReason(rawStopReason);
1570
+ output.stopReason = isProviderSafetyStop ? "error" : mapStopReason(rawStopReason);
1550
1571
  sawTerminalEnvelope = true;
1551
1572
  }
1552
- const stopDetails = event.delta.stop_details;
1553
- if (stopDetails && stopDetails.type === "refusal") {
1554
- const explanation = stopDetails.explanation?.trim();
1555
- const category = stopDetails.category;
1556
- const label = category ? `Refusal (${category})` : "Refusal";
1557
- 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
+ }
1558
1589
  } else if (output.stopReason === "error" && !output.errorMessage) {
1559
- // Anthropic flagged an error-class stop (refusal / sensitive) without
1560
- // populating stop_details. Surface the raw reason instead of falling
1561
- // through to the generic "unknown error" string when we throw below.
1562
- output.errorMessage =
1563
- rawStopReason === "refusal"
1564
- ? "Refusal (no details provided)"
1565
- : rawStopReason === "sensitive"
1566
- ? "Content flagged by safety filters"
1567
- : `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"}`;
1568
1594
  }
1569
1595
  if (event.usage.input_tokens != null) {
1570
1596
  output.usage.input = event.usage.input_tokens;
@@ -1607,6 +1633,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1607
1633
  break;
1608
1634
  } catch (streamError) {
1609
1635
  const streamFailure = activeAbortTracker.getLocalAbortReason() ?? streamError;
1636
+ if (sawProviderSafetyStop) {
1637
+ throw streamFailure;
1638
+ }
1610
1639
  if (
1611
1640
  !disableStrictTools &&
1612
1641
  firstTokenTime === undefined &&
@@ -1614,19 +1643,13 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1614
1643
  isAnthropicStrictGrammarTooLargeError(streamFailure)
1615
1644
  ) {
1616
1645
  strictFallbackErrorMessage = await finalizeErrorMessage(streamFailure, rawRequestDump);
1617
- output.errorMessage = strictFallbackErrorMessage;
1618
1646
  if (providerSessionState) {
1619
1647
  providerSessionState.strictToolsDisabled = true;
1620
1648
  }
1621
1649
  disableStrictTools = true;
1622
1650
  params = await prepareParams();
1623
1651
  providerRetryAttempt = 0;
1624
- output.content.length = 0;
1625
- output.responseId = undefined;
1626
- output.providerPayload = undefined;
1627
- output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
1628
- output.stopReason = "stop";
1629
- firstTokenTime = undefined;
1652
+ resetOutputForRetry();
1630
1653
  continue;
1631
1654
  }
1632
1655
  if (
@@ -1654,12 +1677,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1654
1677
  droppedForcedToolChoice = true;
1655
1678
  params = await prepareParams({ dropForcedToolChoice: true });
1656
1679
  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;
1680
+ resetOutputForRetry();
1663
1681
  continue;
1664
1682
  }
1665
1683
  if (
@@ -1674,12 +1692,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1674
1692
  thinkingRepairAttempted = true;
1675
1693
  params = await prepareParams({ repairLatestAssistantThinking: true });
1676
1694
  providerRetryAttempt = 0;
1677
- output.content.length = 0;
1678
- output.responseId = undefined;
1679
- output.providerPayload = undefined;
1680
- output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
1681
- output.stopReason = "stop";
1682
- firstTokenTime = undefined;
1695
+ resetOutputForRetry();
1683
1696
  continue;
1684
1697
  }
1685
1698
  if (
@@ -1698,12 +1711,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1698
1711
  dropFastMode = true;
1699
1712
  params = await prepareParams();
1700
1713
  providerRetryAttempt = 0;
1701
- output.content.length = 0;
1702
- output.responseId = undefined;
1703
- output.providerPayload = undefined;
1704
- output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
1705
- output.stopReason = "stop";
1706
- firstTokenTime = undefined;
1714
+ resetOutputForRetry();
1707
1715
  continue;
1708
1716
  }
1709
1717
  const isTransientEnvelopeFailure =
@@ -1725,13 +1733,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1725
1733
  } else {
1726
1734
  await scheduler.wait(delayMs, { signal: options?.signal });
1727
1735
  }
1728
- output.content.length = 0;
1729
- output.responseId = undefined;
1730
- output.errorMessage = strictFallbackErrorMessage;
1731
- output.providerPayload = undefined;
1732
- output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
1733
- output.stopReason = "stop";
1734
- firstTokenTime = undefined;
1736
+ resetOutputForRetry();
1735
1737
  }
1736
1738
  }
1737
1739
 
@@ -1750,7 +1752,10 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1750
1752
  const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
1751
1753
  output.stopReason = activeAbortTracker.wasCallerAbort() ? "aborted" : "error";
1752
1754
  output.errorStatus = extractHttpStatusFromError(error);
1753
- 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
+ }
1754
1759
  output.errorMessage = rewriteCopilotError(output.errorMessage, error, model.provider);
1755
1760
  output.duration = Date.now() - startTime;
1756
1761
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
@@ -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
+ }