@oh-my-pi/pi-ai 15.2.4 → 15.3.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/CHANGELOG.md +7 -0
- package/dist/types/provider-models/openai-compat.d.ts +1 -1
- package/dist/types/utils/oauth/deepseek.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/dist/types/utils/oauth/xiaomi.d.ts +2 -2
- package/package.json +2 -2
- package/src/auth-storage.ts +6 -0
- package/src/cli.ts +1 -0
- package/src/models.json +10 -10
- package/src/provider-models/openai-compat.ts +35 -18
- package/src/providers/openai-completions.ts +2 -2
- package/src/providers/openai-responses.ts +18 -3
- package/src/utils/oauth/deepseek.ts +16 -0
- package/src/utils/oauth/index.ts +5 -0
- package/src/utils/oauth/types.ts +1 -0
- package/src/utils/oauth/xiaomi.ts +71 -39
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.3.0] - 2026-05-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added DeepSeek to the built-in API-key login provider catalog so `omp login deepseek` stores a reusable `DEEPSEEK_API_KEY` credential for the bundled DeepSeek models.
|
|
10
|
+
|
|
5
11
|
## [15.2.4] - 2026-05-22
|
|
6
12
|
|
|
7
13
|
### Fixed
|
|
@@ -68,6 +74,7 @@
|
|
|
68
74
|
|
|
69
75
|
### Fixed
|
|
70
76
|
|
|
77
|
+
- Fixed OpenCode-Go and OpenCode-Zen chat-completions replay to omit stored reasoning fields on Kimi assistant tool-call messages, avoiding provider 400s for rejected `messages[].reasoning` payloads. ([#1157](https://github.com/can1357/oh-my-pi/issues/1157))
|
|
71
78
|
- Fixed OpenAI Responses and Codex tool schema normalization to emit `properties: {}` for no-argument object schemas without rewriting literal payloads. ([#1147](https://github.com/can1357/oh-my-pi/issues/1147))
|
|
72
79
|
- Fixed Anthropic 400 (`unexpected tool_use_id found in tool_result blocks ... Each tool_result block must have a corresponding tool_use block in the previous message`) when handoff/compaction folds an assistant `tool_use` into the handoff summary string but leaves the matching user-side `tool_result` message in the history. `transformMessages` now indexes every `tool_use` id surviving the first pass and drops orphan `tool_result` messages whose originator was compacted away, preserving the text payload as a user-level `<stale-tool-result>` note so the model still sees what the tool returned. The note is emitted with `role: "user"` rather than `role: "developer"` so providers that elevate developer-role messages (Ollama: `developer` → `system`; OpenAI chat-completions reasoning models: `developer` → `developer`) cannot lift stale tool output to an instruction-priority tier above the surrounding user/developer messages.
|
|
73
80
|
- Fixed streaming authentication retry to trigger when a provider emits a 401 `error` event after a `start` event but before any replay-unsafe content is emitted
|
|
@@ -165,7 +165,7 @@ export interface XiaomiModelManagerConfig {
|
|
|
165
165
|
apiKey?: string;
|
|
166
166
|
baseUrl?: string;
|
|
167
167
|
}
|
|
168
|
-
export declare function xiaomiModelManagerOptions(config?: XiaomiModelManagerConfig): ModelManagerOptions<"
|
|
168
|
+
export declare function xiaomiModelManagerOptions(config?: XiaomiModelManagerConfig): ModelManagerOptions<"openai-completions">;
|
|
169
169
|
export interface LiteLLMModelManagerConfig {
|
|
170
170
|
apiKey?: string;
|
|
171
171
|
baseUrl?: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const loginDeepSeek: (options: import("./types").OAuthController) => Promise<string>;
|
|
@@ -7,7 +7,7 @@ export type OAuthCredentials = {
|
|
|
7
7
|
email?: string;
|
|
8
8
|
accountId?: string;
|
|
9
9
|
};
|
|
10
|
-
export type OAuthProvider = "alibaba-coding-plan" | "anthropic" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "fireworks" | "firepass" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xiaomi" | "zenmux" | "zai";
|
|
10
|
+
export type OAuthProvider = "alibaba-coding-plan" | "anthropic" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "fireworks" | "firepass" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xiaomi" | "zenmux" | "zai";
|
|
11
11
|
export type OAuthProviderId = OAuthProvider | (string & {});
|
|
12
12
|
export type OAuthPrompt = {
|
|
13
13
|
message: string;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Xiaomi MiMo login flow.
|
|
3
3
|
*
|
|
4
|
-
* Xiaomi MiMo provides
|
|
5
|
-
* https://api.xiaomimimo.com/
|
|
4
|
+
* Xiaomi MiMo provides OpenAI-compatible models via
|
|
5
|
+
* https://api.xiaomimimo.com/v1.
|
|
6
6
|
*
|
|
7
7
|
* This is not OAuth - it's a simple API key flow:
|
|
8
8
|
* 1. Open browser to Xiaomi MiMo API key console
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.3.0",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
45
45
|
"@bufbuild/protobuf": "^2.12.0",
|
|
46
|
-
"@oh-my-pi/pi-utils": "15.
|
|
46
|
+
"@oh-my-pi/pi-utils": "15.3.0",
|
|
47
47
|
"openai": "^6.36.0",
|
|
48
48
|
"partial-json": "^0.1.7",
|
|
49
49
|
"zod": "4.4.3"
|
package/src/auth-storage.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { kimiUsageProvider } from "./usage/kimi";
|
|
|
29
29
|
import { codexRankingStrategy, openaiCodexUsageProvider } from "./usage/openai-codex";
|
|
30
30
|
import { zaiUsageProvider } from "./usage/zai";
|
|
31
31
|
import { getOAuthApiKey, getOAuthProvider, refreshOAuthToken } from "./utils/oauth";
|
|
32
|
+
import { loginDeepSeek } from "./utils/oauth/deepseek";
|
|
32
33
|
import { loginOpenAICodexDevice } from "./utils/oauth/openai-codex";
|
|
33
34
|
import type { OAuthController, OAuthCredentials, OAuthProvider, OAuthProviderId } from "./utils/oauth/types";
|
|
34
35
|
|
|
@@ -1378,6 +1379,11 @@ export class AuthStorage {
|
|
|
1378
1379
|
await saveApiKeyCredential(apiKey);
|
|
1379
1380
|
return;
|
|
1380
1381
|
}
|
|
1382
|
+
case "deepseek": {
|
|
1383
|
+
const apiKey = await loginDeepSeek(ctrl);
|
|
1384
|
+
await saveApiKeyCredential(apiKey);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1381
1387
|
case "fireworks": {
|
|
1382
1388
|
const { loginFireworks } = await import("./utils/oauth/fireworks");
|
|
1383
1389
|
const apiKey = await loginFireworks(ctrl);
|
package/src/cli.ts
CHANGED
package/src/models.json
CHANGED
|
@@ -52450,9 +52450,9 @@
|
|
|
52450
52450
|
"mimo-v2-flash": {
|
|
52451
52451
|
"id": "mimo-v2-flash",
|
|
52452
52452
|
"name": "MiMo-V2-Flash",
|
|
52453
|
-
"api": "
|
|
52453
|
+
"api": "openai-completions",
|
|
52454
52454
|
"provider": "xiaomi",
|
|
52455
|
-
"baseUrl": "https://api.xiaomimimo.com/
|
|
52455
|
+
"baseUrl": "https://api.xiaomimimo.com/v1",
|
|
52456
52456
|
"reasoning": true,
|
|
52457
52457
|
"input": [
|
|
52458
52458
|
"text"
|
|
@@ -52474,9 +52474,9 @@
|
|
|
52474
52474
|
"mimo-v2-omni": {
|
|
52475
52475
|
"id": "mimo-v2-omni",
|
|
52476
52476
|
"name": "MiMo-V2-Omni",
|
|
52477
|
-
"api": "
|
|
52477
|
+
"api": "openai-completions",
|
|
52478
52478
|
"provider": "xiaomi",
|
|
52479
|
-
"baseUrl": "https://api.xiaomimimo.com/
|
|
52479
|
+
"baseUrl": "https://api.xiaomimimo.com/v1",
|
|
52480
52480
|
"reasoning": true,
|
|
52481
52481
|
"input": [
|
|
52482
52482
|
"text",
|
|
@@ -52499,9 +52499,9 @@
|
|
|
52499
52499
|
"mimo-v2-pro": {
|
|
52500
52500
|
"id": "mimo-v2-pro",
|
|
52501
52501
|
"name": "MiMo-V2-Pro",
|
|
52502
|
-
"api": "
|
|
52502
|
+
"api": "openai-completions",
|
|
52503
52503
|
"provider": "xiaomi",
|
|
52504
|
-
"baseUrl": "https://api.xiaomimimo.com/
|
|
52504
|
+
"baseUrl": "https://api.xiaomimimo.com/v1",
|
|
52505
52505
|
"reasoning": true,
|
|
52506
52506
|
"input": [
|
|
52507
52507
|
"text"
|
|
@@ -52523,9 +52523,9 @@
|
|
|
52523
52523
|
"mimo-v2.5": {
|
|
52524
52524
|
"id": "mimo-v2.5",
|
|
52525
52525
|
"name": "MiMo-V2.5",
|
|
52526
|
-
"api": "
|
|
52526
|
+
"api": "openai-completions",
|
|
52527
52527
|
"provider": "xiaomi",
|
|
52528
|
-
"baseUrl": "https://api.xiaomimimo.com/
|
|
52528
|
+
"baseUrl": "https://api.xiaomimimo.com/v1",
|
|
52529
52529
|
"reasoning": true,
|
|
52530
52530
|
"input": [
|
|
52531
52531
|
"text",
|
|
@@ -52548,9 +52548,9 @@
|
|
|
52548
52548
|
"mimo-v2.5-pro": {
|
|
52549
52549
|
"id": "mimo-v2.5-pro",
|
|
52550
52550
|
"name": "MiMo-V2.5-Pro",
|
|
52551
|
-
"api": "
|
|
52551
|
+
"api": "openai-completions",
|
|
52552
52552
|
"provider": "xiaomi",
|
|
52553
|
-
"baseUrl": "https://api.xiaomimimo.com/
|
|
52553
|
+
"baseUrl": "https://api.xiaomimimo.com/v1",
|
|
52554
52554
|
"reasoning": true,
|
|
52555
52555
|
"input": [
|
|
52556
52556
|
"text"
|
|
@@ -1408,28 +1408,26 @@ export interface XiaomiModelManagerConfig {
|
|
|
1408
1408
|
|
|
1409
1409
|
export function xiaomiModelManagerOptions(
|
|
1410
1410
|
config?: XiaomiModelManagerConfig,
|
|
1411
|
-
): ModelManagerOptions<"
|
|
1411
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
1412
1412
|
const apiKey = config?.apiKey;
|
|
1413
1413
|
// Xiaomi splits API keys across two backends: standard `sk-` keys hit
|
|
1414
|
-
// api.xiaomimimo.com; "token plan" `tp-` keys hit the
|
|
1415
|
-
//
|
|
1416
|
-
const
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
//
|
|
1421
|
-
|
|
1422
|
-
const
|
|
1423
|
-
const discoveryBaseUrl = toAnthropicDiscoveryBaseUrl(discoveryRoot);
|
|
1424
|
-
const references = createBundledReferenceMap<"anthropic-messages">("xiaomi");
|
|
1414
|
+
// api.xiaomimimo.com; "token plan" `tp-` keys hit either the SG or EU
|
|
1415
|
+
// token-plan host. Try SGP first; if discovery fails, retry AMS.
|
|
1416
|
+
const TOKEN_PLAN_SGP_BASE_URL = "https://token-plan-sgp.xiaomimimo.com/v1";
|
|
1417
|
+
const TOKEN_PLAN_AMS_BASE_URL = "https://token-plan-ams.xiaomimimo.com/v1";
|
|
1418
|
+
const defaultBaseUrl = apiKey?.startsWith("tp-") ? TOKEN_PLAN_SGP_BASE_URL : "https://api.xiaomimimo.com/v1";
|
|
1419
|
+
// Token-plan keys always use the TP baseUrl; config?.baseUrl (from catalog)
|
|
1420
|
+
// would incorrectly pin to the standard endpoint (api.xiaomimimo.com).
|
|
1421
|
+
const baseUrl = apiKey?.startsWith("tp-") ? defaultBaseUrl : (config?.baseUrl ?? defaultBaseUrl);
|
|
1422
|
+
const references = createBundledReferenceMap<"openai-completions">("xiaomi");
|
|
1425
1423
|
return {
|
|
1426
1424
|
providerId: "xiaomi",
|
|
1427
1425
|
...(apiKey && {
|
|
1428
|
-
fetchDynamicModels: () =>
|
|
1429
|
-
fetchOpenAICompatibleModels({
|
|
1430
|
-
api: "
|
|
1426
|
+
fetchDynamicModels: async () => {
|
|
1427
|
+
const sgpResult = await fetchOpenAICompatibleModels({
|
|
1428
|
+
api: "openai-completions",
|
|
1431
1429
|
provider: "xiaomi",
|
|
1432
|
-
baseUrl
|
|
1430
|
+
baseUrl,
|
|
1433
1431
|
apiKey,
|
|
1434
1432
|
filterModel: (_entry, model) => !model.id.includes("-tts"),
|
|
1435
1433
|
mapModel: (entry, defaults) => {
|
|
@@ -1438,10 +1436,29 @@ export function xiaomiModelManagerOptions(
|
|
|
1438
1436
|
return {
|
|
1439
1437
|
...model,
|
|
1440
1438
|
name: toModelName(entry.display_name, model.name),
|
|
1441
|
-
baseUrl,
|
|
1442
1439
|
};
|
|
1443
1440
|
},
|
|
1444
|
-
})
|
|
1441
|
+
});
|
|
1442
|
+
if (sgpResult || !apiKey?.startsWith("tp-")) {
|
|
1443
|
+
return sgpResult;
|
|
1444
|
+
}
|
|
1445
|
+
// Token-plan discovery failed with SGP; retry with AMS
|
|
1446
|
+
return fetchOpenAICompatibleModels({
|
|
1447
|
+
api: "openai-completions",
|
|
1448
|
+
provider: "xiaomi",
|
|
1449
|
+
baseUrl: TOKEN_PLAN_AMS_BASE_URL,
|
|
1450
|
+
apiKey,
|
|
1451
|
+
filterModel: (_entry, model) => !model.id.includes("-tts"),
|
|
1452
|
+
mapModel: (entry, defaults) => {
|
|
1453
|
+
const reference = references.get(defaults.id);
|
|
1454
|
+
const model = mapWithBundledReference(entry, defaults, reference);
|
|
1455
|
+
return {
|
|
1456
|
+
...model,
|
|
1457
|
+
name: toModelName(entry.display_name, model.name),
|
|
1458
|
+
};
|
|
1459
|
+
},
|
|
1460
|
+
});
|
|
1461
|
+
},
|
|
1445
1462
|
}),
|
|
1446
1463
|
};
|
|
1447
1464
|
}
|
|
@@ -1499,7 +1499,7 @@ export function convertMessages(
|
|
|
1499
1499
|
} else {
|
|
1500
1500
|
assistantMsg.content = [{ type: "text", text: thinkingText }];
|
|
1501
1501
|
}
|
|
1502
|
-
} else {
|
|
1502
|
+
} else if (compat.requiresReasoningContentForToolCalls) {
|
|
1503
1503
|
// Use the signature from the first thinking block if available, but only for
|
|
1504
1504
|
// recognized OpenAI-compat reasoning field names. Opaque signatures from other
|
|
1505
1505
|
// providers (Anthropic encrypted, OpenAI Responses JSON) are not valid property names.
|
|
@@ -1511,7 +1511,7 @@ export function convertMessages(
|
|
|
1511
1511
|
}
|
|
1512
1512
|
}
|
|
1513
1513
|
|
|
1514
|
-
if (compat.thinkingFormat === "openai") {
|
|
1514
|
+
if (compat.thinkingFormat === "openai" && compat.requiresReasoningContentForToolCalls) {
|
|
1515
1515
|
const streamedReasoningField = nonEmptyThinkingBlocks[0]?.thinkingSignature;
|
|
1516
1516
|
const reasoningField =
|
|
1517
1517
|
streamedReasoningField === "reasoning_content" ||
|
|
@@ -391,10 +391,24 @@ function buildParams(
|
|
|
391
391
|
const messages: ResponseInput = [...conversationMessages];
|
|
392
392
|
|
|
393
393
|
const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
|
|
394
|
+
let systemInstructions: string | undefined;
|
|
394
395
|
if (systemPrompts.length > 0) {
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
396
|
+
const needsDeveloperRole = model.reasoning && supportsDeveloperRole(resolvedBaseUrl ?? model);
|
|
397
|
+
if (needsDeveloperRole) {
|
|
398
|
+
// Reasoning models on known OpenAI-compatible endpoints require the
|
|
399
|
+
// `developer` role. Send all system prompts inline in `input`.
|
|
400
|
+
messages.unshift(
|
|
401
|
+
...systemPrompts.map(systemPrompt => ({ role: "developer" as const, content: systemPrompt })),
|
|
402
|
+
);
|
|
403
|
+
} else {
|
|
404
|
+
// All other endpoints (including third-party /v1/responses proxies) use
|
|
405
|
+
// the canonical top-level `instructions` field so that proxies that
|
|
406
|
+
// reject `input[{role:"system"}]` work out of the box.
|
|
407
|
+
systemInstructions = systemPrompts[0];
|
|
408
|
+
if (systemPrompts.length > 1) {
|
|
409
|
+
messages.unshift(...systemPrompts.slice(1).map(p => ({ role: "system" as const, content: p })));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
398
412
|
}
|
|
399
413
|
|
|
400
414
|
const cacheRetention = resolveCacheRetention(options?.cacheRetention);
|
|
@@ -402,6 +416,7 @@ function buildParams(
|
|
|
402
416
|
const params: OpenAIResponsesSamplingParams = {
|
|
403
417
|
model: model.id,
|
|
404
418
|
input: messages,
|
|
419
|
+
instructions: systemInstructions,
|
|
405
420
|
stream: true,
|
|
406
421
|
prompt_cache_key: promptCacheKey,
|
|
407
422
|
prompt_cache_retention: promptCacheKey ? getPromptCacheRetention(model.baseUrl, cacheRetention) : undefined,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** DeepSeek login flow (API key paste against https://api.deepseek.com). */
|
|
2
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
3
|
+
|
|
4
|
+
export const loginDeepSeek = createApiKeyLogin({
|
|
5
|
+
providerLabel: "DeepSeek",
|
|
6
|
+
authUrl: "https://platform.deepseek.com/api_keys",
|
|
7
|
+
instructions: "Create or copy your API key from the DeepSeek dashboard",
|
|
8
|
+
promptMessage: "Paste your DeepSeek API key",
|
|
9
|
+
placeholder: "sk-...",
|
|
10
|
+
validation: {
|
|
11
|
+
kind: "chat-completions",
|
|
12
|
+
provider: "deepseek",
|
|
13
|
+
baseUrl: "https://api.deepseek.com/v1",
|
|
14
|
+
model: "deepseek-v4-pro",
|
|
15
|
+
},
|
|
16
|
+
});
|
package/src/utils/oauth/index.ts
CHANGED
package/src/utils/oauth/types.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Xiaomi MiMo login flow.
|
|
3
3
|
*
|
|
4
|
-
* Xiaomi MiMo provides
|
|
5
|
-
* https://api.xiaomimimo.com/
|
|
4
|
+
* Xiaomi MiMo provides OpenAI-compatible models via
|
|
5
|
+
* https://api.xiaomimimo.com/v1.
|
|
6
6
|
*
|
|
7
7
|
* This is not OAuth - it's a simple API key flow:
|
|
8
8
|
* 1. Open browser to Xiaomi MiMo API key console
|
|
@@ -15,8 +15,9 @@ import type { OAuthController } from "./types";
|
|
|
15
15
|
const PROVIDER_ID = "xiaomi";
|
|
16
16
|
const PROVIDER_NAME = "Xiaomi MiMo";
|
|
17
17
|
const STANDARD_AUTH_URL = "https://platform.xiaomimimo.com/#/console/api-keys";
|
|
18
|
-
const STANDARD_API_BASE_URL = "https://api.xiaomimimo.com/
|
|
19
|
-
const
|
|
18
|
+
const STANDARD_API_BASE_URL = "https://api.xiaomimimo.com/v1";
|
|
19
|
+
const TOKEN_PLAN_SGP_API_BASE_URL = "https://token-plan-sgp.xiaomimimo.com/v1";
|
|
20
|
+
const TOKEN_PLAN_AMS_API_BASE_URL = "https://token-plan-ams.xiaomimimo.com/v1";
|
|
20
21
|
const TOKEN_PLAN_KEY_PREFIX = "tp-";
|
|
21
22
|
const STANDARD_VALIDATION_MODEL = "mimo-v2-flash";
|
|
22
23
|
const TOKEN_PLAN_VALIDATION_MODEL = "mimo-v2.5";
|
|
@@ -25,50 +26,81 @@ function isTokenPlanKey(apiKey: string): boolean {
|
|
|
25
26
|
return apiKey.startsWith(TOKEN_PLAN_KEY_PREFIX);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
function resolveEndpoint(apiKey: string): { baseUrl: string; model: string } {
|
|
29
|
-
if (isTokenPlanKey(apiKey)) {
|
|
30
|
-
return { baseUrl: TOKEN_PLAN_API_BASE_URL, model: TOKEN_PLAN_VALIDATION_MODEL };
|
|
31
|
-
}
|
|
32
|
-
return { baseUrl: STANDARD_API_BASE_URL, model: STANDARD_VALIDATION_MODEL };
|
|
33
|
-
}
|
|
34
|
-
const ANTHROPIC_VERSION = "2023-06-01";
|
|
35
29
|
const VALIDATION_TIMEOUT_MS = 15_000;
|
|
36
30
|
|
|
37
31
|
async function validateXiaomiApiKey(apiKey: string, signal?: AbortSignal): Promise<void> {
|
|
38
32
|
const timeoutSignal = AbortSignal.timeout(VALIDATION_TIMEOUT_MS);
|
|
39
33
|
const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
40
|
-
const { baseUrl, model } = resolveEndpoint(apiKey);
|
|
41
34
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
model,
|
|
51
|
-
max_tokens: 1,
|
|
52
|
-
messages: [{ role: "user", content: "ping" }],
|
|
53
|
-
}),
|
|
54
|
-
signal: requestSignal,
|
|
55
|
-
});
|
|
35
|
+
// For token-plan keys try SGP first, then AMS as fallback.
|
|
36
|
+
// Standard sk- keys only hit the one endpoint.
|
|
37
|
+
const endpoints = isTokenPlanKey(apiKey)
|
|
38
|
+
? [
|
|
39
|
+
{ baseUrl: TOKEN_PLAN_SGP_API_BASE_URL, model: TOKEN_PLAN_VALIDATION_MODEL },
|
|
40
|
+
{ baseUrl: TOKEN_PLAN_AMS_API_BASE_URL, model: TOKEN_PLAN_VALIDATION_MODEL },
|
|
41
|
+
]
|
|
42
|
+
: [{ baseUrl: STANDARD_API_BASE_URL, model: STANDARD_VALIDATION_MODEL }];
|
|
56
43
|
|
|
57
|
-
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
44
|
+
let lastError: Error | null = null;
|
|
60
45
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
46
|
+
for (const ep of endpoints) {
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(`${ep.baseUrl}/chat/completions`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: {
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
"x-api-key": apiKey,
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
model: ep.model,
|
|
56
|
+
max_tokens: 1,
|
|
57
|
+
messages: [{ role: "user", content: "ping" }],
|
|
58
|
+
}),
|
|
59
|
+
signal: requestSignal,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (response.ok) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
67
65
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
// 401 means this endpoint didn't accept the key; try the next one
|
|
67
|
+
if (response.status === 401) {
|
|
68
|
+
let details = "";
|
|
69
|
+
try {
|
|
70
|
+
details = (await response.text()).trim();
|
|
71
|
+
} catch {
|
|
72
|
+
// ignore body parse errors, status is enough
|
|
73
|
+
}
|
|
74
|
+
lastError = new Error(
|
|
75
|
+
details
|
|
76
|
+
? `${PROVIDER_NAME} API key validation failed (${response.status}): ${details}`
|
|
77
|
+
: `${PROVIDER_NAME} API key validation failed (${response.status})`,
|
|
78
|
+
);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Non-auth errors are real failures
|
|
83
|
+
let details = "";
|
|
84
|
+
try {
|
|
85
|
+
details = (await response.text()).trim();
|
|
86
|
+
} catch {
|
|
87
|
+
// ignore body parse errors, status is enough
|
|
88
|
+
}
|
|
89
|
+
const message = details
|
|
90
|
+
? `${PROVIDER_NAME} API key validation failed (${response.status}): ${details}`
|
|
91
|
+
: `${PROVIDER_NAME} API key validation failed (${response.status})`;
|
|
92
|
+
throw new Error(message);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
// Only re-throw AbortError when the caller explicitly cancelled.
|
|
95
|
+
// Timeout aborts (from AbortSignal.timeout) should fall through to
|
|
96
|
+
// the next endpoint so SGP→AMS fallback works during regional outages.
|
|
97
|
+
if (e instanceof DOMException && e.name === "AbortError" && signal?.aborted) {
|
|
98
|
+
throw e;
|
|
99
|
+
}
|
|
100
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw lastError ?? new Error(`${PROVIDER_NAME} API key validation failed`);
|
|
72
104
|
}
|
|
73
105
|
|
|
74
106
|
/**
|