@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
|
@@ -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> {
|
|
@@ -40,10 +40,14 @@ import {
|
|
|
40
40
|
convertMessages,
|
|
41
41
|
convertTools,
|
|
42
42
|
type GoogleThinkingLevel,
|
|
43
|
+
getGooglePromptBlockReason,
|
|
44
|
+
isGoogleCandidateSafetyStopReason,
|
|
45
|
+
isGooglePromptSafetyStopReason,
|
|
43
46
|
isThinkingPart,
|
|
44
47
|
mapStopReasonString,
|
|
45
48
|
mapToolChoice,
|
|
46
49
|
nextToolCallId,
|
|
50
|
+
PROVIDER_SAFETY_STOP,
|
|
47
51
|
pushBlockEndEvent,
|
|
48
52
|
pushToolCallEvents,
|
|
49
53
|
retainThoughtSignature,
|
|
@@ -255,6 +259,9 @@ interface CloudCodeAssistResponseChunk {
|
|
|
255
259
|
};
|
|
256
260
|
finishReason?: string;
|
|
257
261
|
}>;
|
|
262
|
+
promptFeedback?: {
|
|
263
|
+
blockReason?: string;
|
|
264
|
+
};
|
|
258
265
|
usageMetadata?: {
|
|
259
266
|
promptTokenCount?: number;
|
|
260
267
|
candidatesTokenCount?: number;
|
|
@@ -537,9 +544,26 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
537
544
|
}
|
|
538
545
|
|
|
539
546
|
if (candidate?.finishReason) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
output.
|
|
547
|
+
if (isGoogleCandidateSafetyStopReason(candidate.finishReason)) {
|
|
548
|
+
hasContent = true;
|
|
549
|
+
output.errorKind = PROVIDER_SAFETY_STOP;
|
|
550
|
+
output.stopReason = "error";
|
|
551
|
+
} else if (output.errorKind !== PROVIDER_SAFETY_STOP) {
|
|
552
|
+
output.stopReason = mapStopReasonString(candidate.finishReason);
|
|
553
|
+
if (output.stopReason === "stop" && output.content.some(b => b.type === "toolCall")) {
|
|
554
|
+
output.stopReason = "toolUse";
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const blockReason = getGooglePromptBlockReason(responseData.promptFeedback);
|
|
560
|
+
if (blockReason) {
|
|
561
|
+
hasContent = true;
|
|
562
|
+
if (isGooglePromptSafetyStopReason(blockReason)) {
|
|
563
|
+
output.errorKind = PROVIDER_SAFETY_STOP;
|
|
564
|
+
output.stopReason = "error";
|
|
565
|
+
} else if (output.errorKind !== PROVIDER_SAFETY_STOP) {
|
|
566
|
+
output.stopReason = "error";
|
|
543
567
|
}
|
|
544
568
|
}
|
|
545
569
|
|
|
@@ -51,6 +51,43 @@ export type {
|
|
|
51
51
|
export { normalizeSchemaForGoogle };
|
|
52
52
|
|
|
53
53
|
type GoogleApiType = "google-generative-ai" | "google-gemini-cli" | "google-vertex";
|
|
54
|
+
export const PROVIDER_SAFETY_STOP = "provider_safety_stop";
|
|
55
|
+
|
|
56
|
+
export function isGoogleCandidateSafetyStopReason(reason: string): boolean {
|
|
57
|
+
switch (reason) {
|
|
58
|
+
case "SAFETY":
|
|
59
|
+
case "IMAGE_SAFETY":
|
|
60
|
+
case "PROHIBITED_CONTENT":
|
|
61
|
+
case "IMAGE_PROHIBITED_CONTENT":
|
|
62
|
+
case "SPII":
|
|
63
|
+
case "BLOCKLIST":
|
|
64
|
+
case "RECITATION":
|
|
65
|
+
case "IMAGE_RECITATION":
|
|
66
|
+
case "MODEL_ARMOR":
|
|
67
|
+
return true;
|
|
68
|
+
default:
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isGooglePromptSafetyStopReason(reason: string): boolean {
|
|
74
|
+
switch (reason) {
|
|
75
|
+
case "SAFETY":
|
|
76
|
+
case "IMAGE_SAFETY":
|
|
77
|
+
case "PROHIBITED_CONTENT":
|
|
78
|
+
case "BLOCKLIST":
|
|
79
|
+
case "MODEL_ARMOR":
|
|
80
|
+
case "JAILBREAK":
|
|
81
|
+
return true;
|
|
82
|
+
default:
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getGooglePromptBlockReason(promptFeedback: { blockReason?: unknown } | undefined): string | undefined {
|
|
88
|
+
const blockReason = promptFeedback?.blockReason;
|
|
89
|
+
return typeof blockReason === "string" && blockReason.length > 0 ? blockReason : undefined;
|
|
90
|
+
}
|
|
54
91
|
|
|
55
92
|
/**
|
|
56
93
|
* Thinking level for Gemini 3 models. Mirrors Google's `ThinkingLevel` enum values.
|
|
@@ -607,9 +644,24 @@ export async function consumeGoogleStream<T extends GoogleApiType>(args: {
|
|
|
607
644
|
}
|
|
608
645
|
|
|
609
646
|
if (candidate?.finishReason) {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
output.stopReason = "
|
|
647
|
+
if (isGoogleCandidateSafetyStopReason(candidate.finishReason)) {
|
|
648
|
+
output.errorKind = PROVIDER_SAFETY_STOP;
|
|
649
|
+
output.stopReason = "error";
|
|
650
|
+
} else if (output.errorKind !== PROVIDER_SAFETY_STOP) {
|
|
651
|
+
output.stopReason = mapStopReason(candidate.finishReason);
|
|
652
|
+
if (output.stopReason === "stop" && output.content.some(b => b.type === "toolCall")) {
|
|
653
|
+
output.stopReason = "toolUse";
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const blockReason = getGooglePromptBlockReason(chunk.promptFeedback);
|
|
659
|
+
if (blockReason) {
|
|
660
|
+
if (isGooglePromptSafetyStopReason(blockReason)) {
|
|
661
|
+
output.errorKind = PROVIDER_SAFETY_STOP;
|
|
662
|
+
output.stopReason = "error";
|
|
663
|
+
} else if (output.errorKind !== PROVIDER_SAFETY_STOP) {
|
|
664
|
+
output.stopReason = "error";
|
|
613
665
|
}
|
|
614
666
|
}
|
|
615
667
|
|
package/src/types.ts
CHANGED
|
@@ -552,6 +552,7 @@ export interface Usage {
|
|
|
552
552
|
}
|
|
553
553
|
|
|
554
554
|
export type StopReason = "stop" | "length" | "toolUse" | "error" | "aborted";
|
|
555
|
+
export type AssistantErrorKind = "provider_safety_stop";
|
|
555
556
|
|
|
556
557
|
export interface OpenAIResponsesHistoryPayload {
|
|
557
558
|
type: "openaiResponsesHistory";
|
|
@@ -594,6 +595,7 @@ export interface AssistantMessage {
|
|
|
594
595
|
usage: Usage;
|
|
595
596
|
stopReason: StopReason;
|
|
596
597
|
errorMessage?: string;
|
|
598
|
+
errorKind?: AssistantErrorKind;
|
|
597
599
|
/** HTTP status surfaced by the provider when the request failed. Populated by every provider's catch block alongside `errorMessage` so consumers (auth retry, telemetry, UI) can branch without regex-scraping the message. */
|
|
598
600
|
errorStatus?: number;
|
|
599
601
|
/**
|