pi-provider-kimi-code 0.5.0 → 0.5.1

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,39 +1,39 @@
1
1
  {
2
2
  "name": "pi-provider-kimi-code",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Use Kimi Code in Pi without losing the Kimi-specific bits",
5
5
  "keywords": [
6
- "pi-package",
7
- "pi-extension",
8
- "pi-provider",
9
- "pi",
10
- "pi-coding-agent",
11
- "pi-mono",
12
- "coding-agent",
13
6
  "ai-provider",
14
- "llm",
15
- "llm-provider",
7
+ "coding-agent",
8
+ "device-code",
16
9
  "kimi",
17
- "kimi-for-coding",
18
10
  "kimi-coder",
11
+ "kimi-for-coding",
19
12
  "kimi-k2",
20
- "kimi-k2.6",
21
13
  "kimi-k2.5",
14
+ "kimi-k2.6",
15
+ "llm",
16
+ "llm-provider",
22
17
  "moonshot",
23
18
  "moonshotai",
24
19
  "oauth",
25
- "device-code",
20
+ "pi",
21
+ "pi-coding-agent",
22
+ "pi-extension",
23
+ "pi-mono",
24
+ "pi-package",
25
+ "pi-provider",
26
26
  "subscription"
27
27
  ],
28
+ "bugs": {
29
+ "url": "https://github.com/Leechael/pi-provider-kimi-code/issues"
30
+ },
28
31
  "license": "MIT",
32
+ "author": "Leechael",
29
33
  "repository": {
30
34
  "type": "git",
31
35
  "url": "https://github.com/Leechael/pi-provider-kimi-code"
32
36
  },
33
- "bugs": {
34
- "url": "https://github.com/Leechael/pi-provider-kimi-code/issues"
35
- },
36
- "author": "Leechael",
37
37
  "files": [
38
38
  "index.ts",
39
39
  "src",
@@ -51,17 +51,19 @@
51
51
  "format:check": "oxfmt . --check",
52
52
  "pre-commit": "prek run"
53
53
  },
54
- "peerDependencies": {
55
- "@earendil-works/pi-coding-agent": "*",
56
- "@earendil-works/pi-ai": "*"
57
- },
58
54
  "devDependencies": {
55
+ "@earendil-works/pi-ai": "^0.78.0",
56
+ "@earendil-works/pi-coding-agent": "^0.78.0",
59
57
  "@j178/prek": "^0.3.8",
60
58
  "@types/node": "^24.0.0",
61
59
  "oxfmt": "^0.44.0",
62
60
  "oxlint": "^1.59.0",
63
61
  "typescript": "^5.9.0"
64
62
  },
63
+ "peerDependencies": {
64
+ "@earendil-works/pi-ai": "*",
65
+ "@earendil-works/pi-coding-agent": "*"
66
+ },
65
67
  "pi": {
66
68
  "extensions": [
67
69
  "./index.ts"
package/src/constants.ts CHANGED
@@ -24,10 +24,9 @@ export const DEFAULT_BASE_URL = IS_OPENAI_PROTOCOL
24
24
  ? "https://api.kimi.com/coding/v1"
25
25
  : "https://api.kimi.com/coding";
26
26
 
27
- export const KIMI_CODE_VERSION = "0.1.1";
28
- export const KIMI_CODE_USER_AGENT_PRODUCT = "kimi-code-cli";
29
- export const KIMI_CODE_USER_AGENT = `${KIMI_CODE_USER_AGENT_PRODUCT}/${KIMI_CODE_VERSION}`;
30
- export const KIMI_PLATFORM = "kimi-code-cli";
27
+ export const KIMI_CODE_VERSION = "0.6.0";
28
+ export const KIMI_CODE_USER_AGENT = `kimi-code-cli/${KIMI_CODE_VERSION}`;
29
+ export const KIMI_PLATFORM = "kimi_code_cli";
31
30
 
32
31
  export function getKimiCodeHome(): string {
33
32
  const value = process.env.KIMI_CODE_HOME?.trim();
package/src/oauth.ts CHANGED
@@ -388,6 +388,7 @@ export async function refreshKimiAuthToken(currentKey: string): Promise<string |
388
388
  console.error("[kimi-coding] auth refresh: requesting new access token");
389
389
  const refreshed = await refreshAccessToken(cred.refresh);
390
390
  const newCred: OAuthCredential = {
391
+ ...cred,
391
392
  type: "oauth",
392
393
  access: refreshed.access_token,
393
394
  refresh: refreshed.refresh_token,
package/src/payload.ts CHANGED
@@ -87,8 +87,7 @@ export function readEnvOverrides(): KimiEnvOverrides {
87
87
  if (temp) out.temperature = parseFloat(temp);
88
88
  const topP = process.env.KIMI_MODEL_TOP_P;
89
89
  if (topP) out.topP = parseFloat(topP);
90
- const maxCompletionTokens =
91
- process.env.KIMI_MODEL_MAX_COMPLETION_TOKENS ?? process.env.KIMI_MODEL_MAX_TOKENS;
90
+ const maxCompletionTokens = process.env.KIMI_MODEL_MAX_COMPLETION_TOKENS;
92
91
  if (maxCompletionTokens) out.maxCompletionTokens = parseInt(maxCompletionTokens, 10);
93
92
  return out;
94
93
  }
@@ -421,13 +420,18 @@ export async function applyKimiPayloadMutations(
421
420
  if (resolved) payload.prompt_cache_key = resolved;
422
421
  }
423
422
 
424
- // 4. Env-level hyperparameter overrides (pre-parsed into numbers by caller).
423
+ // 4. Normalize deprecated max_tokens and apply env-level hyperparameter
424
+ // overrides (pre-parsed into numbers by caller).
425
+ if (payload.max_completion_tokens === undefined && typeof payload.max_tokens === "number") {
426
+ payload.max_completion_tokens = payload.max_tokens;
427
+ }
428
+ delete payload.max_tokens;
429
+
425
430
  const { temperature, topP, maxCompletionTokens } = ctx.envOverrides;
426
431
  if (temperature !== undefined) payload.temperature = temperature;
427
432
  if (topP !== undefined) payload.top_p = topP;
428
433
  if (maxCompletionTokens !== undefined) {
429
434
  payload.max_completion_tokens = maxCompletionTokens;
430
- delete payload.max_tokens;
431
435
  }
432
436
 
433
437
  // 5. Reasoning effort mapping.
package/src/stream.ts CHANGED
@@ -46,24 +46,50 @@ export async function* filterEmptyResponseStream(
46
46
  ): AsyncIterable<AssistantMessageEvent> {
47
47
  const suppressedIndices = new Set<number>();
48
48
  let textBuffer: AssistantMessageEvent[] = [];
49
+ let bufferedText = "";
49
50
  let bufferingIndex: number | null = null;
50
51
 
52
+ const suppressBufferedTextBlock = (): void => {
53
+ if (bufferingIndex !== null) suppressedIndices.add(bufferingIndex);
54
+ textBuffer = [];
55
+ bufferedText = "";
56
+ bufferingIndex = null;
57
+ };
58
+
59
+ const flushBufferedTextBlock = async function* (): AsyncIterable<AssistantMessageEvent> {
60
+ for (const buffered of textBuffer) yield buffered;
61
+ textBuffer = [];
62
+ bufferedText = "";
63
+ bufferingIndex = null;
64
+ };
65
+
51
66
  for await (const event of upstream) {
52
- // Start buffering when a new text block begins.
67
+ // Start buffering only long enough to decide whether this text block is
68
+ // Kimi's synthetic "(Empty response: ...)" wrapper. Normal text is flushed
69
+ // as soon as it diverges from the marker so output still streams.
53
70
  if (event.type === "text_start") {
54
71
  bufferingIndex = event.contentIndex;
55
72
  textBuffer = [event];
73
+ bufferedText = "";
56
74
  continue;
57
75
  }
58
76
 
59
- // Accumulate text deltas + detect the empty-response marker on text_end.
60
77
  if (
61
78
  bufferingIndex !== null &&
62
79
  "contentIndex" in event &&
63
80
  event.contentIndex === bufferingIndex
64
81
  ) {
65
82
  if (event.type === "text_delta") {
83
+ bufferedText += event.delta;
66
84
  textBuffer.push(event);
85
+ if (bufferedText.startsWith(EMPTY_RESPONSE_PREFIX)) {
86
+ suppressBufferedTextBlock();
87
+ continue;
88
+ }
89
+ if (EMPTY_RESPONSE_PREFIX.startsWith(bufferedText)) {
90
+ continue;
91
+ }
92
+ yield* flushBufferedTextBlock();
67
93
  continue;
68
94
  }
69
95
  if (event.type === "text_end") {
@@ -72,14 +98,11 @@ export async function* filterEmptyResponseStream(
72
98
  // array: it is a shared reference into session state, and mutating
73
99
  // it would shift subsequent contentIndex values, corrupting the
74
100
  // stream.
75
- suppressedIndices.add(bufferingIndex);
101
+ suppressBufferedTextBlock();
76
102
  } else {
77
- // Legitimate text block — flush buffered events + end event.
78
- for (const buffered of textBuffer) yield buffered;
103
+ yield* flushBufferedTextBlock();
79
104
  yield event;
80
105
  }
81
- textBuffer = [];
82
- bufferingIndex = null;
83
106
  continue;
84
107
  }
85
108
  }
@@ -103,6 +126,10 @@ export async function* filterEmptyResponseStream(
103
126
 
104
127
  yield event;
105
128
  }
129
+
130
+ if (bufferingIndex !== null) {
131
+ yield* flushBufferedTextBlock();
132
+ }
106
133
  }
107
134
 
108
135
  // =============================================================================
@@ -3,6 +3,7 @@ import { Type, type Static } from "@earendil-works/pi-ai";
3
3
 
4
4
  import { PROVIDER_ID } from "../constants.ts";
5
5
  import { getCommonHeaders } from "../device.ts";
6
+ import { refreshKimiAuthToken } from "../oauth.ts";
6
7
 
7
8
  const MOONSHOT_BASE_V1 = "https://api.kimi.com/coding/v1";
8
9
  const MOONSHOT_TIMEOUT_MS = 180_000;
@@ -47,6 +48,7 @@ export interface MoonshotFetchResult {
47
48
  interface MoonshotToolDeps {
48
49
  fetch: typeof fetch;
49
50
  getAccessToken: () => string | null;
51
+ refreshAccessToken: (currentToken: string) => Promise<string | null>;
50
52
  }
51
53
 
52
54
  export interface BuildMoonshotToolOptions {
@@ -68,6 +70,7 @@ function buildDeps(options: BuildMoonshotToolOptions = {}): MoonshotToolDeps {
68
70
  return {
69
71
  fetch: options.deps?.fetch ?? fetch,
70
72
  getAccessToken: options.deps?.getAccessToken ?? defaultGetAccessToken,
73
+ refreshAccessToken: options.deps?.refreshAccessToken ?? refreshKimiAuthToken,
71
74
  };
72
75
  }
73
76
 
@@ -196,6 +199,19 @@ async function readErrorBody(response: Response): Promise<string> {
196
199
  return response.text().catch(() => "");
197
200
  }
198
201
 
202
+ async function fetchWithAuthRetry(
203
+ deps: MoonshotToolDeps,
204
+ accessToken: string,
205
+ request: (token: string) => Promise<Response>,
206
+ ): Promise<Response> {
207
+ const response = await request(accessToken);
208
+ if (response.status !== 401) return response;
209
+
210
+ const refreshed = await deps.refreshAccessToken(accessToken);
211
+ if (!refreshed || refreshed === accessToken) return response;
212
+ return request(refreshed);
213
+ }
214
+
199
215
  export function buildMoonshotSearchTool(options: BuildMoonshotToolOptions = {}) {
200
216
  const deps = buildDeps(options);
201
217
  const defaultCollapsed = options.defaultCollapsed ?? true;
@@ -219,17 +235,19 @@ export function buildMoonshotSearchTool(options: BuildMoonshotToolOptions = {})
219
235
  const includeContent = params.include_content === true;
220
236
  const timeout = buildTimeoutSignal(signal);
221
237
  try {
222
- const response = await deps.fetch(`${MOONSHOT_BASE_V1}/search`, {
223
- method: "POST",
224
- headers: buildHeaders(accessToken, toolCallId),
225
- body: JSON.stringify({
226
- text_query: params.query,
227
- limit,
228
- enable_page_crawling: includeContent,
229
- timeout_seconds: 30,
238
+ const response = await fetchWithAuthRetry(deps, accessToken, (token) =>
239
+ deps.fetch(`${MOONSHOT_BASE_V1}/search`, {
240
+ method: "POST",
241
+ headers: buildHeaders(token, toolCallId),
242
+ body: JSON.stringify({
243
+ text_query: params.query,
244
+ limit,
245
+ enable_page_crawling: includeContent,
246
+ timeout_seconds: 30,
247
+ }),
248
+ signal: timeout.signal,
230
249
  }),
231
- signal: timeout.signal,
232
- });
250
+ );
233
251
 
234
252
  if (!response.ok) {
235
253
  const body = await readErrorBody(response);
@@ -279,15 +297,17 @@ export function buildMoonshotFetchTool(options: BuildMoonshotToolOptions = {}) {
279
297
 
280
298
  const timeout = buildTimeoutSignal(signal);
281
299
  try {
282
- const response = await deps.fetch(`${MOONSHOT_BASE_V1}/fetch`, {
283
- method: "POST",
284
- headers: {
285
- ...buildHeaders(accessToken, toolCallId),
286
- Accept: "text/markdown",
287
- },
288
- body: JSON.stringify({ url: params.url }),
289
- signal: timeout.signal,
290
- });
300
+ const response = await fetchWithAuthRetry(deps, accessToken, (token) =>
301
+ deps.fetch(`${MOONSHOT_BASE_V1}/fetch`, {
302
+ method: "POST",
303
+ headers: {
304
+ ...buildHeaders(token, toolCallId),
305
+ Accept: "text/markdown",
306
+ },
307
+ body: JSON.stringify({ url: params.url }),
308
+ signal: timeout.signal,
309
+ }),
310
+ );
291
311
 
292
312
  if (!response.ok) {
293
313
  const body = await readErrorBody(response);