@dreb/ai 2.43.0 → 2.43.2
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/README.md +1 -1
- package/dist/models.d.ts +1 -0
- package/dist/models.d.ts.map +1 -1
- package/dist/models.generated.d.ts +80 -0
- package/dist/models.generated.d.ts.map +1 -1
- package/dist/models.generated.js +77 -24
- package/dist/models.generated.js.map +1 -1
- package/dist/models.js +3 -0
- package/dist/models.js.map +1 -1
- package/dist/providers/openai-completions.d.ts.map +1 -1
- package/dist/providers/openai-completions.js +6 -6
- package/dist/providers/openai-completions.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/oauth/kimi-coding.d.ts +13 -3
- package/dist/utils/oauth/kimi-coding.d.ts.map +1 -1
- package/dist/utils/oauth/kimi-coding.js +347 -89
- package/dist/utils/oauth/kimi-coding.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,55 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Kimi For Coding OAuth flow (device code)
|
|
3
3
|
*
|
|
4
|
-
* Authenticates against Moonshot's Kimi API (auth.kimi.com)
|
|
4
|
+
* Authenticates against Moonshot's Kimi API (auth.kimi.com) using the current Kimi Code device identity.
|
|
5
5
|
* Uses the device authorization grant flow to obtain access/refresh tokens,
|
|
6
6
|
* then discovers the user's model entitlement via the /models endpoint.
|
|
7
7
|
*/
|
|
8
8
|
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
9
10
|
import * as fs from "node:fs";
|
|
10
11
|
import * as os from "node:os";
|
|
11
12
|
import * as path from "node:path";
|
|
12
13
|
// ============================================================================
|
|
13
14
|
// Constants
|
|
14
15
|
// ============================================================================
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
function readDrebVersion() {
|
|
17
|
+
try {
|
|
18
|
+
const pkg = JSON.parse(fs.readFileSync(new URL("../../../package.json", import.meta.url), "utf-8"));
|
|
19
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0)
|
|
20
|
+
return pkg.version;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Fall back for unusual bundlers that omit package.json.
|
|
24
|
+
}
|
|
25
|
+
return "unknown";
|
|
26
|
+
}
|
|
27
|
+
const DREB_VERSION = readDrebVersion();
|
|
28
|
+
const USER_AGENT = `dreb/${DREB_VERSION}`;
|
|
17
29
|
const OAUTH_HOST = "https://auth.kimi.com";
|
|
18
30
|
const OAUTH_DEVICE_AUTH_URL = `${OAUTH_HOST}/api/oauth/device_authorization`;
|
|
19
31
|
const OAUTH_TOKEN_URL = `${OAUTH_HOST}/api/oauth/token`;
|
|
20
32
|
const OAUTH_CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
|
|
21
|
-
const OAUTH_SCOPE = "kimi-code";
|
|
22
33
|
const OAUTH_DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
23
34
|
const OAUTH_REFRESH_GRANT = "refresh_token";
|
|
24
35
|
const API_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
25
|
-
const
|
|
36
|
+
const KIMI_CODE_HOME = path.join(os.homedir(), ".kimi-code");
|
|
37
|
+
const DEVICE_ID_PATH = path.join(KIMI_CODE_HOME, "device_id");
|
|
26
38
|
const MAX_REFRESH_RETRIES = 3;
|
|
39
|
+
const MAX_DEVICE_FLOW_MS = 15 * 60 * 1000;
|
|
27
40
|
// ============================================================================
|
|
28
41
|
// Device ID
|
|
29
42
|
// ============================================================================
|
|
30
|
-
|
|
31
|
-
// UUID v4 without dashes (hex only, 32 chars)
|
|
32
|
-
return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".replace(/x/g, () => Math.floor(Math.random() * 16).toString(16));
|
|
33
|
-
}
|
|
43
|
+
let sessionDeviceId;
|
|
34
44
|
function getDeviceId() {
|
|
45
|
+
if (sessionDeviceId)
|
|
46
|
+
return sessionDeviceId;
|
|
35
47
|
try {
|
|
36
48
|
if (fs.existsSync(DEVICE_ID_PATH)) {
|
|
37
49
|
const id = fs.readFileSync(DEVICE_ID_PATH, "utf-8").trim();
|
|
38
|
-
if (
|
|
50
|
+
if (id.length > 0) {
|
|
51
|
+
sessionDeviceId = id;
|
|
39
52
|
return id;
|
|
40
53
|
}
|
|
41
54
|
}
|
|
42
55
|
}
|
|
43
56
|
catch {
|
|
44
|
-
// Fall through to generate
|
|
57
|
+
// Fall through to generate.
|
|
45
58
|
}
|
|
46
|
-
const id =
|
|
59
|
+
const id = randomUUID();
|
|
60
|
+
sessionDeviceId = id;
|
|
47
61
|
try {
|
|
48
|
-
fs.mkdirSync(
|
|
49
|
-
fs.writeFileSync(DEVICE_ID_PATH, id, "utf-8");
|
|
62
|
+
fs.mkdirSync(KIMI_CODE_HOME, { recursive: true, mode: 0o700 });
|
|
63
|
+
fs.writeFileSync(DEVICE_ID_PATH, id, { encoding: "utf-8", mode: 0o600 });
|
|
50
64
|
}
|
|
51
65
|
catch {
|
|
52
|
-
// If we can't persist, just use the generated ID for this session
|
|
66
|
+
// If we can't persist, just use the generated ID for this session.
|
|
53
67
|
}
|
|
54
68
|
return id;
|
|
55
69
|
}
|
|
@@ -59,19 +73,36 @@ function getDeviceId() {
|
|
|
59
73
|
/**
|
|
60
74
|
* Strip non-ASCII characters from a string for use in HTTP header values.
|
|
61
75
|
*/
|
|
62
|
-
function asciiHeaderValue(value) {
|
|
63
|
-
|
|
76
|
+
function asciiHeaderValue(value, fallback = "unknown") {
|
|
77
|
+
const cleaned = value.replace(/[^\x20-\x7E]/g, "").trim();
|
|
78
|
+
return cleaned.length > 0 ? cleaned : fallback;
|
|
79
|
+
}
|
|
80
|
+
function customKimiHeaders() {
|
|
81
|
+
const raw = process.env.KIMI_CODE_CUSTOM_HEADERS?.trim();
|
|
82
|
+
if (!raw)
|
|
83
|
+
return {};
|
|
84
|
+
const headers = {};
|
|
85
|
+
for (const line of raw.split("\n")) {
|
|
86
|
+
const colon = line.indexOf(":");
|
|
87
|
+
if (colon < 0)
|
|
88
|
+
continue;
|
|
89
|
+
const name = line.slice(0, colon).trim();
|
|
90
|
+
if (!name)
|
|
91
|
+
continue;
|
|
92
|
+
headers[name] = line.slice(colon + 1).trim();
|
|
93
|
+
}
|
|
94
|
+
return headers;
|
|
64
95
|
}
|
|
65
96
|
/**
|
|
66
97
|
* Determine the device model string, mirroring kimi-cli logic.
|
|
67
98
|
*/
|
|
68
99
|
function kimiDeviceModel() {
|
|
69
100
|
const platform = os.platform();
|
|
70
|
-
const machine = os.
|
|
101
|
+
const machine = os.arch();
|
|
71
102
|
if (platform === "darwin") {
|
|
72
103
|
let version;
|
|
73
104
|
try {
|
|
74
|
-
version = execFileSync("sw_vers", ["-productVersion"], { encoding: "utf-8", timeout: 3000 }).trim();
|
|
105
|
+
version = execFileSync("/usr/bin/sw_vers", ["-productVersion"], { encoding: "utf-8", timeout: 3000 }).trim();
|
|
75
106
|
}
|
|
76
107
|
catch {
|
|
77
108
|
version = os.release();
|
|
@@ -79,31 +110,111 @@ function kimiDeviceModel() {
|
|
|
79
110
|
return `macOS ${version} ${machine}`;
|
|
80
111
|
}
|
|
81
112
|
if (platform === "win32") {
|
|
82
|
-
|
|
83
|
-
const buildNumber = Number.parseInt(release.split(".").pop() || "0", 10);
|
|
84
|
-
const label = buildNumber >= 22000 ? "11" : "10";
|
|
85
|
-
return `Windows ${label} ${machine}`;
|
|
113
|
+
return `Windows ${os.release()} ${machine}`;
|
|
86
114
|
}
|
|
87
115
|
// Linux and other
|
|
88
|
-
return `${os.type()} ${os.release()} ${machine}
|
|
116
|
+
return `${os.type()} ${os.release()} ${machine}`.trim();
|
|
89
117
|
}
|
|
90
118
|
/**
|
|
91
119
|
* Build the standard set of headers required on every Kimi API request.
|
|
92
120
|
*/
|
|
93
121
|
export function buildKimiHeaders() {
|
|
94
122
|
return {
|
|
123
|
+
...customKimiHeaders(),
|
|
95
124
|
"User-Agent": USER_AGENT,
|
|
96
|
-
"X-Msh-Platform": "
|
|
97
|
-
"X-Msh-Version":
|
|
125
|
+
"X-Msh-Platform": "kimi_code_cli",
|
|
126
|
+
"X-Msh-Version": DREB_VERSION,
|
|
98
127
|
"X-Msh-Device-Name": asciiHeaderValue(os.hostname()),
|
|
99
128
|
"X-Msh-Device-Model": asciiHeaderValue(kimiDeviceModel()),
|
|
100
129
|
"X-Msh-Device-Id": getDeviceId(),
|
|
101
|
-
"X-Msh-Os-Version": asciiHeaderValue(os.
|
|
130
|
+
"X-Msh-Os-Version": asciiHeaderValue(os.release()),
|
|
102
131
|
};
|
|
103
132
|
}
|
|
133
|
+
function parseTokenSuccess(response) {
|
|
134
|
+
if (typeof response.access_token !== "string" ||
|
|
135
|
+
response.access_token.length === 0 ||
|
|
136
|
+
typeof response.refresh_token !== "string" ||
|
|
137
|
+
response.refresh_token.length === 0 ||
|
|
138
|
+
typeof response.expires_in !== "number" ||
|
|
139
|
+
!Number.isFinite(response.expires_in) ||
|
|
140
|
+
response.expires_in <= 0) {
|
|
141
|
+
throw new Error("Invalid token response fields");
|
|
142
|
+
}
|
|
143
|
+
return response;
|
|
144
|
+
}
|
|
145
|
+
function isRecord(value) {
|
|
146
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
147
|
+
}
|
|
148
|
+
function parseStringArray(value) {
|
|
149
|
+
if (!Array.isArray(value))
|
|
150
|
+
return undefined;
|
|
151
|
+
const out = value.filter((v) => typeof v === "string" && v.length > 0);
|
|
152
|
+
return out.length > 0 ? out : undefined;
|
|
153
|
+
}
|
|
154
|
+
function parseSupportsThinkingType(value) {
|
|
155
|
+
return value === "only" || value === "no" || value === "both" ? value : undefined;
|
|
156
|
+
}
|
|
157
|
+
function parseModelProtocol(value) {
|
|
158
|
+
return value === "anthropic" ? "anthropic" : undefined;
|
|
159
|
+
}
|
|
160
|
+
function parseThinkEfforts(value) {
|
|
161
|
+
if (!isRecord(value) || value.support !== true)
|
|
162
|
+
return undefined;
|
|
163
|
+
return {
|
|
164
|
+
support: true,
|
|
165
|
+
valid_efforts: parseStringArray(value.valid_efforts),
|
|
166
|
+
default_effort: typeof value.default_effort === "string" && value.default_effort.length > 0 ? value.default_effort : undefined,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function toKimiModelInfo(item) {
|
|
170
|
+
if (!isRecord(item) || typeof item.id !== "string" || item.id.length === 0) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
const contextLength = Number(item.context_length);
|
|
174
|
+
if (!Number.isInteger(contextLength) || contextLength <= 0) {
|
|
175
|
+
throw new Error(`Kimi Code model "${item.id}" must include a positive context_length.`);
|
|
176
|
+
}
|
|
177
|
+
const displayName = item.display_name;
|
|
178
|
+
const normalizedDisplayName = typeof displayName === "string" && displayName.length > 0 ? displayName : undefined;
|
|
179
|
+
const optionalBoolean = (value) => (typeof value === "boolean" ? value : undefined);
|
|
180
|
+
const parsed = {
|
|
181
|
+
id: item.id,
|
|
182
|
+
context_length: contextLength,
|
|
183
|
+
display_name: normalizedDisplayName,
|
|
184
|
+
};
|
|
185
|
+
const supportsReasoning = optionalBoolean(item.supports_reasoning);
|
|
186
|
+
if (supportsReasoning !== undefined)
|
|
187
|
+
parsed.supports_reasoning = supportsReasoning;
|
|
188
|
+
const supportsImageIn = optionalBoolean(item.supports_image_in);
|
|
189
|
+
if (supportsImageIn !== undefined)
|
|
190
|
+
parsed.supports_image_in = supportsImageIn;
|
|
191
|
+
const supportsVideoIn = optionalBoolean(item.supports_video_in);
|
|
192
|
+
if (supportsVideoIn !== undefined)
|
|
193
|
+
parsed.supports_video_in = supportsVideoIn;
|
|
194
|
+
if (Object.hasOwn(item, "supports_tool_use")) {
|
|
195
|
+
const supportsToolUse = optionalBoolean(item.supports_tool_use);
|
|
196
|
+
if (supportsToolUse !== undefined)
|
|
197
|
+
parsed.supports_tool_use = supportsToolUse;
|
|
198
|
+
}
|
|
199
|
+
const supportsThinkingType = parseSupportsThinkingType(item.supports_thinking_type);
|
|
200
|
+
if (supportsThinkingType !== undefined)
|
|
201
|
+
parsed.supports_thinking_type = supportsThinkingType;
|
|
202
|
+
const protocol = parseModelProtocol(item.protocol);
|
|
203
|
+
if (protocol !== undefined)
|
|
204
|
+
parsed.protocol = protocol;
|
|
205
|
+
const thinkEfforts = parseThinkEfforts(item.think_efforts);
|
|
206
|
+
if (thinkEfforts !== undefined)
|
|
207
|
+
parsed.think_efforts = thinkEfforts;
|
|
208
|
+
return parsed;
|
|
209
|
+
}
|
|
104
210
|
// ============================================================================
|
|
105
211
|
// Network helpers
|
|
106
212
|
// ============================================================================
|
|
213
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
214
|
+
function requestSignal(signal) {
|
|
215
|
+
const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
|
|
216
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
217
|
+
}
|
|
107
218
|
async function fetchJson(url, init) {
|
|
108
219
|
const response = await fetch(url, init);
|
|
109
220
|
if (!response.ok) {
|
|
@@ -135,11 +246,12 @@ function abortableSleep(ms, signal) {
|
|
|
135
246
|
* List available models from the Kimi API.
|
|
136
247
|
* Returns the model info array from the response's `data` field.
|
|
137
248
|
*/
|
|
138
|
-
export async function listModels(accessToken) {
|
|
249
|
+
export async function listModels(accessToken, signal) {
|
|
139
250
|
const raw = await fetchJson(`${API_BASE_URL}/models`, {
|
|
251
|
+
signal: requestSignal(signal),
|
|
140
252
|
headers: {
|
|
141
|
-
Authorization: `Bearer ${accessToken}`,
|
|
142
253
|
...buildKimiHeaders(),
|
|
254
|
+
Authorization: `Bearer ${accessToken}`,
|
|
143
255
|
},
|
|
144
256
|
});
|
|
145
257
|
if (!raw || typeof raw !== "object") {
|
|
@@ -149,22 +261,20 @@ export async function listModels(accessToken) {
|
|
|
149
261
|
if (!Array.isArray(data)) {
|
|
150
262
|
throw new Error("Invalid models response: expected data array");
|
|
151
263
|
}
|
|
152
|
-
return data;
|
|
264
|
+
return data.map((item) => toKimiModelInfo(item)).filter((item) => item !== undefined);
|
|
153
265
|
}
|
|
154
266
|
// ============================================================================
|
|
155
267
|
// Device flow
|
|
156
268
|
// ============================================================================
|
|
157
|
-
async function startDeviceFlow() {
|
|
269
|
+
async function startDeviceFlow(signal) {
|
|
158
270
|
const data = await fetchJson(OAUTH_DEVICE_AUTH_URL, {
|
|
159
271
|
method: "POST",
|
|
272
|
+
signal: requestSignal(signal),
|
|
160
273
|
headers: {
|
|
161
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
162
274
|
...buildKimiHeaders(),
|
|
275
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
163
276
|
},
|
|
164
|
-
body: new URLSearchParams({
|
|
165
|
-
client_id: OAUTH_CLIENT_ID,
|
|
166
|
-
scope: OAUTH_SCOPE,
|
|
167
|
-
}),
|
|
277
|
+
body: new URLSearchParams({ client_id: OAUTH_CLIENT_ID }),
|
|
168
278
|
});
|
|
169
279
|
if (!data || typeof data !== "object") {
|
|
170
280
|
throw new Error("Invalid device code response");
|
|
@@ -172,20 +282,30 @@ async function startDeviceFlow() {
|
|
|
172
282
|
const d = data;
|
|
173
283
|
const device_code = d.device_code;
|
|
174
284
|
const user_code = d.user_code;
|
|
175
|
-
const
|
|
285
|
+
const verification_uri_complete = typeof d.verification_uri_complete === "string" && d.verification_uri_complete.length > 0
|
|
286
|
+
? d.verification_uri_complete
|
|
287
|
+
: typeof d.verification_uri === "string" && d.verification_uri.length > 0
|
|
288
|
+
? `${d.verification_uri}${d.verification_uri.includes("?") ? "&" : "?"}user_code=${encodeURIComponent(user_code)}`
|
|
289
|
+
: undefined;
|
|
176
290
|
const interval = d.interval;
|
|
177
291
|
const expires_in = d.expires_in;
|
|
178
292
|
if (typeof device_code !== "string" ||
|
|
179
293
|
typeof user_code !== "string" ||
|
|
180
|
-
typeof
|
|
294
|
+
typeof verification_uri_complete !== "string" ||
|
|
181
295
|
typeof interval !== "number" ||
|
|
182
296
|
typeof expires_in !== "number") {
|
|
183
297
|
throw new Error("Invalid device code response fields");
|
|
184
298
|
}
|
|
185
|
-
return { device_code, user_code,
|
|
299
|
+
return { device_code, user_code, verification_uri_complete, interval, expires_in };
|
|
186
300
|
}
|
|
187
|
-
|
|
188
|
-
|
|
301
|
+
class DeviceCodeExpiredError extends Error {
|
|
302
|
+
constructor() {
|
|
303
|
+
super("Device code expired");
|
|
304
|
+
this.name = "DeviceCodeExpiredError";
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async function pollForAccessToken(deviceCode, intervalSeconds, expiresIn, signal, overallDeadline = Date.now() + MAX_DEVICE_FLOW_MS) {
|
|
308
|
+
const deadline = Math.min(Date.now() + expiresIn * 1000, overallDeadline);
|
|
189
309
|
let intervalMs = Math.max(1000, Math.floor(intervalSeconds * 1000));
|
|
190
310
|
while (Date.now() < deadline) {
|
|
191
311
|
if (signal?.aborted) {
|
|
@@ -196,9 +316,10 @@ async function pollForAccessToken(deviceCode, intervalSeconds, expiresIn, signal
|
|
|
196
316
|
await abortableSleep(waitMs, signal);
|
|
197
317
|
const tokenResponse = await fetch(OAUTH_TOKEN_URL, {
|
|
198
318
|
method: "POST",
|
|
319
|
+
signal: requestSignal(signal),
|
|
199
320
|
headers: {
|
|
200
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
201
321
|
...buildKimiHeaders(),
|
|
322
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
202
323
|
},
|
|
203
324
|
body: new URLSearchParams({
|
|
204
325
|
client_id: OAUTH_CLIENT_ID,
|
|
@@ -211,7 +332,7 @@ async function pollForAccessToken(deviceCode, intervalSeconds, expiresIn, signal
|
|
|
211
332
|
const resp = (await tokenResponse.json());
|
|
212
333
|
// Success: has access_token
|
|
213
334
|
if (typeof resp.access_token === "string") {
|
|
214
|
-
return resp;
|
|
335
|
+
return parseTokenSuccess(resp);
|
|
215
336
|
}
|
|
216
337
|
// Error response (RFC 8628 §3.5)
|
|
217
338
|
if (typeof resp.error === "string") {
|
|
@@ -229,7 +350,7 @@ async function pollForAccessToken(deviceCode, intervalSeconds, expiresIn, signal
|
|
|
229
350
|
continue;
|
|
230
351
|
}
|
|
231
352
|
if (error === "expired_token") {
|
|
232
|
-
throw new
|
|
353
|
+
throw new DeviceCodeExpiredError();
|
|
233
354
|
}
|
|
234
355
|
const descriptionSuffix = description ? `: ${description}` : "";
|
|
235
356
|
throw new Error(`Device flow failed: ${error}${descriptionSuffix}`);
|
|
@@ -255,7 +376,7 @@ class RetriableError extends Error {
|
|
|
255
376
|
* recognizable substrings in the message.
|
|
256
377
|
*/
|
|
257
378
|
function isNetworkError(error) {
|
|
258
|
-
if (error instanceof TypeError)
|
|
379
|
+
if (error instanceof TypeError || error.name === "TimeoutError")
|
|
259
380
|
return true;
|
|
260
381
|
const msg = error.message.toLowerCase();
|
|
261
382
|
return ["fetch failed", "econnrefused", "etimedout", "enotfound", "econnreset", "socket hang up"].some((s) => msg.includes(s));
|
|
@@ -269,9 +390,10 @@ async function refreshWithRetry(refreshToken, signal) {
|
|
|
269
390
|
try {
|
|
270
391
|
const response = await fetch(OAUTH_TOKEN_URL, {
|
|
271
392
|
method: "POST",
|
|
393
|
+
signal: requestSignal(signal),
|
|
272
394
|
headers: {
|
|
273
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
274
395
|
...buildKimiHeaders(),
|
|
396
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
275
397
|
},
|
|
276
398
|
body: new URLSearchParams({
|
|
277
399
|
client_id: OAUTH_CLIENT_ID,
|
|
@@ -288,12 +410,13 @@ async function refreshWithRetry(refreshToken, signal) {
|
|
|
288
410
|
throw new Error(`Token refresh failed: ${response.status} ${response.statusText}: ${text}`);
|
|
289
411
|
}
|
|
290
412
|
const raw = await response.json();
|
|
291
|
-
if (!raw || typeof raw !== "object"
|
|
413
|
+
if (!raw || typeof raw !== "object")
|
|
292
414
|
throw new Error("Invalid token refresh response");
|
|
293
|
-
|
|
294
|
-
return raw;
|
|
415
|
+
return parseTokenSuccess(raw);
|
|
295
416
|
}
|
|
296
417
|
catch (error) {
|
|
418
|
+
if (signal?.aborted)
|
|
419
|
+
throw new Error("Refresh cancelled");
|
|
297
420
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
298
421
|
// Wrap network errors (TypeError from fetch, or common network failure indicators) as retriable
|
|
299
422
|
if (!(lastError instanceof RetriableError) && isNetworkError(lastError)) {
|
|
@@ -314,22 +437,34 @@ async function refreshWithRetry(refreshToken, signal) {
|
|
|
314
437
|
// Login flow
|
|
315
438
|
// ============================================================================
|
|
316
439
|
export async function loginKimiCoding(options) {
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
440
|
+
const overallDeadline = Date.now() + MAX_DEVICE_FLOW_MS;
|
|
441
|
+
let tokenResp;
|
|
442
|
+
while (true) {
|
|
443
|
+
const device = await startDeviceFlow(options.signal);
|
|
444
|
+
options.onAuth({
|
|
445
|
+
url: device.verification_uri_complete,
|
|
446
|
+
instructions: `Enter code: ${device.user_code}`,
|
|
447
|
+
});
|
|
448
|
+
try {
|
|
449
|
+
tokenResp = await pollForAccessToken(device.device_code, device.interval, device.expires_in, options.signal, overallDeadline);
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
if (error instanceof DeviceCodeExpiredError && Date.now() < overallDeadline)
|
|
454
|
+
continue;
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Discover model entitlement. Undefined means discovery failed; an empty
|
|
459
|
+
// array is a successful, authoritative response.
|
|
327
460
|
options.onProgress?.("Discovering available models...");
|
|
328
|
-
let models
|
|
461
|
+
let models;
|
|
329
462
|
try {
|
|
330
|
-
models = await listModels(tokenResp.access_token);
|
|
463
|
+
models = await listModels(tokenResp.access_token, options.signal);
|
|
331
464
|
}
|
|
332
465
|
catch {
|
|
466
|
+
if (options.signal?.aborted)
|
|
467
|
+
throw new Error("Login cancelled");
|
|
333
468
|
// Proceed without model enrichment if the models endpoint fails
|
|
334
469
|
}
|
|
335
470
|
const credentials = {
|
|
@@ -337,11 +472,14 @@ export async function loginKimiCoding(options) {
|
|
|
337
472
|
access: tokenResp.access_token,
|
|
338
473
|
expires: Date.now() + tokenResp.expires_in * 1000,
|
|
339
474
|
};
|
|
340
|
-
if (models
|
|
475
|
+
if (models) {
|
|
476
|
+
credentials.models = models;
|
|
341
477
|
const primary = models[0];
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
478
|
+
if (primary) {
|
|
479
|
+
credentials.modelId = primary.id;
|
|
480
|
+
credentials.contextLength = primary.context_length;
|
|
481
|
+
credentials.modelDisplay = primary.display_name;
|
|
482
|
+
}
|
|
345
483
|
}
|
|
346
484
|
return credentials;
|
|
347
485
|
}
|
|
@@ -350,27 +488,33 @@ export async function loginKimiCoding(options) {
|
|
|
350
488
|
// ============================================================================
|
|
351
489
|
export async function refreshKimiCodingToken(credentials, signal) {
|
|
352
490
|
const tokenResp = await refreshWithRetry(credentials.refresh, signal);
|
|
353
|
-
// Re-discover model entitlement
|
|
354
|
-
|
|
491
|
+
// Re-discover model entitlement. Undefined means discovery failed; an empty
|
|
492
|
+
// array is a successful, authoritative response.
|
|
493
|
+
let models;
|
|
355
494
|
try {
|
|
356
|
-
models = await listModels(tokenResp.access_token);
|
|
495
|
+
models = await listModels(tokenResp.access_token, signal);
|
|
357
496
|
}
|
|
358
497
|
catch {
|
|
498
|
+
if (signal?.aborted)
|
|
499
|
+
throw new Error("Refresh cancelled");
|
|
359
500
|
// Proceed without model enrichment if the models endpoint fails
|
|
360
501
|
}
|
|
502
|
+
const oldCreds = credentials;
|
|
361
503
|
const fresh = {
|
|
362
504
|
refresh: tokenResp.refresh_token ?? credentials.refresh,
|
|
363
505
|
access: tokenResp.access_token,
|
|
364
506
|
expires: Date.now() + tokenResp.expires_in * 1000,
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
507
|
+
models: oldCreds.models,
|
|
508
|
+
modelId: oldCreds.modelId,
|
|
509
|
+
contextLength: oldCreds.contextLength,
|
|
510
|
+
modelDisplay: oldCreds.modelDisplay,
|
|
368
511
|
};
|
|
369
|
-
if (models
|
|
512
|
+
if (models) {
|
|
513
|
+
fresh.models = models;
|
|
370
514
|
const primary = models[0];
|
|
371
|
-
fresh.modelId = primary
|
|
372
|
-
fresh.contextLength = primary
|
|
373
|
-
fresh.modelDisplay = primary
|
|
515
|
+
fresh.modelId = primary?.id;
|
|
516
|
+
fresh.contextLength = primary?.context_length;
|
|
517
|
+
fresh.modelDisplay = primary?.display_name;
|
|
374
518
|
}
|
|
375
519
|
return fresh;
|
|
376
520
|
}
|
|
@@ -396,24 +540,138 @@ export const kimiCodingOAuthProvider = {
|
|
|
396
540
|
modifyModels(models, credentials) {
|
|
397
541
|
const creds = credentials;
|
|
398
542
|
const headers = buildKimiHeaders();
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
543
|
+
const staticModels = models.filter((m) => m.provider === "kimi-coding-oauth");
|
|
544
|
+
if (staticModels.length === 0) {
|
|
545
|
+
return models;
|
|
546
|
+
}
|
|
547
|
+
const injectHeaders = (m) => ({
|
|
548
|
+
...m,
|
|
549
|
+
headers: { ...(m.headers || {}), ...headers },
|
|
550
|
+
});
|
|
551
|
+
const staticById = new Map(staticModels.map((m) => [m.id, m]));
|
|
552
|
+
const fallbackTemplate = staticById.get("kimi-for-coding") ?? staticModels[0];
|
|
553
|
+
// No discovery (or failed discovery): keep the static fallback list intact.
|
|
554
|
+
// Legacy credentials describe one discovered model, so enrich or append only
|
|
555
|
+
// that model rather than collapsing every static entry to the same ID.
|
|
556
|
+
const discovered = creds.models;
|
|
557
|
+
if (!discovered) {
|
|
558
|
+
const fallbackModels = models.map((m) => {
|
|
559
|
+
if (m.provider !== "kimi-coding-oauth")
|
|
560
|
+
return m;
|
|
561
|
+
const updated = {
|
|
562
|
+
...m,
|
|
563
|
+
// The OAuth coding endpoint accepts OpenAI-style image_url data URLs;
|
|
564
|
+
// keep this capability even if static metadata is stale.
|
|
565
|
+
input: Array.from(new Set([...m.input, "image"])),
|
|
566
|
+
};
|
|
567
|
+
if (creds.modelId === m.id && creds.contextLength) {
|
|
568
|
+
updated.contextWindow = creds.contextLength;
|
|
569
|
+
}
|
|
570
|
+
if (creds.modelId === m.id && creds.modelDisplay) {
|
|
571
|
+
updated.name = creds.modelDisplay;
|
|
572
|
+
}
|
|
573
|
+
return injectHeaders(updated);
|
|
574
|
+
});
|
|
575
|
+
if (creds.modelId && !staticById.has(creds.modelId)) {
|
|
576
|
+
fallbackModels.push(injectHeaders({
|
|
577
|
+
...fallbackTemplate,
|
|
578
|
+
id: creds.modelId,
|
|
579
|
+
name: creds.modelDisplay || creds.modelId,
|
|
580
|
+
contextWindow: creds.contextLength || fallbackTemplate.contextWindow,
|
|
581
|
+
}));
|
|
582
|
+
}
|
|
583
|
+
return fallbackModels;
|
|
584
|
+
}
|
|
585
|
+
const discoveredInput = (base, info) => {
|
|
586
|
+
if (info.supports_image_in === true)
|
|
587
|
+
return Array.from(new Set([...base.input, "image"]));
|
|
588
|
+
if (info.supports_image_in === false)
|
|
589
|
+
return base.input.filter((input) => input !== "image");
|
|
590
|
+
return base.input;
|
|
591
|
+
};
|
|
592
|
+
const discoveredCompat = (base, info) => {
|
|
593
|
+
const valid = info.think_efforts?.support ? info.think_efforts.valid_efforts : undefined;
|
|
594
|
+
if (!valid || valid.length === 0)
|
|
595
|
+
return base.compat;
|
|
596
|
+
const efforts = new Set(valid);
|
|
597
|
+
const declaredDefault = info.think_efforts?.default_effort;
|
|
598
|
+
const validDefault = declaredDefault && efforts.has(declaredDefault) ? declaredDefault : undefined;
|
|
599
|
+
const choose = (...preferences) => preferences.find((effort) => efforts.has(effort)) ?? validDefault ?? valid[0];
|
|
600
|
+
return {
|
|
601
|
+
...base.compat,
|
|
602
|
+
reasoningEffortMap: {
|
|
603
|
+
minimal: choose("minimal", "low", "medium", "high", "max"),
|
|
604
|
+
low: choose("low", "minimal", "medium", "high", "max"),
|
|
605
|
+
medium: choose("medium", "high", "low", "max"),
|
|
606
|
+
high: choose("high", "medium", "max", "low"),
|
|
607
|
+
xhigh: choose("max", "xhigh", "high", "medium", "low"),
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
};
|
|
611
|
+
// Apply discovered metadata to a static model, preserving its static shape.
|
|
612
|
+
const applyDiscovery = (staticModel, info) => {
|
|
402
613
|
const updated = {
|
|
403
|
-
...
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
614
|
+
...staticModel,
|
|
615
|
+
id: info.id,
|
|
616
|
+
name: info.display_name || info.id,
|
|
617
|
+
contextWindow: info.context_length || staticModel.contextWindow,
|
|
618
|
+
input: discoveredInput(staticModel, info),
|
|
619
|
+
compat: discoveredCompat(staticModel, info),
|
|
408
620
|
};
|
|
409
|
-
if (
|
|
410
|
-
updated.
|
|
621
|
+
if (info.supports_thinking_type === "only" || info.supports_thinking_type === "both") {
|
|
622
|
+
updated.reasoning = true;
|
|
411
623
|
}
|
|
412
|
-
if (
|
|
413
|
-
updated.
|
|
624
|
+
else if (info.supports_thinking_type === "no") {
|
|
625
|
+
updated.reasoning = false;
|
|
414
626
|
}
|
|
415
|
-
|
|
416
|
-
|
|
627
|
+
else if (typeof info.supports_reasoning === "boolean") {
|
|
628
|
+
updated.reasoning = info.supports_reasoning;
|
|
629
|
+
}
|
|
630
|
+
return injectHeaders(updated);
|
|
631
|
+
};
|
|
632
|
+
// Safely template a future/discovered model ID using the static fallback.
|
|
633
|
+
const templateDiscovery = (info) => {
|
|
634
|
+
const templated = {
|
|
635
|
+
...fallbackTemplate,
|
|
636
|
+
id: info.id,
|
|
637
|
+
name: info.display_name || info.id,
|
|
638
|
+
contextWindow: info.context_length || fallbackTemplate.contextWindow,
|
|
639
|
+
reasoning: info.supports_thinking_type === undefined
|
|
640
|
+
? (info.supports_reasoning ?? false)
|
|
641
|
+
: info.supports_thinking_type !== "no",
|
|
642
|
+
input: info.supports_image_in === true ? ["text", "image"] : ["text"],
|
|
643
|
+
compat: discoveredCompat(fallbackTemplate, info),
|
|
644
|
+
};
|
|
645
|
+
return injectHeaders(templated);
|
|
646
|
+
};
|
|
647
|
+
// The official client treats only the explicit "anthropic" protocol as a
|
|
648
|
+
// separate wire format; absent and future values use the default Kimi route.
|
|
649
|
+
const supportedDiscovered = discovered.filter((info) => info.supports_tool_use !== false && info.protocol !== "anthropic");
|
|
650
|
+
const result = [];
|
|
651
|
+
const seen = new Set();
|
|
652
|
+
// 1. Walk the original model list to preserve order, replacing discovered
|
|
653
|
+
// static models and dropping undiscovered entries. A successful response
|
|
654
|
+
// is authoritative for the subscription's current entitlements.
|
|
655
|
+
for (const m of models) {
|
|
656
|
+
if (m.provider !== "kimi-coding-oauth") {
|
|
657
|
+
result.push(m);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const info = staticById.has(m.id) ? supportedDiscovered.find((d) => d.id === m.id) : undefined;
|
|
661
|
+
if (info) {
|
|
662
|
+
result.push(applyDiscovery(m, info));
|
|
663
|
+
seen.add(info.id);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// 2. Discovered models with IDs not present in the static list are templated
|
|
667
|
+
// from the fallback so future model IDs are safely usable.
|
|
668
|
+
for (const info of supportedDiscovered) {
|
|
669
|
+
if (!seen.has(info.id)) {
|
|
670
|
+
result.push(templateDiscovery(info));
|
|
671
|
+
seen.add(info.id);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return result;
|
|
417
675
|
},
|
|
418
676
|
};
|
|
419
677
|
//# sourceMappingURL=kimi-coding.js.map
|