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.
- package/index.ts +86 -1
- 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
|
|