@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.
@@ -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) with scope "kimi-code".
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
- const KIMI_CLI_VERSION = "1.35.0";
16
- const USER_AGENT = `KimiCLI/${KIMI_CLI_VERSION}`;
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 DEVICE_ID_PATH = path.join(os.homedir(), ".kimi", "device_id");
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
- function generateDeviceId() {
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 (/^[0-9a-f]{32}$/i.test(id)) {
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 = generateDeviceId();
59
+ const id = randomUUID();
60
+ sessionDeviceId = id;
47
61
  try {
48
- fs.mkdirSync(path.dirname(DEVICE_ID_PATH), { recursive: true });
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
- return value.replace(/[^\x20-\x7E]/g, "");
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.machine?.() || process.arch;
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
- const release = os.release();
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": "kimi_cli",
97
- "X-Msh-Version": KIMI_CLI_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.version?.() || `${os.type()} ${os.release()}`),
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 verification_uri = d.verification_uri;
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 verification_uri !== "string" ||
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, verification_uri, interval, expires_in };
299
+ return { device_code, user_code, verification_uri_complete, interval, expires_in };
186
300
  }
187
- async function pollForAccessToken(deviceCode, intervalSeconds, expiresIn, signal) {
188
- const deadline = Date.now() + expiresIn * 1000;
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 Error("Device code expired. Please try logging in again.");
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" || typeof raw.access_token !== "string") {
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 device = await startDeviceFlow();
318
- // Kimi's device page expects user_code as a query parameter
319
- const authUrl = new URL(device.verification_uri);
320
- authUrl.searchParams.set("user_code", device.user_code);
321
- options.onAuth({
322
- url: authUrl.toString(),
323
- instructions: `Enter code: ${device.user_code}`,
324
- });
325
- const tokenResp = await pollForAccessToken(device.device_code, device.interval, device.expires_in, options.signal);
326
- // Discover model entitlement
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.length > 0) {
475
+ if (models) {
476
+ credentials.models = models;
341
477
  const primary = models[0];
342
- credentials.modelId = primary.id;
343
- credentials.contextLength = primary.context_length;
344
- credentials.modelDisplay = primary.display_name;
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
- let models = [];
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
- modelId: credentials.modelId,
366
- contextLength: credentials.contextLength,
367
- modelDisplay: credentials.modelDisplay,
507
+ models: oldCreds.models,
508
+ modelId: oldCreds.modelId,
509
+ contextLength: oldCreds.contextLength,
510
+ modelDisplay: oldCreds.modelDisplay,
368
511
  };
369
- if (models.length > 0) {
512
+ if (models) {
513
+ fresh.models = models;
370
514
  const primary = models[0];
371
- fresh.modelId = primary.id;
372
- fresh.contextLength = primary.context_length;
373
- fresh.modelDisplay = primary.display_name;
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
- return models.map((m) => {
400
- if (m.provider !== "kimi-coding-oauth")
401
- return m;
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
- ...m,
404
- // The OAuth coding endpoint accepts OpenAI-style image_url data URLs for
405
- // kimi-for-coding; keep this capability even if static metadata is stale.
406
- input: Array.from(new Set([...m.input, "image"])),
407
- headers: { ...headers, ...(m.headers || {}) },
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 (creds.modelId) {
410
- updated.id = creds.modelId;
621
+ if (info.supports_thinking_type === "only" || info.supports_thinking_type === "both") {
622
+ updated.reasoning = true;
411
623
  }
412
- if (creds.contextLength) {
413
- updated.contextWindow = creds.contextLength;
624
+ else if (info.supports_thinking_type === "no") {
625
+ updated.reasoning = false;
414
626
  }
415
- return updated;
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