pi-provider-kimi-code 0.1.4 → 0.1.7

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.
Files changed (2) hide show
  1. package/index.ts +86 -1
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -11,7 +11,8 @@
11
11
 
12
12
  import { randomBytes } from "node:crypto";
13
13
  import os from "node:os";
14
- import type { OAuthCredentials, OAuthLoginCallbacks } from "@mariozechner/pi-ai";
14
+ import type { OAuthCredentials, OAuthLoginCallbacks, AssistantMessageEvent, Context, Model, SimpleStreamOptions } from "@mariozechner/pi-ai";
15
+ import { streamSimpleAnthropic, AssistantMessageEventStream } from "@mariozechner/pi-ai";
15
16
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
16
17
 
17
18
  // =============================================================================
@@ -266,6 +267,89 @@ async function refreshKimiCodeToken(credentials: OAuthCredentials): Promise<OAut
266
267
  };
267
268
  }
268
269
 
270
+ // =============================================================================
271
+ // Stream wrapper: strip "(Empty response: ...)" text blocks from Kimi API
272
+ // =============================================================================
273
+ // The Kimi API wraps thinking-only responses (no text content) into a text
274
+ // block like: (Empty response: {'content': [{'type': 'thinking', ...}]})
275
+ // This leaks internal state to the user. We detect and suppress such blocks.
276
+
277
+ const EMPTY_RESPONSE_PREFIX = "(Empty response:";
278
+
279
+ function streamSimpleKimi(
280
+ model: Model<"anthropic-messages">,
281
+ context: Context,
282
+ options?: SimpleStreamOptions,
283
+ ): AssistantMessageEventStream {
284
+ const upstream = streamSimpleAnthropic(model, context, options);
285
+ const filtered = new AssistantMessageEventStream();
286
+
287
+ // Buffer text block events so we can suppress the entire block if it
288
+ // turns out to be a Kimi "(Empty response: ...)" wrapper.
289
+ const suppressedIndices = new Set<number>();
290
+ let textBuffer: AssistantMessageEvent[] = [];
291
+ let bufferingIndex: number | null = null;
292
+
293
+ void (async () => {
294
+ try {
295
+ for await (const event of upstream) {
296
+ // Start buffering when a new text block begins
297
+ if (event.type === "text_start") {
298
+ bufferingIndex = event.contentIndex;
299
+ textBuffer = [event];
300
+ continue;
301
+ }
302
+
303
+ // Accumulate text deltas while buffering
304
+ if (bufferingIndex !== null && "contentIndex" in event && event.contentIndex === bufferingIndex) {
305
+ if (event.type === "text_delta") {
306
+ textBuffer.push(event);
307
+ continue;
308
+ }
309
+ if (event.type === "text_end") {
310
+ if (event.content.startsWith(EMPTY_RESPONSE_PREFIX)) {
311
+ // Suppress entire text block — discard buffered events.
312
+ // Do NOT splice content array: it is a shared reference
313
+ // into session state, and mutating it shifts subsequent
314
+ // contentIndex values, corrupting the stream.
315
+ suppressedIndices.add(bufferingIndex);
316
+ } else {
317
+ // Legitimate text block — flush buffer + end event
318
+ for (const buffered of textBuffer) filtered.push(buffered);
319
+ filtered.push(event);
320
+ }
321
+ textBuffer = [];
322
+ bufferingIndex = null;
323
+ continue;
324
+ }
325
+ }
326
+
327
+ // Skip events for already-suppressed indices
328
+ if ("contentIndex" in event && suppressedIndices.has(event.contentIndex)) {
329
+ continue;
330
+ }
331
+
332
+ // Clean suppressed blocks from the final message
333
+ if (event.type === "done" && suppressedIndices.size > 0) {
334
+ event.message.content = event.message.content.filter(
335
+ (block) => !(block.type === "text" && typeof block.text === "string" && block.text.startsWith(EMPTY_RESPONSE_PREFIX)),
336
+ );
337
+ }
338
+
339
+ filtered.push(event);
340
+ }
341
+ } catch (err) {
342
+ filtered.push({
343
+ type: "error",
344
+ reason: "error",
345
+ error: { content: [], stopReason: "error", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 } },
346
+ } as AssistantMessageEvent & { type: "error" });
347
+ }
348
+ })();
349
+
350
+ return filtered;
351
+ }
352
+
269
353
  // =============================================================================
270
354
  // Extension Entry Point
271
355
  // =============================================================================
@@ -275,6 +359,7 @@ export default function (pi: ExtensionAPI) {
275
359
  baseUrl: "https://api.kimi.com/coding",
276
360
  apiKey: "KIMI_API_KEY",
277
361
  api: "anthropic-messages",
362
+ streamSimple: streamSimpleKimi,
278
363
 
279
364
  headers: getCommonHeaders(),
280
365
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-kimi-code",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
4
4
  "description": "pi custom provider extension for Kimi Code models with OAuth device-code login",
5
5
  "type": "module",
6
6
  "license": "MIT",