pi-provider-kimi-code 0.6.2 → 0.6.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-kimi-code",
3
- "version": "0.6.2",
3
+ "version": "0.6.5",
4
4
  "description": "Use Kimi Code in Pi without losing the Kimi-specific bits",
5
5
  "keywords": [
6
6
  "ai-provider",
@@ -50,9 +50,9 @@
50
50
  "pre-commit": "prek run"
51
51
  },
52
52
  "devDependencies": {
53
- "@earendil-works/pi-ai": "^0.78.0",
54
- "@earendil-works/pi-coding-agent": "^0.78.0",
55
- "@earendil-works/pi-tui": "^0.78.1",
53
+ "@earendil-works/pi-ai": "^0.79.10",
54
+ "@earendil-works/pi-coding-agent": "^0.79.10",
55
+ "@earendil-works/pi-tui": "^0.79.10",
56
56
  "@j178/prek": "^0.4.4",
57
57
  "@types/node": "^24.0.0",
58
58
  "oxfmt": "^0.44.0",
package/src/config.ts CHANGED
@@ -9,6 +9,8 @@ export type KimiToolName = (typeof KIMI_TOOL_NAMES)[number];
9
9
 
10
10
  export type KimiConfigSource = "runtime" | "env" | "project" | "home" | "default";
11
11
 
12
+ export const PI_CONFIG_DIR_NAME = ".pi";
13
+
12
14
  export type KimiInputModality = "text" | "image" | "video";
13
15
 
14
16
  export interface ModelReasoningEntry {
@@ -36,6 +38,8 @@ export interface ModelConfig {
36
38
 
37
39
  export interface KimiResolvedModelConfig extends ModelConfig {
38
40
  supportsThinkingType?: "only" | "no" | "both";
41
+ supportEfforts?: string[];
42
+ defaultEffort?: string;
39
43
  }
40
44
 
41
45
  export interface KimiCodeConfig {
@@ -83,6 +87,7 @@ export interface LoadKimiCodeConfigOptions {
83
87
  cwd: string;
84
88
  home: string;
85
89
  env?: NodeJS.ProcessEnv;
90
+ includeProject?: boolean;
86
91
  }
87
92
 
88
93
  export class ConfigError extends Error {
@@ -147,11 +152,11 @@ export function getRuntimeKimiCodeConfigOverride(): KimiCodeConfigPatch {
147
152
  }
148
153
 
149
154
  export function getGlobalKimiCodeConfigPath(home: string): string {
150
- return join(home, ".pi", "providers", PROVIDER_ID, "config.json");
155
+ return join(home, PI_CONFIG_DIR_NAME, "providers", PROVIDER_ID, "config.json");
151
156
  }
152
157
 
153
158
  export function getProjectKimiCodeConfigPath(cwd: string): string {
154
- return join(cwd, ".pi", "providers", PROVIDER_ID, "config.json");
159
+ return join(cwd, PI_CONFIG_DIR_NAME, "providers", PROVIDER_ID, "config.json");
155
160
  }
156
161
 
157
162
  export function kimiCodeConfigPath(home: string): string {
@@ -483,15 +488,23 @@ function buildSources(
483
488
  function loadLayers(
484
489
  options: LoadKimiCodeConfigOptions,
485
490
  ): Array<{ source: KimiConfigSource; config: Record<string, unknown> }> {
486
- return [
491
+ const layers: Array<{ source: KimiConfigSource; config: Record<string, unknown> }> = [
487
492
  { source: "home", config: readConfigFile(getGlobalKimiCodeConfigPath(options.home)) },
488
- { source: "project", config: readConfigFile(getProjectKimiCodeConfigPath(options.cwd)) },
493
+ ];
494
+ if (options.includeProject !== false) {
495
+ layers.push({
496
+ source: "project",
497
+ config: readConfigFile(getProjectKimiCodeConfigPath(options.cwd)),
498
+ });
499
+ }
500
+ layers.push(
489
501
  {
490
502
  source: "env",
491
503
  config: envConfigPatch(options.env ?? process.env) as Record<string, unknown>,
492
504
  },
493
505
  { source: "runtime", config: runtimeOverride as Record<string, unknown> },
494
- ];
506
+ );
507
+ return layers;
495
508
  }
496
509
 
497
510
  export function loadKimiCodeConfig(
package/src/constants.ts CHANGED
@@ -41,12 +41,12 @@ export function getDefaultBaseUrl(protocol: KimiWireProtocol): string {
41
41
 
42
42
  export const DEFAULT_BASE_URL = getDefaultBaseUrl(ENV_KIMI_CODE_PROTOCOL);
43
43
 
44
- export const PROVIDER_VERSION = "0.6.2";
44
+ export const PROVIDER_VERSION = "0.6.5";
45
45
 
46
46
  // Upstream kimi-code CLI version — used in User-Agent and X-Msh-Version
47
47
  // headers to match the official client's identity. Update this when
48
48
  // syncing with https://github.com/anthropics/kimi-code.
49
- export const KIMI_UPSTREAM_VERSION = "0.18.0";
49
+ export const KIMI_UPSTREAM_VERSION = "0.23.4";
50
50
  export const KIMI_CODE_USER_AGENT = `kimi-code-cli/${KIMI_UPSTREAM_VERSION}`;
51
51
  export const KIMI_PLATFORM = "kimi_code_cli";
52
52
 
package/src/device.ts CHANGED
@@ -152,6 +152,22 @@ function getStableDeviceId(): string {
152
152
  return DEVICE_ID;
153
153
  }
154
154
 
155
+ export function parseKimiCodeCustomHeaders(
156
+ env: NodeJS.ProcessEnv = process.env,
157
+ ): Record<string, string> {
158
+ const raw = env.KIMI_CODE_CUSTOM_HEADERS?.trim();
159
+ if (!raw) return {};
160
+ const headers: Record<string, string> = {};
161
+ for (const line of raw.split("\n")) {
162
+ const colon = line.indexOf(":");
163
+ if (colon < 0) continue;
164
+ const name = line.slice(0, colon).trim();
165
+ if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) continue;
166
+ headers[name] = asciiHeaderValue(line.slice(colon + 1));
167
+ }
168
+ return headers;
169
+ }
170
+
155
171
  export function getCommonHeaders(): Record<string, string> {
156
172
  const headers = {
157
173
  "User-Agent": KIMI_CODE_USER_AGENT,
@@ -166,3 +182,28 @@ export function getCommonHeaders(): Record<string, string> {
166
182
  Object.entries(headers).map(([key, value]) => [key, asciiHeaderValue(value)]),
167
183
  ) as Record<string, string>;
168
184
  }
185
+
186
+ const RESERVED_PROVIDER_HEADERS = new Set([
187
+ "accept",
188
+ "anthropic-version",
189
+ "authorization",
190
+ "content-type",
191
+ "user-agent",
192
+ "x-api-key",
193
+ ]);
194
+
195
+ function isReservedProviderHeader(name: string): boolean {
196
+ const normalized = name.toLowerCase();
197
+ return RESERVED_PROVIDER_HEADERS.has(normalized) || normalized.startsWith("x-msh-");
198
+ }
199
+
200
+ export function getKimiProviderHeaders(
201
+ env: NodeJS.ProcessEnv = process.env,
202
+ ): Record<string, string> {
203
+ const customHeaders = Object.fromEntries(
204
+ Object.entries(parseKimiCodeCustomHeaders(env)).filter(
205
+ ([name]) => !isReservedProviderHeader(name),
206
+ ),
207
+ );
208
+ return { ...customHeaders, ...getCommonHeaders() };
209
+ }
package/src/models.ts CHANGED
@@ -5,9 +5,9 @@ import type { Api, Model, OAuthCredentials } from "@earendil-works/pi-ai";
5
5
  import type { KimiInputModality, KimiResolvedModelConfig, ModelConfig } from "./config.ts";
6
6
 
7
7
  import { type KimiWireProtocol, getBaseUrl } from "./constants.ts";
8
- import { getCommonHeaders } from "./device.ts";
8
+ import { getKimiProviderHeaders } from "./device.ts";
9
9
 
10
- export interface KimiOAuthExtras {
10
+ export interface KimiModelMetadata {
11
11
  wireModelId?: string;
12
12
  modelDisplay?: string;
13
13
  contextLength?: number;
@@ -15,6 +15,13 @@ export interface KimiOAuthExtras {
15
15
  supportsImageIn?: boolean;
16
16
  supportsVideoIn?: boolean;
17
17
  supportsThinkingType?: "only" | "no" | "both";
18
+ protocol?: KimiWireProtocol;
19
+ supportEfforts?: string[];
20
+ defaultEffort?: string;
21
+ }
22
+
23
+ export interface KimiOAuthExtras extends KimiModelMetadata {
24
+ modelCatalog?: Record<string, KimiModelMetadata>;
18
25
  }
19
26
 
20
27
  export type KimiOAuthCredentials = OAuthCredentials & KimiOAuthExtras;
@@ -47,6 +54,9 @@ function mergeInputModalities(
47
54
  const COST_STANDARD = { input: 0.897, output: 3.724, cacheRead: 0.179, cacheWrite: 0.897 };
48
55
  const COST_HIGH_SPEED = { input: 1.793, output: 7.448, cacheRead: 0.359, cacheWrite: 1.793 };
49
56
 
57
+ export const KIMI_CODING_MODEL_ID = "kimi-for-coding";
58
+ export const KIMI_CODING_HIGHSPEED_MODEL_ID = "kimi-for-coding-highspeed";
59
+
50
60
  function resolveModelCost(modelDisplay: string | undefined): {
51
61
  input: number;
52
62
  output: number;
@@ -57,13 +67,17 @@ function resolveModelCost(modelDisplay: string | undefined): {
57
67
  return COST_STANDARD;
58
68
  }
59
69
 
60
- export function buildKimiModelFromConfig(config: ModelConfig): Model<Api> {
70
+ export function buildKimiModelFromConfig(
71
+ config: ModelConfig,
72
+ modelId = KIMI_CODING_MODEL_ID,
73
+ ): Model<Api> {
74
+ const isHighSpeed = modelId === KIMI_CODING_HIGHSPEED_MODEL_ID;
61
75
  return {
62
- id: "kimi-for-coding",
63
- name: "Kimi for Coding",
76
+ id: modelId,
77
+ name: isHighSpeed ? "Kimi for Coding High Speed" : "Kimi for Coding",
64
78
  reasoning: config.reasoning,
65
79
  input: [...config.input] as unknown as ("text" | "image" | "video")[],
66
- cost: { ...COST_STANDARD },
80
+ cost: { ...(isHighSpeed ? COST_HIGH_SPEED : COST_STANDARD) },
67
81
  contextWindow: config.contextWindow,
68
82
  maxTokens: config.maxTokens,
69
83
  } as Model<Api>;
@@ -86,6 +100,8 @@ export function resolveKimiModelConfig(
86
100
  if (typeof extras.supportsImageIn === "boolean" || typeof extras.supportsVideoIn === "boolean") {
87
101
  resolved.input = mergeInputModalities(resolved.input, extras);
88
102
  }
103
+ if (extras.supportEfforts) resolved.supportEfforts = [...extras.supportEfforts];
104
+ if (extras.defaultEffort) resolved.defaultEffort = extras.defaultEffort;
89
105
  return resolved;
90
106
  }
91
107
 
@@ -97,6 +113,8 @@ interface KimiServerModel {
97
113
  supports_image_in?: unknown;
98
114
  supports_video_in?: unknown;
99
115
  supports_thinking_type?: unknown;
116
+ protocol?: unknown;
117
+ think_efforts?: unknown;
100
118
  }
101
119
 
102
120
  function parseSupportsThinkingType(value: unknown): "only" | "no" | "both" | undefined {
@@ -104,6 +122,54 @@ function parseSupportsThinkingType(value: unknown): "only" | "no" | "both" | und
104
122
  return undefined;
105
123
  }
106
124
 
125
+ function isRecord(value: unknown): value is Record<string, unknown> {
126
+ return typeof value === "object" && value !== null && !Array.isArray(value);
127
+ }
128
+
129
+ function parseThinkEfforts(value: unknown): {
130
+ supportEfforts?: string[];
131
+ defaultEffort?: string;
132
+ } {
133
+ if (!isRecord(value) || value.support !== true) return {};
134
+ const validEfforts = Array.isArray(value.valid_efforts)
135
+ ? value.valid_efforts.filter(
136
+ (effort): effort is string => typeof effort === "string" && !!effort,
137
+ )
138
+ : [];
139
+ return {
140
+ ...(validEfforts.length > 0 ? { supportEfforts: validEfforts } : {}),
141
+ ...(typeof value.default_effort === "string" && value.default_effort
142
+ ? { defaultEffort: value.default_effort }
143
+ : {}),
144
+ };
145
+ }
146
+
147
+ function parseKimiModelMetadata(model: KimiServerModel): KimiModelMetadata | undefined {
148
+ if (typeof model.id !== "string" || !model.id) return undefined;
149
+ const metadata: KimiModelMetadata = { wireModelId: model.id };
150
+ if (typeof model.display_name === "string") metadata.modelDisplay = model.display_name;
151
+ if (typeof model.context_length === "number" && model.context_length > 0) {
152
+ metadata.contextLength = model.context_length;
153
+ }
154
+ const thinkingType = parseSupportsThinkingType(model.supports_thinking_type);
155
+ if (thinkingType) {
156
+ metadata.supportsThinkingType = thinkingType;
157
+ } else if (typeof model.supports_reasoning === "boolean") {
158
+ metadata.supportsReasoning = model.supports_reasoning;
159
+ }
160
+ if (typeof model.supports_image_in === "boolean") {
161
+ metadata.supportsImageIn = model.supports_image_in;
162
+ }
163
+ if (typeof model.supports_video_in === "boolean") {
164
+ metadata.supportsVideoIn = model.supports_video_in;
165
+ }
166
+ if (model.protocol === "openai" || model.protocol === "anthropic") {
167
+ metadata.protocol = model.protocol;
168
+ }
169
+ Object.assign(metadata, parseThinkEfforts(model.think_efforts));
170
+ return metadata;
171
+ }
172
+
107
173
  export function buildModelsUrl(baseUrl: string): string {
108
174
  const base = baseUrl.replace(/\/+$/, "");
109
175
  return base.endsWith("/v1") ? `${base}/models` : `${base}/v1/models`;
@@ -127,7 +193,7 @@ export async function discoverKimiModelMetadata(
127
193
  const response = await fetch(getModelsUrl(protocol), {
128
194
  signal: controller.signal,
129
195
  headers: {
130
- ...getCommonHeaders(),
196
+ ...getKimiProviderHeaders(),
131
197
  Authorization: `Bearer ${accessToken}`,
132
198
  Accept: "application/json",
133
199
  },
@@ -135,26 +201,18 @@ export async function discoverKimiModelMetadata(
135
201
  if (!response.ok) return {};
136
202
  const json = (await response.json()) as { data?: unknown };
137
203
  const list = Array.isArray(json.data) ? (json.data as KimiServerModel[]) : [];
138
- const preferred = list.find((m) => typeof m.id === "string" && m.id === "kimi-for-coding");
139
- if (!preferred || typeof preferred.id !== "string") return {};
140
- const extras: KimiOAuthExtras = { wireModelId: preferred.id };
141
- if (typeof preferred.display_name === "string") extras.modelDisplay = preferred.display_name;
142
- if (typeof preferred.context_length === "number" && preferred.context_length > 0) {
143
- extras.contextLength = preferred.context_length;
144
- }
145
- const thinkingType = parseSupportsThinkingType(preferred.supports_thinking_type);
146
- if (thinkingType) {
147
- extras.supportsThinkingType = thinkingType;
148
- } else if (typeof preferred.supports_reasoning === "boolean") {
149
- extras.supportsReasoning = preferred.supports_reasoning;
150
- }
151
- if (typeof preferred.supports_image_in === "boolean") {
152
- extras.supportsImageIn = preferred.supports_image_in;
204
+ const supportedIds = new Set([KIMI_CODING_MODEL_ID, KIMI_CODING_HIGHSPEED_MODEL_ID]);
205
+ const modelCatalog: Record<string, KimiModelMetadata> = {};
206
+ for (const model of list) {
207
+ if (typeof model.id !== "string" || !supportedIds.has(model.id)) continue;
208
+ const metadata = parseKimiModelMetadata(model);
209
+ if (metadata) modelCatalog[model.id] = metadata;
153
210
  }
154
- if (typeof preferred.supports_video_in === "boolean") {
155
- extras.supportsVideoIn = preferred.supports_video_in;
211
+ const preferred = modelCatalog[KIMI_CODING_MODEL_ID];
212
+ if (!preferred) {
213
+ return Object.keys(modelCatalog).length > 0 ? { modelCatalog } : {};
156
214
  }
157
- return extras;
215
+ return { ...preferred, modelCatalog };
158
216
  } catch {
159
217
  return {};
160
218
  } finally {
@@ -162,12 +220,23 @@ export async function discoverKimiModelMetadata(
162
220
  }
163
221
  }
164
222
 
223
+ export function getKimiModelMetadata(extras: KimiOAuthExtras, modelId: string): KimiModelMetadata {
224
+ const discovered = extras.modelCatalog?.[modelId];
225
+ if (discovered) return discovered;
226
+ return modelId === KIMI_CODING_MODEL_ID ? extras : {};
227
+ }
228
+
165
229
  export function applyKimiOAuthExtrasToModel(
166
230
  model: Model<Api>,
167
- extras: KimiOAuthExtras,
231
+ extras: KimiModelMetadata,
168
232
  ): Model<Api> {
169
- const next: Model<Api> & { wireModelId?: string; supportsThinkingType?: "only" | "no" | "both" } =
170
- { ...model };
233
+ const next: Model<Api> & {
234
+ wireModelId?: string;
235
+ supportsThinkingType?: "only" | "no" | "both";
236
+ wireProtocol?: KimiWireProtocol;
237
+ supportEfforts?: string[];
238
+ defaultEffort?: string;
239
+ } = { ...model };
171
240
  if (typeof extras.modelDisplay === "string" && extras.modelDisplay) {
172
241
  next.name = extras.modelDisplay;
173
242
  next.cost = resolveModelCost(extras.modelDisplay);
@@ -189,5 +258,8 @@ export function applyKimiOAuthExtrasToModel(
189
258
  const input = mergeInputModalities(next.input as KimiInputModality[], extras);
190
259
  (next as unknown as { input: string[] }).input = input;
191
260
  }
261
+ if (extras.protocol) next.wireProtocol = extras.protocol;
262
+ if (extras.supportEfforts) next.supportEfforts = [...extras.supportEfforts];
263
+ if (extras.defaultEffort) next.defaultEffort = extras.defaultEffort;
192
264
  return next;
193
265
  }
package/src/oauth.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  // reuse, the login / refresh handlers wired into pi's OAuth interface, and
3
3
  // the stream-level auth refresh used by the streaming handler.
4
4
 
5
- import { existsSync, readFileSync } from "node:fs";
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import os from "node:os";
7
- import { join } from "node:path";
7
+ import { dirname, join } from "node:path";
8
8
 
9
9
  import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
10
10
  import type { OAuthCredential } from "@earendil-works/pi-coding-agent";
@@ -123,6 +123,16 @@ interface RefreshAccessTokenOptions {
123
123
  sleep?: (ms: number) => Promise<void>;
124
124
  }
125
125
 
126
+ export const KIMI_LOGIN_REQUIRED_MESSAGE =
127
+ "Kimi Code authorization is no longer valid. Sign in again with /login kimi-coding.";
128
+
129
+ export class KimiLoginRequiredError extends Error {
130
+ constructor() {
131
+ super(KIMI_LOGIN_REQUIRED_MESSAGE);
132
+ this.name = "KimiLoginRequiredError";
133
+ }
134
+ }
135
+
126
136
  function sleep(ms: number): Promise<void> {
127
137
  return new Promise((resolve) => setTimeout(resolve, ms));
128
138
  }
@@ -152,8 +162,15 @@ export async function refreshAccessToken(
152
162
 
153
163
  if (!response.ok) {
154
164
  const text = await response.text().catch(() => "");
155
- if (response.status === 401 || response.status === 403) {
156
- throw new Error(`Token refresh unauthorized: ${text}`);
165
+ let errorCode = "";
166
+ try {
167
+ const body = JSON.parse(text) as { error?: unknown };
168
+ if (typeof body.error === "string") errorCode = body.error;
169
+ } catch {
170
+ // Non-JSON OAuth errors are classified by status below.
171
+ }
172
+ if (response.status === 401 || response.status === 403 || errorCode === "invalid_grant") {
173
+ throw new KimiLoginRequiredError();
157
174
  }
158
175
  if (RETRYABLE_REFRESH_STATUSES.has(response.status)) {
159
176
  throw new Error(`Token refresh retryable: ${response.status} ${text}`);
@@ -170,7 +187,7 @@ export async function refreshAccessToken(
170
187
  } catch (error) {
171
188
  lastError = error;
172
189
  const message = error instanceof Error ? error.message : String(error);
173
- if (message.startsWith("Token refresh unauthorized:")) throw error;
190
+ if (error instanceof KimiLoginRequiredError) throw error;
174
191
  if (message.startsWith("Token refresh failed:")) throw error;
175
192
  if (attempt < maxRetries - 1) {
176
193
  await wait(2 ** attempt * 1000);
@@ -196,15 +213,19 @@ interface KimiCliCredentialsFile {
196
213
  access_token?: string;
197
214
  refresh_token?: string;
198
215
  expires_at?: number; // Unix seconds (upstream convention)
216
+ scope?: string;
217
+ token_type?: string;
218
+ expires_in?: number;
199
219
  }
200
220
 
201
- function getKimiCredentialPaths(): string[] {
221
+ function getKimiCodeCredentialPath(): string {
202
222
  const kimiCodeHome = process.env.KIMI_CODE_HOME || join(os.homedir(), ".kimi-code");
223
+ return join(kimiCodeHome, "credentials", "kimi-code.json");
224
+ }
225
+
226
+ function getKimiCredentialPaths(): string[] {
203
227
  const shareDir = process.env.KIMI_SHARE_DIR || join(os.homedir(), ".kimi");
204
- return [
205
- join(kimiCodeHome, "credentials", "kimi-code.json"),
206
- join(shareDir, "credentials", "kimi-code.json"),
207
- ];
228
+ return [getKimiCodeCredentialPath(), join(shareDir, "credentials", "kimi-code.json")];
208
229
  }
209
230
 
210
231
  function readKimiCliCredentials(): KimiCliCredentialsFile | null {
@@ -221,6 +242,25 @@ function readKimiCliCredentials(): KimiCliCredentialsFile | null {
221
242
  return null;
222
243
  }
223
244
 
245
+ function kimiCodeCredentialExists(): boolean {
246
+ return existsSync(getKimiCodeCredentialPath());
247
+ }
248
+
249
+ function writeKimiCodeCredentials(access: string, refresh: string, expiresMs: number): void {
250
+ const path = getKimiCodeCredentialPath();
251
+ const dir = dirname(path);
252
+ mkdirSync(dir, { recursive: true });
253
+ const data: KimiCliCredentialsFile = {
254
+ access_token: access,
255
+ refresh_token: refresh,
256
+ expires_at: Math.floor(expiresMs / 1000),
257
+ scope: "",
258
+ token_type: "Bearer",
259
+ expires_in: Math.max(0, Math.floor((expiresMs - Date.now()) / 1000)),
260
+ };
261
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
262
+ }
263
+
224
264
  async function tryReuseKimiCliCredentials(
225
265
  callbacks: OAuthLoginCallbacks,
226
266
  ): Promise<KimiOAuthCredentials | null> {
@@ -247,11 +287,13 @@ async function tryReuseKimiCliCredentials(
247
287
  callbacks.onProgress?.("Kimi access token expired, refreshing.");
248
288
  try {
249
289
  const token = await refreshAccessToken(data.refresh_token!);
290
+ const expiresMs = Date.now() + token.expires_in * 1000;
291
+ writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
250
292
  const extras = await discoverKimiModelMetadata(token.access_token);
251
293
  return {
252
294
  access: token.access_token,
253
295
  refresh: token.refresh_token,
254
- expires: Date.now() + token.expires_in * 1000,
296
+ expires: expiresMs,
255
297
  ...extras,
256
298
  };
257
299
  } catch {
@@ -319,11 +361,13 @@ export async function loginKimiCode(callbacks: OAuthLoginCallbacks): Promise<Kim
319
361
  }
320
362
 
321
363
  if (token) {
364
+ const expiresMs = Date.now() + token.expires_in * 1000;
365
+ writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
322
366
  const extras = await discoverKimiModelMetadata(token.access_token);
323
367
  return {
324
368
  access: token.access_token,
325
369
  refresh: token.refresh_token,
326
- expires: Date.now() + token.expires_in * 1000,
370
+ expires: expiresMs,
327
371
  ...extras,
328
372
  };
329
373
  }
@@ -335,12 +379,24 @@ export async function loginKimiCode(callbacks: OAuthLoginCallbacks): Promise<Kim
335
379
  export async function refreshKimiCodeToken(
336
380
  credentials: OAuthCredentials,
337
381
  ): Promise<KimiOAuthCredentials> {
338
- const token = await refreshAccessToken(credentials.refresh);
382
+ const kimiCred = kimiCodeCredentialExists() ? readKimiCliCredentials() : null;
383
+ let token: Awaited<ReturnType<typeof refreshAccessToken>>;
384
+ try {
385
+ token = await refreshAccessToken(credentials.refresh);
386
+ } catch (error) {
387
+ if (kimiCred?.refresh_token && kimiCred.refresh_token !== credentials.refresh) {
388
+ token = await refreshAccessToken(kimiCred.refresh_token);
389
+ } else {
390
+ throw error;
391
+ }
392
+ }
393
+ const expiresMs = Date.now() + token.expires_in * 1000;
394
+ writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
339
395
  const extras = await discoverKimiModelMetadata(token.access_token);
340
396
  return {
341
397
  access: token.access_token,
342
398
  refresh: token.refresh_token,
343
- expires: Date.now() + token.expires_in * 1000,
399
+ expires: expiresMs,
344
400
  ...extras,
345
401
  };
346
402
  }
@@ -356,6 +412,10 @@ export async function refreshKimiCodeToken(
356
412
  // new credentials under a file lock), and retry once.
357
413
  // =============================================================================
358
414
 
415
+ export function getKimiApiKey(credentials: OAuthCredentials): string {
416
+ return credentials.access;
417
+ }
418
+
359
419
  export function isKimiAuthErrorMessage(message: unknown): boolean {
360
420
  const text = String(message ?? "").toLowerCase();
361
421
  return (
@@ -368,37 +428,79 @@ export function isKimiAuthErrorMessage(message: unknown): boolean {
368
428
 
369
429
  export async function refreshKimiAuthToken(currentKey: string): Promise<string | null> {
370
430
  try {
431
+ const kimiCred = readKimiCliCredentials();
371
432
  const storage = AuthStorage.create();
372
- const cred = storage.get(PROVIDER_ID);
373
- if (!cred || cred.type !== "oauth") {
433
+ const piCred = storage.get(PROVIDER_ID);
434
+ const piOAuth = piCred?.type === "oauth" ? piCred : null;
435
+
436
+ if (piOAuth) {
437
+ if (piOAuth.access !== currentKey && Date.now() < piOAuth.expires) {
438
+ console.error("[kimi-coding] auth refresh: reusing newer on-disk token");
439
+ return piOAuth.access;
440
+ }
441
+
442
+ console.error("[kimi-coding] auth refresh: requesting new access token");
443
+ let refreshed: Awaited<ReturnType<typeof refreshAccessToken>>;
444
+ try {
445
+ refreshed = await refreshAccessToken(piOAuth.refresh);
446
+ } catch (error) {
447
+ if (!kimiCred?.refresh_token || kimiCred.refresh_token === piOAuth.refresh) throw error;
448
+ const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
449
+ if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
450
+ const recovered: OAuthCredential = {
451
+ ...piOAuth,
452
+ type: "oauth",
453
+ access: kimiCred.access_token!,
454
+ refresh: kimiCred.refresh_token,
455
+ expires: kimiExpiresMs,
456
+ };
457
+ storage.set(PROVIDER_ID, recovered);
458
+ console.error("[kimi-coding] auth refresh: recovered newer kimi-code token");
459
+ return recovered.access;
460
+ }
461
+ console.error("[kimi-coding] auth refresh: pi token rejected, trying kimi-code token");
462
+ refreshed = await refreshAccessToken(kimiCred.refresh_token);
463
+ }
464
+
465
+ const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
466
+ const newCred: OAuthCredential = {
467
+ ...piOAuth,
468
+ type: "oauth",
469
+ access: refreshed.access_token,
470
+ refresh: refreshed.refresh_token,
471
+ expires: newExpiresMs,
472
+ };
473
+ storage.set(PROVIDER_ID, newCred);
474
+ writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
475
+ console.error("[kimi-coding] auth refresh: new token persisted");
476
+ return newCred.access;
477
+ }
478
+
479
+ if (!kimiCred) {
374
480
  console.error(
375
481
  `[kimi-coding] auth refresh skipped: no OAuth credentials for ${PROVIDER_ID} on disk`,
376
482
  );
377
483
  return null;
378
484
  }
379
485
 
380
- // If disk already has a different valid token (e.g., another process or a
381
- // previous retry refreshed it while the caller's in-memory cache went
382
- // stale), reuse it without hitting the OAuth endpoint.
383
- if (cred.access !== currentKey && Date.now() < cred.expires) {
384
- console.error("[kimi-coding] auth refresh: reusing newer on-disk token");
385
- return cred.access;
486
+ const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
487
+ if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
488
+ console.error("[kimi-coding] auth refresh: reusing newer kimi-code token");
489
+ return kimiCred.access_token!;
386
490
  }
387
491
 
388
- console.error("[kimi-coding] auth refresh: requesting new access token");
389
- const refreshed = await refreshAccessToken(cred.refresh);
390
- const newCred: OAuthCredential = {
391
- ...cred,
392
- type: "oauth",
393
- access: refreshed.access_token,
394
- refresh: refreshed.refresh_token,
395
- expires: Date.now() + refreshed.expires_in * 1000,
396
- };
397
- storage.set(PROVIDER_ID, newCred);
398
- console.error("[kimi-coding] auth refresh: new token persisted");
399
- return newCred.access;
492
+ console.error("[kimi-coding] auth refresh: requesting new access token via kimi-code");
493
+ const refreshed = await refreshAccessToken(kimiCred.refresh_token!);
494
+ const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
495
+ writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
496
+ console.error("[kimi-coding] auth refresh: new token persisted to kimi-code");
497
+ return refreshed.access_token;
400
498
  } catch (err) {
401
- console.error("[kimi-coding] auth refresh failed:", err);
499
+ if (err instanceof KimiLoginRequiredError) {
500
+ console.error(`[kimi-coding] ${KIMI_LOGIN_REQUIRED_MESSAGE}`);
501
+ } else {
502
+ console.error("[kimi-coding] auth refresh failed:", err);
503
+ }
402
504
  return null;
403
505
  }
404
506
  }