@oh-my-pi/pi-ai 17.2.11 → 17.2.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/CHANGELOG.md +25 -0
- package/dist/types/auth-retry.d.ts +3 -3
- package/dist/types/auth-storage.d.ts +2 -0
- package/dist/types/error/auth-classify.d.ts +5 -5
- package/dist/types/error/aws.d.ts +5 -1
- package/dist/types/error/flags.d.ts +4 -0
- package/dist/types/providers/aws-credentials.d.ts +4 -3
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +14 -0
- package/dist/types/providers/openai-shared.d.ts +9 -1
- package/dist/types/registry/api-key-login.d.ts +6 -4
- package/dist/types/registry/cloudflare-ai-gateway.d.ts +2 -2
- package/dist/types/registry/kagi.d.ts +2 -2
- package/dist/types/registry/litellm.d.ts +2 -2
- package/dist/types/registry/llama-cpp.d.ts +2 -2
- package/dist/types/registry/lm-studio.d.ts +2 -2
- package/dist/types/registry/parallel.d.ts +2 -2
- package/dist/types/registry/tavily.d.ts +1 -1
- package/dist/types/registry/vercel-ai-gateway.d.ts +2 -2
- package/dist/types/registry/vllm.d.ts +2 -2
- package/dist/types/types.d.ts +7 -0
- package/dist/types/usage/cursor.d.ts +11 -0
- package/dist/types/usage/openai-codex-reset.d.ts +0 -20
- package/dist/types/usage/shared.d.ts +13 -1
- package/dist/types/utils/block-symbols.d.ts +12 -0
- package/package.json +5 -5
- package/src/auth-retry.ts +5 -4
- package/src/auth-storage.ts +77 -33
- package/src/dialect/owned-stream.ts +3 -0
- package/src/error/auth-classify.ts +7 -6
- package/src/error/aws.ts +5 -1
- package/src/error/flags.ts +14 -0
- package/src/providers/amazon-bedrock.ts +38 -0
- package/src/providers/aws-credentials.ts +222 -29
- package/src/providers/cursor/exec-modern.ts +1 -0
- package/src/providers/cursor-pi-args.ts +22 -0
- package/src/providers/cursor.ts +81 -1
- package/src/providers/google-gemini-cli.ts +49 -15
- package/src/providers/google-shared.ts +7 -1
- package/src/providers/openai-codex/request-transformer.ts +38 -17
- package/src/providers/openai-codex-responses.ts +2 -3
- package/src/providers/openai-responses.ts +4 -0
- package/src/providers/openai-shared.ts +55 -1
- package/src/providers/pi-native-server.ts +1 -0
- package/src/providers/register-builtins.ts +18 -14
- package/src/registry/api-key-login.ts +26 -12
- package/src/registry/aws.ts +13 -6
- package/src/registry/cloudflare-ai-gateway.ts +10 -29
- package/src/registry/kagi.ts +11 -29
- package/src/registry/litellm.ts +11 -29
- package/src/registry/llama-cpp.ts +11 -21
- package/src/registry/lm-studio.ts +9 -20
- package/src/registry/oauth/callback-server.ts +93 -5
- package/src/registry/parallel.ts +10 -28
- package/src/registry/tavily.ts +9 -27
- package/src/registry/vercel-ai-gateway.ts +10 -28
- package/src/registry/vllm.ts +11 -21
- package/src/stream.ts +10 -7
- package/src/types.ts +7 -0
- package/src/usage/alibaba-token-plan.ts +8 -15
- package/src/usage/claude.ts +12 -19
- package/src/usage/cursor.ts +179 -59
- package/src/usage/gemini.ts +3 -2
- package/src/usage/github-copilot.ts +3 -2
- package/src/usage/google-antigravity.ts +10 -17
- package/src/usage/kimi.ts +35 -17
- package/src/usage/minimax-code.ts +7 -27
- package/src/usage/openai-codex-reset.ts +3 -2
- package/src/usage/openai-codex.ts +5 -5
- package/src/usage/opencode-go.ts +1 -2
- package/src/usage/shared.ts +33 -10
- package/src/usage/synthetic.ts +4 -11
- package/src/usage/umans.ts +2 -2
- package/src/usage/xai-oauth.ts +13 -26
- package/src/usage/zai.ts +4 -6
- package/src/utils/aws-profile.ts +39 -1
- package/src/utils/block-symbols.ts +18 -0
- package/src/utils/leaked-thinking-stream.ts +3 -0
- package/src/utils/openrouter-headers.ts +3 -3
package/src/registry/litellm.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
1
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
2
|
+
import type { OAuthLoginCallbacks } from "./oauth/types";
|
|
3
3
|
import type { ProviderDefinition } from "./types";
|
|
4
4
|
|
|
5
5
|
const AUTH_URL = "https://docs.litellm.ai/docs/proxy/deploy";
|
|
@@ -10,33 +10,15 @@ const AUTH_URL = "https://docs.litellm.ai/docs/proxy/deploy";
|
|
|
10
10
|
* Opens browser to LiteLLM setup docs, prompts user to paste their API key.
|
|
11
11
|
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
12
12
|
*/
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const apiKey = await options.onPrompt({
|
|
25
|
-
message: "Paste your LiteLLM API key (master key or virtual key)",
|
|
26
|
-
placeholder: "sk-...",
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
if (options.signal?.aborted) {
|
|
30
|
-
throw new AIError.LoginCancelledError();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const trimmed = apiKey.trim();
|
|
34
|
-
if (!trimmed) {
|
|
35
|
-
throw new AIError.ApiKeyRequiredError();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return trimmed;
|
|
39
|
-
}
|
|
13
|
+
export const loginLiteLLM = createApiKeyLogin({
|
|
14
|
+
providerLabel: "LiteLLM",
|
|
15
|
+
authUrl: AUTH_URL,
|
|
16
|
+
instructions:
|
|
17
|
+
"Run LiteLLM proxy (default http://localhost:4000/v1; set LITELLM_BASE_URL to customize it), then copy your master key or virtual key",
|
|
18
|
+
promptMessage: "Paste your LiteLLM API key (master key or virtual key)",
|
|
19
|
+
placeholder: "sk-...",
|
|
20
|
+
validation: null,
|
|
21
|
+
});
|
|
40
22
|
|
|
41
23
|
export const litellmProvider = {
|
|
42
24
|
id: "litellm",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
1
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
2
|
+
import type { OAuthLoginCallbacks } from "./oauth/types";
|
|
3
3
|
import type { ProviderDefinition } from "./types";
|
|
4
4
|
|
|
5
5
|
const PROVIDER_ID = "llama.cpp";
|
|
@@ -7,25 +7,15 @@ const AUTH_URL = "https://github.com/ggml-org/llama.cpp#quick-start";
|
|
|
7
7
|
const DEFAULT_LOCAL_BASE_URL = "http://127.0.0.1:8080";
|
|
8
8
|
const DEFAULT_LOCAL_TOKEN = "llama-cpp-local";
|
|
9
9
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
message: "Paste your llama.cpp API key (optional for local no-auth)",
|
|
20
|
-
placeholder: DEFAULT_LOCAL_TOKEN,
|
|
21
|
-
allowEmpty: true,
|
|
22
|
-
});
|
|
23
|
-
if (options.signal?.aborted) {
|
|
24
|
-
throw new AIError.LoginCancelledError();
|
|
25
|
-
}
|
|
26
|
-
const trimmed = apiKey.trim();
|
|
27
|
-
return trimmed || DEFAULT_LOCAL_TOKEN;
|
|
28
|
-
}
|
|
10
|
+
export const loginLlamaCpp = createApiKeyLogin({
|
|
11
|
+
providerLabel: PROVIDER_ID,
|
|
12
|
+
authUrl: AUTH_URL,
|
|
13
|
+
instructions: `Paste your llama.cpp API key if your server requires auth. Leave empty for local no-auth mode (default base URL: ${DEFAULT_LOCAL_BASE_URL}; set LLAMA_CPP_BASE_URL to customize).`,
|
|
14
|
+
promptMessage: "Paste your llama.cpp API key (optional for local no-auth)",
|
|
15
|
+
placeholder: DEFAULT_LOCAL_TOKEN,
|
|
16
|
+
validation: null,
|
|
17
|
+
emptyKeyFallback: DEFAULT_LOCAL_TOKEN,
|
|
18
|
+
});
|
|
29
19
|
|
|
30
20
|
export const llamaCppProvider = {
|
|
31
21
|
id: PROVIDER_ID,
|
|
@@ -1,28 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
1
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
2
|
+
import type { OAuthLoginCallbacks } from "./oauth/types";
|
|
3
3
|
import type { ProviderDefinition } from "./types";
|
|
4
4
|
|
|
5
5
|
const PROVIDER_ID = "lm-studio";
|
|
6
6
|
export const DEFAULT_LOCAL_TOKEN = "lm-studio-local";
|
|
7
7
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
placeholder: DEFAULT_LOCAL_TOKEN,
|
|
16
|
-
allowEmpty: true,
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
if (options.signal?.aborted) {
|
|
20
|
-
throw new AIError.LoginCancelledError();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const trimmed = apiKey.trim();
|
|
24
|
-
return trimmed || DEFAULT_LOCAL_TOKEN;
|
|
25
|
-
}
|
|
8
|
+
export const loginLmStudio = createApiKeyLogin({
|
|
9
|
+
providerLabel: PROVIDER_ID,
|
|
10
|
+
promptMessage: "Optional: Paste LM Studio API key (to customize endpoint URL, set LM_STUDIO_BASE_URL env var)",
|
|
11
|
+
placeholder: DEFAULT_LOCAL_TOKEN,
|
|
12
|
+
validation: null,
|
|
13
|
+
emptyKeyFallback: DEFAULT_LOCAL_TOKEN,
|
|
14
|
+
});
|
|
26
15
|
|
|
27
16
|
export const lmStudioProvider = {
|
|
28
17
|
id: "lm-studio",
|
|
@@ -17,6 +17,15 @@ import type { OAuthController, OAuthCredentials } from "./types";
|
|
|
17
17
|
const DEFAULT_TIMEOUT = 300_000;
|
|
18
18
|
const DEFAULT_HOSTNAME = "localhost";
|
|
19
19
|
const CALLBACK_PATH = "/callback";
|
|
20
|
+
const IPV4_LOOPBACK = "127.0.0.1";
|
|
21
|
+
const IPV6_LOOPBACK = "::1";
|
|
22
|
+
/**
|
|
23
|
+
* How many times a random-port bind may be redrawn when the ephemeral port it
|
|
24
|
+
* landed on is already held on {@link IPV6_LOOPBACK} by that exact address.
|
|
25
|
+
* Small on purpose: each redraw picks a fresh port, so a repeat collision is
|
|
26
|
+
* vanishingly unlikely.
|
|
27
|
+
*/
|
|
28
|
+
const IPV6_COMPANION_ATTEMPTS = 4;
|
|
20
29
|
/**
|
|
21
30
|
* Path served by {@link OAuthCallbackFlow} that 302-redirects to the pending
|
|
22
31
|
* authorization URL. Kept out of {@link OAuthCallbackFlowOptions} because it
|
|
@@ -28,6 +37,27 @@ const LAUNCH_PATH = "/launch";
|
|
|
28
37
|
|
|
29
38
|
export type CallbackResult = { code: string; state: string };
|
|
30
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Subset of {@link Bun.Server} this flow depends on, so a `localhost` flow can
|
|
42
|
+
* hand back one listener per loopback address family while still looking like a
|
|
43
|
+
* single server to callers.
|
|
44
|
+
*/
|
|
45
|
+
interface CallbackServer {
|
|
46
|
+
readonly port: Bun.Server<unknown>["port"];
|
|
47
|
+
stop: Bun.Server<unknown>["stop"];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether a failed bind means "another process already holds this port".
|
|
52
|
+
* Bun surfaces `EADDRINUSE` on the error's `code` where the platform reports
|
|
53
|
+
* it, and otherwise only in the message, so both are checked.
|
|
54
|
+
*/
|
|
55
|
+
function isAddressInUse(error: unknown): boolean {
|
|
56
|
+
const code = (error as { code?: unknown } | null | undefined)?.code;
|
|
57
|
+
if (typeof code === "string") return code === "EADDRINUSE";
|
|
58
|
+
return error instanceof Error && /EADDRINUSE|in use/i.test(error.message);
|
|
59
|
+
}
|
|
60
|
+
|
|
31
61
|
export interface OAuthCallbackFlowOptions {
|
|
32
62
|
preferredPort: number;
|
|
33
63
|
callbackPath?: string;
|
|
@@ -192,7 +222,7 @@ export abstract class OAuthCallbackFlow {
|
|
|
192
222
|
*/
|
|
193
223
|
async #startCallbackServer(
|
|
194
224
|
expectedState: string,
|
|
195
|
-
): Promise<{ server:
|
|
225
|
+
): Promise<{ server: CallbackServer; redirectUri: string; launchUrl: string | undefined }> {
|
|
196
226
|
try {
|
|
197
227
|
const server = this.#createServer(this.preferredPort, expectedState);
|
|
198
228
|
// `preferredPort: 0` opts into a random port — read the actual bound
|
|
@@ -233,7 +263,7 @@ export abstract class OAuthCallbackFlow {
|
|
|
233
263
|
* but every callback flow uses TCP; a missing port here indicates a
|
|
234
264
|
* configuration error rather than a fallback case.
|
|
235
265
|
*/
|
|
236
|
-
#resolveServerPort(server:
|
|
266
|
+
#resolveServerPort(server: CallbackServer): number {
|
|
237
267
|
const port = server.port;
|
|
238
268
|
if (typeof port !== "number") {
|
|
239
269
|
throw new AIError.ConfigurationError(
|
|
@@ -277,10 +307,68 @@ export abstract class OAuthCallbackFlow {
|
|
|
277
307
|
}
|
|
278
308
|
|
|
279
309
|
/**
|
|
280
|
-
* Create HTTP
|
|
310
|
+
* Create the HTTP listener(s) for the OAuth callback.
|
|
311
|
+
*
|
|
312
|
+
* `localhost` is not a single endpoint: it resolves to both
|
|
313
|
+
* {@link IPV4_LOOPBACK} and {@link IPV6_LOOPBACK}, and clients commonly try
|
|
314
|
+
* `::1` first. Binding only the IPv4 literal hands the authorization code to
|
|
315
|
+
* whatever holds the IPv6 loopback on the same port — a dev server on
|
|
316
|
+
* `*:3000` is the common case — which answers from its own routes while this
|
|
317
|
+
* flow waits out the full {@link DEFAULT_TIMEOUT}. Nothing detects it either:
|
|
318
|
+
* a specific-address bind coexists with another process's wildcard bind, so
|
|
319
|
+
* `Bun.serve` reports the port as free and the random-port fallback in
|
|
320
|
+
* {@link #startCallbackServer} never runs.
|
|
321
|
+
*
|
|
322
|
+
* Binding both loopback literals fixes the delivery rather than dodging it:
|
|
323
|
+
* the kernel routes a connection to the most specific matching bind, so our
|
|
324
|
+
* `::1` listener receives `localhost` traffic that would otherwise reach a
|
|
325
|
+
* process bound to the `::` wildcard. Both listeners answer the same routes,
|
|
326
|
+
* so which family the client resolves stops mattering.
|
|
327
|
+
*
|
|
328
|
+
* A genuine collision — another process on exactly this loopback address and
|
|
329
|
+
* port — still raises EADDRINUSE and reaches the caller's in-use policy. A
|
|
330
|
+
* host that cannot bind `::1` at all (IPv6 disabled, address unavailable) is
|
|
331
|
+
* not a collision: the IPv4 listener is the only reachable endpoint there, so
|
|
332
|
+
* it serves alone.
|
|
281
333
|
*/
|
|
282
|
-
#createServer(port: number, expectedState: string):
|
|
283
|
-
|
|
334
|
+
#createServer(port: number, expectedState: string): CallbackServer {
|
|
335
|
+
if (this.callbackHostname !== DEFAULT_HOSTNAME) {
|
|
336
|
+
return this.#serve(this.callbackHostname, port, expectedState);
|
|
337
|
+
}
|
|
338
|
+
for (let attempt = 0; ; attempt++) {
|
|
339
|
+
const primary = this.#serve(IPV4_LOOPBACK, port, expectedState);
|
|
340
|
+
const boundPort = primary.port;
|
|
341
|
+
// A non-TCP endpoint has no port for the companion to target;
|
|
342
|
+
// #resolveServerPort reports that case precisely.
|
|
343
|
+
if (typeof boundPort !== "number") return primary;
|
|
344
|
+
let companion: Bun.Server<unknown>;
|
|
345
|
+
try {
|
|
346
|
+
companion = this.#serve(IPV6_LOOPBACK, boundPort, expectedState);
|
|
347
|
+
} catch (cause) {
|
|
348
|
+
if (!isAddressInUse(cause)) return primary;
|
|
349
|
+
void primary.stop(true);
|
|
350
|
+
// A pinned port has no alternative, so surface it as in use and let
|
|
351
|
+
// the caller apply its fallback or diagnostic policy. A random port
|
|
352
|
+
// can just be redrawn, since only that one number clashed.
|
|
353
|
+
if (port !== 0 || attempt >= IPV6_COMPANION_ATTEMPTS) throw cause;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
// One server to callers. The IPv4 listener stays authoritative for
|
|
357
|
+
// `port` because the companion was bound to the port it resolved.
|
|
358
|
+
return {
|
|
359
|
+
get port() {
|
|
360
|
+
return primary.port;
|
|
361
|
+
},
|
|
362
|
+
stop: (closeActiveConnections?: boolean) => {
|
|
363
|
+
void companion.stop(closeActiveConnections);
|
|
364
|
+
return primary.stop(closeActiveConnections);
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Bind one loopback listener serving the callback and launch routes. */
|
|
371
|
+
#serve(hostname: string, port: number, expectedState: string): Bun.Server<unknown> {
|
|
284
372
|
return Bun.serve({
|
|
285
373
|
hostname,
|
|
286
374
|
port,
|
package/src/registry/parallel.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
1
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
2
|
+
import type { OAuthLoginCallbacks } from "./oauth/types";
|
|
3
3
|
import type { ProviderDefinition } from "./types";
|
|
4
4
|
|
|
5
5
|
const AUTH_URL = "https://platform.parallel.ai/settings?tab=api-keys";
|
|
@@ -10,32 +10,14 @@ const AUTH_URL = "https://platform.parallel.ai/settings?tab=api-keys";
|
|
|
10
10
|
* Opens browser to the API keys page, prompts the user to paste their API key,
|
|
11
11
|
* and returns the API key directly.
|
|
12
12
|
*/
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const apiKey = await options.onPrompt({
|
|
24
|
-
message: "Paste your Parallel API key",
|
|
25
|
-
placeholder: "sk_...",
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
if (options.signal?.aborted) {
|
|
29
|
-
throw new AIError.LoginCancelledError();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const trimmed = apiKey.trim();
|
|
33
|
-
if (!trimmed) {
|
|
34
|
-
throw new AIError.ApiKeyRequiredError();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return trimmed;
|
|
38
|
-
}
|
|
13
|
+
export const loginParallel = createApiKeyLogin({
|
|
14
|
+
providerLabel: "Parallel",
|
|
15
|
+
authUrl: AUTH_URL,
|
|
16
|
+
instructions: "Copy your Parallel API key from the Parallel settings page.",
|
|
17
|
+
promptMessage: "Paste your Parallel API key",
|
|
18
|
+
placeholder: "sk_...",
|
|
19
|
+
validation: null,
|
|
20
|
+
});
|
|
39
21
|
|
|
40
22
|
export const parallelProvider = {
|
|
41
23
|
id: "parallel",
|
package/src/registry/tavily.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
2
2
|
import type { OAuthLoginCallbacks } from "./oauth/types";
|
|
3
3
|
import type { ProviderDefinition } from "./types";
|
|
4
4
|
|
|
@@ -10,32 +10,14 @@ const AUTH_URL = "https://app.tavily.com/home";
|
|
|
10
10
|
* Opens browser to API keys page and prompts user to paste their API key.
|
|
11
11
|
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
12
12
|
*/
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const apiKey = await options.onPrompt({
|
|
24
|
-
message: "Paste your Tavily API key",
|
|
25
|
-
placeholder: "tvly-...",
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
if (options.signal?.aborted) {
|
|
29
|
-
throw new AIError.LoginCancelledError();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const trimmed = apiKey.trim();
|
|
33
|
-
if (!trimmed) {
|
|
34
|
-
throw new AIError.ApiKeyRequiredError();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return trimmed;
|
|
38
|
-
}
|
|
13
|
+
export const loginTavily = createApiKeyLogin({
|
|
14
|
+
providerLabel: "Tavily",
|
|
15
|
+
authUrl: AUTH_URL,
|
|
16
|
+
instructions: "Copy your Tavily API key from the API Keys page.",
|
|
17
|
+
promptMessage: "Paste your Tavily API key",
|
|
18
|
+
placeholder: "tvly-...",
|
|
19
|
+
validation: null,
|
|
20
|
+
});
|
|
39
21
|
|
|
40
22
|
export const tavilyProvider = {
|
|
41
23
|
id: "tavily",
|
|
@@ -1,35 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
1
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
2
|
+
import type { OAuthLoginCallbacks } from "./oauth/types";
|
|
3
3
|
import type { ProviderDefinition } from "./types";
|
|
4
4
|
|
|
5
5
|
const AUTH_URL = "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway%2Fapi-keys&title=AI+Gateway+API+Keys";
|
|
6
6
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const apiKey = await options.onPrompt({
|
|
18
|
-
message: "Paste your Vercel AI Gateway API key",
|
|
19
|
-
placeholder: "vck_...",
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
if (options.signal?.aborted) {
|
|
23
|
-
throw new AIError.LoginCancelledError();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const trimmed = apiKey.trim();
|
|
27
|
-
if (!trimmed) {
|
|
28
|
-
throw new AIError.ApiKeyRequiredError();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return trimmed;
|
|
32
|
-
}
|
|
7
|
+
export const loginVercelAiGateway = createApiKeyLogin({
|
|
8
|
+
providerLabel: "Vercel AI Gateway",
|
|
9
|
+
authUrl: AUTH_URL,
|
|
10
|
+
instructions: "Copy your Vercel AI Gateway API key from the Vercel dashboard",
|
|
11
|
+
promptMessage: "Paste your Vercel AI Gateway API key",
|
|
12
|
+
placeholder: "vck_...",
|
|
13
|
+
validation: null,
|
|
14
|
+
});
|
|
33
15
|
|
|
34
16
|
export const vercelAiGatewayProvider = {
|
|
35
17
|
id: "vercel-ai-gateway",
|
package/src/registry/vllm.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
1
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
2
|
+
import type { OAuthLoginCallbacks, OAuthProvider } from "./oauth/types";
|
|
3
3
|
import type { ProviderDefinition } from "./types";
|
|
4
4
|
|
|
5
5
|
const PROVIDER_ID: OAuthProvider = "vllm";
|
|
@@ -7,25 +7,15 @@ const AUTH_URL = "https://docs.vllm.ai/en/latest/serving/openai_compatible_serve
|
|
|
7
7
|
const DEFAULT_LOCAL_BASE_URL = "http://127.0.0.1:8000/v1";
|
|
8
8
|
const DEFAULT_LOCAL_TOKEN = "vllm-local";
|
|
9
9
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
message: "Paste your vLLM API key (optional for local no-auth)",
|
|
20
|
-
placeholder: DEFAULT_LOCAL_TOKEN,
|
|
21
|
-
allowEmpty: true,
|
|
22
|
-
});
|
|
23
|
-
if (options.signal?.aborted) {
|
|
24
|
-
throw new AIError.LoginCancelledError();
|
|
25
|
-
}
|
|
26
|
-
const trimmed = apiKey.trim();
|
|
27
|
-
return trimmed || DEFAULT_LOCAL_TOKEN;
|
|
28
|
-
}
|
|
10
|
+
export const loginVllm = createApiKeyLogin({
|
|
11
|
+
providerLabel: PROVIDER_ID,
|
|
12
|
+
authUrl: AUTH_URL,
|
|
13
|
+
instructions: `Paste your vLLM API key if your server requires auth. Leave empty for local no-auth mode (default base URL: ${DEFAULT_LOCAL_BASE_URL}).`,
|
|
14
|
+
promptMessage: "Paste your vLLM API key (optional for local no-auth)",
|
|
15
|
+
placeholder: DEFAULT_LOCAL_TOKEN,
|
|
16
|
+
validation: null,
|
|
17
|
+
emptyKeyFallback: DEFAULT_LOCAL_TOKEN,
|
|
18
|
+
});
|
|
29
19
|
|
|
30
20
|
export const vllmProvider = {
|
|
31
21
|
id: "vllm",
|
package/src/stream.ts
CHANGED
|
@@ -990,17 +990,19 @@ function extractStatusFromAssistantError(message: AssistantMessage): number | un
|
|
|
990
990
|
function isRetryableUpstreamError(error: unknown, status: number | undefined, message: string | undefined): boolean {
|
|
991
991
|
// 401 means the credential is bad; 403 is its valid-token twin (access
|
|
992
992
|
// denied by plan, model policy, or org restriction — a sibling account may
|
|
993
|
-
// not share it).
|
|
993
|
+
// not share it). Explicit account-scoped policy errors such as Codex
|
|
994
|
+
// `cyber_policy` are likewise rotatable: another account may carry the
|
|
995
|
+
// required approval. Usage-limit phrasing (Codex's
|
|
994
996
|
// "You have hit your ChatGPT usage limit", Anthropic's "usage_limit_reached",
|
|
995
997
|
// Google's "resource_exhausted", OpenAI's "insufficient_quota") and 429s
|
|
996
998
|
// without transient rate-limit wording mean this account is parked but a
|
|
997
999
|
// sibling credential can usually pick the request up. Both are rotatable
|
|
998
|
-
// via `onAuthError` — the auth-gateway maps
|
|
999
|
-
// `invalidateCredentialMatching` and
|
|
1000
|
-
//
|
|
1001
|
-
//
|
|
1002
|
-
//
|
|
1003
|
-
|
|
1000
|
+
// via `onAuthError` — the auth-gateway maps hard auth failures to
|
|
1001
|
+
// `invalidateCredentialMatching` and temporary account constraints to a
|
|
1002
|
+
// credential block. Transient 429s ("Too many requests", per-minute caps)
|
|
1003
|
+
// classify as RATE_LIMIT_EXCEEDED in `parseRateLimitReason` and stay in the
|
|
1004
|
+
// provider's own backoff layer instead of burning siblings.
|
|
1005
|
+
if (AIError.isAccountPolicyError(error)) return true;
|
|
1004
1006
|
if (AIError.isUsageLimit(error)) return true;
|
|
1005
1007
|
if (isInvalidatedOAuthTokenError(error)) return true;
|
|
1006
1008
|
if (status === 401 || (status === 403 && !isConcurrencyCapExclusion(status, message))) return true;
|
|
@@ -1506,6 +1508,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1506
1508
|
execHandlers: options?.execHandlers,
|
|
1507
1509
|
fetch: options?.fetch,
|
|
1508
1510
|
fallbacks: options?.fallbacks,
|
|
1511
|
+
acceptEmptyResponse: options?.acceptEmptyResponse,
|
|
1509
1512
|
...simpleProviderOptions,
|
|
1510
1513
|
};
|
|
1511
1514
|
|
package/src/types.ts
CHANGED
|
@@ -552,6 +552,13 @@ export interface StreamOptions {
|
|
|
552
552
|
* Optional retry delay hook for tests and transports that need custom scheduling.
|
|
553
553
|
*/
|
|
554
554
|
providerRetryWait?: (delayMs: number, signal?: AbortSignal) => Promise<void>;
|
|
555
|
+
/**
|
|
556
|
+
* Accept a Google `STOP` response with no visible text or tool call as a
|
|
557
|
+
* successful completion. Passive callers such as advisors use this because
|
|
558
|
+
* silence is a valid result; interactive agent turns retain empty-response
|
|
559
|
+
* retries by default. Ignored by non-Google providers.
|
|
560
|
+
*/
|
|
561
|
+
acceptEmptyResponse?: boolean;
|
|
555
562
|
/**
|
|
556
563
|
* Optional `fetch` implementation override. Providers route every HTTP
|
|
557
564
|
* request — direct calls, SDK clients, and retry helpers — through this
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toNumber } from "@oh-my-pi/pi-catalog/utils";
|
|
1
2
|
import { parseAlibabaTokenPlanCredential } from "@oh-my-pi/pi-catalog/wire/alibaba-token-plan";
|
|
2
3
|
import type {
|
|
3
4
|
CredentialRankingStrategy,
|
|
@@ -8,7 +9,7 @@ import type {
|
|
|
8
9
|
UsageReport,
|
|
9
10
|
} from "../usage";
|
|
10
11
|
import { isRecord } from "../utils";
|
|
11
|
-
import {
|
|
12
|
+
import { HOUR_MS, parsePositiveTimestamp, WEEK_MS } from "./shared";
|
|
12
13
|
|
|
13
14
|
const PROVIDER = "alibaba-token-plan";
|
|
14
15
|
const CONSOLE_ORIGIN = "https://home.qwencloud.com";
|
|
@@ -19,8 +20,6 @@ const USAGE_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage";
|
|
|
19
20
|
const USAGE_URL = `https://cs-data.qwencloud.com/data/api.json?product=sfm_bailian&action=${GATEWAY_ACTION}&api=${encodeURIComponent(USAGE_API)}`;
|
|
20
21
|
const BROWSER_USER_AGENT =
|
|
21
22
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36";
|
|
22
|
-
const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
|
|
23
|
-
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
24
23
|
const CONSOLE_CORNERSTONE_PARAM = {
|
|
25
24
|
domain: "home.qwencloud.com",
|
|
26
25
|
consoleSite: "QWENCLOUD",
|
|
@@ -55,12 +54,6 @@ function unwrapGatewayData(value: Record<string, unknown>): Record<string, unkno
|
|
|
55
54
|
return current;
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
function parseResetTime(value: unknown): number | undefined {
|
|
59
|
-
const parsed = toNumber(value);
|
|
60
|
-
if (parsed === undefined || parsed <= 0) return undefined;
|
|
61
|
-
return parsed < 1_000_000_000_000 ? parsed * 1000 : parsed;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
57
|
function parseUsedFraction(value: unknown): number | undefined {
|
|
65
58
|
const parsed = toNumber(value);
|
|
66
59
|
if (parsed === undefined || parsed < 0) return undefined;
|
|
@@ -178,17 +171,17 @@ async function fetchAlibabaTokenPlanUsage(
|
|
|
178
171
|
buildLimit(
|
|
179
172
|
"5h",
|
|
180
173
|
"5 Hour Credits",
|
|
181
|
-
|
|
174
|
+
5 * HOUR_MS,
|
|
182
175
|
parseUsedFraction(responseData.per5HourPercentage),
|
|
183
|
-
|
|
176
|
+
parsePositiveTimestamp(responseData.per5HourResetTime),
|
|
184
177
|
accountId,
|
|
185
178
|
),
|
|
186
179
|
buildLimit(
|
|
187
180
|
"7d",
|
|
188
181
|
"7 Day Credits",
|
|
189
|
-
|
|
182
|
+
WEEK_MS,
|
|
190
183
|
parseUsedFraction(responseData.per1WeekPercentage),
|
|
191
|
-
|
|
184
|
+
parsePositiveTimestamp(responseData.per1WeekResetTime),
|
|
192
185
|
accountId,
|
|
193
186
|
),
|
|
194
187
|
].filter((limit): limit is UsageLimit => limit !== undefined);
|
|
@@ -224,7 +217,7 @@ export const alibabaTokenPlanRankingStrategy: CredentialRankingStrategy = {
|
|
|
224
217
|
secondary: report.limits.find(limit => limit.id === "credits:7d"),
|
|
225
218
|
}),
|
|
226
219
|
windowDefaults: {
|
|
227
|
-
primaryMs:
|
|
228
|
-
secondaryMs:
|
|
220
|
+
primaryMs: 5 * HOUR_MS,
|
|
221
|
+
secondaryMs: WEEK_MS,
|
|
229
222
|
},
|
|
230
223
|
};
|