@gajae-code/ai 0.5.3 → 0.5.4
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 +6 -0
- package/dist/types/provider-models/openai-compat.d.ts +8 -1
- package/dist/types/stream.d.ts +19 -0
- package/dist/types/types.d.ts +1 -1
- package/dist/types/utils/discovery/openai-compatible.d.ts +4 -2
- package/dist/types/utils/oauth/types.d.ts +2 -1
- package/dist/types/utils/oauth/xiaomi.d.ts +10 -4
- package/package.json +2 -2
- package/src/auth-storage.ts +18 -0
- package/src/model-manager.ts +6 -2
- package/src/model-thinking.ts +1 -1
- package/src/models.json +584 -20
- package/src/provider-models/descriptors.ts +18 -0
- package/src/provider-models/openai-compat.ts +107 -44
- package/src/stream.ts +57 -3
- package/src/types.ts +3 -0
- package/src/utils/discovery/openai-compatible.ts +8 -2
- package/src/utils/oauth/index.ts +15 -0
- package/src/utils/oauth/types.ts +4 -0
- package/src/utils/oauth/xiaomi.ts +80 -18
|
@@ -2,7 +2,7 @@ import { $env, $inheritedEnv } from "@gajae-code/utils";
|
|
|
2
2
|
import type { ModelManagerOptions } from "../model-manager";
|
|
3
3
|
import { Effort } from "../model-thinking";
|
|
4
4
|
import { getBundledModels } from "../models";
|
|
5
|
-
import type { Api, Model, ThinkingConfig } from "../types";
|
|
5
|
+
import type { Api, FetchImpl, Model, Provider, ThinkingConfig } from "../types";
|
|
6
6
|
import { isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
|
|
7
7
|
import {
|
|
8
8
|
fetchOpenAICompatibleModels,
|
|
@@ -1414,64 +1414,99 @@ export function cloudflareAiGatewayModelManagerOptions(
|
|
|
1414
1414
|
// ---------------------------------------------------------------------------
|
|
1415
1415
|
// 20. Xiaomi
|
|
1416
1416
|
// ---------------------------------------------------------------------------
|
|
1417
|
+
/** Region codes for Xiaomi Token Plan clusters exposed as separate login providers. */
|
|
1418
|
+
export type XiaomiTokenPlanRegion = "sgp" | "ams" | "cn";
|
|
1417
1419
|
|
|
1420
|
+
/** Configures Xiaomi standard or regional Token Plan OpenAI-compatible model discovery. */
|
|
1418
1421
|
export interface XiaomiModelManagerConfig {
|
|
1419
1422
|
apiKey?: string;
|
|
1420
1423
|
baseUrl?: string;
|
|
1424
|
+
fetch?: FetchImpl;
|
|
1425
|
+
providerId?: Provider;
|
|
1426
|
+
tokenPlanRegion?: XiaomiTokenPlanRegion;
|
|
1427
|
+
}
|
|
1428
|
+
const XIAOMI_TOKEN_PLAN_BASE_URLS: Record<XiaomiTokenPlanRegion, string> = {
|
|
1429
|
+
sgp: "https://token-plan-sgp.xiaomimimo.com/v1",
|
|
1430
|
+
ams: "https://token-plan-ams.xiaomimimo.com/v1",
|
|
1431
|
+
cn: "https://token-plan-cn.xiaomimimo.com/v1",
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
const XIAOMI_TOKEN_PLAN_FALLBACK_BASE_URLS = [
|
|
1435
|
+
XIAOMI_TOKEN_PLAN_BASE_URLS.sgp,
|
|
1436
|
+
XIAOMI_TOKEN_PLAN_BASE_URLS.ams,
|
|
1437
|
+
XIAOMI_TOKEN_PLAN_BASE_URLS.cn,
|
|
1438
|
+
];
|
|
1439
|
+
|
|
1440
|
+
function inferXiaomiTokenPlanRegion(providerId: Provider): XiaomiTokenPlanRegion | undefined {
|
|
1441
|
+
if (providerId === "xiaomi-token-plan-sgp") return "sgp";
|
|
1442
|
+
if (providerId === "xiaomi-token-plan-ams") return "ams";
|
|
1443
|
+
if (providerId === "xiaomi-token-plan-cn") return "cn";
|
|
1444
|
+
return undefined;
|
|
1421
1445
|
}
|
|
1422
1446
|
|
|
1447
|
+
/** Builds a Xiaomi model manager, preserving Token Plan region provider ids during discovery. */
|
|
1423
1448
|
export function xiaomiModelManagerOptions(
|
|
1424
1449
|
config?: XiaomiModelManagerConfig,
|
|
1425
1450
|
): ModelManagerOptions<"openai-completions"> {
|
|
1426
1451
|
const apiKey = config?.apiKey;
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1452
|
+
const providerId = config?.providerId ?? "xiaomi";
|
|
1453
|
+
const tokenPlanRegion = config?.tokenPlanRegion ?? inferXiaomiTokenPlanRegion(providerId);
|
|
1454
|
+
const tokenPlanBaseUrls = tokenPlanRegion
|
|
1455
|
+
? [XIAOMI_TOKEN_PLAN_BASE_URLS[tokenPlanRegion]]
|
|
1456
|
+
: XIAOMI_TOKEN_PLAN_FALLBACK_BASE_URLS;
|
|
1457
|
+
const XIAOMI_STANDARD_BASE_URL = "https://api.xiaomimimo.com/v1";
|
|
1458
|
+
const isTokenPlanProvider = tokenPlanRegion !== undefined || providerId.startsWith("xiaomi-token-plan-");
|
|
1459
|
+
const isTokenPlanKey = isTokenPlanProvider || apiKey?.startsWith("tp-");
|
|
1460
|
+
// Token-plan keys always use a TP cluster; config?.baseUrl (from catalog)
|
|
1434
1461
|
// would incorrectly pin to the standard endpoint (api.xiaomimimo.com).
|
|
1435
|
-
const baseUrl =
|
|
1462
|
+
const baseUrl = isTokenPlanKey ? tokenPlanBaseUrls[0] : (config?.baseUrl ?? XIAOMI_STANDARD_BASE_URL);
|
|
1436
1463
|
const references = createBundledReferenceMap<"openai-completions">("xiaomi");
|
|
1464
|
+
const throwOnAuthStatus = (response: Response): Error | undefined => {
|
|
1465
|
+
if (response.status === 401 || response.status === 403) {
|
|
1466
|
+
return new Error(`Authentication failed (${response.status}) for ${response.url}`);
|
|
1467
|
+
}
|
|
1468
|
+
return undefined;
|
|
1469
|
+
};
|
|
1470
|
+
const fetchModels = (url: string) =>
|
|
1471
|
+
fetchOpenAICompatibleModels({
|
|
1472
|
+
api: "openai-completions",
|
|
1473
|
+
provider: providerId,
|
|
1474
|
+
baseUrl: url,
|
|
1475
|
+
apiKey,
|
|
1476
|
+
filterModel: (_entry, model) => !model.id.includes("-tts"),
|
|
1477
|
+
mapModel: (entry, defaults) => {
|
|
1478
|
+
const reference = references.get(defaults.id);
|
|
1479
|
+
const model = mapWithBundledReference(entry, defaults, reference);
|
|
1480
|
+
return {
|
|
1481
|
+
...model,
|
|
1482
|
+
api: "openai-completions",
|
|
1483
|
+
provider: providerId,
|
|
1484
|
+
baseUrl: defaults.baseUrl,
|
|
1485
|
+
name: toModelName(entry.display_name, model.name),
|
|
1486
|
+
};
|
|
1487
|
+
},
|
|
1488
|
+
fetch: config?.fetch,
|
|
1489
|
+
throwOnStatus: throwOnAuthStatus,
|
|
1490
|
+
});
|
|
1437
1491
|
return {
|
|
1438
|
-
providerId
|
|
1492
|
+
providerId,
|
|
1439
1493
|
...(apiKey && {
|
|
1440
1494
|
fetchDynamicModels: async () => {
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
provider: "xiaomi",
|
|
1444
|
-
baseUrl,
|
|
1445
|
-
apiKey,
|
|
1446
|
-
filterModel: (_entry, model) => !model.id.includes("-tts"),
|
|
1447
|
-
mapModel: (entry, defaults) => {
|
|
1448
|
-
const reference = references.get(defaults.id);
|
|
1449
|
-
const model = mapWithBundledReference(entry, defaults, reference);
|
|
1450
|
-
return {
|
|
1451
|
-
...model,
|
|
1452
|
-
name: toModelName(entry.display_name, model.name),
|
|
1453
|
-
};
|
|
1454
|
-
},
|
|
1455
|
-
});
|
|
1456
|
-
if (sgpResult || !apiKey?.startsWith("tp-")) {
|
|
1457
|
-
return sgpResult;
|
|
1495
|
+
if (!isTokenPlanKey) {
|
|
1496
|
+
return fetchModels(baseUrl);
|
|
1458
1497
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
name: toModelName(entry.display_name, model.name),
|
|
1472
|
-
};
|
|
1473
|
-
},
|
|
1474
|
-
});
|
|
1498
|
+
for (const url of tokenPlanBaseUrls) {
|
|
1499
|
+
try {
|
|
1500
|
+
const result = await fetchModels(url);
|
|
1501
|
+
if (result) return result;
|
|
1502
|
+
} catch (error) {
|
|
1503
|
+
// Auth errors (401/403) should fail fast, not retry other regions
|
|
1504
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1505
|
+
if (message.includes("Authentication failed")) throw error;
|
|
1506
|
+
// Network/timeout errors: try next URL
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return null;
|
|
1475
1510
|
},
|
|
1476
1511
|
}),
|
|
1477
1512
|
};
|
|
@@ -2166,9 +2201,37 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS: readonly ModelsDevProviderDe
|
|
|
2166
2201
|
// --- zAI ---
|
|
2167
2202
|
anthropicMessagesDescriptor("zai-coding-plan", "zai", "https://api.z.ai/api/anthropic"),
|
|
2168
2203
|
// --- Xiaomi ---
|
|
2169
|
-
|
|
2204
|
+
openAiCompletionsDescriptor("xiaomi", "xiaomi", "https://api.xiaomimimo.com/v1", {
|
|
2205
|
+
defaultContextWindow: 262144,
|
|
2206
|
+
defaultMaxTokens: 8192,
|
|
2207
|
+
compat: {
|
|
2208
|
+
supportsStore: false,
|
|
2209
|
+
thinkingFormat: "zai",
|
|
2210
|
+
},
|
|
2211
|
+
}),
|
|
2212
|
+
openAiCompletionsDescriptor("xiaomi", "xiaomi-token-plan-sgp", "https://token-plan-sgp.xiaomimimo.com/v1", {
|
|
2170
2213
|
defaultContextWindow: 262144,
|
|
2171
2214
|
defaultMaxTokens: 8192,
|
|
2215
|
+
compat: {
|
|
2216
|
+
supportsStore: false,
|
|
2217
|
+
thinkingFormat: "zai",
|
|
2218
|
+
},
|
|
2219
|
+
}),
|
|
2220
|
+
openAiCompletionsDescriptor("xiaomi", "xiaomi-token-plan-ams", "https://token-plan-ams.xiaomimimo.com/v1", {
|
|
2221
|
+
defaultContextWindow: 262144,
|
|
2222
|
+
defaultMaxTokens: 8192,
|
|
2223
|
+
compat: {
|
|
2224
|
+
supportsStore: false,
|
|
2225
|
+
thinkingFormat: "zai",
|
|
2226
|
+
},
|
|
2227
|
+
}),
|
|
2228
|
+
openAiCompletionsDescriptor("xiaomi", "xiaomi-token-plan-cn", "https://token-plan-cn.xiaomimimo.com/v1", {
|
|
2229
|
+
defaultContextWindow: 262144,
|
|
2230
|
+
defaultMaxTokens: 8192,
|
|
2231
|
+
compat: {
|
|
2232
|
+
supportsStore: false,
|
|
2233
|
+
thinkingFormat: "zai",
|
|
2234
|
+
},
|
|
2172
2235
|
}),
|
|
2173
2236
|
// --- MiniMax Coding Plan ---
|
|
2174
2237
|
openAiCompletionsDescriptor("minimax-coding-plan", "minimax-code", "https://api.minimax.io/v1", {
|
package/src/stream.ts
CHANGED
|
@@ -194,6 +194,60 @@ export function listProvidersWithEnvKey(): string[] {
|
|
|
194
194
|
return Object.keys(serviceProviderMap);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Subscription-style providers whose "subscription" is delivered as an API key
|
|
199
|
+
* (created at https://opencode.ai/auth), not a separate OAuth/session token.
|
|
200
|
+
* Used to give OpenCode users an accurate headless auth diagnostic (#755).
|
|
201
|
+
*/
|
|
202
|
+
const OPENCODE_SUBSCRIPTION_PROVIDERS = new Set(["opencode-go", "opencode-zen"]);
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Provider-specific credential guidance appended to "no credential" errors.
|
|
206
|
+
*
|
|
207
|
+
* Headless GJC has no interactive `/login` TUI, so a bare "No API key" /
|
|
208
|
+
* "No credentials" error left users — OpenCode Go subscribers especially
|
|
209
|
+
* (#755) — unsure what signal GJC actually reads. OpenCode subscriptions are
|
|
210
|
+
* themselves API keys, so this names the env var GJC reads for the provider,
|
|
211
|
+
* warns that a project `.env` is intentionally ignored for provider
|
|
212
|
+
* credentials, and points OpenCode users at one-time interactive CLI credential capture.
|
|
213
|
+
*
|
|
214
|
+
* Returns an empty string when the provider has no env-var key and no special
|
|
215
|
+
* handling, so callers can append it unconditionally.
|
|
216
|
+
*/
|
|
217
|
+
export function formatProviderCredentialHint(provider: string): string {
|
|
218
|
+
const resolver = serviceProviderMap[provider];
|
|
219
|
+
const envVar = typeof resolver === "string" ? resolver : undefined;
|
|
220
|
+
const isOpenCodeSubscription = OPENCODE_SUBSCRIPTION_PROVIDERS.has(provider);
|
|
221
|
+
const parts: string[] = [];
|
|
222
|
+
if (isOpenCodeSubscription) {
|
|
223
|
+
parts.push(
|
|
224
|
+
"OpenCode subscriptions authenticate with an API key (created at https://opencode.ai/auth), not a separate session/OAuth token.",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (envVar) {
|
|
228
|
+
parts.push(
|
|
229
|
+
`Headless GJC reads this provider's key from ${envVar} (exported in your shell or set in ~/.gjc/.env).`,
|
|
230
|
+
);
|
|
231
|
+
parts.push("A value set only in a project .env is intentionally ignored for provider credentials.");
|
|
232
|
+
}
|
|
233
|
+
if (isOpenCodeSubscription) {
|
|
234
|
+
parts.push(
|
|
235
|
+
`Or run \`gjc auth-broker login ${provider}\` once before headless/print mode to store the key interactively.`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return parts.join(" ");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Build an actionable "missing API key" error for a provider, used by the
|
|
243
|
+
* low-level `stream`/`complete` entry points (#755).
|
|
244
|
+
*/
|
|
245
|
+
export function formatMissingApiKeyError(provider: string): string {
|
|
246
|
+
const base = `No API key for provider: ${provider}.`;
|
|
247
|
+
const hint = formatProviderCredentialHint(provider);
|
|
248
|
+
return hint ? `${base} ${hint}` : base;
|
|
249
|
+
}
|
|
250
|
+
|
|
197
251
|
export function stream<TApi extends Api>(
|
|
198
252
|
model: Model<TApi>,
|
|
199
253
|
context: Context,
|
|
@@ -208,7 +262,7 @@ export function stream<TApi extends Api>(
|
|
|
208
262
|
if (isGitLabDuoModel(model)) {
|
|
209
263
|
const apiKey = (options as StreamOptions | undefined)?.apiKey || getEnvApiKey(model.provider);
|
|
210
264
|
if (!apiKey) {
|
|
211
|
-
throw new Error(
|
|
265
|
+
throw new Error(formatMissingApiKeyError(model.provider));
|
|
212
266
|
}
|
|
213
267
|
return streamGitLabDuo(model, context, {
|
|
214
268
|
...(options as SimpleStreamOptions | undefined),
|
|
@@ -226,7 +280,7 @@ export function stream<TApi extends Api>(
|
|
|
226
280
|
|
|
227
281
|
const apiKey = options?.apiKey || getEnvApiKey(model.provider);
|
|
228
282
|
if (!apiKey) {
|
|
229
|
-
throw new Error(
|
|
283
|
+
throw new Error(formatMissingApiKeyError(model.provider));
|
|
230
284
|
}
|
|
231
285
|
const providerOptions = { ...options, apiKey };
|
|
232
286
|
|
|
@@ -410,7 +464,7 @@ export function streamSimple<TApi extends Api>(
|
|
|
410
464
|
|
|
411
465
|
const apiKey = options?.apiKey || getEnvApiKey(model.provider);
|
|
412
466
|
if (!apiKey) {
|
|
413
|
-
throw new Error(
|
|
467
|
+
throw new Error(formatMissingApiKeyError(model.provider));
|
|
414
468
|
}
|
|
415
469
|
|
|
416
470
|
// GitLab Duo - wraps Anthropic/OpenAI behind GitLab AI Gateway direct access tokens
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { UNK_CONTEXT_WINDOW, UNK_MAX_TOKENS } from "@gajae-code/ai";
|
|
2
2
|
import * as z from "zod/v4";
|
|
3
|
-
import type { Api, Model, Provider } from "../../types";
|
|
3
|
+
import type { Api, FetchImpl, Model, Provider } from "../../types";
|
|
4
4
|
|
|
5
5
|
const MODELS_PATH = "/models";
|
|
6
6
|
|
|
@@ -81,7 +81,9 @@ export interface FetchOpenAICompatibleModelsOptions<TApi extends Api> {
|
|
|
81
81
|
/** Optional AbortSignal for request cancellation. */
|
|
82
82
|
signal?: AbortSignal;
|
|
83
83
|
/** Optional fetch implementation override for testing/custom runtimes. */
|
|
84
|
-
fetch?:
|
|
84
|
+
fetch?: FetchImpl;
|
|
85
|
+
/** Optional HTTP status predicate for provider-specific hard failures. */
|
|
86
|
+
throwOnStatus?: (response: Response) => Error | undefined;
|
|
85
87
|
/**
|
|
86
88
|
* Optional post-normalization filter.
|
|
87
89
|
* Return false to skip a model.
|
|
@@ -133,6 +135,10 @@ export async function fetchOpenAICompatibleModels<TApi extends Api>(
|
|
|
133
135
|
}
|
|
134
136
|
|
|
135
137
|
if (!response.ok) {
|
|
138
|
+
const hardFailure = options.throwOnStatus?.(response);
|
|
139
|
+
if (hardFailure) {
|
|
140
|
+
throw hardFailure;
|
|
141
|
+
}
|
|
136
142
|
return null;
|
|
137
143
|
}
|
|
138
144
|
|
package/src/utils/oauth/index.ts
CHANGED
|
@@ -140,6 +140,21 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
|
|
|
140
140
|
name: "Xiaomi MiMo",
|
|
141
141
|
available: true,
|
|
142
142
|
},
|
|
143
|
+
{
|
|
144
|
+
id: "xiaomi-token-plan-sgp",
|
|
145
|
+
name: "Xiaomi Token Plan (Singapore)",
|
|
146
|
+
available: true,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "xiaomi-token-plan-ams",
|
|
150
|
+
name: "Xiaomi Token Plan (Europe)",
|
|
151
|
+
available: true,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "xiaomi-token-plan-cn",
|
|
155
|
+
name: "Xiaomi Token Plan (China)",
|
|
156
|
+
available: true,
|
|
157
|
+
},
|
|
143
158
|
{
|
|
144
159
|
id: "opencode-zen",
|
|
145
160
|
name: "OpenCode Zen",
|
package/src/utils/oauth/types.ts
CHANGED
|
@@ -50,6 +50,9 @@ export type OAuthProvider =
|
|
|
50
50
|
| "vllm"
|
|
51
51
|
| "xai"
|
|
52
52
|
| "xiaomi"
|
|
53
|
+
| "xiaomi-token-plan-sgp"
|
|
54
|
+
| "xiaomi-token-plan-ams"
|
|
55
|
+
| "xiaomi-token-plan-cn"
|
|
53
56
|
| "zenmux"
|
|
54
57
|
| "zai";
|
|
55
58
|
|
|
@@ -78,6 +81,7 @@ export interface OAuthController {
|
|
|
78
81
|
onManualCodeInput?(): Promise<string>;
|
|
79
82
|
onPrompt?(prompt: OAuthPrompt): Promise<string>;
|
|
80
83
|
signal?: AbortSignal;
|
|
84
|
+
fetch?: typeof globalThis.fetch;
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
export interface OAuthLoginCallbacks extends OAuthController {
|
|
@@ -4,23 +4,44 @@
|
|
|
4
4
|
* Xiaomi MiMo provides OpenAI-compatible models via
|
|
5
5
|
* https://api.xiaomimimo.com/v1.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 2. User copies their API key
|
|
10
|
-
* 3. User pastes the API key into the CLI
|
|
7
|
+
* Standard Xiaomi login opens the pay-as-you-go API key console. Token Plan
|
|
8
|
+
* login opens plan management so users copy the regional `tp-...` key.
|
|
11
9
|
*/
|
|
12
10
|
|
|
11
|
+
import type { FetchImpl } from "../../types";
|
|
13
12
|
import type { OAuthController } from "./types";
|
|
14
13
|
|
|
15
14
|
const PROVIDER_ID = "xiaomi";
|
|
16
15
|
const PROVIDER_NAME = "Xiaomi MiMo";
|
|
17
16
|
const STANDARD_AUTH_URL = "https://platform.xiaomimimo.com/#/console/api-keys";
|
|
17
|
+
const TOKEN_PLAN_AUTH_URL = "https://platform.xiaomimimo.com/console/plan-manage";
|
|
18
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";
|
|
21
19
|
const TOKEN_PLAN_KEY_PREFIX = "tp-";
|
|
22
20
|
const STANDARD_VALIDATION_MODEL = "mimo-v2-flash";
|
|
23
21
|
const TOKEN_PLAN_VALIDATION_MODEL = "mimo-v2.5";
|
|
22
|
+
const TOKEN_PLAN_SGP_API_BASE_URL = "https://token-plan-sgp.xiaomimimo.com/v1";
|
|
23
|
+
const TOKEN_PLAN_AMS_API_BASE_URL = "https://token-plan-ams.xiaomimimo.com/v1";
|
|
24
|
+
const TOKEN_PLAN_CN_API_BASE_URL = "https://token-plan-cn.xiaomimimo.com/v1";
|
|
25
|
+
|
|
26
|
+
/** Region codes accepted by the Xiaomi Token Plan login flow. */
|
|
27
|
+
export type XiaomiTokenPlanRegion = "sgp" | "ams" | "cn";
|
|
28
|
+
|
|
29
|
+
type XiaomiValidationEndpoint = {
|
|
30
|
+
baseUrl: string;
|
|
31
|
+
model: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const TOKEN_PLAN_VALIDATION_ENDPOINTS: Record<XiaomiTokenPlanRegion, XiaomiValidationEndpoint> = {
|
|
35
|
+
sgp: { baseUrl: TOKEN_PLAN_SGP_API_BASE_URL, model: TOKEN_PLAN_VALIDATION_MODEL },
|
|
36
|
+
ams: { baseUrl: TOKEN_PLAN_AMS_API_BASE_URL, model: TOKEN_PLAN_VALIDATION_MODEL },
|
|
37
|
+
cn: { baseUrl: TOKEN_PLAN_CN_API_BASE_URL, model: TOKEN_PLAN_VALIDATION_MODEL },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const TOKEN_PLAN_REGION_NAMES: Record<XiaomiTokenPlanRegion, string> = {
|
|
41
|
+
sgp: "Singapore",
|
|
42
|
+
ams: "Europe",
|
|
43
|
+
cn: "China",
|
|
44
|
+
};
|
|
24
45
|
|
|
25
46
|
function isTokenPlanKey(apiKey: string): boolean {
|
|
26
47
|
return apiKey.startsWith(TOKEN_PLAN_KEY_PREFIX);
|
|
@@ -28,15 +49,24 @@ function isTokenPlanKey(apiKey: string): boolean {
|
|
|
28
49
|
|
|
29
50
|
const VALIDATION_TIMEOUT_MS = 15_000;
|
|
30
51
|
|
|
31
|
-
async function validateXiaomiApiKey(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
async function validateXiaomiApiKey(
|
|
53
|
+
apiKey: string,
|
|
54
|
+
tokenPlanRegion: XiaomiTokenPlanRegion | undefined,
|
|
55
|
+
signal?: AbortSignal,
|
|
56
|
+
fetchOverride?: FetchImpl,
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
const fetchImpl = fetchOverride ?? fetch;
|
|
59
|
+
// Region-specific Token Plan logins must validate against the selected
|
|
60
|
+
// cluster. Generic Xiaomi login keeps the historical SGP → AMS → CN fallback.
|
|
61
|
+
const endpoints = tokenPlanRegion
|
|
62
|
+
? [TOKEN_PLAN_VALIDATION_ENDPOINTS[tokenPlanRegion]]
|
|
63
|
+
: isTokenPlanKey(apiKey)
|
|
64
|
+
? [
|
|
65
|
+
TOKEN_PLAN_VALIDATION_ENDPOINTS.sgp,
|
|
66
|
+
TOKEN_PLAN_VALIDATION_ENDPOINTS.ams,
|
|
67
|
+
TOKEN_PLAN_VALIDATION_ENDPOINTS.cn,
|
|
68
|
+
]
|
|
69
|
+
: [{ baseUrl: STANDARD_API_BASE_URL, model: STANDARD_VALIDATION_MODEL }];
|
|
40
70
|
|
|
41
71
|
let lastError: Error | null = null;
|
|
42
72
|
|
|
@@ -47,11 +77,11 @@ async function validateXiaomiApiKey(apiKey: string, signal?: AbortSignal): Promi
|
|
|
47
77
|
const timeoutSignal = AbortSignal.timeout(VALIDATION_TIMEOUT_MS);
|
|
48
78
|
const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
49
79
|
try {
|
|
50
|
-
const response = await
|
|
80
|
+
const response = await fetchImpl(`${ep.baseUrl}/chat/completions`, {
|
|
51
81
|
method: "POST",
|
|
52
82
|
headers: {
|
|
53
83
|
"Content-Type": "application/json",
|
|
54
|
-
|
|
84
|
+
Authorization: `Bearer ${apiKey}`,
|
|
55
85
|
},
|
|
56
86
|
body: JSON.stringify({
|
|
57
87
|
model: ep.model,
|
|
@@ -112,6 +142,7 @@ async function validateXiaomiApiKey(apiKey: string, signal?: AbortSignal): Promi
|
|
|
112
142
|
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
113
143
|
*/
|
|
114
144
|
export async function loginXiaomi(options: OAuthController): Promise<string> {
|
|
145
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
115
146
|
if (!options.onPrompt) {
|
|
116
147
|
throw new Error(`${PROVIDER_NAME} login requires onPrompt callback`);
|
|
117
148
|
}
|
|
@@ -132,6 +163,37 @@ export async function loginXiaomi(options: OAuthController): Promise<string> {
|
|
|
132
163
|
}
|
|
133
164
|
|
|
134
165
|
options.onProgress?.(`Validating ${PROVIDER_ID} API key...`);
|
|
135
|
-
await validateXiaomiApiKey(trimmed, options.signal);
|
|
166
|
+
await validateXiaomiApiKey(trimmed, undefined, options.signal, fetchImpl);
|
|
167
|
+
return trimmed;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Login to a regional Xiaomi Token Plan endpoint.
|
|
172
|
+
*
|
|
173
|
+
* Prompts for a token-plan API key and validates it against the selected region.
|
|
174
|
+
*/
|
|
175
|
+
export async function loginXiaomiTokenPlan(options: OAuthController, region: XiaomiTokenPlanRegion): Promise<string> {
|
|
176
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
177
|
+
if (!options.onPrompt) {
|
|
178
|
+
throw new Error(`Xiaomi Token Plan (${TOKEN_PLAN_REGION_NAMES[region]}) login requires onPrompt callback`);
|
|
179
|
+
}
|
|
180
|
+
options.onAuth?.({
|
|
181
|
+
url: TOKEN_PLAN_AUTH_URL,
|
|
182
|
+
instructions: `Copy your token-plan API key for the ${TOKEN_PLAN_REGION_NAMES[region]} region`,
|
|
183
|
+
});
|
|
184
|
+
const apiKey = await options.onPrompt({
|
|
185
|
+
message: `Paste your Xiaomi Token Plan ${TOKEN_PLAN_REGION_NAMES[region]} API key (tp-...)`,
|
|
186
|
+
placeholder: "tp-...",
|
|
187
|
+
});
|
|
188
|
+
if (options.signal?.aborted) {
|
|
189
|
+
throw new Error("Login cancelled");
|
|
190
|
+
}
|
|
191
|
+
const trimmed = apiKey.trim();
|
|
192
|
+
if (!trimmed) {
|
|
193
|
+
throw new Error("API key is required");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
options.onProgress?.(`Validating Xiaomi Token Plan (${TOKEN_PLAN_REGION_NAMES[region]}) API key...`);
|
|
197
|
+
await validateXiaomiApiKey(trimmed, region, options.signal, fetchImpl);
|
|
136
198
|
return trimmed;
|
|
137
199
|
}
|