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/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 IS_OPENAI_PROTOCOL = process.env.KIMI_CODE_PROTOCOL !== "anthropic";
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 = IS_OPENAI_PROTOCOL ? "openai-completions" : "anthropic-messages";
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 = IS_OPENAI_PROTOCOL
20
- ? "kimi-openai-completions"
21
- : "kimi-anthropic-messages";
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 = IS_OPENAI_PROTOCOL
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 value = process.env.KIMI_CODE_BASE_URL || process.env.KIMI_BASE_URL || DEFAULT_BASE_URL;
52
- return value.trim() || DEFAULT_BASE_URL;
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 and the
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(accessToken: string): Promise<KimiOAuthExtras> {
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 = ["text"];
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
- thinkingKeep?: string;
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 mapThinkingLevel(level?: string): { effort: string | null; enabled: boolean } | undefined {
50
- if (!level) return undefined;
51
- if (level === "none" || level === "off") return { effort: null, enabled: false };
52
- if (level === "minimal" || level === "low") return { effort: "low", enabled: true };
53
- if (level === "medium") return { effort: "medium", enabled: true };
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 = mapThinkingLevel(ctx.reasoning);
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 = parseInlineUploadThreshold(process.env.KIMI_CODE_UPLOAD_THRESHOLD_BYTES);
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. Normalize deprecated max_tokens and apply env-level hyperparameter
435
- // overrides (pre-parsed into numbers by caller).
436
- if (payload.max_completion_tokens === undefined && typeof payload.max_tokens === "number") {
437
- payload.max_completion_tokens = payload.max_tokens;
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
- const { temperature, topP, maxCompletionTokens } = ctx.envOverrides;
442
- if (temperature !== undefined) payload.temperature = temperature;
443
- if (topP !== undefined) payload.top_p = topP;
444
- if (maxCompletionTokens !== undefined) {
445
- payload.max_completion_tokens = maxCompletionTokens;
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
- // 5. Reasoning effort mapping.
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 = mapThinkingLevel(resolvedReasoning);
460
+ const mapped = resolveReasoningForLevel(resolvedReasoning, ctx.modelConfig);
452
461
  if (mapped) {
453
- payload.reasoning_effort = mapped.effort;
454
- const extraBody = isRecord(payload.extra_body) ? payload.extra_body : {};
455
- const oldThinking = isRecord(extraBody.thinking) ? extraBody.thinking : {};
456
- extraBody.thinking = {
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
- (extraBody.thinking as JsonRecord).keep = ctx.thinkingKeep;
472
+ if (mapped.enabled && ctx.modelConfig.thinkingKeep) {
473
+ (payload.thinking as JsonRecord).keep = ctx.modelConfig.thinkingKeep;
462
474
  }
463
- payload.extra_body = extraBody;
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
  }