@oh-my-pi/pi-ai 16.1.13 → 16.1.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/CHANGELOG.md +26 -0
- package/dist/types/auth-retry.d.ts +6 -2
- package/dist/types/providers/aws-credentials.d.ts +2 -0
- package/dist/types/providers/openai-shared.d.ts +3 -0
- package/dist/types/rate-limit-utils.d.ts +26 -0
- package/dist/types/registry/huggingface.d.ts +2 -2
- package/dist/types/registry/nvidia.d.ts +0 -1
- package/dist/types/registry/oauth/minimax-code.d.ts +2 -3
- package/dist/types/registry/qianfan.d.ts +2 -2
- package/dist/types/registry/registry.d.ts +4 -1
- package/dist/types/registry/sakana.d.ts +7 -0
- package/dist/types/registry/types.d.ts +0 -2
- package/dist/types/registry/venice.d.ts +2 -2
- package/dist/types/registry/zai.d.ts +2 -2
- package/dist/types/registry/zhipu-coding-plan.d.ts +2 -2
- package/dist/types/utils/deterministic-id.d.ts +16 -0
- package/dist/types/utils/proxy.d.ts +29 -0
- package/package.json +4 -4
- package/src/auth-gateway/server.ts +7 -8
- package/src/auth-retry.ts +12 -7
- package/src/auth-storage.ts +7 -11
- package/src/providers/amazon-bedrock.ts +1 -0
- package/src/providers/aws-credentials.ts +23 -10
- package/src/providers/cursor.ts +12 -15
- package/src/providers/devin.ts +7 -14
- package/src/providers/openai-responses.ts +2 -1
- package/src/providers/openai-shared.ts +33 -6
- package/src/rate-limit-utils.ts +49 -1
- package/src/registry/huggingface.ts +13 -35
- package/src/registry/nvidia.ts +0 -1
- package/src/registry/oauth/minimax-code.ts +19 -48
- package/src/registry/qianfan.ts +12 -34
- package/src/registry/registry.ts +2 -0
- package/src/registry/sakana.ts +22 -0
- package/src/registry/types.ts +0 -2
- package/src/registry/venice.ts +12 -34
- package/src/registry/zai.ts +12 -33
- package/src/registry/zhipu-coding-plan.ts +12 -33
- package/src/stream.ts +29 -19
- package/src/utils/deterministic-id.ts +20 -0
- package/src/utils/proxy.ts +239 -0
package/src/providers/devin.ts
CHANGED
|
@@ -42,6 +42,7 @@ import type {
|
|
|
42
42
|
Tool,
|
|
43
43
|
ToolCall,
|
|
44
44
|
} from "../types";
|
|
45
|
+
import { deterministicUuid } from "../utils/deterministic-id";
|
|
45
46
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
46
47
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
47
48
|
import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
|
|
@@ -427,7 +428,7 @@ function buildDevinChatRequest(
|
|
|
427
428
|
plannerMode: ConversationalPlannerMode.DEFAULT,
|
|
428
429
|
toolChoice: create(ChatToolChoiceSchema, { choice: { case: "optionName", value: "auto" } }),
|
|
429
430
|
systemPromptCacheOptions: create(PromptCacheOptionsSchema, { type: CacheControlType.EPHEMERAL }),
|
|
430
|
-
disableParallelToolCalls:
|
|
431
|
+
disableParallelToolCalls: true,
|
|
431
432
|
cascadeId,
|
|
432
433
|
executionId: crypto.randomUUID(),
|
|
433
434
|
configuration: create(CompletionConfigurationSchema, {
|
|
@@ -455,6 +456,8 @@ function buildDevinChatRequest(
|
|
|
455
456
|
/** Map omp `Message` history onto Cascade `ChatMessagePrompt`s (USER / SYSTEM / TOOL channels). */
|
|
456
457
|
function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMessagePrompt[] {
|
|
457
458
|
const prompts: ChatMessagePrompt[] = [];
|
|
459
|
+
// messageId seeds are `cascadeId\0index\0role[...]` — prompt text is excluded
|
|
460
|
+
// so ids stay stable across content edits / history rebuilds.
|
|
458
461
|
for (const [index, msg] of messages.entries()) {
|
|
459
462
|
if (msg.role === "user" || msg.role === "developer") {
|
|
460
463
|
let promptText = "";
|
|
@@ -472,7 +475,7 @@ function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMe
|
|
|
472
475
|
}
|
|
473
476
|
prompts.push(
|
|
474
477
|
create(ChatMessagePromptSchema, {
|
|
475
|
-
messageId:
|
|
478
|
+
messageId: deterministicUuid(`${cascadeId}\0${index}\0${msg.role}`),
|
|
476
479
|
source: ChatMessageSource.USER,
|
|
477
480
|
prompt: promptText,
|
|
478
481
|
images,
|
|
@@ -501,7 +504,7 @@ function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMe
|
|
|
501
504
|
}
|
|
502
505
|
prompts.push(
|
|
503
506
|
create(ChatMessagePromptSchema, {
|
|
504
|
-
messageId: msg.responseId ?? `bot-${
|
|
507
|
+
messageId: msg.responseId ?? `bot-${deterministicUuid(`${cascadeId}\0${index}\0assistant`)}`,
|
|
505
508
|
source: ChatMessageSource.SYSTEM,
|
|
506
509
|
prompt: promptText,
|
|
507
510
|
thinking: thinkingText,
|
|
@@ -522,7 +525,7 @@ function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMe
|
|
|
522
525
|
}
|
|
523
526
|
prompts.push(
|
|
524
527
|
create(ChatMessagePromptSchema, {
|
|
525
|
-
messageId:
|
|
528
|
+
messageId: deterministicUuid(`${cascadeId}\0${index}\0tool\0${msg.toolCallId}`),
|
|
526
529
|
source: ChatMessageSource.TOOL,
|
|
527
530
|
toolCallId: msg.toolCallId,
|
|
528
531
|
toolResultIsError: msg.isError,
|
|
@@ -535,16 +538,6 @@ function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMe
|
|
|
535
538
|
return prompts;
|
|
536
539
|
}
|
|
537
540
|
|
|
538
|
-
/**
|
|
539
|
-
* Deterministic UUID-shaped message id derived from a stable per-message seed.
|
|
540
|
-
* Seed is `${cascadeId}\0${index}\0${role}[...]` — prompt text is intentionally
|
|
541
|
-
* excluded so ids stay stable across content edits / history rebuilds.
|
|
542
|
-
*/
|
|
543
|
-
function deterministicMessageId(seed: string): string {
|
|
544
|
-
const hex = new Bun.CryptoHasher("sha256").update(seed).digest("hex");
|
|
545
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
541
|
/**
|
|
549
542
|
* Parse a Connect end-of-stream JSON trailer and return a human-readable error
|
|
550
543
|
* string when it carries `{ error: { code, message } }`, else `null`. The trailer
|
|
@@ -436,7 +436,8 @@ const streamOpenAIResponsesOnce = (
|
|
|
436
436
|
? buildOpenAIResponsesChainedParams(params, trailingScaffoldingItems, chainState)
|
|
437
437
|
: { params };
|
|
438
438
|
sentPreviousResponseId = chained.previousResponseId;
|
|
439
|
-
const idleTimeoutMs =
|
|
439
|
+
const idleTimeoutMs =
|
|
440
|
+
options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs(model.compat.streamIdleTimeoutMs);
|
|
440
441
|
const firstEventTimeoutMs =
|
|
441
442
|
options?.streamFirstEventTimeoutMs ?? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs);
|
|
442
443
|
const requestTimeoutMs =
|
|
@@ -130,6 +130,17 @@ export interface OpenAIRequestSetup {
|
|
|
130
130
|
requestHeaders: Record<string, string>;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
function normalizeSakanaRequestBaseUrl(baseUrl: string | undefined): string | undefined {
|
|
134
|
+
const value = baseUrl?.trim();
|
|
135
|
+
if (!value) return undefined;
|
|
136
|
+
const normalized = value.replace(/\/+$/, "");
|
|
137
|
+
return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resolveSakanaRequestBaseUrl(): string | undefined {
|
|
141
|
+
return normalizeSakanaRequestBaseUrl($env.SAKANA_BASE_URL) ?? normalizeSakanaRequestBaseUrl($env.FUGU_BASE_URL);
|
|
142
|
+
}
|
|
143
|
+
|
|
133
144
|
export function resolveOpenAIRequestSetup(
|
|
134
145
|
model: OpenAIRequestSetupModel,
|
|
135
146
|
options: OpenAIRequestSetupOptions,
|
|
@@ -165,6 +176,12 @@ export function resolveOpenAIRequestSetup(
|
|
|
165
176
|
baseUrl = moonshotBaseUrl;
|
|
166
177
|
}
|
|
167
178
|
}
|
|
179
|
+
if (model.provider === "sakana") {
|
|
180
|
+
const sakanaBaseUrl = resolveSakanaRequestBaseUrl();
|
|
181
|
+
if (sakanaBaseUrl) {
|
|
182
|
+
baseUrl = sakanaBaseUrl;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
168
185
|
if (model.provider === "github-copilot") {
|
|
169
186
|
apiKey = parseGitHubCopilotApiKey(rawApiKey).accessToken;
|
|
170
187
|
const copilot = buildCopilotDynamicHeaders({
|
|
@@ -2384,19 +2401,29 @@ export function populateResponsesUsageFromResponse(
|
|
|
2384
2401
|
input_tokens_details?: {
|
|
2385
2402
|
cached_tokens?: number | null;
|
|
2386
2403
|
cache_write_tokens?: number | null;
|
|
2404
|
+
orchestration_input_tokens?: number | null;
|
|
2405
|
+
orchestration_input_cached_tokens?: number | null;
|
|
2406
|
+
} | null;
|
|
2407
|
+
output_tokens_details?: {
|
|
2408
|
+
reasoning_tokens?: number | null;
|
|
2409
|
+
orchestration_output_tokens?: number | null;
|
|
2387
2410
|
} | null;
|
|
2388
|
-
output_tokens_details?: { reasoning_tokens?: number | null } | null;
|
|
2389
2411
|
}
|
|
2390
2412
|
| null
|
|
2391
2413
|
| undefined,
|
|
2392
2414
|
): void {
|
|
2393
2415
|
if (!usage) return;
|
|
2416
|
+
const details = usage.input_tokens_details;
|
|
2417
|
+
const outputDetails = usage.output_tokens_details;
|
|
2418
|
+
const orchestrationInputTokens = details?.orchestration_input_tokens ?? 0;
|
|
2419
|
+
const orchestrationInputCachedTokens = details?.orchestration_input_cached_tokens ?? 0;
|
|
2420
|
+
const orchestrationOutputTokens = outputDetails?.orchestration_output_tokens ?? 0;
|
|
2394
2421
|
const accounting = calculateOpenAIUsageAccounting({
|
|
2395
|
-
promptTokens: usage.input_tokens ?? 0,
|
|
2396
|
-
outputTokens: usage.output_tokens ?? 0,
|
|
2397
|
-
cachedTokens:
|
|
2398
|
-
reasoningTokens:
|
|
2399
|
-
cacheWriteOpenRouter:
|
|
2422
|
+
promptTokens: (usage.input_tokens ?? 0) + orchestrationInputTokens,
|
|
2423
|
+
outputTokens: (usage.output_tokens ?? 0) + orchestrationOutputTokens,
|
|
2424
|
+
cachedTokens: (details?.cached_tokens ?? usage.prompt_cache_hit_tokens ?? 0) + orchestrationInputCachedTokens,
|
|
2425
|
+
reasoningTokens: outputDetails?.reasoning_tokens ?? 0,
|
|
2426
|
+
cacheWriteOpenRouter: details?.cache_write_tokens ?? undefined,
|
|
2400
2427
|
cacheWriteDeepSeek: usage.prompt_cache_miss_tokens ?? undefined,
|
|
2401
2428
|
hasDeepSeekCacheHitAndMiss:
|
|
2402
2429
|
usage.prompt_cache_hit_tokens !== undefined && usage.prompt_cache_miss_tokens !== undefined,
|
package/src/rate-limit-utils.ts
CHANGED
|
@@ -100,7 +100,55 @@ export function calculateRateLimitBackoffMs(reason: RateLimitReason): number {
|
|
|
100
100
|
|
|
101
101
|
/** Detect usage/quota limit errors in error messages (persistent, requires credential switch). */
|
|
102
102
|
const USAGE_LIMIT_PATTERN =
|
|
103
|
-
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|quota.?exceeded|quota.?reached|resource.?exhausted|exhausted your capacity|quota will reset|insufficient.?balance/i;
|
|
103
|
+
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|quota.?exceeded|quota.?reached|resource.?exhausted|exhausted your capacity|quota will reset|insufficient.?(?:balance|quota)/i;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* HTTP status codes that, absent richer body classification, represent an
|
|
107
|
+
* account-local usage cap rather than a bad credential or a transient blip.
|
|
108
|
+
* Always combine with {@link isUsageLimitOutcome} when a message is available
|
|
109
|
+
* — a 429 carrying transient rate-limit wording is NOT a usage cap.
|
|
110
|
+
*/
|
|
111
|
+
export function isUsageLimitStatus(status: number | undefined): boolean {
|
|
112
|
+
return status === 429;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns true for failures that should burn one credential and rotate to a
|
|
117
|
+
* sibling account. Decision tree:
|
|
118
|
+
*
|
|
119
|
+
* 1. Body matches {@link isUsageLimitError} (Codex `usage_limit_reached`,
|
|
120
|
+
* Anthropic account rate-limit, Google `resource_exhausted`, OpenAI
|
|
121
|
+
* `insufficient_quota`, …) → rotate.
|
|
122
|
+
* 2. Status is not 429 → backoff (caller's domain).
|
|
123
|
+
* 3. Body is absent or {@link isOpaqueStatusBody opaque} (just the status,
|
|
124
|
+
* empty JSON, HTTP framing only) → rotate conservatively: the server
|
|
125
|
+
* gave us nothing else to go on.
|
|
126
|
+
* 4. Body has content → defer to {@link parseRateLimitReason}. Only
|
|
127
|
+
* `QUOTA_EXHAUSTED` rotates; `RATE_LIMIT_EXCEEDED` (`Too many requests`,
|
|
128
|
+
* per-minute caps), `MODEL_CAPACITY_EXHAUSTED` (`Service overloaded`),
|
|
129
|
+
* `SERVER_ERROR`, and `UNKNOWN` (`Please retry in 5s`) stay in the
|
|
130
|
+
* provider's own backoff layer so transient 429s don't burn sibling
|
|
131
|
+
* credentials.
|
|
132
|
+
*/
|
|
133
|
+
export function isUsageLimitOutcome(status: number | undefined, message: string | undefined): boolean {
|
|
134
|
+
if (message && isUsageLimitError(message)) return true;
|
|
135
|
+
if (!isUsageLimitStatus(status)) return false;
|
|
136
|
+
if (!message || isOpaqueStatusBody(message)) return true;
|
|
137
|
+
return parseRateLimitReason(message) === "QUOTA_EXHAUSTED";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A 429 body is opaque when it carries no signal beyond the status itself —
|
|
142
|
+
* empty, whitespace-only, the status digits with HTTP/JSON framing, or
|
|
143
|
+
* generic punctuation. Anything else (retry hints, capacity wording, error
|
|
144
|
+
* descriptions) is informative enough to defer to the classifier.
|
|
145
|
+
*/
|
|
146
|
+
function isOpaqueStatusBody(message: string): boolean {
|
|
147
|
+
const cleaned = message
|
|
148
|
+
.replace(/\b429\b/g, "")
|
|
149
|
+
.replace(/\b(?:http|https|status|error|code|response|message)\b/gi, "");
|
|
150
|
+
return !/[a-z\d]{3,}/i.test(cleaned);
|
|
151
|
+
}
|
|
104
152
|
|
|
105
153
|
export function isUsageLimitError(errorMessage: string): boolean {
|
|
106
154
|
return USAGE_LIMIT_PATTERN.test(errorMessage) || ACCOUNT_RATE_LIMIT_PATTERN.test(errorMessage);
|
|
@@ -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 =
|
|
@@ -7,42 +7,20 @@ const AUTH_URL =
|
|
|
7
7
|
const API_BASE_URL = "https://router.huggingface.co/v1";
|
|
8
8
|
const VALIDATION_MODEL = "openai/gpt-oss-120b";
|
|
9
9
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const apiKey = await options.onPrompt({
|
|
22
|
-
message: "Paste your Hugging Face token (HUGGINGFACE_HUB_TOKEN / HF_TOKEN)",
|
|
23
|
-
placeholder: "hf_...",
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
if (options.signal?.aborted) {
|
|
27
|
-
throw new Error("Login cancelled");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const trimmed = apiKey.trim();
|
|
31
|
-
if (!trimmed) {
|
|
32
|
-
throw new Error("API key is required");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
options.onProgress?.("Validating API key...");
|
|
36
|
-
await validateOpenAICompatibleApiKey({
|
|
10
|
+
export const loginHuggingface = createApiKeyLogin({
|
|
11
|
+
providerLabel: "Hugging Face",
|
|
12
|
+
authUrl: AUTH_URL,
|
|
13
|
+
instructions:
|
|
14
|
+
"Create/copy a token with Make calls to Inference Providers permission (usable as HUGGINGFACE_HUB_TOKEN or HF_TOKEN)",
|
|
15
|
+
promptMessage: "Paste your Hugging Face token (HUGGINGFACE_HUB_TOKEN / HF_TOKEN)",
|
|
16
|
+
placeholder: "hf_...",
|
|
17
|
+
validation: {
|
|
18
|
+
kind: "chat-completions",
|
|
37
19
|
provider: "Hugging Face",
|
|
38
|
-
apiKey: trimmed,
|
|
39
20
|
baseUrl: API_BASE_URL,
|
|
40
21
|
model: VALIDATION_MODEL,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return trimmed;
|
|
45
|
-
}
|
|
22
|
+
},
|
|
23
|
+
});
|
|
46
24
|
|
|
47
25
|
export const huggingfaceProvider = {
|
|
48
26
|
id: "huggingface",
|
package/src/registry/nvidia.ts
CHANGED
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
* China: https://api.minimaxi.com/v1
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
17
|
-
import type { OAuthController } from "./types";
|
|
16
|
+
import { createApiKeyLogin } from "../api-key-login";
|
|
18
17
|
|
|
19
18
|
const AUTH_URL_INTL = "https://platform.minimax.io/subscribe/token-plan";
|
|
20
19
|
const AUTH_URL_CN = "https://platform.minimaxi.com/subscribe/token-plan";
|
|
@@ -22,61 +21,33 @@ const API_BASE_URL_INTL = "https://api.minimax.io/v1";
|
|
|
22
21
|
const API_BASE_URL_CN = "https://api.minimaxi.com/v1";
|
|
23
22
|
const VALIDATION_MODEL = "MiniMax-M3";
|
|
24
23
|
|
|
24
|
+
function createMiniMaxLogin(authUrl: string, baseUrl: string, provider: string) {
|
|
25
|
+
return createApiKeyLogin({
|
|
26
|
+
providerLabel: "MiniMax Token Plan",
|
|
27
|
+
authUrl,
|
|
28
|
+
instructions: "Subscribe to Token Plan and copy your API key",
|
|
29
|
+
promptMessage: "Paste your MiniMax Token Plan API key",
|
|
30
|
+
placeholder: "sk-...",
|
|
31
|
+
validation: {
|
|
32
|
+
kind: "chat-completions",
|
|
33
|
+
provider,
|
|
34
|
+
baseUrl,
|
|
35
|
+
model: VALIDATION_MODEL,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
/**
|
|
26
41
|
* Login to MiniMax Token Plan (international).
|
|
27
42
|
*
|
|
28
43
|
* Opens browser to subscription page, prompts user to paste their API key.
|
|
29
44
|
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
30
45
|
*/
|
|
31
|
-
export
|
|
32
|
-
return loginMiniMaxCodeWithBaseUrl(options, AUTH_URL_INTL, API_BASE_URL_INTL, "MiniMax Token Plan");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function loginMiniMaxCodeWithBaseUrl(
|
|
36
|
-
options: OAuthController,
|
|
37
|
-
authUrl: string,
|
|
38
|
-
baseUrl: string,
|
|
39
|
-
providerName: string,
|
|
40
|
-
): Promise<string> {
|
|
41
|
-
const fetchImpl = options.fetch ?? fetch;
|
|
42
|
-
if (!options.onPrompt) {
|
|
43
|
-
throw new Error("MiniMax Token Plan login requires onPrompt callback");
|
|
44
|
-
}
|
|
45
|
-
// Open browser to subscription page
|
|
46
|
-
options.onAuth?.({
|
|
47
|
-
url: authUrl,
|
|
48
|
-
instructions: "Subscribe to Token Plan and copy your API key",
|
|
49
|
-
});
|
|
50
|
-
// Prompt user to paste their API key
|
|
51
|
-
const apiKey = await options.onPrompt({
|
|
52
|
-
message: "Paste your MiniMax Token Plan API key",
|
|
53
|
-
placeholder: "sk-...",
|
|
54
|
-
});
|
|
55
|
-
if (options.signal?.aborted) {
|
|
56
|
-
throw new Error("Login cancelled");
|
|
57
|
-
}
|
|
58
|
-
const trimmed = apiKey.trim();
|
|
59
|
-
if (!trimmed) {
|
|
60
|
-
throw new Error("API key is required");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
options.onProgress?.("Validating API key...");
|
|
64
|
-
await validateOpenAICompatibleApiKey({
|
|
65
|
-
provider: providerName,
|
|
66
|
-
apiKey: trimmed,
|
|
67
|
-
baseUrl,
|
|
68
|
-
model: VALIDATION_MODEL,
|
|
69
|
-
signal: options.signal,
|
|
70
|
-
fetch: fetchImpl,
|
|
71
|
-
});
|
|
72
|
-
return trimmed;
|
|
73
|
-
}
|
|
46
|
+
export const loginMiniMaxCode = createMiniMaxLogin(AUTH_URL_INTL, API_BASE_URL_INTL, "MiniMax Token Plan");
|
|
74
47
|
|
|
75
48
|
/**
|
|
76
49
|
* Login to MiniMax Token Plan (China).
|
|
77
50
|
*
|
|
78
51
|
* Same flow as international but uses China endpoint.
|
|
79
52
|
*/
|
|
80
|
-
export
|
|
81
|
-
return loginMiniMaxCodeWithBaseUrl(options, AUTH_URL_CN, API_BASE_URL_CN, "MiniMax Token Plan (China)");
|
|
82
|
-
}
|
|
53
|
+
export const loginMiniMaxCodeCn = createMiniMaxLogin(AUTH_URL_CN, API_BASE_URL_CN, "MiniMax Token Plan (China)");
|
package/src/registry/qianfan.ts
CHANGED
|
@@ -1,46 +1,24 @@
|
|
|
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://console.bce.baidu.com/qianfan/ais/console/apiKey";
|
|
6
6
|
const API_BASE_URL = "https://qianfan.baidubce.com/v2";
|
|
7
7
|
const VALIDATION_MODEL = "deepseek-v3.2";
|
|
8
8
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
const apiKey = await options.onPrompt({
|
|
20
|
-
message: "Paste your Qianfan API key",
|
|
21
|
-
placeholder: "bce-v3/ALTAK-...",
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
if (options.signal?.aborted) {
|
|
25
|
-
throw new Error("Login cancelled");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const trimmed = apiKey.trim();
|
|
29
|
-
if (!trimmed) {
|
|
30
|
-
throw new Error("API key is required");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
options.onProgress?.("Validating API key...");
|
|
34
|
-
await validateOpenAICompatibleApiKey({
|
|
9
|
+
export const loginQianfan = createApiKeyLogin({
|
|
10
|
+
providerLabel: "Qianfan",
|
|
11
|
+
authUrl: AUTH_URL,
|
|
12
|
+
instructions: "Copy your Qianfan API key from the console",
|
|
13
|
+
promptMessage: "Paste your Qianfan API key",
|
|
14
|
+
placeholder: "bce-v3/ALTAK-...",
|
|
15
|
+
validation: {
|
|
16
|
+
kind: "chat-completions",
|
|
35
17
|
provider: "qianfan",
|
|
36
|
-
apiKey: trimmed,
|
|
37
18
|
baseUrl: API_BASE_URL,
|
|
38
19
|
model: VALIDATION_MODEL,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return trimmed;
|
|
43
|
-
}
|
|
20
|
+
},
|
|
21
|
+
});
|
|
44
22
|
|
|
45
23
|
export const qianfanProvider = {
|
|
46
24
|
id: "qianfan",
|
package/src/registry/registry.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { parallelProvider } from "./parallel";
|
|
|
44
44
|
import { perplexityProvider } from "./perplexity";
|
|
45
45
|
import { qianfanProvider } from "./qianfan";
|
|
46
46
|
import { qwenPortalProvider } from "./qwen-portal";
|
|
47
|
+
import { sakanaProvider } from "./sakana";
|
|
47
48
|
import { syntheticProvider } from "./synthetic";
|
|
48
49
|
import { tavilyProvider } from "./tavily";
|
|
49
50
|
import { togetherProvider } from "./together";
|
|
@@ -90,6 +91,7 @@ const ALL = [
|
|
|
90
91
|
zhipuCodingPlanProvider,
|
|
91
92
|
umansProvider,
|
|
92
93
|
qwenPortalProvider,
|
|
94
|
+
sakanaProvider,
|
|
93
95
|
minimaxCodeProvider,
|
|
94
96
|
minimaxCodeCnProvider,
|
|
95
97
|
xiaomiProvider,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
2
|
+
import type { OAuthLoginCallbacks } from "./oauth/types";
|
|
3
|
+
import type { ProviderDefinition } from "./types";
|
|
4
|
+
|
|
5
|
+
export const loginSakana = createApiKeyLogin({
|
|
6
|
+
providerLabel: "Sakana AI",
|
|
7
|
+
authUrl: "https://console.sakana.ai/api-keys",
|
|
8
|
+
instructions: "Copy your API key from the Sakana AI console",
|
|
9
|
+
promptMessage: "Paste your Sakana AI API key",
|
|
10
|
+
placeholder: "sk-...",
|
|
11
|
+
validation: {
|
|
12
|
+
kind: "models-endpoint",
|
|
13
|
+
provider: "Sakana AI",
|
|
14
|
+
modelsUrl: "https://api.sakana.ai/v1/models",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const sakanaProvider = {
|
|
19
|
+
id: "sakana",
|
|
20
|
+
name: "Sakana AI",
|
|
21
|
+
login: (cb: OAuthLoginCallbacks) => loginSakana(cb),
|
|
22
|
+
} as const satisfies ProviderDefinition;
|
package/src/registry/types.ts
CHANGED
|
@@ -44,8 +44,6 @@ export interface ProviderDefinition {
|
|
|
44
44
|
readonly envKeys?: KeyResolver;
|
|
45
45
|
// --- interactive login (OAuthProviderInterface-compatible) ---
|
|
46
46
|
readonly login?: (callbacks: OAuthLoginCallbacks) => Promise<OAuthCredentials | string>;
|
|
47
|
-
/** String-returning login appends API keys instead of replacing the provider's existing key. */
|
|
48
|
-
readonly appendApiKeyLogin?: boolean;
|
|
49
47
|
readonly refreshToken?: (credentials: OAuthCredentials) => Promise<OAuthCredentials>;
|
|
50
48
|
readonly getApiKey?: (credentials: OAuthCredentials) => string;
|
|
51
49
|
/** Store OAuth credentials under a different provider id (e.g. `openai-codex-device` ⇒ `openai-codex`). */
|
package/src/registry/venice.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://venice.ai/settings/api";
|
|
@@ -12,41 +12,19 @@ const VALIDATION_MODEL = "qwen3-4b";
|
|
|
12
12
|
* Opens browser to API keys page, prompts user to paste their API key.
|
|
13
13
|
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
14
14
|
*/
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const apiKey = await options.onPrompt({
|
|
26
|
-
message: "Paste your Venice API key",
|
|
27
|
-
placeholder: "vapi_...",
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
if (options.signal?.aborted) {
|
|
31
|
-
throw new Error("Login cancelled");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const trimmed = apiKey.trim();
|
|
35
|
-
if (!trimmed) {
|
|
36
|
-
throw new Error("API key is required");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
options.onProgress?.("Validating API key...");
|
|
40
|
-
await validateOpenAICompatibleApiKey({
|
|
15
|
+
export const loginVenice = createApiKeyLogin({
|
|
16
|
+
providerLabel: "Venice",
|
|
17
|
+
authUrl: AUTH_URL,
|
|
18
|
+
instructions: "Copy your API key from the Venice dashboard",
|
|
19
|
+
promptMessage: "Paste your Venice API key",
|
|
20
|
+
placeholder: "vapi_...",
|
|
21
|
+
validation: {
|
|
22
|
+
kind: "chat-completions",
|
|
41
23
|
provider: "Venice",
|
|
42
|
-
apiKey: trimmed,
|
|
43
24
|
baseUrl: API_BASE_URL,
|
|
44
25
|
model: VALIDATION_MODEL,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return trimmed;
|
|
49
|
-
}
|
|
26
|
+
},
|
|
27
|
+
});
|
|
50
28
|
|
|
51
29
|
export const veniceProvider = {
|
|
52
30
|
id: "venice",
|
package/src/registry/zai.ts
CHANGED
|
@@ -1,45 +1,24 @@
|
|
|
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://z.ai/manage-apikey/apikey-list";
|
|
6
6
|
const API_BASE_URL = "https://api.z.ai/api/coding/paas/v4";
|
|
7
7
|
const VALIDATION_MODEL = "glm-5.2";
|
|
8
8
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
const apiKey = await options.onPrompt({
|
|
20
|
-
message: "Paste your Z.AI API key",
|
|
21
|
-
placeholder: "sk-...",
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
if (options.signal?.aborted) {
|
|
25
|
-
throw new Error("Login cancelled");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const trimmed = apiKey.trim();
|
|
29
|
-
if (!trimmed) {
|
|
30
|
-
throw new Error("API key is required");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
options.onProgress?.("Validating API key...");
|
|
34
|
-
await validateOpenAICompatibleApiKey({
|
|
9
|
+
export const loginZai = createApiKeyLogin({
|
|
10
|
+
providerLabel: "Z.AI",
|
|
11
|
+
authUrl: AUTH_URL,
|
|
12
|
+
instructions: "Copy your API key from the dashboard",
|
|
13
|
+
promptMessage: "Paste your Z.AI API key",
|
|
14
|
+
placeholder: "sk-...",
|
|
15
|
+
validation: {
|
|
16
|
+
kind: "chat-completions",
|
|
35
17
|
provider: "Z.AI",
|
|
36
|
-
apiKey: trimmed,
|
|
37
18
|
baseUrl: API_BASE_URL,
|
|
38
19
|
model: VALIDATION_MODEL,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return trimmed;
|
|
42
|
-
}
|
|
20
|
+
},
|
|
21
|
+
});
|
|
43
22
|
|
|
44
23
|
export const zaiProvider = {
|
|
45
24
|
id: "zai",
|
|
@@ -1,45 +1,24 @@
|
|
|
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://bigmodel.cn/coding-plan/personal/overview";
|
|
6
6
|
const API_BASE_URL = "https://open.bigmodel.cn/api/coding/paas/v4";
|
|
7
7
|
const VALIDATION_MODEL = "glm-5.1";
|
|
8
8
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
const apiKey = await options.onPrompt({
|
|
20
|
-
message: "Paste your Zhipu API key",
|
|
21
|
-
placeholder: "<id>.<secret>",
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
if (options.signal?.aborted) {
|
|
25
|
-
throw new Error("Login cancelled");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const trimmed = apiKey.trim();
|
|
29
|
-
if (!trimmed) {
|
|
30
|
-
throw new Error("API key is required");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
options.onProgress?.("Validating API key...");
|
|
34
|
-
await validateOpenAICompatibleApiKey({
|
|
9
|
+
export const loginZhipuCodingPlan = createApiKeyLogin({
|
|
10
|
+
providerLabel: "Zhipu Coding Plan",
|
|
11
|
+
authUrl: AUTH_URL,
|
|
12
|
+
instructions: "Copy your API key from the Coding Plan dashboard",
|
|
13
|
+
promptMessage: "Paste your Zhipu API key",
|
|
14
|
+
placeholder: "<id>.<secret>",
|
|
15
|
+
validation: {
|
|
16
|
+
kind: "chat-completions",
|
|
35
17
|
provider: "Zhipu",
|
|
36
|
-
apiKey: trimmed,
|
|
37
18
|
baseUrl: API_BASE_URL,
|
|
38
19
|
model: VALIDATION_MODEL,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return trimmed;
|
|
42
|
-
}
|
|
20
|
+
},
|
|
21
|
+
});
|
|
43
22
|
|
|
44
23
|
export const zhipuCodingPlanProvider = {
|
|
45
24
|
id: "zhipu-coding-plan",
|