pi-provider-kimi-code 0.6.12 → 0.6.14
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/CHANGELOG.md +21 -0
- package/README.md +25 -7
- package/index.ts +218 -170
- package/package.json +16 -13
- package/src/config.ts +33 -1
- package/src/constants.ts +86 -9
- package/src/errors.ts +31 -0
- package/src/models.ts +95 -13
- package/src/oauth.ts +60 -1
- package/src/payload.ts +266 -1
- package/src/schema-dedup.ts +8 -0
- package/src/settings-ui.ts +28 -0
- package/src/stream.ts +17 -3
- package/src/tools/common.ts +2 -3
- package/src/tools/datasource-catalog.ts +420 -0
- package/src/tools/datasource.ts +54 -68
- package/src/tools/moonshot.ts +7 -7
- package/src/usage.ts +116 -7
package/src/payload.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface KimiPayloadContext {
|
|
|
37
37
|
cacheKey?: string;
|
|
38
38
|
cacheRetention: CacheRetention;
|
|
39
39
|
reasoning?: ThinkingLevel;
|
|
40
|
+
/** Per-request cap from the caller (pi options.maxTokens); wins over the omission policy. */
|
|
41
|
+
requestMaxTokens?: number;
|
|
40
42
|
modelConfig: KimiResolvedModelConfig;
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -264,6 +266,155 @@ async function transformOpenAIPayloadFiles(
|
|
|
264
266
|
}
|
|
265
267
|
}
|
|
266
268
|
|
|
269
|
+
// -----------------------------------------------------------------------------
|
|
270
|
+
// Per-request inline media budget (upstream kimi-code #3784,
|
|
271
|
+
// mediaResolverService.applyMediaBudget): accumulated inline media count
|
|
272
|
+
// against a 20 MB per-request base64 wire-size budget (not decoded media
|
|
273
|
+
// bytes); when exceeded, the oldest items are dropped until the total is under
|
|
274
|
+
// the 10 MB base64 wire-size low-water mark and replaced with an omission
|
|
275
|
+
// placeholder. OpenAI data URLs count the full data: URL string, including its
|
|
276
|
+
// small prefix overhead, while Anthropic base64 blocks count the source data
|
|
277
|
+
// string. Uploaded ms:// references are tiny and never counted. Media is
|
|
278
|
+
// deduped by content hash so a repeated image counts once and every occurrence
|
|
279
|
+
// is dropped together.
|
|
280
|
+
// -----------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
const REQUEST_MEDIA_BUDGET_BYTES = 20 * 1024 * 1024;
|
|
283
|
+
const REQUEST_MEDIA_BUDGET_LOW_BYTES = 10 * 1024 * 1024;
|
|
284
|
+
|
|
285
|
+
interface InlineMediaBudgetEntry {
|
|
286
|
+
kind: "image" | "video";
|
|
287
|
+
key: string;
|
|
288
|
+
bytes: number;
|
|
289
|
+
/** Indices into messages[...].content[...] (a third index addresses a tool_result's content). */
|
|
290
|
+
path: number[];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function inlineMediaKey(kind: string, payload: string): string {
|
|
294
|
+
return createHash("sha256").update(kind).update("\0").update(payload).digest("hex");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function collectOpenAIInlineMedia(payload: JsonRecord): InlineMediaBudgetEntry[] {
|
|
298
|
+
const entries: InlineMediaBudgetEntry[] = [];
|
|
299
|
+
if (!Array.isArray(payload.messages)) return entries;
|
|
300
|
+
for (const [messageIndex, message] of payload.messages.entries()) {
|
|
301
|
+
if (!isRecord(message) || !Array.isArray(message.content)) continue;
|
|
302
|
+
for (const [blockIndex, block] of (message.content as unknown[]).entries()) {
|
|
303
|
+
if (!isRecord(block)) continue;
|
|
304
|
+
const field =
|
|
305
|
+
block.type === "image_url" ? "image_url" : block.type === "video_url" ? "video_url" : null;
|
|
306
|
+
if (!field) continue;
|
|
307
|
+
const kind = block.type === "image_url" ? "image" : "video";
|
|
308
|
+
const value = block[field];
|
|
309
|
+
const url =
|
|
310
|
+
typeof value === "string"
|
|
311
|
+
? value
|
|
312
|
+
: isRecord(value) && typeof value.url === "string"
|
|
313
|
+
? value.url
|
|
314
|
+
: null;
|
|
315
|
+
if (!url || !url.startsWith("data:")) continue;
|
|
316
|
+
entries.push({
|
|
317
|
+
kind,
|
|
318
|
+
key: inlineMediaKey(kind, url),
|
|
319
|
+
bytes: url.length,
|
|
320
|
+
path: [messageIndex, blockIndex],
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return entries;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function collectAnthropicInlineMedia(payload: JsonRecord): InlineMediaBudgetEntry[] {
|
|
328
|
+
const entries: InlineMediaBudgetEntry[] = [];
|
|
329
|
+
if (!Array.isArray(payload.messages)) return entries;
|
|
330
|
+
const collect = (messageIndex: number, path: number[], block: unknown): void => {
|
|
331
|
+
if (!isRecord(block)) return;
|
|
332
|
+
const kind = block.type === "image" || block.type === "video" ? block.type : null;
|
|
333
|
+
if (
|
|
334
|
+
kind &&
|
|
335
|
+
isRecord(block.source) &&
|
|
336
|
+
block.source.type === "base64" &&
|
|
337
|
+
typeof block.source.media_type === "string" &&
|
|
338
|
+
typeof block.source.data === "string"
|
|
339
|
+
) {
|
|
340
|
+
const data = block.source.data;
|
|
341
|
+
entries.push({
|
|
342
|
+
kind,
|
|
343
|
+
key: inlineMediaKey(kind, `${block.source.media_type}\0${data}`),
|
|
344
|
+
bytes: data.length,
|
|
345
|
+
path: [messageIndex, ...path],
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
for (const [messageIndex, message] of payload.messages.entries()) {
|
|
350
|
+
if (!isRecord(message) || !Array.isArray(message.content)) continue;
|
|
351
|
+
for (const [blockIndex, block] of (message.content as unknown[]).entries()) {
|
|
352
|
+
if (isRecord(block) && block.type === "tool_result" && Array.isArray(block.content)) {
|
|
353
|
+
for (const [nestedIndex, nested] of (block.content as unknown[]).entries()) {
|
|
354
|
+
collect(messageIndex, [blockIndex, nestedIndex], nested);
|
|
355
|
+
}
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
collect(messageIndex, [blockIndex], block);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return entries;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function applyInlineMediaBudget(
|
|
365
|
+
payload: JsonRecord,
|
|
366
|
+
api: "anthropic-messages" | "openai-completions",
|
|
367
|
+
budgetBytes: number = REQUEST_MEDIA_BUDGET_BYTES,
|
|
368
|
+
lowBytes: number = REQUEST_MEDIA_BUDGET_LOW_BYTES,
|
|
369
|
+
): boolean {
|
|
370
|
+
const entries =
|
|
371
|
+
api === "anthropic-messages"
|
|
372
|
+
? collectAnthropicInlineMedia(payload)
|
|
373
|
+
: collectOpenAIInlineMedia(payload);
|
|
374
|
+
if (entries.length === 0) return false;
|
|
375
|
+
|
|
376
|
+
// Deduplicate by content: repeated media counts once, drops hit every copy.
|
|
377
|
+
const bytesByKey = new Map<string, number>();
|
|
378
|
+
for (const entry of entries) {
|
|
379
|
+
if (!bytesByKey.has(entry.key)) bytesByKey.set(entry.key, entry.bytes);
|
|
380
|
+
}
|
|
381
|
+
let total = 0;
|
|
382
|
+
for (const bytes of bytesByKey.values()) total += bytes;
|
|
383
|
+
if (total <= budgetBytes) return false;
|
|
384
|
+
|
|
385
|
+
const dropped = new Set<string>();
|
|
386
|
+
for (const [key, bytes] of bytesByKey) {
|
|
387
|
+
if (total <= lowBytes) break;
|
|
388
|
+
if (bytes === 0) continue;
|
|
389
|
+
dropped.add(key);
|
|
390
|
+
total -= bytes;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const messages = payload.messages as unknown[];
|
|
394
|
+
let droppedCount = 0;
|
|
395
|
+
for (const entry of entries) {
|
|
396
|
+
if (!dropped.has(entry.key)) continue;
|
|
397
|
+
const message = messages[entry.path[0]] as JsonRecord;
|
|
398
|
+
const content = message.content as unknown[];
|
|
399
|
+
const placeholder = {
|
|
400
|
+
type: "text",
|
|
401
|
+
text: `[${entry.kind} omitted: dropped to fit the request media budget]`,
|
|
402
|
+
};
|
|
403
|
+
if (entry.path.length === 2) {
|
|
404
|
+
content[entry.path[1]] = placeholder;
|
|
405
|
+
} else {
|
|
406
|
+
const toolResult = content[entry.path[1]] as JsonRecord;
|
|
407
|
+
(toolResult.content as unknown[])[entry.path[2]] = placeholder;
|
|
408
|
+
}
|
|
409
|
+
droppedCount += 1;
|
|
410
|
+
}
|
|
411
|
+
console.warn(
|
|
412
|
+
`[kimi-coding] inline media exceeded the ${String(budgetBytes / (1024 * 1024))} MB ` +
|
|
413
|
+
`per-request budget; ${String(droppedCount)} media item(s) omitted`,
|
|
414
|
+
);
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
|
|
267
418
|
function isEffectivelyEmptyOpenAIContent(content: unknown): boolean {
|
|
268
419
|
if (typeof content === "string") return content.trim() === "";
|
|
269
420
|
if (!Array.isArray(content)) return false;
|
|
@@ -472,6 +623,75 @@ async function transformAnthropicPayloadFiles(
|
|
|
472
623
|
|
|
473
624
|
const RESPONSES_EFFORTS = new Set(["low", "high", "max"]);
|
|
474
625
|
|
|
626
|
+
// Empty/whitespace-only text parts in the Responses `input` array. Assistant
|
|
627
|
+
// turns that produced no text (reasoning + tool calls only) replay as
|
|
628
|
+
// output_text:"" and upstream 400s on text non-emptiness; see issue #78.
|
|
629
|
+
const RESPONSES_TEXT_PART_TYPES = new Set(["output_text", "text", "input_text"]);
|
|
630
|
+
|
|
631
|
+
function isEmptyTextPart(part: unknown): boolean {
|
|
632
|
+
return (
|
|
633
|
+
isRecord(part) &&
|
|
634
|
+
typeof part.type === "string" &&
|
|
635
|
+
RESPONSES_TEXT_PART_TYPES.has(part.type) &&
|
|
636
|
+
(typeof part.text !== "string" || part.text.trim() === "")
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export function stripEmptyResponsesTextParts(payload: JsonRecord): boolean {
|
|
641
|
+
if (!Array.isArray(payload.input)) return false;
|
|
642
|
+
let changed = false;
|
|
643
|
+
const input: unknown[] = [];
|
|
644
|
+
for (const item of payload.input) {
|
|
645
|
+
if (!isRecord(item)) {
|
|
646
|
+
input.push(item);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
// Typed non-message items (function_call, reasoning, ...) pass through.
|
|
650
|
+
if (typeof item.type === "string" && item.type !== "message") {
|
|
651
|
+
input.push(item);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const role = item.role;
|
|
655
|
+
const content = item.content;
|
|
656
|
+
const isAssistantLike = role === "assistant" || role == null;
|
|
657
|
+
if (typeof content === "string") {
|
|
658
|
+
if (isAssistantLike && content.trim() === "") {
|
|
659
|
+
changed = true;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
input.push(item);
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (!Array.isArray(content)) {
|
|
666
|
+
// A message item with missing/non-array content carries nothing to
|
|
667
|
+
// render; drop assistant-shaped ones instead of replaying a
|
|
668
|
+
// contentless message (the upstream 400 path this fix targets).
|
|
669
|
+
if (isAssistantLike && (content === undefined || content === null)) {
|
|
670
|
+
changed = true;
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
input.push(item);
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
const next = content.filter((part) => !isEmptyTextPart(part));
|
|
677
|
+
// An assistant message with nothing left — either filtered here or
|
|
678
|
+
// arriving empty — carries no information; drop it rather than send a
|
|
679
|
+
// contentless message. User/system messages keep their content.
|
|
680
|
+
if (next.length === 0 && isAssistantLike) {
|
|
681
|
+
changed = true;
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (next.length === content.length) {
|
|
685
|
+
input.push(item);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
changed = true;
|
|
689
|
+
input.push({ ...item, content: next });
|
|
690
|
+
}
|
|
691
|
+
if (changed) payload.input = input;
|
|
692
|
+
return changed;
|
|
693
|
+
}
|
|
694
|
+
|
|
475
695
|
function responsesSupportedEfforts(ctx: KimiPayloadContext): string[] {
|
|
476
696
|
const advertised = ctx.modelConfig.supportEfforts;
|
|
477
697
|
if (advertised?.length) return advertised.filter((effort) => RESPONSES_EFFORTS.has(effort));
|
|
@@ -547,6 +767,19 @@ export async function applyKimiPayloadMutations(
|
|
|
547
767
|
);
|
|
548
768
|
}
|
|
549
769
|
|
|
770
|
+
// 1b. Responses wire: drop empty text parts replayed from text-less
|
|
771
|
+
// assistant turns (issue #78). pi-ai's Responses serializer keeps empty
|
|
772
|
+
// output_text blocks — unlike its Chat Completions serializer, which
|
|
773
|
+
// filters them — so a reasoning/tool-only turn is replayed as
|
|
774
|
+
// {role:"assistant", content:[{type:"output_text", text:""}]} on every
|
|
775
|
+
// subsequent request, and upstream endpoints reject the whole payload with
|
|
776
|
+
// a 400-class text-non-emptiness error. function_call / reasoning / all
|
|
777
|
+
// other part types are untouched; an assistant message left with no
|
|
778
|
+
// content is dropped entirely.
|
|
779
|
+
if (ctx.api === "openai-responses") {
|
|
780
|
+
stripEmptyResponsesTextParts(payload);
|
|
781
|
+
}
|
|
782
|
+
|
|
550
783
|
// 2. File upload dispatch (protocol-specific).
|
|
551
784
|
if (ctx.upload) {
|
|
552
785
|
const uploadCache = ctx.uploadCacheScope ? uploadedFileCache : new Map<string, string>();
|
|
@@ -557,6 +790,11 @@ export async function applyKimiPayloadMutations(
|
|
|
557
790
|
await transformAnthropicPayloadFiles(payload, ctx.upload, uploadCache, uploadCacheScope);
|
|
558
791
|
}
|
|
559
792
|
}
|
|
793
|
+
if (ctx.api === "openai-completions" || ctx.api === "anthropic-messages") {
|
|
794
|
+
// Runs after the upload transforms so media already promoted to ms://
|
|
795
|
+
// references (tiny URLs) are never counted against the budget.
|
|
796
|
+
applyInlineMediaBudget(payload, ctx.api);
|
|
797
|
+
}
|
|
560
798
|
if (ctx.api === "openai-completions") {
|
|
561
799
|
normalizeOpenAIAssistantToolCalls(payload);
|
|
562
800
|
}
|
|
@@ -590,8 +828,12 @@ export async function applyKimiPayloadMutations(
|
|
|
590
828
|
|
|
591
829
|
// 5. Spread extra_body into the top-level payload before normalization and
|
|
592
830
|
// config caps. Top-level fields retain precedence over extra_body.
|
|
831
|
+
let extraBodyHadCap = false;
|
|
593
832
|
if (isRecord(payload.extra_body)) {
|
|
594
833
|
const extraBody = payload.extra_body as JsonRecord;
|
|
834
|
+
extraBodyHadCap = ["max_tokens", "max_completion_tokens", "max_output_tokens"].some(
|
|
835
|
+
(key) => typeof extraBody[key] === "number",
|
|
836
|
+
);
|
|
595
837
|
delete payload.extra_body;
|
|
596
838
|
for (const [key, value] of Object.entries(extraBody)) {
|
|
597
839
|
if (payload[key] === undefined) {
|
|
@@ -620,6 +862,29 @@ export async function applyKimiPayloadMutations(
|
|
|
620
862
|
}
|
|
621
863
|
|
|
622
864
|
const generation = ctx.modelConfig.generation;
|
|
865
|
+
// Output cap policy (mirrors upstream kimi-code #4091): omit the completion
|
|
866
|
+
// cap on the OpenAI wires unless explicitly configured. pi-ai seeds a cap
|
|
867
|
+
// clamped to the context window (clampMaxTokensToContext), and forwarding
|
|
868
|
+
// that value breaks strict serving stacks (e.g. bare vLLM) that reject a
|
|
869
|
+
// cap above the model's real output limit with repeated 400s. A cap counts
|
|
870
|
+
// as explicit when generation.maxCompletionTokens is configured
|
|
871
|
+
// (KIMI_MODEL_MAX_COMPLETION_TOKENS / config generation.maxCompletionTokens),
|
|
872
|
+
// model.maxTokens holds a non-window value, or the caller supplies a
|
|
873
|
+
// per-request cap through options.maxTokens / extra_body. Anthropic /messages requires
|
|
874
|
+
// max_tokens, so that wire keeps pi-ai's window-clamped cap — the same
|
|
875
|
+
// window - usedContextTokens value upstream fills in when unset.
|
|
876
|
+
if (ctx.api === "openai-completions" || ctx.api === "openai-responses") {
|
|
877
|
+
const capKey = ctx.api === "openai-responses" ? "max_output_tokens" : "max_completion_tokens";
|
|
878
|
+
const explicitModelCap =
|
|
879
|
+
typeof ctx.modelConfig.maxTokens === "number" &&
|
|
880
|
+
ctx.modelConfig.maxTokens > 0 &&
|
|
881
|
+
ctx.modelConfig.maxTokens !== ctx.modelConfig.contextWindow;
|
|
882
|
+
const explicitRequestCap =
|
|
883
|
+
extraBodyHadCap || (typeof ctx.requestMaxTokens === "number" && ctx.requestMaxTokens > 0);
|
|
884
|
+
if (generation.maxCompletionTokens === undefined && !explicitModelCap && !explicitRequestCap) {
|
|
885
|
+
delete payload[capKey];
|
|
886
|
+
}
|
|
887
|
+
}
|
|
623
888
|
// Official kimi-code sends temperature/top_p only when explicitly configured
|
|
624
889
|
// (env KIMI_MODEL_TEMPERATURE / KIMI_MODEL_TOP_P → generation.*); otherwise it
|
|
625
890
|
// omits them and lets the server apply its own defaults. Mirror that exactly:
|
|
@@ -720,7 +985,7 @@ export async function applyKimiPayloadMutations(
|
|
|
720
985
|
}
|
|
721
986
|
}
|
|
722
987
|
|
|
723
|
-
// 8.
|
|
988
|
+
// 8. Kimi coding endpoint constraints: the server rejects tool_choice "required" /
|
|
724
989
|
// function-specific when thinking is enabled (always-on). Responses only
|
|
725
990
|
// accepts auto. temperature/top_p are handled in step 6 — omitted unless
|
|
726
991
|
// explicitly configured, matching the official kimi-code client rather
|
package/src/schema-dedup.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
// Tool schema deduplication: extract repeated sub-schemas into $defs/$ref
|
|
2
2
|
// to stay under Moonshot's ~15 KB per-tool function.parameters limit.
|
|
3
|
+
//
|
|
4
|
+
// Known risk (upstream kimi-code #4489-era): the official client does the
|
|
5
|
+
// OPPOSITE for its Anthropic-protocol path — it fully dereferences $ref
|
|
6
|
+
// (normalizeKimiToolSchema) because Anthropic-style serving stacks do not
|
|
7
|
+
// resolve $defs. No managed Kimi model declares `protocol: "anthropic"` in
|
|
8
|
+
// /models yet (verified 2026-11), so dedup is safe on the OpenAI-compatible
|
|
9
|
+
// path this extension uses today. If a managed model ever ships with the
|
|
10
|
+
// anthropic protocol, gate this dedup off for that protocol.
|
|
3
11
|
|
|
4
12
|
import { createHash } from "node:crypto";
|
|
5
13
|
|
package/src/settings-ui.ts
CHANGED
|
@@ -58,10 +58,24 @@ export function setProtocol(
|
|
|
58
58
|
return { ...config, protocol };
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export function setRegion(
|
|
62
|
+
config: KimiCodeConfig,
|
|
63
|
+
region: KimiCodeConfig["region"],
|
|
64
|
+
): KimiCodeConfig {
|
|
65
|
+
return { ...config, region };
|
|
66
|
+
}
|
|
67
|
+
|
|
61
68
|
export function setUploadThreshold(config: KimiCodeConfig, thresholdBytes: number): KimiCodeConfig {
|
|
62
69
|
return { ...config, uploads: { ...config.uploads, thresholdBytes } };
|
|
63
70
|
}
|
|
64
71
|
|
|
72
|
+
export function setThinkingKeep(
|
|
73
|
+
config: KimiCodeConfig,
|
|
74
|
+
thinkingKeep: KimiCodeConfig["model"]["thinkingKeep"],
|
|
75
|
+
): KimiCodeConfig {
|
|
76
|
+
return { ...config, model: { ...config.model, thinkingKeep } };
|
|
77
|
+
}
|
|
78
|
+
|
|
65
79
|
export function buildKimiMainTitle(
|
|
66
80
|
config: KimiCodeConfig,
|
|
67
81
|
extras: KimiOAuthExtras,
|
|
@@ -73,7 +87,9 @@ export function buildKimiMainTitle(
|
|
|
73
87
|
"",
|
|
74
88
|
`Model: ${modelName}`,
|
|
75
89
|
`Protocol: ${config.protocol} (${sources.protocol})`,
|
|
90
|
+
`Region: ${formatRegionValue(config.region)} (${sources.region})`,
|
|
76
91
|
`Upload threshold: ${formatByteSize(config.uploads.thresholdBytes)} (${sources.uploads.thresholdBytes})`,
|
|
92
|
+
`Thinking keep: ${config.model.thinkingKeep ?? "default"} (${sources.model.thinkingKeep})`,
|
|
77
93
|
"",
|
|
78
94
|
"Effective tools:",
|
|
79
95
|
...KIMI_TOOL_NAMES.map((toolName) => {
|
|
@@ -138,6 +154,18 @@ export function toolMenuItem(config: KimiCodeConfig, toolName: KimiToolName): st
|
|
|
138
154
|
return `${toolName} -> ${formatToolStatus(config, toolName)}`;
|
|
139
155
|
}
|
|
140
156
|
|
|
157
|
+
export function formatRegionValue(region: KimiCodeConfig["region"]): string {
|
|
158
|
+
return region ?? "auto";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function formatRegionStatus(config: KimiCodeConfig): string {
|
|
162
|
+
return `region: ${formatRegionValue(config.region)}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function regionMenuItem(config: KimiCodeConfig): string {
|
|
166
|
+
return `Region -> ${formatRegionValue(config.region)}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
141
169
|
export function protocolMenuItem(config: KimiCodeConfig): string {
|
|
142
170
|
return `Protocol -> ${config.protocol}`;
|
|
143
171
|
}
|
package/src/stream.ts
CHANGED
|
@@ -83,6 +83,16 @@ import {
|
|
|
83
83
|
getBaseUrl,
|
|
84
84
|
} from "./constants.ts";
|
|
85
85
|
import { getKimiProviderHeaders } from "./device.ts";
|
|
86
|
+
import { improveKimiErrorMessage } from "./errors.ts";
|
|
87
|
+
|
|
88
|
+
function improveErrorEvent(event: AssistantMessageEvent): AssistantMessageEvent {
|
|
89
|
+
if (event.type !== "error") return event;
|
|
90
|
+
const message = event.error?.errorMessage;
|
|
91
|
+
if (!message) return event;
|
|
92
|
+
const improved = improveKimiErrorMessage(message);
|
|
93
|
+
if (improved === message || !event.error) return event;
|
|
94
|
+
return { ...event, error: { ...event.error, errorMessage: improved } };
|
|
95
|
+
}
|
|
86
96
|
import {
|
|
87
97
|
isKimiAuthErrorMessage,
|
|
88
98
|
readStoredOAuthCredential,
|
|
@@ -360,6 +370,7 @@ export function streamSimpleKimi(
|
|
|
360
370
|
cacheKey,
|
|
361
371
|
cacheRetention,
|
|
362
372
|
reasoning: options?.reasoning,
|
|
373
|
+
requestMaxTokens: options?.maxTokens,
|
|
363
374
|
modelConfig,
|
|
364
375
|
});
|
|
365
376
|
if (
|
|
@@ -470,10 +481,11 @@ export function streamSimpleKimi(
|
|
|
470
481
|
}
|
|
471
482
|
|
|
472
483
|
// First non-start, non-retry event: flush buffered prefix, then
|
|
473
|
-
// stream normally.
|
|
484
|
+
// stream normally. Quota-exhaustion errors get an actionable
|
|
485
|
+
// rewrite (hard limit, not a transient 429).
|
|
474
486
|
for (const e of prefixBuffer) filtered.push(e);
|
|
475
487
|
prefixBuffer = [];
|
|
476
|
-
filtered.push(event);
|
|
488
|
+
filtered.push(improveErrorEvent(event));
|
|
477
489
|
}
|
|
478
490
|
|
|
479
491
|
// Stream ended normally: flush any remaining buffered starts.
|
|
@@ -508,7 +520,9 @@ export function streamSimpleKimi(
|
|
|
508
520
|
model: model.id,
|
|
509
521
|
content: [],
|
|
510
522
|
stopReason: "error",
|
|
511
|
-
errorMessage:
|
|
523
|
+
errorMessage: improveKimiErrorMessage(
|
|
524
|
+
err instanceof Error ? err.message : String(err),
|
|
525
|
+
),
|
|
512
526
|
usage: {
|
|
513
527
|
input: 0,
|
|
514
528
|
output: 0,
|
package/src/tools/common.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
|
-
import { PROVIDER_ID, getBaseUrl } from "../constants.ts";
|
|
4
|
+
import { PROVIDER_ID, getBaseUrl, normalizeKimiBaseV1 } from "../constants.ts";
|
|
5
5
|
import { getKimiProviderHeaders } from "../device.ts";
|
|
6
6
|
import {
|
|
7
7
|
KIMI_LOGIN_REQUIRED_MESSAGE,
|
|
@@ -57,8 +57,7 @@ export function buildTimeoutSignal(signal: AbortSignal | undefined): {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
export function getKimiBaseV1(): string {
|
|
60
|
-
|
|
61
|
-
return base.endsWith("/v1") ? base : `${base}/v1`;
|
|
60
|
+
return normalizeKimiBaseV1(getBaseUrl());
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
export function getKimiDatasourceUrl(): string {
|