pi-provider-kimi-code 0.5.1 → 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 +94 -46
- package/index.ts +310 -110
- package/package.json +8 -8
- package/src/config.ts +475 -72
- package/src/constants.ts +29 -11
- package/src/device.ts +14 -0
- package/src/models.ts +115 -51
- package/src/payload.ts +90 -47
- package/src/schema-dedup.ts +256 -0
- package/src/stream.ts +70 -17
- package/src/tools/common.ts +115 -0
- package/src/tools/datasource.ts +239 -0
- package/src/tools/moonshot.ts +22 -111
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,16 +13,95 @@ export interface KimiOAuthExtras {
|
|
|
13
13
|
contextLength?: number;
|
|
14
14
|
supportsReasoning?: boolean;
|
|
15
15
|
supportsImageIn?: boolean;
|
|
16
|
+
supportsVideoIn?: boolean;
|
|
17
|
+
supportsThinkingType?: "only" | "no" | "both";
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export type KimiOAuthCredentials = OAuthCredentials & KimiOAuthExtras;
|
|
19
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
|
+
|
|
20
92
|
interface KimiServerModel {
|
|
21
93
|
id?: unknown;
|
|
22
94
|
display_name?: unknown;
|
|
23
95
|
context_length?: unknown;
|
|
24
96
|
supports_reasoning?: unknown;
|
|
25
97
|
supports_image_in?: unknown;
|
|
98
|
+
supports_video_in?: unknown;
|
|
99
|
+
supports_thinking_type?: unknown;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseSupportsThinkingType(value: unknown): "only" | "no" | "both" | undefined {
|
|
103
|
+
if (value === "only" || value === "no" || value === "both") return value;
|
|
104
|
+
return undefined;
|
|
26
105
|
}
|
|
27
106
|
|
|
28
107
|
export function buildModelsUrl(baseUrl: string): string {
|
|
@@ -30,14 +109,23 @@ export function buildModelsUrl(baseUrl: string): string {
|
|
|
30
109
|
return base.endsWith("/v1") ? `${base}/models` : `${base}/v1/models`;
|
|
31
110
|
}
|
|
32
111
|
|
|
33
|
-
function getModelsUrl(): string {
|
|
34
|
-
return buildModelsUrl(getBaseUrl());
|
|
112
|
+
function getModelsUrl(protocol?: KimiWireProtocol): string {
|
|
113
|
+
return buildModelsUrl(getBaseUrl(protocol));
|
|
35
114
|
}
|
|
36
115
|
|
|
37
|
-
export async function discoverKimiModelMetadata(
|
|
116
|
+
export async function discoverKimiModelMetadata(
|
|
117
|
+
accessToken: string,
|
|
118
|
+
protocol?: KimiWireProtocol,
|
|
119
|
+
options: DiscoverKimiModelMetadataOptions = {},
|
|
120
|
+
): Promise<KimiOAuthExtras> {
|
|
38
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;
|
|
39
126
|
try {
|
|
40
|
-
const response = await fetch(getModelsUrl(), {
|
|
127
|
+
const response = await fetch(getModelsUrl(protocol), {
|
|
128
|
+
signal: controller.signal,
|
|
41
129
|
headers: {
|
|
42
130
|
...getCommonHeaders(),
|
|
43
131
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -47,23 +135,30 @@ export async function discoverKimiModelMetadata(accessToken: string): Promise<Ki
|
|
|
47
135
|
if (!response.ok) return {};
|
|
48
136
|
const json = (await response.json()) as { data?: unknown };
|
|
49
137
|
const list = Array.isArray(json.data) ? (json.data as KimiServerModel[]) : [];
|
|
50
|
-
const preferred =
|
|
51
|
-
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");
|
|
52
139
|
if (!preferred || typeof preferred.id !== "string") return {};
|
|
53
140
|
const extras: KimiOAuthExtras = { wireModelId: preferred.id };
|
|
54
141
|
if (typeof preferred.display_name === "string") extras.modelDisplay = preferred.display_name;
|
|
55
142
|
if (typeof preferred.context_length === "number" && preferred.context_length > 0) {
|
|
56
143
|
extras.contextLength = preferred.context_length;
|
|
57
144
|
}
|
|
58
|
-
|
|
145
|
+
const thinkingType = parseSupportsThinkingType(preferred.supports_thinking_type);
|
|
146
|
+
if (thinkingType) {
|
|
147
|
+
extras.supportsThinkingType = thinkingType;
|
|
148
|
+
} else if (typeof preferred.supports_reasoning === "boolean") {
|
|
59
149
|
extras.supportsReasoning = preferred.supports_reasoning;
|
|
60
150
|
}
|
|
61
151
|
if (typeof preferred.supports_image_in === "boolean") {
|
|
62
152
|
extras.supportsImageIn = preferred.supports_image_in;
|
|
63
153
|
}
|
|
154
|
+
if (typeof preferred.supports_video_in === "boolean") {
|
|
155
|
+
extras.supportsVideoIn = preferred.supports_video_in;
|
|
156
|
+
}
|
|
64
157
|
return extras;
|
|
65
158
|
} catch {
|
|
66
159
|
return {};
|
|
160
|
+
} finally {
|
|
161
|
+
if (timeout) clearTimeout(timeout);
|
|
67
162
|
}
|
|
68
163
|
}
|
|
69
164
|
|
|
@@ -71,9 +166,11 @@ export function applyKimiOAuthExtrasToModel(
|
|
|
71
166
|
model: Model<Api>,
|
|
72
167
|
extras: KimiOAuthExtras,
|
|
73
168
|
): Model<Api> {
|
|
74
|
-
const next: Model<Api> & { wireModelId?: string
|
|
169
|
+
const next: Model<Api> & { wireModelId?: string; supportsThinkingType?: "only" | "no" | "both" } =
|
|
170
|
+
{ ...model };
|
|
75
171
|
if (typeof extras.modelDisplay === "string" && extras.modelDisplay) {
|
|
76
172
|
next.name = extras.modelDisplay;
|
|
173
|
+
next.cost = resolveModelCost(extras.modelDisplay);
|
|
77
174
|
}
|
|
78
175
|
if (typeof extras.contextLength === "number" && extras.contextLength > 0) {
|
|
79
176
|
next.contextWindow = extras.contextLength;
|
|
@@ -81,49 +178,16 @@ export function applyKimiOAuthExtrasToModel(
|
|
|
81
178
|
if (typeof extras.wireModelId === "string" && extras.wireModelId) {
|
|
82
179
|
next.wireModelId = extras.wireModelId;
|
|
83
180
|
}
|
|
84
|
-
if (typeof extras.
|
|
181
|
+
if (typeof extras.supportsThinkingType === "string") {
|
|
182
|
+
next.reasoning = extras.supportsThinkingType !== "no";
|
|
183
|
+
next.supportsThinkingType = extras.supportsThinkingType;
|
|
184
|
+
} else if (typeof extras.supportsReasoning === "boolean") {
|
|
85
185
|
next.reasoning = extras.supportsReasoning;
|
|
186
|
+
next.supportsThinkingType = undefined;
|
|
86
187
|
}
|
|
87
|
-
if (typeof extras.supportsImageIn === "boolean") {
|
|
88
|
-
const input = [
|
|
89
|
-
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);
|
|
90
190
|
(next as unknown as { input: string[] }).input = input;
|
|
91
191
|
}
|
|
92
192
|
return next;
|
|
93
193
|
}
|
|
94
|
-
|
|
95
|
-
function parseKimiModelCapabilities(value: string | undefined): KimiOAuthExtras | null {
|
|
96
|
-
if (!value) return null;
|
|
97
|
-
const caps = new Set(
|
|
98
|
-
value
|
|
99
|
-
.split(",")
|
|
100
|
-
.map((cap) => cap.trim().toLowerCase())
|
|
101
|
-
.filter(Boolean),
|
|
102
|
-
);
|
|
103
|
-
return {
|
|
104
|
-
supportsReasoning: caps.has("thinking") || caps.has("always_thinking"),
|
|
105
|
-
supportsImageIn: caps.has("image_in"),
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function applyKimiEnvOverridesToModel(model: Model<Api>): Model<Api> {
|
|
110
|
-
const extras: KimiOAuthExtras = {};
|
|
111
|
-
const modelName = process.env.KIMI_MODEL_NAME?.trim();
|
|
112
|
-
if (modelName) {
|
|
113
|
-
extras.wireModelId = modelName;
|
|
114
|
-
extras.modelDisplay = modelName;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const maxContextSize = process.env.KIMI_MODEL_MAX_CONTEXT_SIZE?.trim();
|
|
118
|
-
if (maxContextSize) {
|
|
119
|
-
const parsed = parseInt(maxContextSize, 10);
|
|
120
|
-
if (Number.isFinite(parsed) && parsed > 0) {
|
|
121
|
-
extras.contextLength = parsed;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const capabilities = parseKimiModelCapabilities(process.env.KIMI_MODEL_CAPABILITIES);
|
|
126
|
-
if (capabilities) Object.assign(extras, capabilities);
|
|
127
|
-
|
|
128
|
-
return applyKimiOAuthExtrasToModel(model, extras);
|
|
129
|
-
}
|
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,21 +33,28 @@ export interface KimiPayloadContext {
|
|
|
37
33
|
cacheKey?: string;
|
|
38
34
|
cacheRetention: CacheRetention;
|
|
39
35
|
reasoning?: ThinkingLevel;
|
|
40
|
-
|
|
41
|
-
envOverrides: KimiEnvOverrides;
|
|
36
|
+
modelConfig: KimiResolvedModelConfig;
|
|
42
37
|
}
|
|
43
38
|
|
|
44
39
|
export function isRecord(value: unknown): value is JsonRecord {
|
|
45
40
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46
41
|
}
|
|
47
42
|
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
43
|
+
export function resolveReasoningForLevel(
|
|
44
|
+
level: string,
|
|
45
|
+
config: KimiResolvedModelConfig,
|
|
46
|
+
): ModelReasoningEntry | undefined {
|
|
47
|
+
return config.reasoningMap[level];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveThinkingLevel(ctx: KimiPayloadContext): ThinkingLevel | undefined {
|
|
51
|
+
if (ctx.modelConfig.supportsThinkingType === "no") return undefined;
|
|
52
|
+
if (ctx.modelConfig.supportsThinkingType === "only") {
|
|
53
|
+
if (!ctx.reasoning) return "low";
|
|
54
|
+
const mapped = resolveReasoningForLevel(ctx.reasoning, ctx.modelConfig);
|
|
55
|
+
if (mapped && !mapped.enabled) return "low";
|
|
56
|
+
}
|
|
57
|
+
return ctx.reasoning;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function parseInlineUploadThreshold(raw: string | undefined): number {
|
|
@@ -81,17 +84,6 @@ function getUploadFilename(mimeType: string): string {
|
|
|
81
84
|
return map[mimeType] ?? "upload.bin";
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
export function readEnvOverrides(): KimiEnvOverrides {
|
|
85
|
-
const out: KimiEnvOverrides = {};
|
|
86
|
-
const temp = process.env.KIMI_MODEL_TEMPERATURE;
|
|
87
|
-
if (temp) out.temperature = parseFloat(temp);
|
|
88
|
-
const topP = process.env.KIMI_MODEL_TOP_P;
|
|
89
|
-
if (topP) out.topP = parseFloat(topP);
|
|
90
|
-
const maxCompletionTokens = process.env.KIMI_MODEL_MAX_COMPLETION_TOKENS;
|
|
91
|
-
if (maxCompletionTokens) out.maxCompletionTokens = parseInt(maxCompletionTokens, 10);
|
|
92
|
-
return out;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
87
|
// =============================================================================
|
|
96
88
|
// File upload (I/O edge)
|
|
97
89
|
// =============================================================================
|
|
@@ -100,10 +92,12 @@ export async function uploadKimiFile(
|
|
|
100
92
|
apiKey: string,
|
|
101
93
|
mimeType: string,
|
|
102
94
|
data: string,
|
|
95
|
+
thresholdBytes?: number,
|
|
103
96
|
): Promise<string | null> {
|
|
104
97
|
const buffer = Buffer.from(data, "base64");
|
|
105
98
|
if (!mimeType.startsWith("image/")) return null;
|
|
106
|
-
const threshold =
|
|
99
|
+
const threshold =
|
|
100
|
+
thresholdBytes ?? parseInlineUploadThreshold(process.env.KIMI_CODE_UPLOAD_THRESHOLD_BYTES);
|
|
107
101
|
if (buffer.length <= threshold) return null;
|
|
108
102
|
|
|
109
103
|
const filename = getUploadFilename(mimeType);
|
|
@@ -406,6 +400,9 @@ export async function applyKimiPayloadMutations(
|
|
|
406
400
|
normalizeOpenAIAssistantToolCalls(payload);
|
|
407
401
|
normalizeOpenAIToolSchemas(payload);
|
|
408
402
|
}
|
|
403
|
+
if (Array.isArray(payload.tools)) {
|
|
404
|
+
payload.tools = optimizeToolSchemas(payload.tools);
|
|
405
|
+
}
|
|
409
406
|
|
|
410
407
|
// 3. prompt_cache_key injection. Respect any key already on the payload,
|
|
411
408
|
// otherwise fall back to the caller-provided cacheKey (sessionId or
|
|
@@ -420,35 +417,81 @@ export async function applyKimiPayloadMutations(
|
|
|
420
417
|
if (resolved) payload.prompt_cache_key = resolved;
|
|
421
418
|
}
|
|
422
419
|
|
|
423
|
-
// 4.
|
|
424
|
-
//
|
|
425
|
-
if (
|
|
426
|
-
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 };
|
|
426
|
+
}
|
|
427
|
+
|
|
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;
|
|
435
|
+
}
|
|
436
|
+
|
|
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;
|
|
427
442
|
}
|
|
428
|
-
delete payload.max_tokens;
|
|
429
443
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (
|
|
433
|
-
|
|
434
|
-
payload.
|
|
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
|
+
}
|
|
435
454
|
}
|
|
436
455
|
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
456
|
+
// 6. Reasoning effort mapping. Merges into any existing top-level thinking
|
|
457
|
+
// field (which may have come from extra_body above).
|
|
458
|
+
const resolvedReasoning = resolveThinkingLevel(ctx);
|
|
459
|
+
if (resolvedReasoning) {
|
|
460
|
+
const mapped = resolveReasoningForLevel(resolvedReasoning, ctx.modelConfig);
|
|
440
461
|
if (mapped) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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 = {
|
|
445
469
|
...oldThinking,
|
|
446
470
|
type: mapped.enabled ? "enabled" : "disabled",
|
|
447
471
|
};
|
|
448
|
-
if (mapped.enabled && ctx.thinkingKeep) {
|
|
449
|
-
(
|
|
472
|
+
if (mapped.enabled && ctx.modelConfig.thinkingKeep) {
|
|
473
|
+
(payload.thinking as JsonRecord).keep = ctx.modelConfig.thinkingKeep;
|
|
450
474
|
}
|
|
451
|
-
|
|
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";
|
|
452
495
|
}
|
|
453
496
|
}
|
|
454
497
|
}
|