pi-provider-kimi-code 0.6.0 → 0.6.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/README.md +34 -9
- package/index.ts +255 -69
- package/package.json +1 -1
- package/src/config.ts +450 -92
- package/src/constants.ts +29 -11
- package/src/device.ts +14 -0
- package/src/models.ts +97 -52
- package/src/payload.ts +81 -50
- package/src/schema-dedup.ts +256 -0
- package/src/stream.ts +54 -20
- package/src/tools/datasource.ts +52 -4
package/src/constants.ts
CHANGED
|
@@ -9,20 +9,37 @@ 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 type KimiWireProtocol = "openai" | "anthropic";
|
|
13
|
+
|
|
12
14
|
// KIMI_CODE_PROTOCOL supports two values: "openai" (default) and "anthropic".
|
|
13
|
-
export const
|
|
15
|
+
export const ENV_KIMI_CODE_PROTOCOL: KimiWireProtocol =
|
|
16
|
+
process.env.KIMI_CODE_PROTOCOL === "anthropic" ? "anthropic" : "openai";
|
|
17
|
+
|
|
18
|
+
export const IS_OPENAI_PROTOCOL = ENV_KIMI_CODE_PROTOCOL === "openai";
|
|
19
|
+
|
|
20
|
+
export function getApiProtocol(
|
|
21
|
+
protocol: KimiWireProtocol,
|
|
22
|
+
): "openai-completions" | "anthropic-messages" {
|
|
23
|
+
return protocol === "openai" ? "openai-completions" : "anthropic-messages";
|
|
24
|
+
}
|
|
14
25
|
|
|
15
|
-
export const PROTOCOL =
|
|
26
|
+
export const PROTOCOL = getApiProtocol(ENV_KIMI_CODE_PROTOCOL);
|
|
27
|
+
|
|
28
|
+
export function getKimiApiType(
|
|
29
|
+
protocol: KimiWireProtocol,
|
|
30
|
+
): "kimi-openai-completions" | "kimi-anthropic-messages" {
|
|
31
|
+
return protocol === "openai" ? "kimi-openai-completions" : "kimi-anthropic-messages";
|
|
32
|
+
}
|
|
16
33
|
|
|
17
34
|
// Use a custom api identifier so this provider never conflicts with the
|
|
18
35
|
// built-in "anthropic-messages" or "openai-completions" stream handlers.
|
|
19
|
-
export const KIMI_API_TYPE =
|
|
20
|
-
|
|
21
|
-
|
|
36
|
+
export const KIMI_API_TYPE = getKimiApiType(ENV_KIMI_CODE_PROTOCOL);
|
|
37
|
+
|
|
38
|
+
export function getDefaultBaseUrl(protocol: KimiWireProtocol): string {
|
|
39
|
+
return protocol === "openai" ? "https://api.kimi.com/coding/v1" : "https://api.kimi.com/coding";
|
|
40
|
+
}
|
|
22
41
|
|
|
23
|
-
export const DEFAULT_BASE_URL =
|
|
24
|
-
? "https://api.kimi.com/coding/v1"
|
|
25
|
-
: "https://api.kimi.com/coding";
|
|
42
|
+
export const DEFAULT_BASE_URL = getDefaultBaseUrl(ENV_KIMI_CODE_PROTOCOL);
|
|
26
43
|
|
|
27
44
|
export const KIMI_CODE_VERSION = "0.6.0";
|
|
28
45
|
export const KIMI_CODE_USER_AGENT = `kimi-code-cli/${KIMI_CODE_VERSION}`;
|
|
@@ -47,7 +64,8 @@ export function getOAuthHost(): string {
|
|
|
47
64
|
return value.trim() || DEFAULT_OAUTH_HOST;
|
|
48
65
|
}
|
|
49
66
|
|
|
50
|
-
export function getBaseUrl(): string {
|
|
51
|
-
const
|
|
52
|
-
|
|
67
|
+
export function getBaseUrl(protocol: KimiWireProtocol = ENV_KIMI_CODE_PROTOCOL): string {
|
|
68
|
+
const defaultBaseUrl = getDefaultBaseUrl(protocol);
|
|
69
|
+
const value = process.env.KIMI_CODE_BASE_URL || process.env.KIMI_BASE_URL || defaultBaseUrl;
|
|
70
|
+
return value.trim() || defaultBaseUrl;
|
|
53
71
|
}
|
package/src/device.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// headers, and the cross-platform device-model string. Pure helpers live near
|
|
3
3
|
// the side-effecting one-shots (file IO, exec) they support.
|
|
4
4
|
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
5
6
|
import { randomBytes } from "node:crypto";
|
|
6
7
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
8
|
import os from "node:os";
|
|
@@ -96,11 +97,24 @@ export function computeDeviceModel(input: DeviceModelInput): string {
|
|
|
96
97
|
return `${system} ${arch}`.trim();
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
function macOsProductVersion(): string | undefined {
|
|
101
|
+
try {
|
|
102
|
+
const version = execFileSync("/usr/bin/sw_vers", ["-productVersion"], {
|
|
103
|
+
encoding: "utf-8",
|
|
104
|
+
timeout: 1000,
|
|
105
|
+
}).trim();
|
|
106
|
+
return version.length > 0 ? version : undefined;
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
99
112
|
function getDeviceModel(): string {
|
|
100
113
|
return computeDeviceModel({
|
|
101
114
|
platform: process.platform,
|
|
102
115
|
release: os.release(),
|
|
103
116
|
arch: os.machine() || process.arch,
|
|
117
|
+
macVersion: process.platform === "darwin" ? macOsProductVersion() : undefined,
|
|
104
118
|
});
|
|
105
119
|
}
|
|
106
120
|
|
package/src/models.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Model identity: discovery against the server's /v1/models endpoint, plus the
|
|
2
|
-
// extras-merging helpers used by both the OAuth modifyModels hook
|
|
3
|
-
// `KIMI_MODEL_*` env-override path.
|
|
2
|
+
// extras-merging helpers used by both registration and the OAuth modifyModels hook.
|
|
4
3
|
|
|
5
4
|
import type { Api, Model, OAuthCredentials } from "@earendil-works/pi-ai";
|
|
5
|
+
import type { KimiInputModality, KimiResolvedModelConfig, ModelConfig } from "./config.ts";
|
|
6
6
|
|
|
7
|
-
import { getBaseUrl } from "./constants.ts";
|
|
7
|
+
import { type KimiWireProtocol, getBaseUrl } from "./constants.ts";
|
|
8
8
|
import { getCommonHeaders } from "./device.ts";
|
|
9
9
|
|
|
10
10
|
export interface KimiOAuthExtras {
|
|
@@ -13,17 +13,89 @@ export interface KimiOAuthExtras {
|
|
|
13
13
|
contextLength?: number;
|
|
14
14
|
supportsReasoning?: boolean;
|
|
15
15
|
supportsImageIn?: boolean;
|
|
16
|
+
supportsVideoIn?: boolean;
|
|
16
17
|
supportsThinkingType?: "only" | "no" | "both";
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export type KimiOAuthCredentials = OAuthCredentials & KimiOAuthExtras;
|
|
20
21
|
|
|
22
|
+
const DEFAULT_DISCOVERY_TIMEOUT_MS = 2500;
|
|
23
|
+
|
|
24
|
+
export interface DiscoverKimiModelMetadataOptions {
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function mergeInputModalities(
|
|
29
|
+
input: readonly KimiInputModality[],
|
|
30
|
+
extras: Partial<Pick<KimiOAuthExtras, "supportsImageIn" | "supportsVideoIn">>,
|
|
31
|
+
): KimiInputModality[] {
|
|
32
|
+
const next = new Set<KimiInputModality>(input);
|
|
33
|
+
next.add("text");
|
|
34
|
+
if (typeof extras.supportsImageIn === "boolean") {
|
|
35
|
+
if (extras.supportsImageIn) next.add("image");
|
|
36
|
+
else next.delete("image");
|
|
37
|
+
}
|
|
38
|
+
if (typeof extras.supportsVideoIn === "boolean") {
|
|
39
|
+
if (extras.supportsVideoIn) next.add("video");
|
|
40
|
+
else next.delete("video");
|
|
41
|
+
}
|
|
42
|
+
return (["text", "image", "video"] as const).filter((modality) => next.has(modality));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Pricing per million tokens in USD (CNY converted at ~7.25).
|
|
46
|
+
// Source: https://platform.kimi.com/docs/pricing/chat-k27-code
|
|
47
|
+
const COST_STANDARD = { input: 0.897, output: 3.724, cacheRead: 0.179, cacheWrite: 0.897 };
|
|
48
|
+
const COST_HIGH_SPEED = { input: 1.793, output: 7.448, cacheRead: 0.359, cacheWrite: 1.793 };
|
|
49
|
+
|
|
50
|
+
function resolveModelCost(modelDisplay: string | undefined): {
|
|
51
|
+
input: number;
|
|
52
|
+
output: number;
|
|
53
|
+
cacheRead: number;
|
|
54
|
+
cacheWrite: number;
|
|
55
|
+
} {
|
|
56
|
+
if (modelDisplay && /high\s*speed/i.test(modelDisplay)) return COST_HIGH_SPEED;
|
|
57
|
+
return COST_STANDARD;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildKimiModelFromConfig(config: ModelConfig): Model<Api> {
|
|
61
|
+
return {
|
|
62
|
+
id: "kimi-for-coding",
|
|
63
|
+
name: "Kimi for Coding",
|
|
64
|
+
reasoning: config.reasoning,
|
|
65
|
+
input: [...config.input] as unknown as ("text" | "image" | "video")[],
|
|
66
|
+
cost: { ...COST_STANDARD },
|
|
67
|
+
contextWindow: config.contextWindow,
|
|
68
|
+
maxTokens: config.maxTokens,
|
|
69
|
+
} as Model<Api>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function resolveKimiModelConfig(
|
|
73
|
+
config: ModelConfig,
|
|
74
|
+
extras: Partial<KimiOAuthExtras>,
|
|
75
|
+
): KimiResolvedModelConfig {
|
|
76
|
+
const resolved: KimiResolvedModelConfig = { ...config, input: [...config.input] };
|
|
77
|
+
if (typeof extras.contextLength === "number" && extras.contextLength > 0) {
|
|
78
|
+
resolved.contextWindow = extras.contextLength;
|
|
79
|
+
}
|
|
80
|
+
if (typeof extras.supportsThinkingType === "string") {
|
|
81
|
+
resolved.supportsThinkingType = extras.supportsThinkingType;
|
|
82
|
+
resolved.reasoning = extras.supportsThinkingType !== "no";
|
|
83
|
+
} else if (typeof extras.supportsReasoning === "boolean") {
|
|
84
|
+
resolved.reasoning = extras.supportsReasoning;
|
|
85
|
+
}
|
|
86
|
+
if (typeof extras.supportsImageIn === "boolean" || typeof extras.supportsVideoIn === "boolean") {
|
|
87
|
+
resolved.input = mergeInputModalities(resolved.input, extras);
|
|
88
|
+
}
|
|
89
|
+
return resolved;
|
|
90
|
+
}
|
|
91
|
+
|
|
21
92
|
interface KimiServerModel {
|
|
22
93
|
id?: unknown;
|
|
23
94
|
display_name?: unknown;
|
|
24
95
|
context_length?: unknown;
|
|
25
96
|
supports_reasoning?: unknown;
|
|
26
97
|
supports_image_in?: unknown;
|
|
98
|
+
supports_video_in?: unknown;
|
|
27
99
|
supports_thinking_type?: unknown;
|
|
28
100
|
}
|
|
29
101
|
|
|
@@ -37,14 +109,23 @@ export function buildModelsUrl(baseUrl: string): string {
|
|
|
37
109
|
return base.endsWith("/v1") ? `${base}/models` : `${base}/v1/models`;
|
|
38
110
|
}
|
|
39
111
|
|
|
40
|
-
function getModelsUrl(): string {
|
|
41
|
-
return buildModelsUrl(getBaseUrl());
|
|
112
|
+
function getModelsUrl(protocol?: KimiWireProtocol): string {
|
|
113
|
+
return buildModelsUrl(getBaseUrl(protocol));
|
|
42
114
|
}
|
|
43
115
|
|
|
44
|
-
export async function discoverKimiModelMetadata(
|
|
116
|
+
export async function discoverKimiModelMetadata(
|
|
117
|
+
accessToken: string,
|
|
118
|
+
protocol?: KimiWireProtocol,
|
|
119
|
+
options: DiscoverKimiModelMetadataOptions = {},
|
|
120
|
+
): Promise<KimiOAuthExtras> {
|
|
45
121
|
if (!accessToken) return {};
|
|
122
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS;
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
const timeout =
|
|
125
|
+
timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs).unref() : undefined;
|
|
46
126
|
try {
|
|
47
|
-
const response = await fetch(getModelsUrl(), {
|
|
127
|
+
const response = await fetch(getModelsUrl(protocol), {
|
|
128
|
+
signal: controller.signal,
|
|
48
129
|
headers: {
|
|
49
130
|
...getCommonHeaders(),
|
|
50
131
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -54,8 +135,7 @@ export async function discoverKimiModelMetadata(accessToken: string): Promise<Ki
|
|
|
54
135
|
if (!response.ok) return {};
|
|
55
136
|
const json = (await response.json()) as { data?: unknown };
|
|
56
137
|
const list = Array.isArray(json.data) ? (json.data as KimiServerModel[]) : [];
|
|
57
|
-
const preferred =
|
|
58
|
-
list.find((m) => typeof m.id === "string" && m.id === "kimi-for-coding") ?? list[0];
|
|
138
|
+
const preferred = list.find((m) => typeof m.id === "string" && m.id === "kimi-for-coding");
|
|
59
139
|
if (!preferred || typeof preferred.id !== "string") return {};
|
|
60
140
|
const extras: KimiOAuthExtras = { wireModelId: preferred.id };
|
|
61
141
|
if (typeof preferred.display_name === "string") extras.modelDisplay = preferred.display_name;
|
|
@@ -71,9 +151,14 @@ export async function discoverKimiModelMetadata(accessToken: string): Promise<Ki
|
|
|
71
151
|
if (typeof preferred.supports_image_in === "boolean") {
|
|
72
152
|
extras.supportsImageIn = preferred.supports_image_in;
|
|
73
153
|
}
|
|
154
|
+
if (typeof preferred.supports_video_in === "boolean") {
|
|
155
|
+
extras.supportsVideoIn = preferred.supports_video_in;
|
|
156
|
+
}
|
|
74
157
|
return extras;
|
|
75
158
|
} catch {
|
|
76
159
|
return {};
|
|
160
|
+
} finally {
|
|
161
|
+
if (timeout) clearTimeout(timeout);
|
|
77
162
|
}
|
|
78
163
|
}
|
|
79
164
|
|
|
@@ -85,6 +170,7 @@ export function applyKimiOAuthExtrasToModel(
|
|
|
85
170
|
{ ...model };
|
|
86
171
|
if (typeof extras.modelDisplay === "string" && extras.modelDisplay) {
|
|
87
172
|
next.name = extras.modelDisplay;
|
|
173
|
+
next.cost = resolveModelCost(extras.modelDisplay);
|
|
88
174
|
}
|
|
89
175
|
if (typeof extras.contextLength === "number" && extras.contextLength > 0) {
|
|
90
176
|
next.contextWindow = extras.contextLength;
|
|
@@ -99,50 +185,9 @@ export function applyKimiOAuthExtrasToModel(
|
|
|
99
185
|
next.reasoning = extras.supportsReasoning;
|
|
100
186
|
next.supportsThinkingType = undefined;
|
|
101
187
|
}
|
|
102
|
-
if (typeof extras.supportsImageIn === "boolean") {
|
|
103
|
-
const input = [
|
|
104
|
-
if (extras.supportsImageIn) input.push("image");
|
|
188
|
+
if (typeof extras.supportsImageIn === "boolean" || typeof extras.supportsVideoIn === "boolean") {
|
|
189
|
+
const input = mergeInputModalities(next.input as KimiInputModality[], extras);
|
|
105
190
|
(next as unknown as { input: string[] }).input = input;
|
|
106
191
|
}
|
|
107
192
|
return next;
|
|
108
193
|
}
|
|
109
|
-
|
|
110
|
-
function parseKimiModelCapabilities(value: string | undefined): KimiOAuthExtras | null {
|
|
111
|
-
if (!value) return null;
|
|
112
|
-
const caps = new Set(
|
|
113
|
-
value
|
|
114
|
-
.split(",")
|
|
115
|
-
.map((cap) => cap.trim().toLowerCase())
|
|
116
|
-
.filter(Boolean),
|
|
117
|
-
);
|
|
118
|
-
return {
|
|
119
|
-
supportsThinkingType: caps.has("always_thinking")
|
|
120
|
-
? "only"
|
|
121
|
-
: caps.has("thinking")
|
|
122
|
-
? "both"
|
|
123
|
-
: "no",
|
|
124
|
-
supportsImageIn: caps.has("image_in"),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function applyKimiEnvOverridesToModel(model: Model<Api>): Model<Api> {
|
|
129
|
-
const extras: KimiOAuthExtras = {};
|
|
130
|
-
const modelName = process.env.KIMI_MODEL_NAME?.trim();
|
|
131
|
-
if (modelName) {
|
|
132
|
-
extras.wireModelId = modelName;
|
|
133
|
-
extras.modelDisplay = modelName;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const maxContextSize = process.env.KIMI_MODEL_MAX_CONTEXT_SIZE?.trim();
|
|
137
|
-
if (maxContextSize) {
|
|
138
|
-
const parsed = parseInt(maxContextSize, 10);
|
|
139
|
-
if (Number.isFinite(parsed) && parsed > 0) {
|
|
140
|
-
extras.contextLength = parsed;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const capabilities = parseKimiModelCapabilities(process.env.KIMI_MODEL_CAPABILITIES);
|
|
145
|
-
if (capabilities) Object.assign(extras, capabilities);
|
|
146
|
-
|
|
147
|
-
return applyKimiOAuthExtrasToModel(model, extras);
|
|
148
|
-
}
|
package/src/payload.ts
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// and the top-level applyKimiPayloadMutations that orchestrates them.
|
|
4
4
|
|
|
5
5
|
import type { CacheRetention, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
6
|
+
import type { KimiResolvedModelConfig, ModelReasoningEntry } from "./config.ts";
|
|
6
7
|
|
|
7
8
|
import { getBaseUrl } from "./constants.ts";
|
|
8
9
|
import { getCommonHeaders } from "./device.ts";
|
|
10
|
+
import { optimizeToolSchemas } from "./schema-dedup.ts";
|
|
9
11
|
|
|
10
12
|
// =============================================================================
|
|
11
13
|
// Shared types + small utilities
|
|
@@ -16,12 +18,6 @@ const DEFAULT_KIMI_INLINE_UPLOAD_THRESHOLD_BYTES = 1 * 1024 * 1024;
|
|
|
16
18
|
export type JsonRecord = Record<string, unknown>;
|
|
17
19
|
export type Uploader = (mimeType: string, data: string) => Promise<string | null>;
|
|
18
20
|
|
|
19
|
-
export interface KimiEnvOverrides {
|
|
20
|
-
temperature?: number;
|
|
21
|
-
topP?: number;
|
|
22
|
-
maxCompletionTokens?: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
21
|
export function resolveCacheRetention(value?: CacheRetention): CacheRetention {
|
|
26
22
|
if (value === "none" || value === "short" || value === "long") return value;
|
|
27
23
|
const envRetention = process.env.PI_CACHE_RETENTION;
|
|
@@ -37,29 +33,25 @@ export interface KimiPayloadContext {
|
|
|
37
33
|
cacheKey?: string;
|
|
38
34
|
cacheRetention: CacheRetention;
|
|
39
35
|
reasoning?: ThinkingLevel;
|
|
40
|
-
|
|
41
|
-
envOverrides: KimiEnvOverrides;
|
|
42
|
-
supportsThinkingType?: "only" | "no" | "both";
|
|
36
|
+
modelConfig: KimiResolvedModelConfig;
|
|
43
37
|
}
|
|
44
38
|
|
|
45
39
|
export function isRecord(value: unknown): value is JsonRecord {
|
|
46
40
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
47
41
|
}
|
|
48
42
|
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (level === "high" || level === "xhigh") return { effort: "high", enabled: true };
|
|
55
|
-
return undefined;
|
|
43
|
+
export function resolveReasoningForLevel(
|
|
44
|
+
level: string,
|
|
45
|
+
config: KimiResolvedModelConfig,
|
|
46
|
+
): ModelReasoningEntry | undefined {
|
|
47
|
+
return config.reasoningMap[level];
|
|
56
48
|
}
|
|
57
49
|
|
|
58
50
|
function resolveThinkingLevel(ctx: KimiPayloadContext): ThinkingLevel | undefined {
|
|
59
|
-
if (ctx.supportsThinkingType === "no") return undefined;
|
|
60
|
-
if (ctx.supportsThinkingType === "only") {
|
|
51
|
+
if (ctx.modelConfig.supportsThinkingType === "no") return undefined;
|
|
52
|
+
if (ctx.modelConfig.supportsThinkingType === "only") {
|
|
61
53
|
if (!ctx.reasoning) return "low";
|
|
62
|
-
const mapped =
|
|
54
|
+
const mapped = resolveReasoningForLevel(ctx.reasoning, ctx.modelConfig);
|
|
63
55
|
if (mapped && !mapped.enabled) return "low";
|
|
64
56
|
}
|
|
65
57
|
return ctx.reasoning;
|
|
@@ -92,17 +84,6 @@ function getUploadFilename(mimeType: string): string {
|
|
|
92
84
|
return map[mimeType] ?? "upload.bin";
|
|
93
85
|
}
|
|
94
86
|
|
|
95
|
-
export function readEnvOverrides(): KimiEnvOverrides {
|
|
96
|
-
const out: KimiEnvOverrides = {};
|
|
97
|
-
const temp = process.env.KIMI_MODEL_TEMPERATURE;
|
|
98
|
-
if (temp) out.temperature = parseFloat(temp);
|
|
99
|
-
const topP = process.env.KIMI_MODEL_TOP_P;
|
|
100
|
-
if (topP) out.topP = parseFloat(topP);
|
|
101
|
-
const maxCompletionTokens = process.env.KIMI_MODEL_MAX_COMPLETION_TOKENS;
|
|
102
|
-
if (maxCompletionTokens) out.maxCompletionTokens = parseInt(maxCompletionTokens, 10);
|
|
103
|
-
return out;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
87
|
// =============================================================================
|
|
107
88
|
// File upload (I/O edge)
|
|
108
89
|
// =============================================================================
|
|
@@ -111,10 +92,12 @@ export async function uploadKimiFile(
|
|
|
111
92
|
apiKey: string,
|
|
112
93
|
mimeType: string,
|
|
113
94
|
data: string,
|
|
95
|
+
thresholdBytes?: number,
|
|
114
96
|
): Promise<string | null> {
|
|
115
97
|
const buffer = Buffer.from(data, "base64");
|
|
116
98
|
if (!mimeType.startsWith("image/")) return null;
|
|
117
|
-
const threshold =
|
|
99
|
+
const threshold =
|
|
100
|
+
thresholdBytes ?? parseInlineUploadThreshold(process.env.KIMI_CODE_UPLOAD_THRESHOLD_BYTES);
|
|
118
101
|
if (buffer.length <= threshold) return null;
|
|
119
102
|
|
|
120
103
|
const filename = getUploadFilename(mimeType);
|
|
@@ -417,6 +400,9 @@ export async function applyKimiPayloadMutations(
|
|
|
417
400
|
normalizeOpenAIAssistantToolCalls(payload);
|
|
418
401
|
normalizeOpenAIToolSchemas(payload);
|
|
419
402
|
}
|
|
403
|
+
if (Array.isArray(payload.tools)) {
|
|
404
|
+
payload.tools = optimizeToolSchemas(payload.tools);
|
|
405
|
+
}
|
|
420
406
|
|
|
421
407
|
// 3. prompt_cache_key injection. Respect any key already on the payload,
|
|
422
408
|
// otherwise fall back to the caller-provided cacheKey (sessionId or
|
|
@@ -431,36 +417,81 @@ export async function applyKimiPayloadMutations(
|
|
|
431
417
|
if (resolved) payload.prompt_cache_key = resolved;
|
|
432
418
|
}
|
|
433
419
|
|
|
434
|
-
// 4.
|
|
435
|
-
//
|
|
436
|
-
if (
|
|
437
|
-
payload.
|
|
420
|
+
// 4. Request usage stats on streaming responses (OpenAI only —
|
|
421
|
+
// Anthropic /messages does not support stream_options).
|
|
422
|
+
if (ctx.api === "openai-completions" && payload.stream === true) {
|
|
423
|
+
payload.stream_options = isRecord(payload.stream_options)
|
|
424
|
+
? { ...(payload.stream_options as JsonRecord), include_usage: true }
|
|
425
|
+
: { include_usage: true };
|
|
438
426
|
}
|
|
439
|
-
delete payload.max_tokens;
|
|
440
427
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (
|
|
444
|
-
|
|
445
|
-
|
|
428
|
+
// 5. Normalize deprecated max_tokens (OpenAI path only — Anthropic
|
|
429
|
+
// /messages uses max_tokens natively).
|
|
430
|
+
if (ctx.api === "openai-completions") {
|
|
431
|
+
if (payload.max_completion_tokens === undefined && typeof payload.max_tokens === "number") {
|
|
432
|
+
payload.max_completion_tokens = payload.max_tokens;
|
|
433
|
+
}
|
|
434
|
+
delete payload.max_tokens;
|
|
446
435
|
}
|
|
447
436
|
|
|
448
|
-
|
|
437
|
+
const generation = ctx.modelConfig.generation;
|
|
438
|
+
if (generation.temperature !== undefined) payload.temperature = generation.temperature;
|
|
439
|
+
if (generation.topP !== undefined) payload.top_p = generation.topP;
|
|
440
|
+
if (generation.maxCompletionTokens !== undefined) {
|
|
441
|
+
payload.max_completion_tokens = generation.maxCompletionTokens;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 5. Spread extra_body into top-level payload before reasoning mapping,
|
|
445
|
+
// matching upstream (Kimi expects thinking etc. at the root).
|
|
446
|
+
if (isRecord(payload.extra_body)) {
|
|
447
|
+
const extraBody = payload.extra_body as JsonRecord;
|
|
448
|
+
delete payload.extra_body;
|
|
449
|
+
for (const [key, value] of Object.entries(extraBody)) {
|
|
450
|
+
if (payload[key] === undefined) {
|
|
451
|
+
payload[key] = value;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 6. Reasoning effort mapping. Merges into any existing top-level thinking
|
|
457
|
+
// field (which may have come from extra_body above).
|
|
449
458
|
const resolvedReasoning = resolveThinkingLevel(ctx);
|
|
450
459
|
if (resolvedReasoning) {
|
|
451
|
-
const mapped =
|
|
460
|
+
const mapped = resolveReasoningForLevel(resolvedReasoning, ctx.modelConfig);
|
|
452
461
|
if (mapped) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
462
|
+
if (mapped.effort !== null) {
|
|
463
|
+
payload.reasoning_effort = mapped.effort;
|
|
464
|
+
} else {
|
|
465
|
+
delete payload.reasoning_effort;
|
|
466
|
+
}
|
|
467
|
+
const oldThinking = isRecord(payload.thinking) ? payload.thinking : {};
|
|
468
|
+
payload.thinking = {
|
|
457
469
|
...oldThinking,
|
|
458
470
|
type: mapped.enabled ? "enabled" : "disabled",
|
|
459
471
|
};
|
|
460
|
-
if (mapped.enabled && ctx.thinkingKeep) {
|
|
461
|
-
(
|
|
472
|
+
if (mapped.enabled && ctx.modelConfig.thinkingKeep) {
|
|
473
|
+
(payload.thinking as JsonRecord).keep = ctx.modelConfig.thinkingKeep;
|
|
462
474
|
}
|
|
463
|
-
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 7. K2.7 Code API constraints: the server rejects non-default values for
|
|
479
|
+
// temperature (must be 1.0) and top_p (must be 0.95), and tool_choice
|
|
480
|
+
// "required" / function-specific when thinking is enabled (always-on).
|
|
481
|
+
if (payload.temperature !== undefined && payload.temperature !== 1) {
|
|
482
|
+
delete payload.temperature;
|
|
483
|
+
}
|
|
484
|
+
if (payload.top_p !== undefined && payload.top_p !== 0.95) {
|
|
485
|
+
delete payload.top_p;
|
|
486
|
+
}
|
|
487
|
+
if (payload.tool_choice !== undefined) {
|
|
488
|
+
const tc = payload.tool_choice;
|
|
489
|
+
const isAllowed =
|
|
490
|
+
tc === "auto" ||
|
|
491
|
+
tc === "none" ||
|
|
492
|
+
(isRecord(tc) && (tc.type === "auto" || tc.type === "none"));
|
|
493
|
+
if (!isAllowed) {
|
|
494
|
+
payload.tool_choice = isRecord(tc) ? { type: "auto" } : "auto";
|
|
464
495
|
}
|
|
465
496
|
}
|
|
466
497
|
}
|