pi-provider-kimi-code 0.6.11 → 0.6.13

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 CHANGED
@@ -3,7 +3,9 @@
3
3
  [![npm](https://img.shields.io/npm/v/pi-provider-kimi-code)](https://www.npmjs.com/package/pi-provider-kimi-code)
4
4
  [![license](https://img.shields.io/npm/l/pi-provider-kimi-code)](./LICENSE)
5
5
 
6
- > **Kimi Code extension for Pi — use K3, K2.7 Code, and HighSpeed models with your Kimi Code Plan.** This extension brings the full Kimi Code surface into Pi: shared OAuth login state with the official `kimi-code` CLI, Kimi Files API uploads, and Kimi-native tools such as `moonshot_search`, `moonshot_fetch`, and `kimi_datasource`. (Pi v0.82.0+ ships a built-in `kimi-coding` provider for the LLM layer; this extension covers everything around it.)
6
+ > **Kimi Code extension for Pi — use K3, K2.8 Code, and HighSpeed models with your Kimi Code Plan.**
7
+ >
8
+ > Why you need it: Pi v0.82.0+ ships a built-in `kimi-coding` provider for model discovery, reasoning controls, and OAuth login, but it does not provide this extension's Kimi Code integrations. Without this extension, Pi and the official `kimi-code` CLI log in separately (a login in one does not authenticate the other), large inline images blow up requests or fail because nothing uploads them through Kimi's Files API, Moonshot's server-side tools (`moonshot_search`, `moonshot_fetch`, `kimi_datasource`) are unavailable, Pi's tool schemas regularly exceed Moonshot's 15 KB per-tool limit and get rejected, and accounts on the wrong side of the mainland-cn / global split (kimi.com vs kimi.ai) fail with 401 because the built-in provider has no region switching. This extension closes those gaps — see [Why this exists](#why-this-exists) below for details.
7
9
 
8
10
  ## Why this exists
9
11
 
@@ -12,7 +14,8 @@ Use the built-in provider if you only want to chat with Kimi models. Install thi
12
14
  - **Share a session with the official `kimi-code` CLI.** Reuse an existing `kimi-code` login, or sync Pi's OAuth login back to `kimi-code` so both CLIs stay authenticated.
13
15
  - **Send files the Kimi way.** Large inline images go through Kimi's Files API and become `ms://` references instead of huge base64 payloads.
14
16
  - **Use Kimi-native tools.** `moonshot_search`, `moonshot_fetch`, and `kimi_datasource` are opt-in tools that call Moonshot's server-side services.
15
- - **Keep Pi's tools working.** Moonshot's API rejects tool schemas over 15 KB — a limit that Pi's extension ecosystem regularly hits ([#16](https://github.com/Leechael/pi-provider-kimi-code/issues/16), [#21](https://github.com/Leechael/pi-provider-kimi-code/issues/21)). This extension automatically deduplicates schemas with `$ref`/`$defs` before sending, so subagents and other extensions don't break.
17
+ - **Meet Kimi's official schema requirements.** Moonshot's API rejects tool schemas over 15 KB — a limit that Pi's extension ecosystem regularly hits ([#16](https://github.com/Leechael/pi-provider-kimi-code/issues/16), [#21](https://github.com/Leechael/pi-provider-kimi-code/issues/21)). This extension adapts Pi's tool schemas to Kimi's requirements by automatically deduplicating them with `$ref`/`$defs` before sending, so subagents and other extensions don't break.
18
+ - **Switch regions.** Kimi runs two managed sides (`mainland-cn` on kimi.com, `global` on kimi.ai). The extension carries the region selection through API endpoints, OAuth, and `/login`, so a kimi.ai account no longer dies with a 401 against the default `.com` endpoints.
16
19
  - **Embed in your own build.** The `KimiCode()` factory lets you ship Kimi Code support inside a custom Pi agent with programmatic config overrides — no file-based extension path needed.
17
20
 
18
21
  ## What this package adds
@@ -72,12 +75,25 @@ Pi's built-in `kimi-coding` provider already supports OAuth login via `/login`.
72
75
  - **Syncs refreshed tokens back to `kimi-code`.** Pi stores credentials at `~/.pi/agent/auth.json`; this extension keeps the official CLI's credential file (`~/.kimi-code/credentials/kimi-code.json`) up to date so both stay authenticated.
73
76
  - **Reuses existing `kimi-code` sessions.** If you already use the official CLI, the extension reads its session directly. Set `KIMI_CODE_HOME` if its home directory lives somewhere else.
74
77
  - **Supports the legacy `~/.kimi` path.** Read-only. Set `KIMI_SHARE_DIR` to override it.
78
+ - **Follows your region.** Managed endpoints follow the selected region (`mainland-cn` default, `global` for kimi.ai accounts) for API, OAuth, and `/login`. See [Region selection](#region-selection).
75
79
  - **`KIMI_API_KEY` fallback.** For CI or key-based access:
76
80
 
77
81
  ```bash
78
82
  KIMI_API_KEY=sk-... pi
79
83
  ```
80
84
 
85
+ ### Region selection
86
+
87
+ Kimi runs two managed sides: `api.kimi.com` / `auth.kimi.com` (mainland China, default) and `api.kimi.ai` / `auth.kimi.ai` (global). If your account lives on the kimi.ai side, a global OAuth session or API key against the default `.com` endpoints fails with 401.
88
+
89
+ Precedence, lowest to highest:
90
+
91
+ 1. Built-in default (`mainland-cn`)
92
+ 2. `KIMI_CODE_REGION` env var (`mainland-cn` or `global`)
93
+ 3. `region` in merged home/project `config.json` (`"mainland-cn"` / `"global"` / `null` = auto; project overrides home); the **Region** row in `/kimi-settings` edits this config
94
+
95
+ `KIMI_CODE_BASE_URL` / `KIMI_CODE_OAUTH_HOST` overrides always win, and unknown values fall back to `mainland-cn`. `/login kimi-coding` follows the selected region end to end. See [docs/ENV.md](docs/ENV.md) for details.
96
+
81
97
  ## Models (legacy provider registration)
82
98
 
83
99
  > **New users:** Use Pi's built-in `kimi-coding` provider for model discovery, reasoning controls, and OAuth login. This extension keeps its own model registration only for backward compatibility with existing sessions.
@@ -90,9 +106,9 @@ When this extension does register models, the official catalog determines what i
90
106
  | `kimi-coding/kimi-for-coding-highspeed` | Kimi for Coding High Speed | Official catalog |
91
107
  | `kimi-coding/k3` | Kimi K3 | Official catalog |
92
108
 
93
- Kimi's official catalog at `https://api.kimi.com/coding/v1/models` is authoritative for model availability and context windows. When discovery is unavailable, the extension uses the fallback models and defaults below.
109
+ Kimi's official catalog at `https://api.kimi.com/coding/v1/models` is authoritative for model availability and context windows. Since the K2.8 Preview rollout (2026-09-11), the `kimi-for-coding` ID is upgraded in place — same model ID, new engine, no config change needed. K2.8 advertises up to 1M tokens of context across all membership tiers; the catalog reports the exact window for your account.
94
110
 
95
- Fallback values:
111
+ When discovery is unavailable, the extension uses the fallback models and defaults below (conservative pre-discovery values):
96
112
 
97
113
  - Context window: `256k` tokens
98
114
  - Max output: `32k` tokens
@@ -113,7 +129,7 @@ Inside Pi, run:
113
129
  /kimi-settings
114
130
  ```
115
131
 
116
- That command shows your signed-in account (via Kimi's `/me` endpoint), the current server-side model name (e.g. "K2.7 Code High Speed"), your Kimi quota and Extra Usage balance, and lets you edit the home or project config. Changes apply to the active session tool set.
132
+ That command shows your signed-in account (via Kimi's `/me` endpoint), the current server-side model name (e.g. "K2.8 Code High Speed"), your Kimi quota and Extra Usage balance, the effective region, and lets you edit the home or project config. Changes apply to the active session tool set.
117
133
 
118
134
  Configurable settings include protocol mode, upload threshold, and per-tool enable/collapse.
119
135
 
@@ -137,6 +153,7 @@ Project config overrides home config with a deep merge. Missing files or missing
137
153
  ```json
138
154
  {
139
155
  "protocol": "openai",
156
+ "region": "global",
140
157
  "uploads": { "thresholdBytes": 1048576 },
141
158
  "tools": {
142
159
  "moonshot_search": { "enabled": true, "default_collapsed": true },
@@ -183,7 +200,8 @@ If you already use another search or fetch tool, pick one path for a session. Ov
183
200
  Most users do not need environment variables. Two are worth knowing:
184
201
 
185
202
  - `KIMI_API_KEY` — static API key for CI or key-based access.
186
- - `KIMI_CODE_PROTOCOL` — `openai` by default; set to `anthropic` for Anthropic-compatible requests.
203
+ - `KIMI_CODE_PROTOCOL` — `openai` by default; set to `anthropic` for Anthropic-compatible requests, or `responses` for the OpenAI Responses API.
204
+ - `KIMI_CODE_REGION` — `mainland-cn` (default) or `global`; selects the managed API/OAuth endpoints for kimi.com vs kimi.ai accounts.
187
205
 
188
206
  Tools, protocol, and upload threshold are all configurable through `/kimi-settings` or JSON config files.
189
207
 
@@ -219,7 +237,7 @@ The login flow always prints a verification URL. Copy it into a browser manually
219
237
 
220
238
  ### "Access denied" or subscription errors after a successful login
221
239
 
222
- Your Moonshot account needs an active Kimi Code Plan. If the same account works in `kimi-code`, re-run `/login kimi-coding` to refresh credentials.
240
+ Your Moonshot account needs an active Kimi Code Plan. If the same account works in `kimi-code`, re-run `/login kimi-coding` to refresh credentials. A 401 right after login can also mean a region mismatch — accounts registered on the kimi.ai side must run with `KIMI_CODE_REGION=global` (or the matching `region` config / `/kimi-settings` row).
223
241
 
224
242
  ### Tools do not show up or return errors
225
243
 
@@ -227,7 +245,7 @@ Run `/kimi-settings` and check whether the tool is enabled in the home or projec
227
245
 
228
246
  ### Large images fail with a payload error
229
247
 
230
- This extension uploads images over `KIMI_CODE_UPLOAD_THRESHOLD_BYTES` (default 1 MB) to Kimi's Files API and references them as `ms://`. Set `KIMI_CODE_DEBUG=1` to see upload decisions in the provider logs.
248
+ This extension uploads images over `KIMI_CODE_UPLOAD_THRESHOLD_BYTES` (default 1 MB) to Kimi's Files API and references them as `ms://`. Independently of that, a per-request inline media budget (~20 MB of base64 on the wire) drops oversized inline images and videos from the payload and leaves a media-kind-specific placeholder, such as `[image omitted: dropped to fit the request media budget]` or `[video omitted: dropped to fit the request media budget]`, so the model knows content was removed. Set `KIMI_CODE_DEBUG=1` to see upload and budget decisions in the provider logs.
231
249
 
232
250
  ### Prompt cache never seems to hit
233
251
 
package/index.ts CHANGED
@@ -43,10 +43,14 @@ import {
43
43
  } from "./src/config.ts";
44
44
  import {
45
45
  KIMI_GLOBAL_FALLBACK_SOURCE_ID,
46
+ KIMI_WIRE_PROTOCOLS,
46
47
  PROVIDER_ID,
47
48
  PROVIDER_VERSION,
49
+ type KimiRegion,
50
+ applyKimiRegionEnvBridge,
48
51
  getBaseUrl,
49
52
  getKimiApiType,
53
+ setKimiRegionOverride,
50
54
  } from "./src/constants.ts";
51
55
  import {
52
56
  type KimiOAuthCredentials,
@@ -73,6 +77,7 @@ import {
73
77
  type KimiConfigScope,
74
78
  buildSettingsTheme,
75
79
  formatByteSize,
80
+ formatRegionValue,
76
81
  formatScopeDescription,
77
82
  parseByteSizeInput,
78
83
  } from "./src/settings-ui.ts";
@@ -138,6 +143,12 @@ function reloadEffectiveKimiRuntimeConfig(
138
143
  state.cwd = cwd;
139
144
  state.config = config;
140
145
  state.projectTrusted = projectTrusted;
146
+ // Push the effective region (project/home JSON wins over the env capture;
147
+ // env KIMI_CODE_REGION already sits in the merged config via the env layer)
148
+ // and re-bridge pi core's OAuth host so /login follows settings changes
149
+ // without a restart.
150
+ setKimiRegionOverride(config.region);
151
+ applyKimiRegionEnvBridge();
141
152
  setStoreResolvedKimiConfig({
142
153
  model: resolveKimiModelConfig(
143
154
  config.model,
@@ -236,6 +247,7 @@ async function openSettingsMenu(
236
247
  list.updateValue(toolName, formatToolMenuValue(toolName));
237
248
  }
238
249
  list.updateValue("protocol", drafts[scope].protocol);
250
+ list.updateValue("region", formatRegionValue(drafts[scope].region));
239
251
  list.updateValue("uploadThreshold", formatByteSize(drafts[scope].uploads.thresholdBytes));
240
252
  };
241
253
 
@@ -268,6 +280,12 @@ async function openSettingsMenu(
268
280
  save();
269
281
  return;
270
282
  }
283
+ if (id === "region") {
284
+ drafts[scope].region = newValue === "auto" ? null : (newValue as KimiRegion);
285
+ list.updateValue(id, newValue);
286
+ save();
287
+ return;
288
+ }
271
289
  if (id === "uploadThreshold") {
272
290
  const bytes = parseByteSizeInput(newValue);
273
291
  if (bytes === undefined || bytes <= 0) {
@@ -340,7 +358,14 @@ async function openSettingsMenu(
340
358
  label: "Protocol",
341
359
  description: "API protocol for Kimi requests",
342
360
  currentValue: drafts[scope].protocol,
343
- values: ["openai", "anthropic"],
361
+ values: ["openai", "anthropic", "responses"],
362
+ });
363
+ items.push({
364
+ id: "region",
365
+ label: "Region",
366
+ description: "Managed deployment region (auto follows KIMI_CODE_REGION / default)",
367
+ currentValue: formatRegionValue(drafts[scope].region),
368
+ values: ["auto", "mainland-cn", "global"],
344
369
  });
345
370
  items.push({
346
371
  id: "uploadThreshold",
@@ -462,8 +487,8 @@ const MODULE_UNAVAILABLE_CODES = new Set([
462
487
  // model was resolved, so a session created before a protocol switch still
463
488
  // carries the other id and would keep crashing. streamSimpleKimi routes on the
464
489
  // model's wireProtocol / the resolved runtime config and never on model.api, so
465
- // both ids dispatch identically.
466
- const KIMI_FALLBACK_APIS = [getKimiApiType("openai"), getKimiApiType("anthropic")];
490
+ // all three ids dispatch identically.
491
+ const KIMI_FALLBACK_APIS = KIMI_WIRE_PROTOCOLS.map((protocol) => getKimiApiType(protocol));
467
492
 
468
493
  type PiAiApiRegistry = {
469
494
  registerApiProvider: (
@@ -653,6 +678,9 @@ function startModelDiscovery(pi: ExtensionAPI, state: KimiRuntimeState): void {
653
678
 
654
679
  export function KimiCode(overrides?: KimiCodeConfigPatch): ExtensionFactory {
655
680
  return async (pi: ExtensionAPI) => {
681
+ // Region handling (setKimiRegionOverride + the OAuth host bridge) happens
682
+ // in reloadEffectiveKimiRuntimeConfig, which runs just below and on every
683
+ // /kimi-settings save.
656
684
  const cwd = process.cwd();
657
685
  const config = loadKimiCodeConfig(
658
686
  { cwd, home: os.homedir(), includeProject: false },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-provider-kimi-code",
3
- "version": "0.6.11",
4
- "description": "Kimi Code models (K3, K2.7, HighSpeed) in Pi: OAuth shared login with kimi-code CLI, file uploads, and Kimi-native tools",
3
+ "version": "0.6.13",
4
+ "description": "Kimi Code models (K3, K2.8, HighSpeed) in Pi: OAuth shared login with kimi-code CLI, file uploads, and Kimi-native tools",
5
5
  "keywords": [
6
6
  "ai-provider",
7
7
  "coding-agent",
@@ -15,6 +15,7 @@
15
15
  "kimi-k2.5",
16
16
  "kimi-k2.6",
17
17
  "kimi-k2.7",
18
+ "kimi-k2.8",
18
19
  "kimi-k3",
19
20
  "llm",
20
21
  "llm-provider",
@@ -57,14 +58,14 @@
57
58
  "proper-lockfile": "^4.1.2"
58
59
  },
59
60
  "devDependencies": {
60
- "@earendil-works/pi-ai": "^0.83.0",
61
- "@earendil-works/pi-coding-agent": "^0.83.0",
62
- "@earendil-works/pi-tui": "^0.83.0",
63
- "@j178/prek": "^0.4.4",
61
+ "@earendil-works/pi-ai": "^0.99.2",
62
+ "@earendil-works/pi-coding-agent": "^0.99.2",
63
+ "@earendil-works/pi-tui": "^0.99.2",
64
+ "@j178/prek": "^0.5.2",
64
65
  "@types/node": "^24.0.0",
65
66
  "@types/proper-lockfile": "^4.1.4",
66
- "oxfmt": "^0.44.0",
67
- "oxlint": "^1.59.0",
67
+ "oxfmt": "^0.67.0",
68
+ "oxlint": "^1.82.0",
68
69
  "typescript": "^5.9.0"
69
70
  },
70
71
  "peerDependencies": {
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 } 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
 
@@ -46,7 +46,14 @@ export interface KimiCodeConfig {
46
46
  model: ModelConfig;
47
47
  tools: Record<KimiToolName, { enabled: boolean; default_collapsed: boolean }>;
48
48
  uploads: { thresholdBytes: number };
49
- protocol: "openai" | "anthropic";
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(
@@ -315,8 +331,26 @@ function requireProtocol(
315
331
  configPath: string,
316
332
  pointer: string,
317
333
  ): KimiCodeConfig["protocol"] {
318
- if (raw === "openai" || raw === "anthropic") return raw;
319
- return fail(configPath, pointer, `expected "openai" | "anthropic", got ${JSON.stringify(raw)}`);
334
+ if (raw === "openai" || raw === "anthropic" || raw === "responses") return raw;
335
+ return fail(
336
+ configPath,
337
+ pointer,
338
+ `expected "openai" | "anthropic" | "responses", got ${JSON.stringify(raw)}`,
339
+ );
340
+ }
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
+ );
320
354
  }
321
355
 
322
356
  function requireInputArray(raw: unknown, configPath: string, pointer: string): KimiInputModality[] {
@@ -434,6 +468,7 @@ export function validateKimiCodeConfig(
434
468
  ),
435
469
  },
436
470
  protocol: requireProtocol(config.protocol, configPath, "/protocol"),
471
+ region: requireRegionOrNull(config.region, configPath, "/region"),
437
472
  };
438
473
  }
439
474
 
@@ -483,6 +518,7 @@ function buildSources(
483
518
  tools,
484
519
  uploads: { thresholdBytes: sourceForPath(layers, ["uploads", "thresholdBytes"]) },
485
520
  protocol: sourceForPath(layers, ["protocol"]),
521
+ region: sourceForPath(layers, ["region"]),
486
522
  };
487
523
  }
488
524
 
package/src/constants.ts CHANGED
@@ -7,41 +7,133 @@ 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
- export const DEFAULT_OAUTH_HOST = "https://auth.kimi.com";
11
10
 
12
- export type KimiWireProtocol = "openai" | "anthropic";
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;
86
+
87
+ export const KIMI_WIRE_PROTOCOLS = ["openai", "anthropic", "responses"] as const;
88
+ export type KimiWireProtocol = (typeof KIMI_WIRE_PROTOCOLS)[number];
89
+ export type KimiApiProtocol = "openai-completions" | "anthropic-messages" | "openai-responses";
90
+ export type KimiApiType =
91
+ | "kimi-openai-completions"
92
+ | "kimi-anthropic-messages"
93
+ | "kimi-openai-responses";
94
+
95
+ // KIMI_CODE_PROTOCOL supports openai (default), anthropic, and responses.
96
+ // Unknown values fall back to openai, matching the previous two-protocol parser.
97
+ export function parseKimiWireProtocol(value: string | undefined): KimiWireProtocol {
98
+ if (value === "anthropic" || value === "responses") return value;
99
+ return "openai";
100
+ }
13
101
 
14
- // KIMI_CODE_PROTOCOL supports two values: "openai" (default) and "anthropic".
15
- export const ENV_KIMI_CODE_PROTOCOL: KimiWireProtocol =
16
- process.env.KIMI_CODE_PROTOCOL === "anthropic" ? "anthropic" : "openai";
102
+ export const ENV_KIMI_CODE_PROTOCOL: KimiWireProtocol = parseKimiWireProtocol(
103
+ process.env.KIMI_CODE_PROTOCOL,
104
+ );
17
105
 
18
106
  export const IS_OPENAI_PROTOCOL = ENV_KIMI_CODE_PROTOCOL === "openai";
19
107
 
20
- export function getApiProtocol(
21
- protocol: KimiWireProtocol,
22
- ): "openai-completions" | "anthropic-messages" {
23
- return protocol === "openai" ? "openai-completions" : "anthropic-messages";
108
+ export function getApiProtocol(protocol: KimiWireProtocol): KimiApiProtocol {
109
+ if (protocol === "anthropic") return "anthropic-messages";
110
+ if (protocol === "responses") return "openai-responses";
111
+ return "openai-completions";
24
112
  }
25
113
 
26
114
  export const PROTOCOL = getApiProtocol(ENV_KIMI_CODE_PROTOCOL);
27
115
 
28
- export function getKimiApiType(
29
- protocol: KimiWireProtocol,
30
- ): "kimi-openai-completions" | "kimi-anthropic-messages" {
31
- return protocol === "openai" ? "kimi-openai-completions" : "kimi-anthropic-messages";
116
+ export function getKimiApiType(protocol: KimiWireProtocol): KimiApiType {
117
+ if (protocol === "anthropic") return "kimi-anthropic-messages";
118
+ if (protocol === "responses") return "kimi-openai-responses";
119
+ return "kimi-openai-completions";
32
120
  }
33
121
 
34
122
  // Use a custom api identifier so this provider never conflicts with the
35
- // built-in "anthropic-messages" or "openai-completions" stream handlers.
123
+ // built-in anthropic-messages / openai-completions / openai-responses handlers.
36
124
  export const KIMI_API_TYPE = getKimiApiType(ENV_KIMI_CODE_PROTOCOL);
37
125
 
38
- export function getDefaultBaseUrl(protocol: KimiWireProtocol): string {
39
- return protocol === "openai" ? "https://api.kimi.com/coding/v1" : "https://api.kimi.com/coding";
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`;
40
132
  }
41
133
 
42
134
  export const DEFAULT_BASE_URL = getDefaultBaseUrl(ENV_KIMI_CODE_PROTOCOL);
43
135
 
44
- export const PROVIDER_VERSION = "0.6.11";
136
+ export const PROVIDER_VERSION = "0.6.13";
45
137
 
46
138
  export const KIMI_CODE_USER_AGENT = `pi-provider-kimi-code/${PROVIDER_VERSION}`;
47
139
  export const KIMI_PLATFORM = "pi";
@@ -64,9 +156,9 @@ export const PROVIDER_ID = "kimi-coding";
64
156
  export const KIMI_GLOBAL_FALLBACK_SOURCE_ID = "pi-provider-kimi-code-global-fallback";
65
157
 
66
158
  export function getOAuthHost(): string {
67
- const value =
68
- process.env.KIMI_CODE_OAUTH_HOST || process.env.KIMI_OAUTH_HOST || DEFAULT_OAUTH_HOST;
69
- return value.trim() || DEFAULT_OAUTH_HOST;
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;
70
162
  }
71
163
 
72
164
  export function getBaseUrl(protocol: KimiWireProtocol = ENV_KIMI_CODE_PROTOCOL): string {
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
@@ -186,6 +195,14 @@ function isRecord(value: unknown): value is Record<string, unknown> {
186
195
  return typeof value === "object" && value !== null && !Array.isArray(value);
187
196
  }
188
197
 
198
+ // Server /v1/models uses "response"; kimi-code maps that to openai_responses.
199
+ // Accept the server value plus the user-facing and internal aliases.
200
+ function parseKimiCatalogProtocol(value: unknown): KimiWireProtocol | undefined {
201
+ if (value === "openai" || value === "anthropic" || value === "responses") return value;
202
+ if (value === "response" || value === "openai_responses") return "responses";
203
+ return undefined;
204
+ }
205
+
189
206
  function parseThinkEfforts(value: unknown): {
190
207
  supportEfforts?: string[];
191
208
  defaultEffort?: string;
@@ -223,9 +240,8 @@ function parseKimiModelMetadata(model: KimiServerModel): KimiModelMetadata | und
223
240
  if (typeof model.supports_video_in === "boolean") {
224
241
  metadata.supportsVideoIn = model.supports_video_in;
225
242
  }
226
- if (model.protocol === "openai" || model.protocol === "anthropic") {
227
- metadata.protocol = model.protocol;
228
- }
243
+ const protocol = parseKimiCatalogProtocol(model.protocol);
244
+ if (protocol) metadata.protocol = protocol;
229
245
  Object.assign(metadata, parseThinkEfforts(model.think_efforts));
230
246
  return metadata;
231
247
  }
@@ -239,10 +255,15 @@ function getModelsUrl(protocol?: KimiWireProtocol): string {
239
255
  return buildModelsUrl(getBaseUrl(protocol));
240
256
  }
241
257
 
258
+ // Both official managed deployments serve /coding/v1/models; global (kimi.ai)
259
+ // users get model discovery too, not just mainland (.com).
242
260
  export function isOfficialKimiModelsUrl(url: string): boolean {
243
261
  try {
244
262
  const parsed = new URL(url);
245
- return parsed.origin === "https://api.kimi.com" && parsed.pathname === "/coding/v1/models";
263
+ return (
264
+ (parsed.origin === "https://api.kimi.com" || parsed.origin === "https://api.kimi.ai") &&
265
+ parsed.pathname === "/coding/v1/models"
266
+ );
246
267
  } catch {
247
268
  return false;
248
269
  }
package/src/oauth.ts CHANGED
@@ -155,7 +155,14 @@ async function lockStoredOAuthCredential(
155
155
  };
156
156
  }
157
157
 
158
- import { CLIENT_ID, PROVIDER_ID, RETRYABLE_REFRESH_STATUSES, getOAuthHost } from "./constants.ts";
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
 
@@ -435,6 +442,25 @@ async function lockKimiCodeCredentials(signal?: AbortSignal): Promise<LockedKimi
435
442
  };
436
443
  }
437
444
 
445
+ // Tokens are region-bound, but the kimi-code CLI credential file does not
446
+ // record which OAuth host minted them — so a fresh credential reused across
447
+ // regions (a mainland CLI login while KIMI_CODE_REGION=global, or vice versa)
448
+ // would 401 every request. Probe the current region's managed API before
449
+ // reuse. Only an explicit 401 disproves usability: network failures must not
450
+ // force an unnecessary re-login. Exported for tests.
451
+ export async function isKimiCredentialRegionValid(accessToken: string): Promise<boolean> {
452
+ const meUrl = `${normalizeKimiBaseV1(getBaseUrl())}/me`;
453
+ try {
454
+ const response = await fetch(meUrl, {
455
+ headers: { ...getCommonHeaders(), Authorization: `Bearer ${accessToken}` },
456
+ signal: AbortSignal.timeout(5_000),
457
+ });
458
+ return response.status !== 401;
459
+ } catch {
460
+ return true;
461
+ }
462
+ }
463
+
438
464
  async function tryReuseKimiCliCredentials(
439
465
  callbacks: OAuthLoginCallbacks,
440
466
  ): Promise<KimiOAuthCredentials | null> {
@@ -449,6 +475,12 @@ async function tryReuseKimiCliCredentials(
449
475
  callbacks.onProgress?.("Found existing Kimi credentials, reusing them.");
450
476
 
451
477
  if (stillFresh) {
478
+ if (!(await isKimiCredentialRegionValid(data.access_token!))) {
479
+ callbacks.onProgress?.(
480
+ "Existing Kimi credentials are not valid for the configured region, starting a fresh login.",
481
+ );
482
+ return null;
483
+ }
452
484
  const extras = await discoverKimiModelMetadata(data.access_token!);
453
485
  return {
454
486
  access: data.access_token!,
@@ -461,6 +493,15 @@ async function tryReuseKimiCliCredentials(
461
493
  callbacks.onProgress?.("Kimi access token expired, refreshing.");
462
494
  try {
463
495
  const token = await refreshAccessToken(data.refresh_token!);
496
+ // Probe before persisting: a token that fails the region check must not
497
+ // be written into the shared CLI credential file, or a later CLI-side
498
+ // reuse (or the next /login) would pick up a token that 401s.
499
+ if (!(await isKimiCredentialRegionValid(token.access_token))) {
500
+ callbacks.onProgress?.(
501
+ "Refreshed Kimi credentials are not valid for the configured region, starting a fresh login.",
502
+ );
503
+ return null;
504
+ }
464
505
  const expiresMs = Date.now() + token.expires_in * 1000;
465
506
  writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
466
507
  const extras = await discoverKimiModelMetadata(token.access_token);