@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.
- package/package.json +24 -25
- package/src/model-cache.ts +1 -1
- package/src/model-manager.ts +14 -3
- package/src/model-retirements.ts +13 -0
- package/src/model-thinking.ts +34 -2
- package/src/models.ts +26 -5
- package/src/provider-models/google.ts +1 -0
- package/src/providers/amazon-bedrock.ts +37 -33
- package/src/providers/anthropic.ts +94 -65
- 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-gemini-headers.ts +6 -1
- package/src/providers/google-shared.ts +55 -3
- package/src/types.ts +2 -0
- package/src/utils/discovery/antigravity.ts +16 -2
- package/dist/types/api-registry.d.ts +0 -30
- package/dist/types/auth-broker/client.d.ts +0 -67
- package/dist/types/auth-broker/index.d.ts +0 -5
- package/dist/types/auth-broker/refresher.d.ts +0 -25
- package/dist/types/auth-broker/remote-store.d.ts +0 -99
- package/dist/types/auth-broker/server.d.ts +0 -32
- package/dist/types/auth-broker/types.d.ts +0 -110
- package/dist/types/auth-broker/wire-schemas.d.ts +0 -443
- package/dist/types/auth-gateway/http.d.ts +0 -40
- package/dist/types/auth-gateway/index.d.ts +0 -3
- package/dist/types/auth-gateway/server.d.ts +0 -17
- package/dist/types/auth-gateway/types.d.ts +0 -115
- package/dist/types/auth-storage.d.ts +0 -695
- package/dist/types/cli.d.ts +0 -2
- package/dist/types/index.d.ts +0 -51
- package/dist/types/model-cache.d.ts +0 -17
- package/dist/types/model-manager.d.ts +0 -62
- package/dist/types/model-thinking.d.ts +0 -74
- package/dist/types/models.d.ts +0 -12
- package/dist/types/provider-details.d.ts +0 -24
- package/dist/types/provider-models/bundled-references.d.ts +0 -4
- package/dist/types/provider-models/descriptors.d.ts +0 -48
- package/dist/types/provider-models/google.d.ts +0 -20
- package/dist/types/provider-models/index.d.ts +0 -5
- package/dist/types/provider-models/ollama.d.ts +0 -7
- package/dist/types/provider-models/openai-compat.d.ts +0 -256
- package/dist/types/provider-models/special.d.ts +0 -19
- package/dist/types/providers/amazon-bedrock.d.ts +0 -60
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +0 -450
- package/dist/types/providers/anthropic-messages-server.d.ts +0 -17
- package/dist/types/providers/anthropic.d.ts +0 -208
- package/dist/types/providers/aws-credentials.d.ts +0 -43
- package/dist/types/providers/aws-eventstream.d.ts +0 -38
- package/dist/types/providers/aws-sigv4.d.ts +0 -55
- package/dist/types/providers/azure-openai-responses.d.ts +0 -15
- package/dist/types/providers/composer-discipline.d.ts +0 -26
- package/dist/types/providers/cursor/client-version.d.ts +0 -10
- package/dist/types/providers/cursor/gen/agent_pb.d.ts +0 -13022
- package/dist/types/providers/cursor.d.ts +0 -45
- package/dist/types/providers/error-message.d.ts +0 -27
- package/dist/types/providers/github-copilot-headers.d.ts +0 -40
- package/dist/types/providers/gitlab-duo.d.ts +0 -27
- package/dist/types/providers/google-auth.d.ts +0 -24
- package/dist/types/providers/google-gemini-cli.d.ts +0 -75
- package/dist/types/providers/google-gemini-headers.d.ts +0 -40
- package/dist/types/providers/google-shared.d.ts +0 -173
- package/dist/types/providers/google-types.d.ts +0 -138
- package/dist/types/providers/google-vertex.d.ts +0 -7
- package/dist/types/providers/google.d.ts +0 -4
- package/dist/types/providers/grammar.d.ts +0 -1
- package/dist/types/providers/kimi.d.ts +0 -27
- package/dist/types/providers/mock.d.ts +0 -177
- package/dist/types/providers/ollama.d.ts +0 -41
- package/dist/types/providers/openai-anthropic-shim.d.ts +0 -31
- package/dist/types/providers/openai-bounded-rate-limits.d.ts +0 -3
- package/dist/types/providers/openai-chat-server-schema.d.ts +0 -815
- package/dist/types/providers/openai-chat-server.d.ts +0 -16
- package/dist/types/providers/openai-codex/constants.d.ts +0 -26
- package/dist/types/providers/openai-codex/request-transformer.d.ts +0 -50
- package/dist/types/providers/openai-codex/response-handler.d.ts +0 -17
- package/dist/types/providers/openai-codex-responses.d.ts +0 -67
- package/dist/types/providers/openai-completions-compat.d.ts +0 -27
- package/dist/types/providers/openai-completions.d.ts +0 -33
- package/dist/types/providers/openai-request-transform.d.ts +0 -4
- package/dist/types/providers/openai-responses-server-schema.d.ts +0 -392
- package/dist/types/providers/openai-responses-server.d.ts +0 -17
- package/dist/types/providers/openai-responses-shared.d.ts +0 -104
- package/dist/types/providers/openai-responses.d.ts +0 -32
- package/dist/types/providers/pi-native-client.d.ts +0 -13
- package/dist/types/providers/pi-native-server.d.ts +0 -68
- package/dist/types/providers/register-builtins.d.ts +0 -31
- package/dist/types/providers/synthetic.d.ts +0 -26
- package/dist/types/providers/transform-messages.d.ts +0 -14
- package/dist/types/providers/vision-guard.d.ts +0 -8
- package/dist/types/rate-limit-utils.d.ts +0 -19
- package/dist/types/stream.d.ts +0 -43
- package/dist/types/types.d.ts +0 -829
- package/dist/types/usage/claude.d.ts +0 -3
- package/dist/types/usage/gemini.d.ts +0 -2
- package/dist/types/usage/github-copilot.d.ts +0 -7
- package/dist/types/usage/google-antigravity.d.ts +0 -2
- package/dist/types/usage/grok-cli.d.ts +0 -10
- package/dist/types/usage/kimi.d.ts +0 -2
- package/dist/types/usage/minimax-code.d.ts +0 -2
- package/dist/types/usage/openai-codex.d.ts +0 -3
- package/dist/types/usage/shared.d.ts +0 -1
- package/dist/types/usage/zai.d.ts +0 -2
- package/dist/types/usage.d.ts +0 -258
- package/dist/types/utils/abort.d.ts +0 -19
- package/dist/types/utils/anthropic-auth.d.ts +0 -31
- package/dist/types/utils/discovery/antigravity.d.ts +0 -61
- package/dist/types/utils/discovery/codex.d.ts +0 -38
- package/dist/types/utils/discovery/cursor.d.ts +0 -23
- package/dist/types/utils/discovery/gemini.d.ts +0 -25
- package/dist/types/utils/discovery/index.d.ts +0 -4
- package/dist/types/utils/discovery/openai-compatible.d.ts +0 -74
- package/dist/types/utils/event-stream.d.ts +0 -33
- package/dist/types/utils/fireworks-model-id.d.ts +0 -10
- package/dist/types/utils/foundry.d.ts +0 -1
- package/dist/types/utils/h2-fetch.d.ts +0 -22
- package/dist/types/utils/http-inspector.d.ts +0 -35
- package/dist/types/utils/idle-iterator.d.ts +0 -67
- package/dist/types/utils/json-parse.d.ts +0 -18
- package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +0 -18
- package/dist/types/utils/oauth/anthropic.d.ts +0 -22
- package/dist/types/utils/oauth/api-key-login.d.ts +0 -35
- package/dist/types/utils/oauth/api-key-validation.d.ts +0 -27
- package/dist/types/utils/oauth/callback-server.d.ts +0 -60
- package/dist/types/utils/oauth/cerebras.d.ts +0 -1
- package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +0 -18
- package/dist/types/utils/oauth/cursor.d.ts +0 -15
- package/dist/types/utils/oauth/deepinfra.d.ts +0 -1
- package/dist/types/utils/oauth/deepseek.d.ts +0 -10
- package/dist/types/utils/oauth/firepass.d.ts +0 -1
- package/dist/types/utils/oauth/fireworks.d.ts +0 -1
- package/dist/types/utils/oauth/fugu.d.ts +0 -1
- package/dist/types/utils/oauth/github-copilot.d.ts +0 -38
- package/dist/types/utils/oauth/gitlab-duo.d.ts +0 -3
- package/dist/types/utils/oauth/glm-zcode.d.ts +0 -71
- package/dist/types/utils/oauth/google-antigravity.d.ts +0 -11
- package/dist/types/utils/oauth/google-gemini-cli.d.ts +0 -10
- package/dist/types/utils/oauth/google-oauth-shared.d.ts +0 -28
- package/dist/types/utils/oauth/huggingface.d.ts +0 -19
- package/dist/types/utils/oauth/index.d.ts +0 -39
- package/dist/types/utils/oauth/kagi.d.ts +0 -17
- package/dist/types/utils/oauth/kilo.d.ts +0 -5
- package/dist/types/utils/oauth/kimi.d.ts +0 -21
- package/dist/types/utils/oauth/litellm.d.ts +0 -18
- package/dist/types/utils/oauth/lm-studio.d.ts +0 -17
- package/dist/types/utils/oauth/minimax-code.d.ts +0 -28
- package/dist/types/utils/oauth/moonshot.d.ts +0 -1
- package/dist/types/utils/oauth/nanogpt.d.ts +0 -1
- package/dist/types/utils/oauth/nvidia.d.ts +0 -18
- package/dist/types/utils/oauth/ollama-cloud.d.ts +0 -2
- package/dist/types/utils/oauth/ollama.d.ts +0 -18
- package/dist/types/utils/oauth/openai-codex.d.ts +0 -21
- package/dist/types/utils/oauth/opencode.d.ts +0 -18
- package/dist/types/utils/oauth/parallel.d.ts +0 -17
- package/dist/types/utils/oauth/perplexity.d.ts +0 -9
- package/dist/types/utils/oauth/pkce.d.ts +0 -8
- package/dist/types/utils/oauth/qianfan.d.ts +0 -17
- package/dist/types/utils/oauth/qwen-portal.d.ts +0 -19
- package/dist/types/utils/oauth/synthetic.d.ts +0 -1
- package/dist/types/utils/oauth/tavily.d.ts +0 -17
- package/dist/types/utils/oauth/together.d.ts +0 -1
- package/dist/types/utils/oauth/types.d.ts +0 -45
- package/dist/types/utils/oauth/venice.d.ts +0 -18
- package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +0 -18
- package/dist/types/utils/oauth/vllm.d.ts +0 -16
- package/dist/types/utils/oauth/xai.d.ts +0 -30
- package/dist/types/utils/oauth/xiaomi.d.ts +0 -25
- package/dist/types/utils/oauth/zai.d.ts +0 -18
- package/dist/types/utils/oauth/zenmux.d.ts +0 -1
- package/dist/types/utils/overflow.d.ts +0 -64
- package/dist/types/utils/parse-bind.d.ts +0 -23
- package/dist/types/utils/provider-response.d.ts +0 -3
- package/dist/types/utils/retry-after.d.ts +0 -3
- package/dist/types/utils/retry-budget.d.ts +0 -1
- package/dist/types/utils/retry.d.ts +0 -26
- package/dist/types/utils/schema/adapt.d.ts +0 -24
- package/dist/types/utils/schema/compatibility.d.ts +0 -30
- package/dist/types/utils/schema/dereference.d.ts +0 -11
- package/dist/types/utils/schema/draft.d.ts +0 -10
- package/dist/types/utils/schema/equality.d.ts +0 -4
- package/dist/types/utils/schema/fields.d.ts +0 -49
- package/dist/types/utils/schema/index.d.ts +0 -14
- package/dist/types/utils/schema/json-schema-validator.d.ts +0 -12
- package/dist/types/utils/schema/meta-validator.d.ts +0 -2
- package/dist/types/utils/schema/normalize.d.ts +0 -93
- package/dist/types/utils/schema/root-combinator.d.ts +0 -12
- package/dist/types/utils/schema/spill.d.ts +0 -8
- package/dist/types/utils/schema/stamps.d.ts +0 -25
- package/dist/types/utils/schema/types.d.ts +0 -4
- package/dist/types/utils/schema/wire.d.ts +0 -54
- package/dist/types/utils/schema/zod-decontaminate.d.ts +0 -31
- package/dist/types/utils/sse-debug.d.ts +0 -10
- package/dist/types/utils/tool-call-healing.d.ts +0 -71
- package/dist/types/utils/tool-choice-capability.d.ts +0 -41
- package/dist/types/utils/tool-choice.d.ts +0 -50
- package/dist/types/utils/validation.d.ts +0 -17
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
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
|
|
1540
|
-
//
|
|
1541
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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 (
|
|
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:
|
|
141
|
+
configIni: AwsIniFile | undefined,
|
|
185
142
|
defaultRegion: string,
|
|
186
143
|
signal: AbortSignal | undefined,
|
|
187
144
|
): Promise<ResolvedCredentials | undefined> {
|