@sayknow-cli/ai 0.4.7 → 0.5.0
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/README.md +6 -0
- package/dist/types/auth-broker/client.d.ts +2 -1
- package/dist/types/auth-broker/remote-store.d.ts +2 -1
- package/dist/types/auth-broker/types.d.ts +3 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +68 -0
- package/dist/types/auth-storage.d.ts +21 -1
- package/dist/types/provider-models/openai-compat.d.ts +19 -2
- package/dist/types/providers/anthropic.d.ts +11 -1
- package/dist/types/providers/azure-openai-responses.d.ts +6 -1
- package/dist/types/providers/google-auth.d.ts +2 -0
- package/dist/types/providers/google-gemini-headers.d.ts +1 -1
- package/dist/types/providers/google-vertex.d.ts +2 -0
- package/dist/types/providers/openai-codex-responses.d.ts +4 -0
- package/dist/types/providers/openai-completions.d.ts +2 -0
- package/dist/types/providers/openai-responses.d.ts +2 -0
- package/dist/types/providers/register-builtins.d.ts +8 -0
- package/dist/types/providers/transform-messages.d.ts +1 -0
- package/dist/types/types.d.ts +3 -1
- package/dist/types/usage/grok-cli.d.ts +3 -1
- package/dist/types/usage/kimi.d.ts +2 -0
- package/dist/types/utils/anthropic-auth.d.ts +8 -0
- package/dist/types/utils/foundry.d.ts +10 -0
- package/dist/types/utils/http-inspector.d.ts +13 -0
- package/dist/types/utils/idle-iterator.d.ts +3 -2
- package/dist/types/utils/oauth/alibaba-token-plan.d.ts +19 -0
- package/dist/types/utils/oauth/bizrouter.d.ts +1 -0
- package/dist/types/utils/oauth/opengateway.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/package.json +2 -2
- package/src/auth-broker/client.ts +13 -0
- package/src/auth-broker/refresher.ts +1 -0
- package/src/auth-broker/remote-store.ts +25 -0
- package/src/auth-broker/server.ts +10 -2
- package/src/auth-broker/types.ts +4 -0
- package/src/auth-broker/wire-schemas.ts +17 -1
- package/src/auth-storage.ts +234 -45
- package/src/cli.ts +2 -0
- package/src/model-thinking.ts +11 -3
- package/src/models.json +3289 -486
- package/src/provider-models/descriptors.ts +19 -6
- package/src/provider-models/openai-compat.ts +99 -18
- package/src/providers/amazon-bedrock.ts +4 -0
- package/src/providers/anthropic.ts +131 -28
- package/src/providers/azure-openai-responses.ts +16 -3
- package/src/providers/google-auth.ts +13 -2
- package/src/providers/google-gemini-headers.ts +1 -1
- package/src/providers/google-vertex.ts +7 -2
- package/src/providers/openai-anthropic-shim.ts +4 -0
- package/src/providers/openai-codex-responses.ts +52 -10
- package/src/providers/openai-completions-compat.ts +2 -2
- package/src/providers/openai-completions.ts +20 -3
- package/src/providers/openai-responses.ts +17 -10
- package/src/providers/register-builtins.ts +21 -2
- package/src/providers/transform-messages.ts +25 -6
- package/src/stream.ts +3 -1
- package/src/types.ts +9 -2
- package/src/usage/claude.ts +21 -3
- package/src/usage/grok-cli.ts +12 -1
- package/src/usage/kimi.ts +16 -2
- package/src/utils/anthropic-auth.ts +11 -3
- package/src/utils/foundry.ts +12 -2
- package/src/utils/http-inspector.ts +77 -0
- package/src/utils/idle-iterator.ts +20 -7
- package/src/utils/oauth/{alibaba-coding-plan.ts → alibaba-token-plan.ts} +12 -11
- package/src/utils/oauth/bizrouter.ts +15 -0
- package/src/utils/oauth/index.ts +15 -2
- package/src/utils/oauth/opengateway.ts +15 -0
- package/src/utils/oauth/types.ts +3 -1
- package/src/utils/validation.ts +17 -2
- package/src/utils.ts +41 -4
- package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +0 -18
|
@@ -26,6 +26,34 @@ type ErrorWithStatus = {
|
|
|
26
26
|
|
|
27
27
|
const SENSITIVE_HEADERS = ["authorization", "x-api-key", "api-key", "cookie", "set-cookie", "proxy-authorization"];
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Connection-level failure codes, meaning the request never reached the
|
|
31
|
+
* provider and no HTTP status exists. Bun reports the first group for `fetch`;
|
|
32
|
+
* the `E*`/`UND_ERR_*` group comes from Node-style DNS and socket errors.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately excludes aborts and TLS/certificate codes: an abort is a
|
|
35
|
+
* user/watchdog outcome with its own display path, and its message must keep
|
|
36
|
+
* matching the abort normalizers in `modes/utils/abort-message`.
|
|
37
|
+
*/
|
|
38
|
+
const TRANSPORT_FAILURE_CODES: ReadonlySet<string> = new Set([
|
|
39
|
+
"ConnectionClosed",
|
|
40
|
+
"ConnectionRefused",
|
|
41
|
+
"ConnectionReset",
|
|
42
|
+
"ConnectionTimeout",
|
|
43
|
+
"FailedToOpenSocket",
|
|
44
|
+
"HTTP2Unsupported",
|
|
45
|
+
"EAI_AGAIN",
|
|
46
|
+
"ECONNREFUSED",
|
|
47
|
+
"ECONNRESET",
|
|
48
|
+
"EHOSTUNREACH",
|
|
49
|
+
"ENETUNREACH",
|
|
50
|
+
"ENOTFOUND",
|
|
51
|
+
"EPIPE",
|
|
52
|
+
"ETIMEDOUT",
|
|
53
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
54
|
+
"UND_ERR_SOCKET",
|
|
55
|
+
]);
|
|
56
|
+
|
|
29
57
|
/**
|
|
30
58
|
* Privacy note appended next to a saved raw HTTP request dump. The dump is
|
|
31
59
|
* sanitized (secrets/thinking redacted) but can still contain prompt content
|
|
@@ -88,6 +116,54 @@ export async function appendRawHttpRequestDumpFor400(
|
|
|
88
116
|
}
|
|
89
117
|
}
|
|
90
118
|
|
|
119
|
+
/** Origin and path of `value`, dropping query, fragment, and credentials so a
|
|
120
|
+
* key carried in the request URL (Google `?key=`, signed URLs) never lands in
|
|
121
|
+
* a user-visible error string. */
|
|
122
|
+
function redactRequestUrl(value: unknown): string | undefined {
|
|
123
|
+
if (typeof value !== "string" || value.trim().length === 0) return undefined;
|
|
124
|
+
try {
|
|
125
|
+
const url = new URL(value);
|
|
126
|
+
return `${url.origin}${url.pathname}`;
|
|
127
|
+
} catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function findTransportFailure(error: unknown, depth: number): { code: string; url?: string } | undefined {
|
|
133
|
+
if (!error || typeof error !== "object" || depth > 2) return undefined;
|
|
134
|
+
const info = error as { code?: unknown; path?: unknown; url?: unknown; cause?: unknown };
|
|
135
|
+
if (typeof info.code === "string" && TRANSPORT_FAILURE_CODES.has(info.code)) {
|
|
136
|
+
return { code: info.code, url: redactRequestUrl(info.path) ?? redactRequestUrl(info.url) };
|
|
137
|
+
}
|
|
138
|
+
return findTransportFailure(info.cause, depth + 1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Name the failed connection when the request never produced an HTTP status.
|
|
143
|
+
*
|
|
144
|
+
* Bun raises DNS and socket failures as a bare `Error` whose message is a
|
|
145
|
+
* standalone hint ("Was there a typo in the url or port?", "Unable to connect.
|
|
146
|
+
* Is the computer able to access the url?") while the actionable facts live on
|
|
147
|
+
* `code` and `path`. Those properties are dropped when only `message` reaches
|
|
148
|
+
* the assistant message, so a provider outage, a local DNS failure, and a
|
|
149
|
+
* mistyped custom base URL all render as the same context-free sentence.
|
|
150
|
+
* Appending the code and the target URL tells the user which host failed and
|
|
151
|
+
* whether the fault is theirs.
|
|
152
|
+
*/
|
|
153
|
+
export function appendTransportFailureContext(
|
|
154
|
+
message: string,
|
|
155
|
+
error: unknown,
|
|
156
|
+
rawRequestDump: RawHttpRequestDump | undefined,
|
|
157
|
+
): string {
|
|
158
|
+
if (extractHttpStatusFromError(error) !== undefined) return message;
|
|
159
|
+
const failure = findTransportFailure(error, 0);
|
|
160
|
+
if (!failure) return message;
|
|
161
|
+
|
|
162
|
+
const url = failure.url ?? redactRequestUrl(rawRequestDump?.url);
|
|
163
|
+
const context = url ? `transport=${failure.code} url=${url}` : `transport=${failure.code}`;
|
|
164
|
+
return message.includes(context) ? message : `${message} (${context})`;
|
|
165
|
+
}
|
|
166
|
+
|
|
91
167
|
export async function finalizeErrorMessage(
|
|
92
168
|
error: unknown,
|
|
93
169
|
rawRequestDump: RawHttpRequestDump | undefined,
|
|
@@ -105,6 +181,7 @@ export async function finalizeErrorMessage(
|
|
|
105
181
|
if (isModelUnavailableError(message, error)) {
|
|
106
182
|
message = `${message}\n\n${formatModelUnavailableGuidance(rawRequestDump)}`;
|
|
107
183
|
}
|
|
184
|
+
message = appendTransportFailureContext(message, error, rawRequestDump);
|
|
108
185
|
return appendRawHttpRequestDumpFor400(message, error, rawRequestDump);
|
|
109
186
|
}
|
|
110
187
|
|
|
@@ -2,6 +2,11 @@ import { $env } from "@sayknow-cli/utils";
|
|
|
2
2
|
|
|
3
3
|
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 120_000;
|
|
4
4
|
const DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS = 100_000;
|
|
5
|
+
const KIMI_CODE_FIRST_EVENT_TIMEOUT_MS = 300_000;
|
|
6
|
+
|
|
7
|
+
export function getProviderFirstEventTimeoutFallbackMs(provider: string): number | undefined {
|
|
8
|
+
return provider === "kimi-code" ? KIMI_CODE_FIRST_EVENT_TIMEOUT_MS : undefined;
|
|
9
|
+
}
|
|
5
10
|
|
|
6
11
|
function normalizeIdleTimeoutMs(value: string | undefined, fallback: number): number | undefined {
|
|
7
12
|
if (value === undefined) return fallback;
|
|
@@ -14,7 +19,7 @@ function normalizeIdleTimeoutMs(value: string | undefined, fallback: number): nu
|
|
|
14
19
|
/**
|
|
15
20
|
* Returns the idle timeout used for provider streaming transports.
|
|
16
21
|
*
|
|
17
|
-
* `
|
|
22
|
+
* `SKC_OPENAI_STREAM_IDLE_TIMEOUT_MS` is honored first; `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` is a backward-compatible alias.
|
|
18
23
|
* Set `PI_STREAM_IDLE_TIMEOUT_MS=0` to disable the watchdog.
|
|
19
24
|
*
|
|
20
25
|
* Providers that legitimately stream much slower than the global default can pass
|
|
@@ -22,17 +27,20 @@ function normalizeIdleTimeoutMs(value: string | undefined, fallback: number): nu
|
|
|
22
27
|
* Caller options still take precedence; env overrides still trump the fallback.
|
|
23
28
|
*/
|
|
24
29
|
export function getStreamIdleTimeoutMs(fallbackMs: number = DEFAULT_STREAM_IDLE_TIMEOUT_MS): number | undefined {
|
|
25
|
-
return normalizeIdleTimeoutMs(
|
|
30
|
+
return normalizeIdleTimeoutMs(
|
|
31
|
+
$env.SKC_OPENAI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS,
|
|
32
|
+
fallbackMs,
|
|
33
|
+
);
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
/**
|
|
29
37
|
* Returns the idle timeout used for OpenAI-family streaming transports.
|
|
30
38
|
*
|
|
31
|
-
*
|
|
39
|
+
* Honors `SKC_OPENAI_STREAM_IDLE_TIMEOUT_MS` first (`PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` is the legacy alias). Set `=0` to disable.
|
|
32
40
|
*/
|
|
33
41
|
export function getOpenAIStreamIdleTimeoutMs(): number | undefined {
|
|
34
42
|
return normalizeIdleTimeoutMs(
|
|
35
|
-
$env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_STREAM_IDLE_TIMEOUT_MS,
|
|
43
|
+
$env.SKC_OPENAI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_STREAM_IDLE_TIMEOUT_MS,
|
|
36
44
|
DEFAULT_STREAM_IDLE_TIMEOUT_MS,
|
|
37
45
|
);
|
|
38
46
|
}
|
|
@@ -168,8 +176,6 @@ export async function* iterateWithIdleTimeout<T>(
|
|
|
168
176
|
}
|
|
169
177
|
}
|
|
170
178
|
|
|
171
|
-
const nextResultPromise = withRacy(iterator.next());
|
|
172
|
-
|
|
173
179
|
const racers: Array<
|
|
174
180
|
Promise<
|
|
175
181
|
| { kind: "next"; result: IteratorResult<T> }
|
|
@@ -177,7 +183,7 @@ export async function* iterateWithIdleTimeout<T>(
|
|
|
177
183
|
| { kind: "timeout" }
|
|
178
184
|
| { kind: "abort" }
|
|
179
185
|
>
|
|
180
|
-
> = [
|
|
186
|
+
> = [];
|
|
181
187
|
|
|
182
188
|
let timer: NodeJS.Timeout | undefined;
|
|
183
189
|
let resolveTimeout: ((value: { kind: "timeout" }) => void) | undefined;
|
|
@@ -202,6 +208,13 @@ export async function* iterateWithIdleTimeout<T>(
|
|
|
202
208
|
racers.push(promise);
|
|
203
209
|
}
|
|
204
210
|
|
|
211
|
+
// Arm timeout/abort races before asking the source for its next item. A
|
|
212
|
+
// periodic keepalive iterator commonly registers its own timer inside
|
|
213
|
+
// `next()`; registering that first lets equal-deadline keepalives win every
|
|
214
|
+
// race and extend the idle window forever. Already-buffered items still
|
|
215
|
+
// settle as microtasks before a 0ms watchdog.
|
|
216
|
+
racers.unshift(withRacy(iterator.next()));
|
|
217
|
+
|
|
205
218
|
try {
|
|
206
219
|
const outcome = await Promise.race(racers);
|
|
207
220
|
if (outcome.kind === "abort") {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Alibaba
|
|
2
|
+
* Alibaba Token Plan login flow.
|
|
3
3
|
*
|
|
4
|
-
* Alibaba
|
|
4
|
+
* Alibaba Token Plan provides OpenAI-compatible models via
|
|
5
|
+
* https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1.
|
|
5
6
|
*
|
|
6
7
|
* This is not OAuth - it's a simple API key flow:
|
|
7
|
-
* 1. Open browser to Alibaba Cloud
|
|
8
|
+
* 1. Open browser to Alibaba Cloud Model Studio console
|
|
8
9
|
* 2. User copies their API key
|
|
9
10
|
* 3. User pastes the API key into the CLI
|
|
10
11
|
*/
|
|
@@ -13,27 +14,27 @@ import { validateOpenAICompatibleApiKey } from "./api-key-validation";
|
|
|
13
14
|
import type { OAuthController } from "./types";
|
|
14
15
|
|
|
15
16
|
const AUTH_URL = "https://modelstudio.console.alibabacloud.com/";
|
|
16
|
-
const API_BASE_URL = "https://
|
|
17
|
-
const VALIDATION_MODEL = "
|
|
17
|
+
const API_BASE_URL = "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1";
|
|
18
|
+
const VALIDATION_MODEL = "deepseek-v4-pro";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
|
-
* Login to Alibaba
|
|
21
|
+
* Login to Alibaba Token Plan.
|
|
21
22
|
*
|
|
22
23
|
* Opens browser to API keys page, prompts user to paste their API key.
|
|
23
24
|
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
24
25
|
*/
|
|
25
|
-
export async function
|
|
26
|
+
export async function loginAlibabaTokenPlan(options: OAuthController): Promise<string> {
|
|
26
27
|
if (!options.onPrompt) {
|
|
27
|
-
throw new Error("Alibaba
|
|
28
|
+
throw new Error("Alibaba Token Plan login requires onPrompt callback");
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
options.onAuth?.({
|
|
31
32
|
url: AUTH_URL,
|
|
32
|
-
instructions: "Copy your API key from the Alibaba Cloud
|
|
33
|
+
instructions: "Copy your API key from the Alibaba Cloud Model Studio console",
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
const apiKey = await options.onPrompt({
|
|
36
|
-
message: "Paste your Alibaba
|
|
37
|
+
message: "Paste your Alibaba Token Plan API key",
|
|
37
38
|
placeholder: "sk-...",
|
|
38
39
|
});
|
|
39
40
|
|
|
@@ -48,7 +49,7 @@ export async function loginAlibabaCodingPlan(options: OAuthController): Promise<
|
|
|
48
49
|
|
|
49
50
|
options.onProgress?.("Validating API key...");
|
|
50
51
|
await validateOpenAICompatibleApiKey({
|
|
51
|
-
provider: "Alibaba
|
|
52
|
+
provider: "Alibaba Token Plan",
|
|
52
53
|
apiKey: trimmed,
|
|
53
54
|
baseUrl: API_BASE_URL,
|
|
54
55
|
model: VALIDATION_MODEL,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** BizRouter login flow (API key paste, validated via /v1/models). */
|
|
2
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
3
|
+
|
|
4
|
+
export const loginBizRouter = createApiKeyLogin({
|
|
5
|
+
providerLabel: "BizRouter",
|
|
6
|
+
authUrl: "https://bizrouter.ai/settings/keys",
|
|
7
|
+
instructions: "Create or copy your BizRouter API key",
|
|
8
|
+
promptMessage: "Paste your BizRouter API key",
|
|
9
|
+
placeholder: "sk-br-v1-...",
|
|
10
|
+
validation: {
|
|
11
|
+
kind: "models-endpoint",
|
|
12
|
+
provider: "BizRouter",
|
|
13
|
+
modelsUrl: "https://api.bizrouter.ai/v1/models",
|
|
14
|
+
},
|
|
15
|
+
});
|
package/src/utils/oauth/index.ts
CHANGED
|
@@ -16,8 +16,8 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
|
|
|
16
16
|
available: true,
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
id: "alibaba-
|
|
20
|
-
name: "Alibaba
|
|
19
|
+
id: "alibaba-token-plan",
|
|
20
|
+
name: "Alibaba Token Plan",
|
|
21
21
|
available: true,
|
|
22
22
|
},
|
|
23
23
|
{
|
|
@@ -240,6 +240,16 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
|
|
|
240
240
|
name: "ZenMux",
|
|
241
241
|
available: true,
|
|
242
242
|
},
|
|
243
|
+
{
|
|
244
|
+
id: "bizrouter",
|
|
245
|
+
name: "BizRouter",
|
|
246
|
+
available: true,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: "opengateway",
|
|
250
|
+
name: "OpenGateway by Sionic AI",
|
|
251
|
+
available: true,
|
|
252
|
+
},
|
|
243
253
|
{
|
|
244
254
|
id: "vllm",
|
|
245
255
|
name: "vLLM (Local OpenAI-compatible)",
|
|
@@ -370,6 +380,7 @@ export async function refreshOAuthToken(
|
|
|
370
380
|
case "together":
|
|
371
381
|
case "litellm":
|
|
372
382
|
case "lm-studio":
|
|
383
|
+
case "alibaba-token-plan":
|
|
373
384
|
case "ollama":
|
|
374
385
|
case "ollama-cloud":
|
|
375
386
|
case "xiaomi":
|
|
@@ -384,6 +395,8 @@ export async function refreshOAuthToken(
|
|
|
384
395
|
case "vercel-ai-gateway":
|
|
385
396
|
case "qwen-portal":
|
|
386
397
|
case "zenmux":
|
|
398
|
+
case "bizrouter":
|
|
399
|
+
case "opengateway":
|
|
387
400
|
case "vllm":
|
|
388
401
|
// API keys / static bearer tokens don't expire, return as-is
|
|
389
402
|
newCredentials = credentials;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** OpenGateway (by Sionic AI) login flow (API key paste, validated via /v1/models). */
|
|
2
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
3
|
+
|
|
4
|
+
export const loginOpenGateway = createApiKeyLogin({
|
|
5
|
+
providerLabel: "OpenGateway by Sionic AI",
|
|
6
|
+
authUrl: "https://opengateway.ai/dashboard",
|
|
7
|
+
instructions: "Create or copy your OpenGateway API key",
|
|
8
|
+
promptMessage: "Paste your OpenGateway API key",
|
|
9
|
+
placeholder: "sk-...",
|
|
10
|
+
validation: {
|
|
11
|
+
kind: "models-endpoint",
|
|
12
|
+
provider: "OpenGateway by Sionic AI",
|
|
13
|
+
modelsUrl: "https://apis.opengateway.ai/v1/models",
|
|
14
|
+
},
|
|
15
|
+
});
|
package/src/utils/oauth/types.ts
CHANGED
|
@@ -9,8 +9,9 @@ export type OAuthCredentials = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export type OAuthProvider =
|
|
12
|
-
| "alibaba-
|
|
12
|
+
| "alibaba-token-plan"
|
|
13
13
|
| "anthropic"
|
|
14
|
+
| "bizrouter"
|
|
14
15
|
| "cerebras"
|
|
15
16
|
| "cloudflare-ai-gateway"
|
|
16
17
|
| "cursor"
|
|
@@ -40,6 +41,7 @@ export type OAuthProvider =
|
|
|
40
41
|
| "openai-codex-device"
|
|
41
42
|
| "opencode-go"
|
|
42
43
|
| "opencode-zen"
|
|
44
|
+
| "opengateway"
|
|
43
45
|
| "parallel"
|
|
44
46
|
| "perplexity"
|
|
45
47
|
| "qianfan"
|
package/src/utils/validation.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { structuredCloneJSON } from "@sayknow-cli/utils";
|
|
26
26
|
import type { ZodType } from "zod/v4";
|
|
27
27
|
import type { $ZodIssue as ZodIssue } from "zod/v4/core";
|
|
28
|
-
import type { Tool, ToolCall } from "../types";
|
|
28
|
+
import type { RawArgumentRejectionCode, Tool, ToolCall } from "../types";
|
|
29
29
|
import { upgradeJsonSchemaTo202012 } from "./schema/draft";
|
|
30
30
|
import {
|
|
31
31
|
isJsonSchemaValueValid,
|
|
@@ -958,6 +958,15 @@ export function validateToolCall(tools: Tool[], toolCall: ToolCall): ToolCall["a
|
|
|
958
958
|
return validateToolArguments(tool, toolCall);
|
|
959
959
|
}
|
|
960
960
|
|
|
961
|
+
const RAW_ARGUMENT_REJECTION_MESSAGES: Record<RawArgumentRejectionCode, string> = {
|
|
962
|
+
"ask-intent-review-requires-positive-round":
|
|
963
|
+
"deepInterview.intent_review is post-Round-0 only and requires a positive round",
|
|
964
|
+
"ask-intent-contract-requires-non-empty-authority":
|
|
965
|
+
"deepInterview.intent_contract requires non-empty items and confirmation_options",
|
|
966
|
+
"ask-deep-interview-metadata-requires-deep-interview-gate":
|
|
967
|
+
"deepInterview metadata cannot be combined with a non-deep-interview workflowGate",
|
|
968
|
+
};
|
|
969
|
+
|
|
961
970
|
/**
|
|
962
971
|
* Validates tool call arguments against the tool's schema (Zod or plain JSON
|
|
963
972
|
* Schema). Applies LLM-quirk coercions (numeric strings, JSON-string
|
|
@@ -969,7 +978,13 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
|
|
|
969
978
|
const originalArgs = toolCall.arguments;
|
|
970
979
|
const rawValidation = tool.rawArgumentValidation?.(originalArgs);
|
|
971
980
|
if (rawValidation?.outcome === "reject") {
|
|
972
|
-
|
|
981
|
+
const base = `Validation failed for tool "${toolCall.name}": raw arguments rejected before coercion`;
|
|
982
|
+
const code = rawValidation.code;
|
|
983
|
+
const correction =
|
|
984
|
+
typeof code === "string" && Object.hasOwn(RAW_ARGUMENT_REJECTION_MESSAGES, code)
|
|
985
|
+
? RAW_ARGUMENT_REJECTION_MESSAGES[code as RawArgumentRejectionCode]
|
|
986
|
+
: undefined;
|
|
987
|
+
throw new Error(correction ? `${base}; ${correction}` : base);
|
|
973
988
|
}
|
|
974
989
|
const rawArgs = rawValidation?.outcome === "accept" ? rawValidation.arguments : originalArgs;
|
|
975
990
|
const ctx = getValidationContext(tool);
|
package/src/utils.ts
CHANGED
|
@@ -271,17 +271,53 @@ function normalizeResponsesImageUrlForReplay(value: unknown): NormalizedResponse
|
|
|
271
271
|
return { imageUrl: stringifyResponsesStringParamForReplay(value) };
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* OpenAI Responses `input_image.image_url` must be a fetchable HTTP(S) URL or an
|
|
276
|
+
* image data URI. Session resident-blob materialization may leave a human-readable
|
|
277
|
+
* placeholder like `[Session resident imageUrl blob missing: sha256:…; …]` in this
|
|
278
|
+
* field; replaying that string as `image_url` makes Codex reject the entire turn
|
|
279
|
+
* with `invalid_value` (#2924).
|
|
280
|
+
*/
|
|
281
|
+
function isProviderSafeResponsesImageUrl(value: string): boolean {
|
|
282
|
+
const url = value.trim();
|
|
283
|
+
if (url.length === 0) return false;
|
|
284
|
+
if (url.startsWith("https://") || url.startsWith("http://")) return true;
|
|
285
|
+
// Accept only image data URIs — other data: schemes are not valid image inputs.
|
|
286
|
+
if (url.startsWith("data:image/")) return true;
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function hasNonEmptyResponsesFileId(part: Record<string, unknown>): boolean {
|
|
291
|
+
return typeof part.file_id === "string" && part.file_id.trim().length > 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
274
294
|
function sanitizeResponsesMessageContentForReplay(content: unknown): unknown {
|
|
275
295
|
if (typeof content === "string") return neutralizeReservedControlTokens(content.toWellFormed());
|
|
276
296
|
if (!Array.isArray(content)) return content;
|
|
277
|
-
|
|
278
|
-
|
|
297
|
+
const sanitizedContent: unknown[] = [];
|
|
298
|
+
for (const part of content) {
|
|
299
|
+
if (!part || typeof part !== "object") {
|
|
300
|
+
sanitizedContent.push(part);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
279
303
|
const sanitizedPart = { ...(part as Record<string, unknown>) };
|
|
280
304
|
if ("text" in sanitizedPart) {
|
|
281
305
|
sanitizedPart.text = normalizeResponsesMessageTextForReplay(sanitizedPart.text);
|
|
282
306
|
}
|
|
283
307
|
if ("image_url" in sanitizedPart) {
|
|
284
308
|
const normalizedImageUrl = normalizeResponsesImageUrlForReplay(sanitizedPart.image_url);
|
|
309
|
+
if (!isProviderSafeResponsesImageUrl(normalizedImageUrl.imageUrl)) {
|
|
310
|
+
// Keep the part when a provider file_id can stand alone; otherwise drop
|
|
311
|
+
// only this image part so neighboring text/history still replays.
|
|
312
|
+
if (!hasNonEmptyResponsesFileId(sanitizedPart)) continue;
|
|
313
|
+
delete sanitizedPart.image_url;
|
|
314
|
+
if (sanitizedPart.type === "image_url") sanitizedPart.type = "input_image";
|
|
315
|
+
if ("detail" in sanitizedPart && !isResponsesImageDetail(sanitizedPart.detail)) {
|
|
316
|
+
delete sanitizedPart.detail;
|
|
317
|
+
}
|
|
318
|
+
sanitizedContent.push(sanitizedPart);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
285
321
|
sanitizedPart.image_url = normalizedImageUrl.imageUrl;
|
|
286
322
|
if (sanitizedPart.type === "image_url") {
|
|
287
323
|
sanitizedPart.type = "input_image";
|
|
@@ -292,8 +328,9 @@ function sanitizeResponsesMessageContentForReplay(content: unknown): unknown {
|
|
|
292
328
|
delete sanitizedPart.detail;
|
|
293
329
|
}
|
|
294
330
|
}
|
|
295
|
-
|
|
296
|
-
}
|
|
331
|
+
sanitizedContent.push(sanitizedPart);
|
|
332
|
+
}
|
|
333
|
+
return sanitizedContent;
|
|
297
334
|
}
|
|
298
335
|
|
|
299
336
|
function sanitizeResponsesStringFieldsForReplay(item: Record<string, unknown>): void {
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Alibaba Coding Plan login flow.
|
|
3
|
-
*
|
|
4
|
-
* Alibaba Coding Plan provides OpenAI-compatible models via https://coding-intl.dashscope.aliyuncs.com/v1.
|
|
5
|
-
*
|
|
6
|
-
* This is not OAuth - it's a simple API key flow:
|
|
7
|
-
* 1. Open browser to Alibaba Cloud DashScope API key settings
|
|
8
|
-
* 2. User copies their API key
|
|
9
|
-
* 3. User pastes the API key into the CLI
|
|
10
|
-
*/
|
|
11
|
-
import type { OAuthController } from "./types";
|
|
12
|
-
/**
|
|
13
|
-
* Login to Alibaba Coding Plan.
|
|
14
|
-
*
|
|
15
|
-
* Opens browser to API keys page, prompts user to paste their API key.
|
|
16
|
-
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
17
|
-
*/
|
|
18
|
-
export declare function loginAlibabaCodingPlan(options: OAuthController): Promise<string>;
|