pi-provider-kimi-code 0.6.11 → 0.6.12
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 +1 -1
- package/index.ts +4 -3
- package/package.json +7 -7
- package/src/config.ts +8 -4
- package/src/constants.ts +30 -15
- package/src/models.ts +10 -3
- package/src/payload.ts +174 -78
- package/src/schema-dedup.ts +9 -4
- package/src/stream.ts +88 -25
package/README.md
CHANGED
|
@@ -183,7 +183,7 @@ If you already use another search or fetch tool, pick one path for a session. Ov
|
|
|
183
183
|
Most users do not need environment variables. Two are worth knowing:
|
|
184
184
|
|
|
185
185
|
- `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.
|
|
186
|
+
- `KIMI_CODE_PROTOCOL` — `openai` by default; set to `anthropic` for Anthropic-compatible requests, or `responses` for the OpenAI Responses API.
|
|
187
187
|
|
|
188
188
|
Tools, protocol, and upload threshold are all configurable through `/kimi-settings` or JSON config files.
|
|
189
189
|
|
package/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ 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,
|
|
48
49
|
getBaseUrl,
|
|
@@ -340,7 +341,7 @@ async function openSettingsMenu(
|
|
|
340
341
|
label: "Protocol",
|
|
341
342
|
description: "API protocol for Kimi requests",
|
|
342
343
|
currentValue: drafts[scope].protocol,
|
|
343
|
-
values: ["openai", "anthropic"],
|
|
344
|
+
values: ["openai", "anthropic", "responses"],
|
|
344
345
|
});
|
|
345
346
|
items.push({
|
|
346
347
|
id: "uploadThreshold",
|
|
@@ -462,8 +463,8 @@ const MODULE_UNAVAILABLE_CODES = new Set([
|
|
|
462
463
|
// model was resolved, so a session created before a protocol switch still
|
|
463
464
|
// carries the other id and would keep crashing. streamSimpleKimi routes on the
|
|
464
465
|
// model's wireProtocol / the resolved runtime config and never on model.api, so
|
|
465
|
-
//
|
|
466
|
-
const KIMI_FALLBACK_APIS =
|
|
466
|
+
// all three ids dispatch identically.
|
|
467
|
+
const KIMI_FALLBACK_APIS = KIMI_WIRE_PROTOCOLS.map((protocol) => getKimiApiType(protocol));
|
|
467
468
|
|
|
468
469
|
type PiAiApiRegistry = {
|
|
469
470
|
registerApiProvider: (
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-provider-kimi-code",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.12",
|
|
4
4
|
"description": "Kimi Code models (K3, K2.7, HighSpeed) in Pi: OAuth shared login with kimi-code CLI, file uploads, and Kimi-native tools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-provider",
|
|
@@ -57,14 +57,14 @@
|
|
|
57
57
|
"proper-lockfile": "^4.1.2"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@earendil-works/pi-ai": "^0.
|
|
61
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
62
|
-
"@earendil-works/pi-tui": "^0.
|
|
63
|
-
"@j178/prek": "^0.
|
|
60
|
+
"@earendil-works/pi-ai": "^0.85.1",
|
|
61
|
+
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
62
|
+
"@earendil-works/pi-tui": "^0.85.1",
|
|
63
|
+
"@j178/prek": "^0.5.2",
|
|
64
64
|
"@types/node": "^24.0.0",
|
|
65
65
|
"@types/proper-lockfile": "^4.1.4",
|
|
66
|
-
"oxfmt": "^0.
|
|
67
|
-
"oxlint": "^1.
|
|
66
|
+
"oxfmt": "^0.67.0",
|
|
67
|
+
"oxlint": "^1.82.0",
|
|
68
68
|
"typescript": "^5.9.0"
|
|
69
69
|
},
|
|
70
70
|
"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 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,7 @@ export interface KimiCodeConfig {
|
|
|
46
46
|
model: ModelConfig;
|
|
47
47
|
tools: Record<KimiToolName, { enabled: boolean; default_collapsed: boolean }>;
|
|
48
48
|
uploads: { thresholdBytes: number };
|
|
49
|
-
protocol:
|
|
49
|
+
protocol: KimiWireProtocol;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export type KimiCodeConfigPatch = Partial<{
|
|
@@ -315,8 +315,12 @@ function requireProtocol(
|
|
|
315
315
|
configPath: string,
|
|
316
316
|
pointer: string,
|
|
317
317
|
): KimiCodeConfig["protocol"] {
|
|
318
|
-
if (raw === "openai" || raw === "anthropic") return raw;
|
|
319
|
-
return fail(
|
|
318
|
+
if (raw === "openai" || raw === "anthropic" || raw === "responses") return raw;
|
|
319
|
+
return fail(
|
|
320
|
+
configPath,
|
|
321
|
+
pointer,
|
|
322
|
+
`expected "openai" | "anthropic" | "responses", got ${JSON.stringify(raw)}`,
|
|
323
|
+
);
|
|
320
324
|
}
|
|
321
325
|
|
|
322
326
|
function requireInputArray(raw: unknown, configPath: string, pointer: string): KimiInputModality[] {
|
package/src/constants.ts
CHANGED
|
@@ -9,39 +9,54 @@ import { join } from "node:path";
|
|
|
9
9
|
export const CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
|
|
10
10
|
export const DEFAULT_OAUTH_HOST = "https://auth.kimi.com";
|
|
11
11
|
|
|
12
|
-
export
|
|
12
|
+
export const KIMI_WIRE_PROTOCOLS = ["openai", "anthropic", "responses"] as const;
|
|
13
|
+
export type KimiWireProtocol = (typeof KIMI_WIRE_PROTOCOLS)[number];
|
|
14
|
+
export type KimiApiProtocol = "openai-completions" | "anthropic-messages" | "openai-responses";
|
|
15
|
+
export type KimiApiType =
|
|
16
|
+
| "kimi-openai-completions"
|
|
17
|
+
| "kimi-anthropic-messages"
|
|
18
|
+
| "kimi-openai-responses";
|
|
19
|
+
|
|
20
|
+
// KIMI_CODE_PROTOCOL supports openai (default), anthropic, and responses.
|
|
21
|
+
// Unknown values fall back to openai, matching the previous two-protocol parser.
|
|
22
|
+
export function parseKimiWireProtocol(value: string | undefined): KimiWireProtocol {
|
|
23
|
+
if (value === "anthropic" || value === "responses") return value;
|
|
24
|
+
return "openai";
|
|
25
|
+
}
|
|
13
26
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
27
|
+
export const ENV_KIMI_CODE_PROTOCOL: KimiWireProtocol = parseKimiWireProtocol(
|
|
28
|
+
process.env.KIMI_CODE_PROTOCOL,
|
|
29
|
+
);
|
|
17
30
|
|
|
18
31
|
export const IS_OPENAI_PROTOCOL = ENV_KIMI_CODE_PROTOCOL === "openai";
|
|
19
32
|
|
|
20
|
-
export function getApiProtocol(
|
|
21
|
-
protocol
|
|
22
|
-
|
|
23
|
-
return
|
|
33
|
+
export function getApiProtocol(protocol: KimiWireProtocol): KimiApiProtocol {
|
|
34
|
+
if (protocol === "anthropic") return "anthropic-messages";
|
|
35
|
+
if (protocol === "responses") return "openai-responses";
|
|
36
|
+
return "openai-completions";
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
export const PROTOCOL = getApiProtocol(ENV_KIMI_CODE_PROTOCOL);
|
|
27
40
|
|
|
28
|
-
export function getKimiApiType(
|
|
29
|
-
protocol
|
|
30
|
-
|
|
31
|
-
return
|
|
41
|
+
export function getKimiApiType(protocol: KimiWireProtocol): KimiApiType {
|
|
42
|
+
if (protocol === "anthropic") return "kimi-anthropic-messages";
|
|
43
|
+
if (protocol === "responses") return "kimi-openai-responses";
|
|
44
|
+
return "kimi-openai-completions";
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
// Use a custom api identifier so this provider never conflicts with the
|
|
35
|
-
// built-in
|
|
48
|
+
// built-in anthropic-messages / openai-completions / openai-responses handlers.
|
|
36
49
|
export const KIMI_API_TYPE = getKimiApiType(ENV_KIMI_CODE_PROTOCOL);
|
|
37
50
|
|
|
38
51
|
export function getDefaultBaseUrl(protocol: KimiWireProtocol): string {
|
|
39
|
-
return protocol === "
|
|
52
|
+
return protocol === "anthropic"
|
|
53
|
+
? "https://api.kimi.com/coding"
|
|
54
|
+
: "https://api.kimi.com/coding/v1";
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
export const DEFAULT_BASE_URL = getDefaultBaseUrl(ENV_KIMI_CODE_PROTOCOL);
|
|
43
58
|
|
|
44
|
-
export const PROVIDER_VERSION = "0.6.
|
|
59
|
+
export const PROVIDER_VERSION = "0.6.12";
|
|
45
60
|
|
|
46
61
|
export const KIMI_CODE_USER_AGENT = `pi-provider-kimi-code/${PROVIDER_VERSION}`;
|
|
47
62
|
export const KIMI_PLATFORM = "pi";
|
package/src/models.ts
CHANGED
|
@@ -186,6 +186,14 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
186
186
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// Server /v1/models uses "response"; kimi-code maps that to openai_responses.
|
|
190
|
+
// Accept the server value plus the user-facing and internal aliases.
|
|
191
|
+
function parseKimiCatalogProtocol(value: unknown): KimiWireProtocol | undefined {
|
|
192
|
+
if (value === "openai" || value === "anthropic" || value === "responses") return value;
|
|
193
|
+
if (value === "response" || value === "openai_responses") return "responses";
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
189
197
|
function parseThinkEfforts(value: unknown): {
|
|
190
198
|
supportEfforts?: string[];
|
|
191
199
|
defaultEffort?: string;
|
|
@@ -223,9 +231,8 @@ function parseKimiModelMetadata(model: KimiServerModel): KimiModelMetadata | und
|
|
|
223
231
|
if (typeof model.supports_video_in === "boolean") {
|
|
224
232
|
metadata.supportsVideoIn = model.supports_video_in;
|
|
225
233
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
234
|
+
const protocol = parseKimiCatalogProtocol(model.protocol);
|
|
235
|
+
if (protocol) metadata.protocol = protocol;
|
|
229
236
|
Object.assign(metadata, parseThinkEfforts(model.think_efforts));
|
|
230
237
|
return metadata;
|
|
231
238
|
}
|
package/src/payload.ts
CHANGED
|
@@ -31,7 +31,7 @@ export function resolveCacheRetention(value?: CacheRetention): CacheRetention {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export interface KimiPayloadContext {
|
|
34
|
-
api: "anthropic-messages" | "openai-completions";
|
|
34
|
+
api: "anthropic-messages" | "openai-completions" | "openai-responses";
|
|
35
35
|
upload?: Uploader;
|
|
36
36
|
uploadCacheScope?: string;
|
|
37
37
|
cacheKey?: string;
|
|
@@ -410,10 +410,12 @@ function recurseJsonSchemaPropertyTypes(node: unknown): void {
|
|
|
410
410
|
function normalizeOpenAIToolSchemas(payload: JsonRecord): void {
|
|
411
411
|
if (!Array.isArray(payload.tools)) return;
|
|
412
412
|
for (const tool of payload.tools) {
|
|
413
|
-
if (!isRecord(tool)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
413
|
+
if (!isRecord(tool)) continue;
|
|
414
|
+
// Chat Completions nests the schema under tool.function.parameters;
|
|
415
|
+
// Responses function tools carry parameters at the top level.
|
|
416
|
+
const holder: JsonRecord = isRecord(tool.function) ? tool.function : tool;
|
|
417
|
+
if (!isRecord(holder.parameters)) continue;
|
|
418
|
+
recurseJsonSchemaPropertyTypes(holder.parameters);
|
|
417
419
|
}
|
|
418
420
|
}
|
|
419
421
|
|
|
@@ -468,6 +470,72 @@ async function transformAnthropicPayloadFiles(
|
|
|
468
470
|
// steps below testable with fixture payloads.
|
|
469
471
|
// =============================================================================
|
|
470
472
|
|
|
473
|
+
const RESPONSES_EFFORTS = new Set(["low", "high", "max"]);
|
|
474
|
+
|
|
475
|
+
function responsesSupportedEfforts(ctx: KimiPayloadContext): string[] {
|
|
476
|
+
const advertised = ctx.modelConfig.supportEfforts;
|
|
477
|
+
if (advertised?.length) return advertised.filter((effort) => RESPONSES_EFFORTS.has(effort));
|
|
478
|
+
return [...RESPONSES_EFFORTS];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function setResponsesReasoning(payload: JsonRecord, effort: string | null): void {
|
|
482
|
+
const oldReasoning = isRecord(payload.reasoning) ? payload.reasoning : {};
|
|
483
|
+
if (!effort) {
|
|
484
|
+
delete payload.reasoning;
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const reasoning: JsonRecord = { ...oldReasoning, effort };
|
|
488
|
+
if (reasoning.summary === undefined) reasoning.summary = "auto";
|
|
489
|
+
payload.reasoning = reasoning;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function normalizeResponsesUpstreamEffort(payload: JsonRecord, ctx: KimiPayloadContext): void {
|
|
493
|
+
const supported = responsesSupportedEfforts(ctx);
|
|
494
|
+
const oldEffort = isRecord(payload.reasoning) ? payload.reasoning.effort : undefined;
|
|
495
|
+
if (typeof oldEffort === "string" && supported.includes(oldEffort)) return;
|
|
496
|
+
if (typeof oldEffort === "string") {
|
|
497
|
+
const mapped = resolveReasoningForLevel(oldEffort, ctx.modelConfig);
|
|
498
|
+
if (mapped?.enabled && mapped.effort && supported.includes(mapped.effort)) {
|
|
499
|
+
setResponsesReasoning(payload, mapped.effort);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const fallback = ctx.modelConfig.defaultEffort;
|
|
504
|
+
setResponsesReasoning(payload, fallback && supported.includes(fallback) ? fallback : null);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function applyResponsesThinking(payload: JsonRecord, ctx: KimiPayloadContext): void {
|
|
508
|
+
delete payload.thinking;
|
|
509
|
+
delete payload.reasoning_effort;
|
|
510
|
+
delete payload.output_config;
|
|
511
|
+
if (ctx.modelConfig.supportsThinkingType === "no") {
|
|
512
|
+
delete payload.reasoning;
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const resolvedReasoning = resolveThinkingLevel(ctx);
|
|
516
|
+
if (!resolvedReasoning) {
|
|
517
|
+
normalizeResponsesUpstreamEffort(payload, ctx);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const mapped = resolveReasoningForLevel(resolvedReasoning, ctx.modelConfig);
|
|
521
|
+
if (!mapped) {
|
|
522
|
+
normalizeResponsesUpstreamEffort(payload, ctx);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (!mapped.enabled) {
|
|
526
|
+
delete payload.reasoning;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const effort = ctx.reasoning ? mapped.effort : (ctx.modelConfig.defaultEffort ?? mapped.effort);
|
|
530
|
+
const supported = responsesSupportedEfforts(ctx);
|
|
531
|
+
const oldReasoning = isRecord(payload.reasoning) ? payload.reasoning : {};
|
|
532
|
+
const reasoning: JsonRecord = { ...oldReasoning };
|
|
533
|
+
delete reasoning.effort;
|
|
534
|
+
if (effort !== null && supported.includes(effort)) reasoning.effort = effort;
|
|
535
|
+
if (reasoning.summary === undefined) reasoning.summary = "auto";
|
|
536
|
+
payload.reasoning = reasoning;
|
|
537
|
+
}
|
|
538
|
+
|
|
471
539
|
export async function applyKimiPayloadMutations(
|
|
472
540
|
payload: JsonRecord,
|
|
473
541
|
ctx: KimiPayloadContext,
|
|
@@ -491,6 +559,8 @@ export async function applyKimiPayloadMutations(
|
|
|
491
559
|
}
|
|
492
560
|
if (ctx.api === "openai-completions") {
|
|
493
561
|
normalizeOpenAIAssistantToolCalls(payload);
|
|
562
|
+
}
|
|
563
|
+
if (ctx.api === "openai-completions" || ctx.api === "openai-responses") {
|
|
494
564
|
normalizeOpenAIToolSchemas(payload);
|
|
495
565
|
}
|
|
496
566
|
if (Array.isArray(payload.tools)) {
|
|
@@ -530,13 +600,23 @@ export async function applyKimiPayloadMutations(
|
|
|
530
600
|
}
|
|
531
601
|
}
|
|
532
602
|
|
|
533
|
-
// 6. Normalize
|
|
534
|
-
// /messages uses max_tokens natively).
|
|
603
|
+
// 6. Normalize output-token field names per protocol.
|
|
535
604
|
if (ctx.api === "openai-completions") {
|
|
536
605
|
if (payload.max_completion_tokens === undefined && typeof payload.max_tokens === "number") {
|
|
537
606
|
payload.max_completion_tokens = payload.max_tokens;
|
|
538
607
|
}
|
|
539
608
|
delete payload.max_tokens;
|
|
609
|
+
} else if (ctx.api === "openai-responses") {
|
|
610
|
+
if (payload.max_output_tokens === undefined) {
|
|
611
|
+
if (typeof payload.max_completion_tokens === "number") {
|
|
612
|
+
payload.max_output_tokens = payload.max_completion_tokens;
|
|
613
|
+
} else if (typeof payload.max_tokens === "number") {
|
|
614
|
+
payload.max_output_tokens = payload.max_tokens;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
delete payload.max_tokens;
|
|
618
|
+
delete payload.max_completion_tokens;
|
|
619
|
+
delete payload.prompt_cache_retention;
|
|
540
620
|
}
|
|
541
621
|
|
|
542
622
|
const generation = ctx.modelConfig.generation;
|
|
@@ -552,7 +632,12 @@ export async function applyKimiPayloadMutations(
|
|
|
552
632
|
// Output cap: an explicit cap (KIMI_MODEL_MAX_COMPLETION_TOKENS / config →
|
|
553
633
|
// generation.maxCompletionTokens) is honored as a hardCap on the request value.
|
|
554
634
|
if (generation.maxCompletionTokens !== undefined) {
|
|
555
|
-
const maxTokensKey =
|
|
635
|
+
const maxTokensKey =
|
|
636
|
+
ctx.api === "anthropic-messages"
|
|
637
|
+
? "max_tokens"
|
|
638
|
+
: ctx.api === "openai-responses"
|
|
639
|
+
? "max_output_tokens"
|
|
640
|
+
: "max_completion_tokens";
|
|
556
641
|
const currentMaxTokens = payload[maxTokensKey];
|
|
557
642
|
payload[maxTokensKey] =
|
|
558
643
|
typeof currentMaxTokens === "number"
|
|
@@ -560,87 +645,98 @@ export async function applyKimiPayloadMutations(
|
|
|
560
645
|
: generation.maxCompletionTokens;
|
|
561
646
|
}
|
|
562
647
|
|
|
563
|
-
// 7. Reasoning effort mapping.
|
|
564
|
-
//
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
648
|
+
// 7. Reasoning effort mapping.
|
|
649
|
+
// Completions/Anthropic: effort lives in thinking / output_config.
|
|
650
|
+
// Responses: pi-ai's native transport uses reasoning.effort; drop the
|
|
651
|
+
// Completions thinking object and clamp effort to the catalog.
|
|
652
|
+
if (ctx.api === "openai-responses") {
|
|
653
|
+
applyResponsesThinking(payload, ctx);
|
|
654
|
+
} else {
|
|
655
|
+
delete payload.reasoning_effort;
|
|
656
|
+
if (ctx.modelConfig.supportsThinkingType === "no") {
|
|
657
|
+
delete payload.thinking;
|
|
658
|
+
}
|
|
659
|
+
const resolvedReasoning = resolveThinkingLevel(ctx);
|
|
660
|
+
if (resolvedReasoning) {
|
|
661
|
+
const mapped = resolveReasoningForLevel(resolvedReasoning, ctx.modelConfig);
|
|
662
|
+
if (mapped) {
|
|
663
|
+
const oldThinking = isRecord(payload.thinking) ? payload.thinking : {};
|
|
664
|
+
const effort = ctx.reasoning
|
|
665
|
+
? mapped.effort
|
|
666
|
+
: (ctx.modelConfig.defaultEffort ?? mapped.effort);
|
|
667
|
+
const effortSupported =
|
|
668
|
+
effort !== null && ctx.modelConfig.supportEfforts?.includes(effort) === true;
|
|
669
|
+
if (ctx.api === "anthropic-messages" && oldThinking.type === "adaptive") {
|
|
670
|
+
// pi-ai >=0.82 builds adaptive thinking for models carrying
|
|
671
|
+
// compat.forceAdaptiveThinking (streamSimpleKimi sets it on the
|
|
672
|
+
// anthropic runtime model). Keep the adaptive shape — effort lives in
|
|
673
|
+
// top-level output_config there, not inside thinking — and only
|
|
674
|
+
// replace it with an explicit disable when the level maps to off.
|
|
675
|
+
if (mapped.enabled) {
|
|
676
|
+
if (effortSupported) payload.output_config = { effort };
|
|
677
|
+
else delete payload.output_config;
|
|
678
|
+
} else {
|
|
679
|
+
payload.thinking = { type: "disabled" };
|
|
680
|
+
delete payload.output_config;
|
|
681
|
+
}
|
|
588
682
|
} else {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
if (mapped.enabled && ctx.modelConfig.thinkingKeep) {
|
|
603
|
-
thinking.keep = ctx.modelConfig.thinkingKeep;
|
|
683
|
+
const thinking: JsonRecord = {
|
|
684
|
+
...oldThinking,
|
|
685
|
+
type: mapped.enabled ? "enabled" : "disabled",
|
|
686
|
+
};
|
|
687
|
+
delete thinking.effort;
|
|
688
|
+
if (!mapped.enabled) delete thinking.keep;
|
|
689
|
+
if (mapped.enabled && effortSupported) {
|
|
690
|
+
thinking.effort = effort;
|
|
691
|
+
}
|
|
692
|
+
if (mapped.enabled && ctx.modelConfig.thinkingKeep) {
|
|
693
|
+
thinking.keep = ctx.modelConfig.thinkingKeep;
|
|
694
|
+
}
|
|
695
|
+
payload.thinking = thinking;
|
|
604
696
|
}
|
|
605
|
-
payload.thinking = thinking;
|
|
606
697
|
}
|
|
607
698
|
}
|
|
608
|
-
}
|
|
609
699
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
700
|
+
// Preserved-thinking Kimi endpoints require every replayed assistant turn to
|
|
701
|
+
// carry reasoning_content, including turns whose reasoning delta was empty.
|
|
702
|
+
// pi-ai drops empty thinking blocks while building Chat Completions history,
|
|
703
|
+
// so restore the explicit empty field after the final thinking mode is known.
|
|
704
|
+
if (
|
|
705
|
+
ctx.api === "openai-completions" &&
|
|
706
|
+
isRecord(payload.thinking) &&
|
|
707
|
+
payload.thinking.type !== "disabled" &&
|
|
708
|
+
payload.thinking.keep === "all" &&
|
|
709
|
+
Array.isArray(payload.messages)
|
|
710
|
+
) {
|
|
711
|
+
for (const message of payload.messages) {
|
|
712
|
+
if (
|
|
713
|
+
isRecord(message) &&
|
|
714
|
+
message.role === "assistant" &&
|
|
715
|
+
message.reasoning_content === undefined
|
|
716
|
+
) {
|
|
717
|
+
message.reasoning_content = "";
|
|
718
|
+
}
|
|
628
719
|
}
|
|
629
720
|
}
|
|
630
721
|
}
|
|
631
722
|
|
|
632
723
|
// 8. K2.7 Code API constraints: the server rejects tool_choice "required" /
|
|
633
|
-
// function-specific when thinking is enabled (always-on).
|
|
634
|
-
// are handled in step 6 — omitted unless
|
|
635
|
-
// official kimi-code client rather
|
|
724
|
+
// function-specific when thinking is enabled (always-on). Responses only
|
|
725
|
+
// accepts auto. temperature/top_p are handled in step 6 — omitted unless
|
|
726
|
+
// explicitly configured, matching the official kimi-code client rather
|
|
727
|
+
// than pinned to 1.0/0.95.
|
|
636
728
|
if (payload.tool_choice !== undefined) {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
tc
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
729
|
+
if (ctx.api === "openai-responses") {
|
|
730
|
+
payload.tool_choice = "auto";
|
|
731
|
+
} else {
|
|
732
|
+
const tc = payload.tool_choice;
|
|
733
|
+
const isAllowed =
|
|
734
|
+
tc === "auto" ||
|
|
735
|
+
tc === "none" ||
|
|
736
|
+
(isRecord(tc) && (tc.type === "auto" || tc.type === "none"));
|
|
737
|
+
if (!isAllowed) {
|
|
738
|
+
payload.tool_choice = isRecord(tc) ? { type: "auto" } : "auto";
|
|
739
|
+
}
|
|
644
740
|
}
|
|
645
741
|
}
|
|
646
742
|
}
|
package/src/schema-dedup.ts
CHANGED
|
@@ -232,9 +232,12 @@ export function optimizeToolSchemas(tools: unknown[]): unknown[] {
|
|
|
232
232
|
|
|
233
233
|
let changed = false;
|
|
234
234
|
const result = tools.map((tool) => {
|
|
235
|
-
if (!isRecord(tool)
|
|
236
|
-
|
|
237
|
-
|
|
235
|
+
if (!isRecord(tool)) return tool;
|
|
236
|
+
// Chat Completions nests the schema under tool.function.parameters;
|
|
237
|
+
// Responses function tools carry parameters at the top level.
|
|
238
|
+
const fn = isRecord(tool.function) ? tool.function : undefined;
|
|
239
|
+
const holder: JsonRecord = fn ?? tool;
|
|
240
|
+
const params = holder.parameters;
|
|
238
241
|
if (!isRecord(params)) return tool;
|
|
239
242
|
|
|
240
243
|
const size = jsonSize(params);
|
|
@@ -242,7 +245,9 @@ export function optimizeToolSchemas(tools: unknown[]): unknown[] {
|
|
|
242
245
|
|
|
243
246
|
const optimized = deduplicateSchema(params);
|
|
244
247
|
changed = true;
|
|
245
|
-
return
|
|
248
|
+
return fn
|
|
249
|
+
? { ...tool, function: { ...fn, parameters: optimized } }
|
|
250
|
+
: { ...tool, parameters: optimized };
|
|
246
251
|
});
|
|
247
252
|
|
|
248
253
|
cachedFingerprint = fp;
|
package/src/stream.ts
CHANGED
|
@@ -32,8 +32,10 @@ type KimiStreamSimple = (
|
|
|
32
32
|
const piAiRuntime = piAi as unknown as {
|
|
33
33
|
anthropicMessagesApi?: () => { streamSimple: KimiStreamSimple };
|
|
34
34
|
openAICompletionsApi?: () => { streamSimple: KimiStreamSimple };
|
|
35
|
+
openAIResponsesApi?: () => { streamSimple: KimiStreamSimple };
|
|
35
36
|
streamSimpleAnthropic?: KimiStreamSimple;
|
|
36
37
|
streamSimpleOpenAICompletions?: KimiStreamSimple;
|
|
38
|
+
streamSimpleOpenAIResponses?: KimiStreamSimple;
|
|
37
39
|
lazyApi?: (load: () => Promise<object>) => { streamSimple: KimiStreamSimple };
|
|
38
40
|
};
|
|
39
41
|
|
|
@@ -43,22 +45,43 @@ const piAiRuntime = piAi as unknown as {
|
|
|
43
45
|
// evaluated in plain-Node environments where the subpaths do resolve.
|
|
44
46
|
const anthropicMessagesModule = "@earendil-works/pi-ai/api/anthropic-messages";
|
|
45
47
|
const openAICompletionsModule = "@earendil-works/pi-ai/api/openai-completions";
|
|
48
|
+
const openAIResponsesModule = "@earendil-works/pi-ai/api/openai-responses";
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
// lazyApi is the last resort; it only exists on pi-ai versions that ship the
|
|
51
|
+
// ./api/* subpath modules. Undefined when none of the three lookup paths
|
|
52
|
+
// resolve (e.g. pi <=0.79 has no responses entry point at all).
|
|
53
|
+
function lazyStreamSimple(load: () => Promise<object>): KimiStreamSimple | undefined {
|
|
54
|
+
return piAiRuntime.lazyApi?.(load)?.streamSimple;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Not readonly: tests swap entries to simulate runtimes missing an API.
|
|
58
|
+
const streamSimpleImpls: Record<KimiWireProtocol, KimiStreamSimple | undefined> = {
|
|
59
|
+
openai:
|
|
60
|
+
piAiRuntime.openAICompletionsApi?.().streamSimple ??
|
|
61
|
+
piAiRuntime.streamSimpleOpenAICompletions ??
|
|
62
|
+
lazyStreamSimple(() => import(openAICompletionsModule)),
|
|
63
|
+
anthropic:
|
|
64
|
+
piAiRuntime.anthropicMessagesApi?.().streamSimple ??
|
|
65
|
+
piAiRuntime.streamSimpleAnthropic ??
|
|
66
|
+
lazyStreamSimple(() => import(anthropicMessagesModule)),
|
|
67
|
+
responses:
|
|
68
|
+
piAiRuntime.openAIResponsesApi?.().streamSimple ??
|
|
69
|
+
piAiRuntime.streamSimpleOpenAIResponses ??
|
|
70
|
+
lazyStreamSimple(() => import(openAIResponsesModule)),
|
|
71
|
+
};
|
|
55
72
|
import {
|
|
56
73
|
DEFAULT_KIMI_CODE_CONFIG,
|
|
57
74
|
type KimiCodeConfig,
|
|
58
75
|
type KimiResolvedModelConfig,
|
|
59
76
|
} from "./config.ts";
|
|
60
77
|
|
|
61
|
-
import {
|
|
78
|
+
import {
|
|
79
|
+
ENV_KIMI_CODE_PROTOCOL,
|
|
80
|
+
PROVIDER_ID,
|
|
81
|
+
type KimiWireProtocol,
|
|
82
|
+
getApiProtocol,
|
|
83
|
+
getBaseUrl,
|
|
84
|
+
} from "./constants.ts";
|
|
62
85
|
import { getKimiProviderHeaders } from "./device.ts";
|
|
63
86
|
import {
|
|
64
87
|
isKimiAuthErrorMessage,
|
|
@@ -224,6 +247,40 @@ export function mergeKimiRequestHeaders(headers?: HeaderMap): HeaderMap {
|
|
|
224
247
|
return { ...getKimiProviderHeaders(), ...headers };
|
|
225
248
|
}
|
|
226
249
|
|
|
250
|
+
function streamSimpleForProtocol(
|
|
251
|
+
protocol: KimiWireProtocol,
|
|
252
|
+
model: Model<Api>,
|
|
253
|
+
context: Context,
|
|
254
|
+
options: SimpleStreamOptions,
|
|
255
|
+
): AssistantMessageEventStream {
|
|
256
|
+
const impl = streamSimpleImpls[protocol];
|
|
257
|
+
// Resolution runs once at module load against whichever pi-ai shape is
|
|
258
|
+
// installed, so an old pi-ai can leave the entry point undefined. Throw a
|
|
259
|
+
// descriptive error here — the caller invokes this inside the retry loop's
|
|
260
|
+
// try, so it surfaces as a stream error event instead of an unhandled
|
|
261
|
+
// rejection from a bare "not a function" TypeError.
|
|
262
|
+
if (!impl) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`installed pi-ai has no streamSimple entry point for ${getApiProtocol(protocol)}; ` +
|
|
265
|
+
"upgrade pi-ai or choose another KIMI_CODE_PROTOCOL",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return impl(model, context, options);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Test hook: swap a resolved stream factory (pass undefined to simulate a
|
|
272
|
+
// pi-ai runtime that lacks the entry point). Returns a restore function.
|
|
273
|
+
export function overrideStreamSimpleForTests(
|
|
274
|
+
protocol: KimiWireProtocol,
|
|
275
|
+
impl: KimiStreamSimple | undefined,
|
|
276
|
+
): () => void {
|
|
277
|
+
const original = streamSimpleImpls[protocol];
|
|
278
|
+
streamSimpleImpls[protocol] = impl;
|
|
279
|
+
return () => {
|
|
280
|
+
streamSimpleImpls[protocol] = original;
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
227
284
|
export function streamSimpleKimi(
|
|
228
285
|
model: Model<Api>,
|
|
229
286
|
context: Context,
|
|
@@ -332,8 +389,9 @@ export function streamSimpleKimi(
|
|
|
332
389
|
const patchedOptions = buildPatchedOptions(currentKey);
|
|
333
390
|
// Route by the module-level protocol flag, not model.api, since we
|
|
334
391
|
// register with a custom api type (kimi-openai-completions /
|
|
335
|
-
// kimi-anthropic-messages) to avoid overriding
|
|
336
|
-
// Anthropic/OpenAI stream handlers.
|
|
392
|
+
// kimi-anthropic-messages / kimi-openai-responses) to avoid overriding
|
|
393
|
+
// the built-in Anthropic/OpenAI stream handlers.
|
|
394
|
+
const existingCompat = (model as { compat?: Record<string, unknown> }).compat;
|
|
337
395
|
const runtimeModel = {
|
|
338
396
|
...model,
|
|
339
397
|
api: apiProtocol,
|
|
@@ -342,33 +400,38 @@ export function streamSimpleKimi(
|
|
|
342
400
|
// rejects/ignores the interleaved-thinking beta header; pi-ai >=0.82
|
|
343
401
|
// keys both behaviors off these compat flags (its own built-in
|
|
344
402
|
// kimi-coding models carry them in generated metadata).
|
|
403
|
+
// Responses rejects prompt_cache_retention (400) unless this is off.
|
|
345
404
|
...(wireProtocol === "anthropic"
|
|
346
405
|
? {
|
|
347
406
|
compat: {
|
|
348
|
-
...
|
|
407
|
+
...existingCompat,
|
|
349
408
|
forceAdaptiveThinking: true,
|
|
350
409
|
allowEmptySignature: true,
|
|
351
410
|
},
|
|
352
411
|
}
|
|
353
|
-
:
|
|
412
|
+
: wireProtocol === "responses"
|
|
413
|
+
? {
|
|
414
|
+
compat: {
|
|
415
|
+
...existingCompat,
|
|
416
|
+
supportsLongCacheRetention: false,
|
|
417
|
+
},
|
|
418
|
+
}
|
|
419
|
+
: {}),
|
|
354
420
|
} as Model<Api>;
|
|
355
|
-
const upstream =
|
|
356
|
-
wireProtocol === "openai"
|
|
357
|
-
? streamSimpleOpenAICompletions(
|
|
358
|
-
runtimeModel as Model<"openai-completions">,
|
|
359
|
-
context,
|
|
360
|
-
patchedOptions,
|
|
361
|
-
)
|
|
362
|
-
: streamSimpleAnthropic(
|
|
363
|
-
runtimeModel as Model<"anthropic-messages">,
|
|
364
|
-
context,
|
|
365
|
-
patchedOptions,
|
|
366
|
-
);
|
|
367
421
|
|
|
368
422
|
let shouldRetry = false;
|
|
369
423
|
let prefixBuffer: AssistantMessageEvent[] = [];
|
|
370
424
|
|
|
371
425
|
try {
|
|
426
|
+
// Inside the try so a missing pi-ai stream entry point (old pi-ai +
|
|
427
|
+
// a protocol it does not ship) surfaces through the catch's error
|
|
428
|
+
// event instead of escaping the async IIFE as an unhandled rejection.
|
|
429
|
+
const upstream = streamSimpleForProtocol(
|
|
430
|
+
wireProtocol,
|
|
431
|
+
runtimeModel,
|
|
432
|
+
context,
|
|
433
|
+
patchedOptions,
|
|
434
|
+
);
|
|
372
435
|
for await (const event of filterEmptyResponseStream(upstream)) {
|
|
373
436
|
// streamAnthropic emits a synthetic "start" event synchronously,
|
|
374
437
|
// before the for-await loop begins iterating and therefore before
|