pi-provider-kimi-code 0.6.12 → 0.6.14
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 +21 -0
- package/README.md +25 -7
- package/index.ts +218 -170
- package/package.json +16 -13
- package/src/config.ts +33 -1
- package/src/constants.ts +86 -9
- package/src/errors.ts +31 -0
- package/src/models.ts +95 -13
- package/src/oauth.ts +60 -1
- package/src/payload.ts +266 -1
- package/src/schema-dedup.ts +8 -0
- package/src/settings-ui.ts +28 -0
- package/src/stream.ts +17 -3
- package/src/tools/common.ts +2 -3
- package/src/tools/datasource-catalog.ts +420 -0
- package/src/tools/datasource.ts +54 -68
- package/src/tools/moonshot.ts +7 -7
- package/src/usage.ts +116 -7
package/src/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
|
|
4
|
-
import { PROVIDER_ID, type KimiWireProtocol } from "./constants.ts";
|
|
4
|
+
import { PROVIDER_ID, type KimiRegion, type KimiWireProtocol } from "./constants.ts";
|
|
5
5
|
|
|
6
6
|
export const KIMI_TOOL_NAMES = ["moonshot_search", "moonshot_fetch", "kimi_datasource"] as const;
|
|
7
7
|
|
|
@@ -47,6 +47,13 @@ export interface KimiCodeConfig {
|
|
|
47
47
|
tools: Record<KimiToolName, { enabled: boolean; default_collapsed: boolean }>;
|
|
48
48
|
uploads: { thresholdBytes: number };
|
|
49
49
|
protocol: KimiWireProtocol;
|
|
50
|
+
/**
|
|
51
|
+
* Managed region for the default endpoints. `null` means "not specified
|
|
52
|
+
* here" — the module-load `KIMI_CODE_REGION` env capture (or the
|
|
53
|
+
* mainland-cn default) then decides. An explicit value from project/home
|
|
54
|
+
* config wins over the env capture once pushed via setKimiRegionOverride.
|
|
55
|
+
*/
|
|
56
|
+
region: KimiRegion | null;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
export type KimiCodeConfigPatch = Partial<{
|
|
@@ -62,6 +69,7 @@ export type KimiCodeConfigPatch = Partial<{
|
|
|
62
69
|
tools: Partial<Record<KimiToolName, Partial<{ enabled: unknown; default_collapsed: unknown }>>>;
|
|
63
70
|
uploads: Partial<{ thresholdBytes: unknown }>;
|
|
64
71
|
protocol: unknown;
|
|
72
|
+
region: unknown;
|
|
65
73
|
}>;
|
|
66
74
|
|
|
67
75
|
export interface KimiCodeConfigSources {
|
|
@@ -81,6 +89,7 @@ export interface KimiCodeConfigSources {
|
|
|
81
89
|
tools: Record<KimiToolName, { enabled: KimiConfigSource; default_collapsed: KimiConfigSource }>;
|
|
82
90
|
uploads: { thresholdBytes: KimiConfigSource };
|
|
83
91
|
protocol: KimiConfigSource;
|
|
92
|
+
region: KimiConfigSource;
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
export interface LoadKimiCodeConfigOptions {
|
|
@@ -132,6 +141,7 @@ export const DEFAULT_KIMI_CODE_CONFIG: KimiCodeConfig = {
|
|
|
132
141
|
tools: makeDefaultTools(),
|
|
133
142
|
uploads: { thresholdBytes: 1048576 },
|
|
134
143
|
protocol: "openai",
|
|
144
|
+
region: null,
|
|
135
145
|
};
|
|
136
146
|
|
|
137
147
|
let runtimeOverride: KimiCodeConfigPatch = {};
|
|
@@ -257,6 +267,12 @@ function envConfigPatch(env: NodeJS.ProcessEnv): KimiCodeConfigPatch {
|
|
|
257
267
|
const protocol = env.KIMI_CODE_PROTOCOL?.trim();
|
|
258
268
|
if (protocol) patch.protocol = protocol;
|
|
259
269
|
|
|
270
|
+
// KIMI_CODE_REGION intentionally does NOT enter the merged config: the env
|
|
271
|
+
// acts as a fallback below the config.json layers (documented precedence:
|
|
272
|
+
// explicit config > env > built-in default), and an invalid value must not
|
|
273
|
+
// crash startup via strict validation — currentKimiRegion() falls back to
|
|
274
|
+
// the module-load capture instead.
|
|
275
|
+
|
|
260
276
|
const capabilities = env.KIMI_MODEL_CAPABILITIES;
|
|
261
277
|
if (capabilities) {
|
|
262
278
|
const caps = new Set(
|
|
@@ -323,6 +339,20 @@ function requireProtocol(
|
|
|
323
339
|
);
|
|
324
340
|
}
|
|
325
341
|
|
|
342
|
+
function requireRegionOrNull(
|
|
343
|
+
raw: unknown,
|
|
344
|
+
configPath: string,
|
|
345
|
+
pointer: string,
|
|
346
|
+
): KimiCodeConfig["region"] {
|
|
347
|
+
if (raw === null) return null;
|
|
348
|
+
if (raw === "mainland-cn" || raw === "global") return raw;
|
|
349
|
+
return fail(
|
|
350
|
+
configPath,
|
|
351
|
+
pointer,
|
|
352
|
+
`expected "mainland-cn" | "global" | null, got ${JSON.stringify(raw)}`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
326
356
|
function requireInputArray(raw: unknown, configPath: string, pointer: string): KimiInputModality[] {
|
|
327
357
|
const valid = new Set(["text", "image", "video"]);
|
|
328
358
|
if (!Array.isArray(raw)) return fail(configPath, pointer, `expected an array, got ${typeof raw}`);
|
|
@@ -438,6 +468,7 @@ export function validateKimiCodeConfig(
|
|
|
438
468
|
),
|
|
439
469
|
},
|
|
440
470
|
protocol: requireProtocol(config.protocol, configPath, "/protocol"),
|
|
471
|
+
region: requireRegionOrNull(config.region, configPath, "/region"),
|
|
441
472
|
};
|
|
442
473
|
}
|
|
443
474
|
|
|
@@ -487,6 +518,7 @@ function buildSources(
|
|
|
487
518
|
tools,
|
|
488
519
|
uploads: { thresholdBytes: sourceForPath(layers, ["uploads", "thresholdBytes"]) },
|
|
489
520
|
protocol: sourceForPath(layers, ["protocol"]),
|
|
521
|
+
region: sourceForPath(layers, ["region"]),
|
|
490
522
|
};
|
|
491
523
|
}
|
|
492
524
|
|
package/src/constants.ts
CHANGED
|
@@ -7,7 +7,82 @@ import os from "node:os";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
|
|
9
9
|
export const CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
// Region profiles mirror upstream kimi-code's packages/oauth region.ts: a
|
|
12
|
+
// region is a bundle of the managed API base origin and the OAuth host. The
|
|
13
|
+
// region only picks the DEFAULT endpoints — explicit env overrides
|
|
14
|
+
// (KIMI_CODE_BASE_URL / KIMI_CODE_OAUTH_HOST) always win. Global users with
|
|
15
|
+
// kimi.ai credentials otherwise hit 401s against the hardcoded .com defaults
|
|
16
|
+
// (issue #77).
|
|
17
|
+
export type KimiRegion = "mainland-cn" | "global";
|
|
18
|
+
|
|
19
|
+
export const KIMI_REGION_PROFILES: Record<KimiRegion, { apiBase: string; oauthHost: string }> = {
|
|
20
|
+
"mainland-cn": { apiBase: "https://api.kimi.com", oauthHost: "https://auth.kimi.com" },
|
|
21
|
+
global: { apiBase: "https://api.kimi.ai", oauthHost: "https://auth.kimi.ai" },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function parseKimiRegion(value: string | undefined): KimiRegion {
|
|
25
|
+
if (value === "global" || value === "mainland-cn") return value;
|
|
26
|
+
return "mainland-cn";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const ENV_KIMI_CODE_REGION: KimiRegion = parseKimiRegion(process.env.KIMI_CODE_REGION);
|
|
30
|
+
|
|
31
|
+
// Effective region pushed from the plugin config (project/home JSON via
|
|
32
|
+
// /kimi-settings), taking precedence over the module-load env capture so a
|
|
33
|
+
// settings change applies without a restart. `null` means "config did not
|
|
34
|
+
// specify" and falls back to ENV_KIMI_CODE_REGION.
|
|
35
|
+
let resolvedRegionOverride: KimiRegion | null = null;
|
|
36
|
+
|
|
37
|
+
export function setKimiRegionOverride(region: KimiRegion | null): void {
|
|
38
|
+
resolvedRegionOverride = region;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function currentKimiRegion(): KimiRegion {
|
|
42
|
+
return resolvedRegionOverride ?? ENV_KIMI_CODE_REGION;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Tracks a KIMI_CODE_OAUTH_HOST value injected by the bridge so a re-apply
|
|
46
|
+
// (e.g. region switched back to mainland in /kimi-settings) can retract it
|
|
47
|
+
// without clobbering a value the user configured in the meantime.
|
|
48
|
+
let regionBridgeOwnedValue: string | undefined;
|
|
49
|
+
|
|
50
|
+
// Bridge the selected region into pi core's built-in Kimi OAuth: pi-ai reads
|
|
51
|
+
// KIMI_CODE_OAUTH_HOST at call time, so filling it makes /login follow the
|
|
52
|
+
// configured region even when the login runs through pi's own kimi-coding
|
|
53
|
+
// auth path instead of this extension's. Only fills the variable when the
|
|
54
|
+
// region is non-default and the user has not configured an explicit override.
|
|
55
|
+
export function applyKimiRegionEnvBridge(
|
|
56
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
57
|
+
region: KimiRegion = currentKimiRegion(),
|
|
58
|
+
): boolean {
|
|
59
|
+
if (regionBridgeOwnedValue !== undefined && env.KIMI_CODE_OAUTH_HOST === regionBridgeOwnedValue) {
|
|
60
|
+
delete env.KIMI_CODE_OAUTH_HOST;
|
|
61
|
+
}
|
|
62
|
+
regionBridgeOwnedValue = undefined;
|
|
63
|
+
// pi core's OAuth reads only KIMI_CODE_OAUTH_HOST; mirror the documented
|
|
64
|
+
// KIMI_OAUTH_HOST alias into it so the alias keeps working on the built-in
|
|
65
|
+
// login path. The alias stays the source of truth.
|
|
66
|
+
if (!env.KIMI_CODE_OAUTH_HOST && env.KIMI_OAUTH_HOST?.trim()) {
|
|
67
|
+
env.KIMI_CODE_OAUTH_HOST = env.KIMI_OAUTH_HOST.trim();
|
|
68
|
+
regionBridgeOwnedValue = env.KIMI_CODE_OAUTH_HOST;
|
|
69
|
+
}
|
|
70
|
+
if (region === "mainland-cn") return false;
|
|
71
|
+
if (env.KIMI_CODE_OAUTH_HOST || env.KIMI_OAUTH_HOST) return false;
|
|
72
|
+
env.KIMI_CODE_OAUTH_HOST = KIMI_REGION_PROFILES[region].oauthHost;
|
|
73
|
+
regionBridgeOwnedValue = env.KIMI_CODE_OAUTH_HOST;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Normalize any managed base URL to its `/v1` form. Single implementation
|
|
78
|
+
* shared by the tool endpoints (src/tools/common.ts), the usage/account URLs
|
|
79
|
+
* (src/usage.ts), and the credential probe (src/oauth.ts). */
|
|
80
|
+
export function normalizeKimiBaseV1(baseUrl: string): string {
|
|
81
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
82
|
+
return base.endsWith("/v1") ? base : `${base}/v1`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const DEFAULT_OAUTH_HOST = KIMI_REGION_PROFILES[ENV_KIMI_CODE_REGION].oauthHost;
|
|
11
86
|
|
|
12
87
|
export const KIMI_WIRE_PROTOCOLS = ["openai", "anthropic", "responses"] as const;
|
|
13
88
|
export type KimiWireProtocol = (typeof KIMI_WIRE_PROTOCOLS)[number];
|
|
@@ -48,15 +123,17 @@ export function getKimiApiType(protocol: KimiWireProtocol): KimiApiType {
|
|
|
48
123
|
// built-in anthropic-messages / openai-completions / openai-responses handlers.
|
|
49
124
|
export const KIMI_API_TYPE = getKimiApiType(ENV_KIMI_CODE_PROTOCOL);
|
|
50
125
|
|
|
51
|
-
export function getDefaultBaseUrl(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
126
|
+
export function getDefaultBaseUrl(
|
|
127
|
+
protocol: KimiWireProtocol,
|
|
128
|
+
region: KimiRegion = currentKimiRegion(),
|
|
129
|
+
): string {
|
|
130
|
+
const apiBase = KIMI_REGION_PROFILES[region].apiBase;
|
|
131
|
+
return protocol === "anthropic" ? `${apiBase}/coding` : `${apiBase}/coding/v1`;
|
|
55
132
|
}
|
|
56
133
|
|
|
57
134
|
export const DEFAULT_BASE_URL = getDefaultBaseUrl(ENV_KIMI_CODE_PROTOCOL);
|
|
58
135
|
|
|
59
|
-
export const PROVIDER_VERSION = "0.6.
|
|
136
|
+
export const PROVIDER_VERSION = "0.6.14";
|
|
60
137
|
|
|
61
138
|
export const KIMI_CODE_USER_AGENT = `pi-provider-kimi-code/${PROVIDER_VERSION}`;
|
|
62
139
|
export const KIMI_PLATFORM = "pi";
|
|
@@ -79,9 +156,9 @@ export const PROVIDER_ID = "kimi-coding";
|
|
|
79
156
|
export const KIMI_GLOBAL_FALLBACK_SOURCE_ID = "pi-provider-kimi-code-global-fallback";
|
|
80
157
|
|
|
81
158
|
export function getOAuthHost(): string {
|
|
82
|
-
const value =
|
|
83
|
-
|
|
84
|
-
return
|
|
159
|
+
const value = process.env.KIMI_CODE_OAUTH_HOST || process.env.KIMI_OAUTH_HOST;
|
|
160
|
+
if (value?.trim()) return value.trim();
|
|
161
|
+
return KIMI_REGION_PROFILES[currentKimiRegion()].oauthHost;
|
|
85
162
|
}
|
|
86
163
|
|
|
87
164
|
export function getBaseUrl(protocol: KimiWireProtocol = ENV_KIMI_CODE_PROTOCOL): string {
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Kimi error-message classification, mirroring upstream kimi-code #4477 /
|
|
2
|
+
// #4703: quota exhaustion is a hard limit with explicit non-retryability, not
|
|
3
|
+
// a transient rate limit — say so instead of letting the raw 429 body read
|
|
4
|
+
// like something a retry would fix.
|
|
5
|
+
|
|
6
|
+
const QUOTA_ERROR_CODE = "exceeded_current_quota_error";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detect Kimi's structured quota error in a flattened error message
|
|
10
|
+
* (status line + JSON body). Both markers are required to avoid matching
|
|
11
|
+
* unrelated 429 rate limits, which ARE transient.
|
|
12
|
+
*/
|
|
13
|
+
export function isKimiQuotaExhaustedMessage(message: unknown): boolean {
|
|
14
|
+
const text = String(message ?? "").toLowerCase();
|
|
15
|
+
return text.includes(QUOTA_ERROR_CODE) && /\b429\b/.test(text);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Rewrite a raw upstream error into actionable text. Returns the input
|
|
20
|
+
* unchanged when nothing applies.
|
|
21
|
+
*/
|
|
22
|
+
export function improveKimiErrorMessage(message: string): string {
|
|
23
|
+
if (!isKimiQuotaExhaustedMessage(message)) return message;
|
|
24
|
+
return (
|
|
25
|
+
"Kimi Code quota exhausted (HTTP 429, exceeded_current_quota_error). " +
|
|
26
|
+
"This is a hard quota limit, not a transient rate limit — retrying will " +
|
|
27
|
+
"keep failing until the quota window resets. Run /kimi-settings to check " +
|
|
28
|
+
"your current usage. " +
|
|
29
|
+
`Original error: ${message}`
|
|
30
|
+
);
|
|
31
|
+
}
|
package/src/models.ts
CHANGED
|
@@ -99,6 +99,15 @@ function resolveModelCost(
|
|
|
99
99
|
// per-request cap still wins (options.maxTokens ?? model.maxTokens). Reconcile
|
|
100
100
|
// wherever contextWindow is authored so the two stay in lockstep.
|
|
101
101
|
//
|
|
102
|
+
// Wire note (upstream kimi-code #4091): the OpenAI wires now omit the
|
|
103
|
+
// completion cap entirely unless one is explicitly configured —
|
|
104
|
+
// applyKimiPayloadMutations drops the pi-ai-seeded cap when the model cap is
|
|
105
|
+
// window-tracked, keeping it only for Anthropic /messages (which requires
|
|
106
|
+
// max_tokens, where the window-clamped value matches upstream's
|
|
107
|
+
// kimiUnsetCompletionTokens). The window-tracked maxTokens therefore feeds
|
|
108
|
+
// pi-ai internals (thinking-budget math) and the Anthropic wire, not the
|
|
109
|
+
// OpenAI payloads.
|
|
110
|
+
//
|
|
102
111
|
// "Explicitly configured" means any value other than the built-in default:
|
|
103
112
|
// ensureKimiCodeConfig materializes the full default config (including
|
|
104
113
|
// maxTokens) into the home config file, so config-source tracking cannot
|
|
@@ -175,6 +184,13 @@ interface KimiServerModel {
|
|
|
175
184
|
supports_thinking_type?: unknown;
|
|
176
185
|
protocol?: unknown;
|
|
177
186
|
think_efforts?: unknown;
|
|
187
|
+
// Unified schema (upstream kimi-code #4209). The server currently sends
|
|
188
|
+
// both shapes; the official client already reads ONLY these fields, so
|
|
189
|
+
// prefer them and treat the flat legacy fields above as fallback.
|
|
190
|
+
limit?: unknown;
|
|
191
|
+
reasoning?: unknown;
|
|
192
|
+
modalities?: unknown;
|
|
193
|
+
tools?: unknown;
|
|
178
194
|
}
|
|
179
195
|
|
|
180
196
|
function parseSupportsThinkingType(value: unknown): "only" | "no" | "both" | undefined {
|
|
@@ -212,28 +228,89 @@ function parseThinkEfforts(value: unknown): {
|
|
|
212
228
|
};
|
|
213
229
|
}
|
|
214
230
|
|
|
231
|
+
// Unified `reasoning` object: { type: "only" | "both" | "none", effort: {
|
|
232
|
+
// valid: [...], default } }. A missing object means the legacy flat fields
|
|
233
|
+
// decide; a present object always wins (type "none" clears a stale thinking
|
|
234
|
+
// default, matching upstream's three-state mapping to internal "no").
|
|
235
|
+
function parseUnifiedReasoning(value: unknown): {
|
|
236
|
+
readonly matched: boolean;
|
|
237
|
+
readonly supportsThinkingType?: "only" | "no" | "both";
|
|
238
|
+
readonly supportsReasoning?: boolean;
|
|
239
|
+
readonly supportEfforts?: string[];
|
|
240
|
+
readonly defaultEffort?: string;
|
|
241
|
+
} {
|
|
242
|
+
if (!isRecord(value)) return { matched: false };
|
|
243
|
+
const rawType = value.type;
|
|
244
|
+
const supportsThinkingType =
|
|
245
|
+
rawType === "only" || rawType === "both" ? rawType : rawType === "none" ? "no" : undefined;
|
|
246
|
+
const effort = isRecord(value.effort) ? value.effort : undefined;
|
|
247
|
+
const supportEfforts = Array.isArray(effort?.valid)
|
|
248
|
+
? effort.valid.filter((e): e is string => typeof e === "string" && !!e)
|
|
249
|
+
: undefined;
|
|
250
|
+
const rawDefault =
|
|
251
|
+
typeof effort?.default === "string" && effort.default ? effort.default : undefined;
|
|
252
|
+
// A default not listed in valid is dropped, same as upstream.
|
|
253
|
+
const defaultEffort =
|
|
254
|
+
rawDefault && supportEfforts && !supportEfforts.includes(rawDefault) ? undefined : rawDefault;
|
|
255
|
+
return {
|
|
256
|
+
matched: true,
|
|
257
|
+
...(supportsThinkingType ? { supportsThinkingType } : {}),
|
|
258
|
+
supportsReasoning: supportsThinkingType !== undefined && supportsThinkingType !== "no",
|
|
259
|
+
...(supportEfforts && supportEfforts.length > 0 ? { supportEfforts } : {}),
|
|
260
|
+
...(defaultEffort ? { defaultEffort } : {}),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function parseInputModalities(value: unknown): {
|
|
265
|
+
supportsImageIn?: boolean;
|
|
266
|
+
supportsVideoIn?: boolean;
|
|
267
|
+
} {
|
|
268
|
+
if (!isRecord(value) || !Array.isArray(value.input)) return {};
|
|
269
|
+
const input = value.input;
|
|
270
|
+
return {
|
|
271
|
+
supportsImageIn: input.includes("image"),
|
|
272
|
+
supportsVideoIn: input.includes("video"),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
215
276
|
function parseKimiModelMetadata(model: KimiServerModel): KimiModelMetadata | undefined {
|
|
216
277
|
if (typeof model.id !== "string" || !model.id) return undefined;
|
|
217
278
|
const metadata: KimiModelMetadata = { wireModelId: model.id };
|
|
218
279
|
if (typeof model.display_name === "string") metadata.modelDisplay = model.display_name;
|
|
219
|
-
|
|
280
|
+
// Unified limit.context wins over legacy context_length.
|
|
281
|
+
const limit = isRecord(model.limit) ? model.limit : undefined;
|
|
282
|
+
if (typeof limit?.context === "number" && limit.context > 0) {
|
|
283
|
+
metadata.contextLength = limit.context;
|
|
284
|
+
} else if (typeof model.context_length === "number" && model.context_length > 0) {
|
|
220
285
|
metadata.contextLength = model.context_length;
|
|
221
286
|
}
|
|
222
|
-
const
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
287
|
+
const unifiedReasoning = parseUnifiedReasoning(model.reasoning);
|
|
288
|
+
if (unifiedReasoning.matched) {
|
|
289
|
+
const { matched: _, ...fields } = unifiedReasoning;
|
|
290
|
+
Object.assign(metadata, fields);
|
|
291
|
+
} else {
|
|
292
|
+
const thinkingType = parseSupportsThinkingType(model.supports_thinking_type);
|
|
293
|
+
if (thinkingType) {
|
|
294
|
+
metadata.supportsThinkingType = thinkingType;
|
|
295
|
+
} else if (typeof model.supports_reasoning === "boolean") {
|
|
296
|
+
metadata.supportsReasoning = model.supports_reasoning;
|
|
297
|
+
}
|
|
298
|
+
Object.assign(metadata, parseThinkEfforts(model.think_efforts));
|
|
230
299
|
}
|
|
231
|
-
|
|
232
|
-
|
|
300
|
+
const modalities = parseInputModalities(model.modalities);
|
|
301
|
+
if (modalities.supportsImageIn !== undefined) {
|
|
302
|
+
metadata.supportsImageIn = modalities.supportsImageIn;
|
|
303
|
+
metadata.supportsVideoIn = modalities.supportsVideoIn;
|
|
304
|
+
} else {
|
|
305
|
+
if (typeof model.supports_image_in === "boolean") {
|
|
306
|
+
metadata.supportsImageIn = model.supports_image_in;
|
|
307
|
+
}
|
|
308
|
+
if (typeof model.supports_video_in === "boolean") {
|
|
309
|
+
metadata.supportsVideoIn = model.supports_video_in;
|
|
310
|
+
}
|
|
233
311
|
}
|
|
234
312
|
const protocol = parseKimiCatalogProtocol(model.protocol);
|
|
235
313
|
if (protocol) metadata.protocol = protocol;
|
|
236
|
-
Object.assign(metadata, parseThinkEfforts(model.think_efforts));
|
|
237
314
|
return metadata;
|
|
238
315
|
}
|
|
239
316
|
|
|
@@ -246,10 +323,15 @@ function getModelsUrl(protocol?: KimiWireProtocol): string {
|
|
|
246
323
|
return buildModelsUrl(getBaseUrl(protocol));
|
|
247
324
|
}
|
|
248
325
|
|
|
326
|
+
// Both official managed deployments serve /coding/v1/models; global (kimi.ai)
|
|
327
|
+
// users get model discovery too, not just mainland (.com).
|
|
249
328
|
export function isOfficialKimiModelsUrl(url: string): boolean {
|
|
250
329
|
try {
|
|
251
330
|
const parsed = new URL(url);
|
|
252
|
-
return
|
|
331
|
+
return (
|
|
332
|
+
(parsed.origin === "https://api.kimi.com" || parsed.origin === "https://api.kimi.ai") &&
|
|
333
|
+
parsed.pathname === "/coding/v1/models"
|
|
334
|
+
);
|
|
253
335
|
} catch {
|
|
254
336
|
return false;
|
|
255
337
|
}
|
package/src/oauth.ts
CHANGED
|
@@ -155,7 +155,14 @@ async function lockStoredOAuthCredential(
|
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
import {
|
|
158
|
+
import {
|
|
159
|
+
CLIENT_ID,
|
|
160
|
+
PROVIDER_ID,
|
|
161
|
+
RETRYABLE_REFRESH_STATUSES,
|
|
162
|
+
getBaseUrl,
|
|
163
|
+
getOAuthHost,
|
|
164
|
+
normalizeKimiBaseV1,
|
|
165
|
+
} from "./constants.ts";
|
|
159
166
|
import { getCommonHeaders } from "./device.ts";
|
|
160
167
|
import { type KimiOAuthCredentials, discoverKimiModelMetadata } from "./models.ts";
|
|
161
168
|
|
|
@@ -211,6 +218,24 @@ export async function requestDeviceAuthorization(): Promise<DeviceAuthorization>
|
|
|
211
218
|
throw new Error("Invalid device authorization response");
|
|
212
219
|
}
|
|
213
220
|
|
|
221
|
+
// Guard against a malicious or buggy server handing us a non-web URL
|
|
222
|
+
// (file:, custom scheme, ...) that the OS URL handler would happily open.
|
|
223
|
+
// Mirrors pi 1.5.0's check for the built-in OAuth providers.
|
|
224
|
+
for (const url of [data.verification_uri, verificationUrl]) {
|
|
225
|
+
if (!url) continue;
|
|
226
|
+
let parsed: URL;
|
|
227
|
+
try {
|
|
228
|
+
parsed = new URL(url);
|
|
229
|
+
} catch {
|
|
230
|
+
throw new Error("Device authorization returned an invalid verification URL");
|
|
231
|
+
}
|
|
232
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Device authorization returned a non-web verification URL (${parsed.protocol})`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
214
239
|
return {
|
|
215
240
|
user_code: data.user_code,
|
|
216
241
|
device_code: data.device_code,
|
|
@@ -435,6 +460,25 @@ async function lockKimiCodeCredentials(signal?: AbortSignal): Promise<LockedKimi
|
|
|
435
460
|
};
|
|
436
461
|
}
|
|
437
462
|
|
|
463
|
+
// Tokens are region-bound, but the kimi-code CLI credential file does not
|
|
464
|
+
// record which OAuth host minted them — so a fresh credential reused across
|
|
465
|
+
// regions (a mainland CLI login while KIMI_CODE_REGION=global, or vice versa)
|
|
466
|
+
// would 401 every request. Probe the current region's managed API before
|
|
467
|
+
// reuse. Only an explicit 401 disproves usability: network failures must not
|
|
468
|
+
// force an unnecessary re-login. Exported for tests.
|
|
469
|
+
export async function isKimiCredentialRegionValid(accessToken: string): Promise<boolean> {
|
|
470
|
+
const meUrl = `${normalizeKimiBaseV1(getBaseUrl())}/me`;
|
|
471
|
+
try {
|
|
472
|
+
const response = await fetch(meUrl, {
|
|
473
|
+
headers: { ...getCommonHeaders(), Authorization: `Bearer ${accessToken}` },
|
|
474
|
+
signal: AbortSignal.timeout(5_000),
|
|
475
|
+
});
|
|
476
|
+
return response.status !== 401;
|
|
477
|
+
} catch {
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
438
482
|
async function tryReuseKimiCliCredentials(
|
|
439
483
|
callbacks: OAuthLoginCallbacks,
|
|
440
484
|
): Promise<KimiOAuthCredentials | null> {
|
|
@@ -449,6 +493,12 @@ async function tryReuseKimiCliCredentials(
|
|
|
449
493
|
callbacks.onProgress?.("Found existing Kimi credentials, reusing them.");
|
|
450
494
|
|
|
451
495
|
if (stillFresh) {
|
|
496
|
+
if (!(await isKimiCredentialRegionValid(data.access_token!))) {
|
|
497
|
+
callbacks.onProgress?.(
|
|
498
|
+
"Existing Kimi credentials are not valid for the configured region, starting a fresh login.",
|
|
499
|
+
);
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
452
502
|
const extras = await discoverKimiModelMetadata(data.access_token!);
|
|
453
503
|
return {
|
|
454
504
|
access: data.access_token!,
|
|
@@ -461,6 +511,15 @@ async function tryReuseKimiCliCredentials(
|
|
|
461
511
|
callbacks.onProgress?.("Kimi access token expired, refreshing.");
|
|
462
512
|
try {
|
|
463
513
|
const token = await refreshAccessToken(data.refresh_token!);
|
|
514
|
+
// Probe before persisting: a token that fails the region check must not
|
|
515
|
+
// be written into the shared CLI credential file, or a later CLI-side
|
|
516
|
+
// reuse (or the next /login) would pick up a token that 401s.
|
|
517
|
+
if (!(await isKimiCredentialRegionValid(token.access_token))) {
|
|
518
|
+
callbacks.onProgress?.(
|
|
519
|
+
"Refreshed Kimi credentials are not valid for the configured region, starting a fresh login.",
|
|
520
|
+
);
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
464
523
|
const expiresMs = Date.now() + token.expires_in * 1000;
|
|
465
524
|
writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
|
|
466
525
|
const extras = await discoverKimiModelMetadata(token.access_token);
|