@sayknow-cli/ai 0.3.12 → 0.3.15
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.
- package/dist/types/auth-broker/wire-schemas.d.ts +8 -8
- package/dist/types/providers/anthropic.d.ts +1 -1
- package/dist/types/providers/aws-credential-config.d.ts +19 -0
- package/dist/types/providers/google-shared.d.ts +6 -0
- package/dist/types/providers/ollama.d.ts +6 -6
- package/dist/types/providers/openai-chat-server-schema.d.ts +54 -54
- package/dist/types/providers/openai-responses-server-schema.d.ts +18 -18
- package/dist/types/types.d.ts +2 -0
- package/dist/types/usage.d.ts +22 -22
- package/package.json +2 -2
- package/src/providers/amazon-bedrock.ts +37 -33
- package/src/providers/anthropic.ts +54 -49
- package/src/providers/aws-credential-config.ts +180 -0
- package/src/providers/aws-credentials.ts +20 -63
- package/src/providers/google-gemini-cli.ts +27 -3
- package/src/providers/google-shared.ts +55 -3
- package/src/types.ts +2 -0
package/dist/types/usage.d.ts
CHANGED
|
@@ -66,19 +66,19 @@ export interface UsageReport {
|
|
|
66
66
|
raw?: unknown;
|
|
67
67
|
}
|
|
68
68
|
export declare const usageUnitSchema: z.ZodEnum<{
|
|
69
|
-
bytes: "bytes";
|
|
70
|
-
minutes: "minutes";
|
|
71
69
|
percent: "percent";
|
|
72
|
-
requests: "requests";
|
|
73
70
|
tokens: "tokens";
|
|
74
|
-
|
|
71
|
+
requests: "requests";
|
|
75
72
|
usd: "usd";
|
|
73
|
+
minutes: "minutes";
|
|
74
|
+
bytes: "bytes";
|
|
75
|
+
unknown: "unknown";
|
|
76
76
|
}>;
|
|
77
77
|
export declare const usageStatusSchema: z.ZodEnum<{
|
|
78
|
-
exhausted: "exhausted";
|
|
79
|
-
ok: "ok";
|
|
80
78
|
unknown: "unknown";
|
|
79
|
+
ok: "ok";
|
|
81
80
|
warning: "warning";
|
|
81
|
+
exhausted: "exhausted";
|
|
82
82
|
}>;
|
|
83
83
|
export declare const usageWindowSchema: z.ZodObject<{
|
|
84
84
|
id: z.ZodString;
|
|
@@ -93,13 +93,13 @@ export declare const usageAmountSchema: z.ZodObject<{
|
|
|
93
93
|
usedFraction: z.ZodOptional<z.ZodNumber>;
|
|
94
94
|
remainingFraction: z.ZodOptional<z.ZodNumber>;
|
|
95
95
|
unit: z.ZodEnum<{
|
|
96
|
-
bytes: "bytes";
|
|
97
|
-
minutes: "minutes";
|
|
98
96
|
percent: "percent";
|
|
99
|
-
requests: "requests";
|
|
100
97
|
tokens: "tokens";
|
|
101
|
-
|
|
98
|
+
requests: "requests";
|
|
102
99
|
usd: "usd";
|
|
100
|
+
minutes: "minutes";
|
|
101
|
+
bytes: "bytes";
|
|
102
|
+
unknown: "unknown";
|
|
103
103
|
}>;
|
|
104
104
|
}, z.core.$strip>;
|
|
105
105
|
export declare const usageScopeSchema: z.ZodObject<{
|
|
@@ -138,20 +138,20 @@ export declare const usageLimitSchema: z.ZodObject<{
|
|
|
138
138
|
usedFraction: z.ZodOptional<z.ZodNumber>;
|
|
139
139
|
remainingFraction: z.ZodOptional<z.ZodNumber>;
|
|
140
140
|
unit: z.ZodEnum<{
|
|
141
|
-
bytes: "bytes";
|
|
142
|
-
minutes: "minutes";
|
|
143
141
|
percent: "percent";
|
|
144
|
-
requests: "requests";
|
|
145
142
|
tokens: "tokens";
|
|
146
|
-
|
|
143
|
+
requests: "requests";
|
|
147
144
|
usd: "usd";
|
|
145
|
+
minutes: "minutes";
|
|
146
|
+
bytes: "bytes";
|
|
147
|
+
unknown: "unknown";
|
|
148
148
|
}>;
|
|
149
149
|
}, z.core.$strip>;
|
|
150
150
|
status: z.ZodOptional<z.ZodEnum<{
|
|
151
|
-
exhausted: "exhausted";
|
|
152
|
-
ok: "ok";
|
|
153
151
|
unknown: "unknown";
|
|
152
|
+
ok: "ok";
|
|
154
153
|
warning: "warning";
|
|
154
|
+
exhausted: "exhausted";
|
|
155
155
|
}>>;
|
|
156
156
|
notes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
157
157
|
}, z.core.$strip>;
|
|
@@ -184,20 +184,20 @@ export declare const usageReportSchema: z.ZodObject<{
|
|
|
184
184
|
usedFraction: z.ZodOptional<z.ZodNumber>;
|
|
185
185
|
remainingFraction: z.ZodOptional<z.ZodNumber>;
|
|
186
186
|
unit: z.ZodEnum<{
|
|
187
|
-
bytes: "bytes";
|
|
188
|
-
minutes: "minutes";
|
|
189
187
|
percent: "percent";
|
|
190
|
-
requests: "requests";
|
|
191
188
|
tokens: "tokens";
|
|
192
|
-
|
|
189
|
+
requests: "requests";
|
|
193
190
|
usd: "usd";
|
|
191
|
+
minutes: "minutes";
|
|
192
|
+
bytes: "bytes";
|
|
193
|
+
unknown: "unknown";
|
|
194
194
|
}>;
|
|
195
195
|
}, z.core.$strip>;
|
|
196
196
|
status: z.ZodOptional<z.ZodEnum<{
|
|
197
|
-
exhausted: "exhausted";
|
|
198
|
-
ok: "ok";
|
|
199
197
|
unknown: "unknown";
|
|
198
|
+
ok: "ok";
|
|
200
199
|
warning: "warning";
|
|
200
|
+
exhausted: "exhausted";
|
|
201
201
|
}>>;
|
|
202
202
|
notes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
203
203
|
}, z.core.$strip>>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@sayknow-cli/ai",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.15",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://sayknow-cli.com",
|
|
7
7
|
"author": "jaybeyond",
|
|
@@ -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.
|
|
46
|
+
"@sayknow-cli/utils": "0.3.15",
|
|
47
47
|
"openai": "^6.36.0",
|
|
48
48
|
"partial-json": "^0.1.7",
|
|
49
49
|
"zod": "4.4.3"
|
|
@@ -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
|
-
|
|
237
|
-
if (
|
|
238
|
-
|
|
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
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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:
|
|
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
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
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
|
|
1560
|
-
//
|
|
1561
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|