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/src/config.ts CHANGED
@@ -1,31 +1,165 @@
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";
5
+
6
+ export const KIMI_TOOL_NAMES = ["moonshot_search", "moonshot_fetch", "kimi_datasource"] as const;
7
+
8
+ export type KimiToolName = (typeof KIMI_TOOL_NAMES)[number];
9
+
10
+ export type KimiConfigSource = "runtime" | "env" | "project" | "home" | "default";
11
+
12
+ export type KimiInputModality = "text" | "image" | "video";
13
+
14
+ export interface ModelReasoningEntry {
15
+ effort: string | null;
16
+ enabled: boolean;
17
+ }
18
+
19
+ export type ModelReasoningMap = Record<string, ModelReasoningEntry>;
20
+
21
+ export interface ModelGeneration {
22
+ temperature?: number;
23
+ topP?: number;
24
+ maxCompletionTokens?: number;
25
+ }
26
+
27
+ export interface ModelConfig {
28
+ contextWindow: number;
29
+ maxTokens: number;
30
+ input: KimiInputModality[];
31
+ reasoning: boolean;
32
+ reasoningMap: ModelReasoningMap;
33
+ thinkingKeep: "all" | "last" | "none" | null;
34
+ generation: ModelGeneration;
35
+ }
36
+
37
+ export interface KimiResolvedModelConfig extends ModelConfig {
38
+ supportsThinkingType?: "only" | "no" | "both";
39
+ }
40
+
4
41
  export interface KimiCodeConfig {
5
- tools: {
6
- moonshot_search: { enabled: boolean; default_collapsed: boolean };
7
- moonshot_fetch: { enabled: boolean; default_collapsed: boolean };
42
+ model: ModelConfig;
43
+ tools: Record<KimiToolName, { enabled: boolean; default_collapsed: boolean }>;
44
+ uploads: { thresholdBytes: number };
45
+ protocol: "openai" | "anthropic";
46
+ }
47
+
48
+ export type KimiCodeConfigPatch = Partial<{
49
+ model: Partial<{
50
+ contextWindow: unknown;
51
+ maxTokens: unknown;
52
+ input: unknown;
53
+ reasoning: unknown;
54
+ reasoningMap: unknown;
55
+ thinkingKeep: unknown;
56
+ generation: unknown;
57
+ }>;
58
+ tools: Partial<Record<KimiToolName, Partial<{ enabled: unknown; default_collapsed: unknown }>>>;
59
+ uploads: Partial<{ thresholdBytes: unknown }>;
60
+ protocol: unknown;
61
+ }>;
62
+
63
+ export interface KimiCodeConfigSources {
64
+ model: {
65
+ contextWindow: KimiConfigSource;
66
+ maxTokens: KimiConfigSource;
67
+ input: KimiConfigSource;
68
+ reasoning: KimiConfigSource;
69
+ reasoningMap: KimiConfigSource;
70
+ thinkingKeep: KimiConfigSource;
71
+ generation: {
72
+ temperature: KimiConfigSource;
73
+ topP: KimiConfigSource;
74
+ maxCompletionTokens: KimiConfigSource;
75
+ };
8
76
  };
77
+ tools: Record<KimiToolName, { enabled: KimiConfigSource; default_collapsed: KimiConfigSource }>;
78
+ uploads: { thresholdBytes: KimiConfigSource };
79
+ protocol: KimiConfigSource;
9
80
  }
10
81
 
11
82
  export interface LoadKimiCodeConfigOptions {
12
83
  cwd: string;
13
84
  home: string;
85
+ env?: NodeJS.ProcessEnv;
86
+ }
87
+
88
+ export class ConfigError extends Error {
89
+ public readonly configPath: string;
90
+ public readonly pointer?: string;
91
+
92
+ constructor(message: string, configPath: string, pointer?: string) {
93
+ super(pointer ? `${configPath}${pointer}: ${message}` : `${configPath}: ${message}`);
94
+ this.name = "ConfigError";
95
+ this.configPath = configPath;
96
+ this.pointer = pointer;
97
+ }
98
+ }
99
+
100
+ function makeDefaultTools(): KimiCodeConfig["tools"] {
101
+ const tools = {} as KimiCodeConfig["tools"];
102
+ for (const name of KIMI_TOOL_NAMES) {
103
+ tools[name] = { enabled: false, default_collapsed: true };
104
+ }
105
+ return tools;
14
106
  }
15
107
 
16
108
  export const DEFAULT_KIMI_CODE_CONFIG: KimiCodeConfig = {
17
- tools: {
18
- moonshot_search: { enabled: false, default_collapsed: true },
19
- moonshot_fetch: { enabled: false, default_collapsed: true },
109
+ model: {
110
+ contextWindow: 262144,
111
+ maxTokens: 32000,
112
+ input: ["text", "image"],
113
+ reasoning: true,
114
+ reasoningMap: {
115
+ none: { effort: null, enabled: false },
116
+ off: { effort: null, enabled: false },
117
+ minimal: { effort: "low", enabled: true },
118
+ low: { effort: "low", enabled: true },
119
+ medium: { effort: "medium", enabled: true },
120
+ high: { effort: "high", enabled: true },
121
+ xhigh: { effort: "high", enabled: true },
122
+ },
123
+ thinkingKeep: "all",
124
+ generation: {},
20
125
  },
126
+ tools: makeDefaultTools(),
127
+ uploads: { thresholdBytes: 1048576 },
128
+ protocol: "openai",
21
129
  };
22
130
 
131
+ let runtimeOverride: KimiCodeConfigPatch = {};
132
+
133
+ export function setRuntimeKimiCodeConfigOverride(patch: KimiCodeConfigPatch): void {
134
+ runtimeOverride = mergeConfigPatch(runtimeOverride, patch);
135
+ }
136
+
137
+ export function replaceRuntimeKimiCodeConfigOverride(patch: KimiCodeConfigPatch): void {
138
+ runtimeOverride = patch;
139
+ }
140
+
141
+ export function clearRuntimeKimiCodeConfigOverride(): void {
142
+ runtimeOverride = {};
143
+ }
144
+
145
+ export function getRuntimeKimiCodeConfigOverride(): KimiCodeConfigPatch {
146
+ return clone(runtimeOverride) as KimiCodeConfigPatch;
147
+ }
148
+
23
149
  export function getGlobalKimiCodeConfigPath(home: string): string {
24
- return join(home, ".pi", "pi-provider-kimi-code.json");
150
+ return join(home, ".pi", "providers", PROVIDER_ID, "config.json");
25
151
  }
26
152
 
27
153
  export function getProjectKimiCodeConfigPath(cwd: string): string {
28
- return join(cwd, ".pi", "pi-provider-kimi-code.json");
154
+ return join(cwd, ".pi", "providers", PROVIDER_ID, "config.json");
155
+ }
156
+
157
+ export function kimiCodeConfigPath(home: string): string {
158
+ return getGlobalKimiCodeConfigPath(home);
159
+ }
160
+
161
+ function clone<T>(value: T): T {
162
+ return JSON.parse(JSON.stringify(value)) as T;
29
163
  }
30
164
 
31
165
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -33,101 +167,379 @@ function isRecord(value: unknown): value is Record<string, unknown> {
33
167
  }
34
168
 
35
169
  function readConfigFile(path: string): Record<string, unknown> {
170
+ if (!existsSync(path)) return {};
171
+
172
+ let contents: string;
36
173
  try {
37
- if (!existsSync(path)) return {};
38
- const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
39
- return isRecord(parsed) ? parsed : {};
174
+ contents = readFileSync(path, "utf8");
40
175
  } catch (error) {
41
- console.error(`[kimi-coding] failed to read config file ${path}:`, error);
42
- return {};
176
+ throw new ConfigError(
177
+ `failed to read config: ${error instanceof Error ? error.message : String(error)}`,
178
+ path,
179
+ );
180
+ }
181
+
182
+ try {
183
+ const parsed = JSON.parse(contents) as unknown;
184
+ if (isRecord(parsed)) return parsed;
185
+ throw new ConfigError("config file must be a JSON object", path);
186
+ } catch (error) {
187
+ if (error instanceof ConfigError) throw error;
188
+ throw new ConfigError(
189
+ `invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
190
+ path,
191
+ );
43
192
  }
44
193
  }
45
194
 
46
195
  function readConfigFileQuiet(path: string): Record<string, unknown> {
47
196
  try {
48
- if (!existsSync(path)) return {};
49
- const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
50
- return isRecord(parsed) ? parsed : {};
51
- } catch {
197
+ return readConfigFile(path);
198
+ } catch (error) {
199
+ console.error(`[kimi-coding] failed to read config file ${path}:`, error);
52
200
  return {};
53
201
  }
54
202
  }
55
203
 
56
- function mergeConfig(
57
- base: Record<string, unknown>,
58
- override: Record<string, unknown>,
59
- ): Record<string, unknown> {
204
+ function mergeConfigPatch<T extends Record<string, unknown>>(
205
+ base: T,
206
+ patch: Record<string, unknown>,
207
+ ): T {
60
208
  const result: Record<string, unknown> = { ...base };
61
- for (const [key, value] of Object.entries(override)) {
209
+ for (const [key, value] of Object.entries(patch)) {
210
+ if (value === undefined) continue;
62
211
  const current = result[key];
63
212
  if (isRecord(current) && isRecord(value)) {
64
- result[key] = mergeConfig(current, value);
213
+ result[key] = mergeConfigPatch(current, value);
65
214
  } else {
66
215
  result[key] = value;
67
216
  }
68
217
  }
218
+ return result as T;
219
+ }
220
+
221
+ function envConfigPatch(env: NodeJS.ProcessEnv): KimiCodeConfigPatch {
222
+ const patch: KimiCodeConfigPatch = {};
223
+
224
+ const maxContext = parsePositiveNumber(env.KIMI_MODEL_MAX_CONTEXT_SIZE);
225
+ if (maxContext !== undefined) patch.model = { ...patch.model, contextWindow: maxContext };
226
+
227
+ const temp = parseFiniteNumber(env.KIMI_MODEL_TEMPERATURE);
228
+ const topP = parseFiniteNumber(env.KIMI_MODEL_TOP_P);
229
+ const maxCompletionTokens = parsePositiveNumber(env.KIMI_MODEL_MAX_COMPLETION_TOKENS);
230
+ const thinkingKeep = env.KIMI_MODEL_THINKING_KEEP?.trim();
231
+ if (
232
+ temp !== undefined ||
233
+ topP !== undefined ||
234
+ maxCompletionTokens !== undefined ||
235
+ thinkingKeep
236
+ ) {
237
+ patch.model = {
238
+ ...patch.model,
239
+ ...(thinkingKeep ? { thinkingKeep } : {}),
240
+ generation: {
241
+ ...(temp !== undefined ? { temperature: temp } : {}),
242
+ ...(topP !== undefined ? { topP } : {}),
243
+ ...(maxCompletionTokens !== undefined ? { maxCompletionTokens } : {}),
244
+ },
245
+ };
246
+ }
247
+
248
+ const uploadThreshold = parsePositiveNumber(env.KIMI_CODE_UPLOAD_THRESHOLD_BYTES);
249
+ if (uploadThreshold !== undefined) patch.uploads = { thresholdBytes: uploadThreshold };
250
+
251
+ const protocol = env.KIMI_CODE_PROTOCOL?.trim();
252
+ if (protocol) patch.protocol = protocol;
253
+
254
+ const capabilities = env.KIMI_MODEL_CAPABILITIES;
255
+ if (capabilities) {
256
+ const caps = new Set(
257
+ capabilities
258
+ .split(",")
259
+ .map((cap) => cap.trim().toLowerCase())
260
+ .filter(Boolean),
261
+ );
262
+ patch.model = {
263
+ ...patch.model,
264
+ reasoning: caps.has("thinking") || caps.has("always_thinking"),
265
+ input: caps.has("image_in") ? ["text", "image"] : ["text"],
266
+ };
267
+ }
268
+
269
+ return patch;
270
+ }
271
+
272
+ function parsePositiveNumber(value: string | undefined): number | undefined {
273
+ if (!value) return undefined;
274
+ const parsed = Number(value);
275
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
276
+ }
277
+
278
+ function parseFiniteNumber(value: string | undefined): number | undefined {
279
+ if (!value) return undefined;
280
+ const parsed = Number(value);
281
+ return Number.isFinite(parsed) ? parsed : undefined;
282
+ }
283
+
284
+ function fail(configPath: string, pointer: string, message: string): never {
285
+ throw new ConfigError(message, configPath, pointer);
286
+ }
287
+
288
+ function requireRecord(raw: unknown, configPath: string, pointer: string): Record<string, unknown> {
289
+ if (isRecord(raw)) return raw;
290
+ return fail(
291
+ configPath,
292
+ pointer,
293
+ `expected an object, got ${Array.isArray(raw) ? "array" : typeof raw}`,
294
+ );
295
+ }
296
+
297
+ function requirePositiveNumber(raw: unknown, configPath: string, pointer: string): number {
298
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) return raw;
299
+ return fail(configPath, pointer, `expected a positive number, got ${JSON.stringify(raw)}`);
300
+ }
301
+
302
+ function requireBoolean(raw: unknown, configPath: string, pointer: string): boolean {
303
+ if (typeof raw === "boolean") return raw;
304
+ return fail(configPath, pointer, `expected a boolean, got ${JSON.stringify(raw)}`);
305
+ }
306
+
307
+ function requireProtocol(
308
+ raw: unknown,
309
+ configPath: string,
310
+ pointer: string,
311
+ ): KimiCodeConfig["protocol"] {
312
+ if (raw === "openai" || raw === "anthropic") return raw;
313
+ return fail(configPath, pointer, `expected "openai" | "anthropic", got ${JSON.stringify(raw)}`);
314
+ }
315
+
316
+ function requireInputArray(raw: unknown, configPath: string, pointer: string): KimiInputModality[] {
317
+ const valid = new Set(["text", "image", "video"]);
318
+ if (!Array.isArray(raw)) return fail(configPath, pointer, `expected an array, got ${typeof raw}`);
319
+ if (raw.length === 0) return fail(configPath, pointer, "expected a non-empty array");
320
+ return raw.map((value, index) => {
321
+ if (typeof value !== "string" || !valid.has(value)) {
322
+ return fail(
323
+ configPath,
324
+ `${pointer}/${index}`,
325
+ `expected one of text, image, video, got ${JSON.stringify(value)}`,
326
+ );
327
+ }
328
+ return value as KimiInputModality;
329
+ });
330
+ }
331
+
332
+ function requireReasoningMap(raw: unknown, configPath: string, pointer: string): ModelReasoningMap {
333
+ const record = requireRecord(raw, configPath, pointer);
334
+ const result: ModelReasoningMap = {};
335
+ for (const [key, value] of Object.entries(record)) {
336
+ const entryPointer = `${pointer}/${key}`;
337
+ const entry = requireRecord(value, configPath, entryPointer);
338
+ result[key] = {
339
+ effort:
340
+ entry.effort === null || typeof entry.effort === "string"
341
+ ? entry.effort
342
+ : fail(configPath, `${entryPointer}/effort`, "expected a string or null"),
343
+ enabled: requireBoolean(entry.enabled, configPath, `${entryPointer}/enabled`),
344
+ };
345
+ }
69
346
  return result;
70
347
  }
71
348
 
72
- function readEnabled(config: Record<string, unknown>, toolName: string): boolean {
73
- const tools = config.tools;
74
- if (!isRecord(tools)) return false;
75
- const tool = tools[toolName];
76
- if (!isRecord(tool)) return false;
77
- return tool.enabled === true;
349
+ function requireThinkingKeep(
350
+ raw: unknown,
351
+ configPath: string,
352
+ pointer: string,
353
+ ): ModelConfig["thinkingKeep"] {
354
+ if (raw === null || raw === "all" || raw === "last" || raw === "none") return raw;
355
+ return fail(
356
+ configPath,
357
+ pointer,
358
+ `expected "all" | "last" | "none" | null, got ${JSON.stringify(raw)}`,
359
+ );
360
+ }
361
+
362
+ function requireGeneration(raw: unknown, configPath: string, pointer: string): ModelGeneration {
363
+ const record = requireRecord(raw, configPath, pointer);
364
+ const result: ModelGeneration = {};
365
+ const knownKeys = ["temperature", "topP", "maxCompletionTokens"] as const;
366
+ for (const key of knownKeys) {
367
+ const value = record[key];
368
+ if (value === undefined || value === null) continue;
369
+ if (typeof value !== "number" || !Number.isFinite(value)) {
370
+ fail(configPath, `${pointer}/${key}`, `expected a number, got ${JSON.stringify(value)}`);
371
+ }
372
+ result[key] = value;
373
+ }
374
+ return result;
78
375
  }
79
376
 
80
- function readDefaultCollapsed(config: Record<string, unknown>, toolName: string): boolean {
81
- const tools = config.tools;
82
- if (!isRecord(tools)) return true;
83
- const tool = tools[toolName];
84
- if (!isRecord(tool)) return true;
85
- return tool.default_collapsed !== false;
377
+ function validateModelConfig(raw: unknown, configPath: string, pointer: string): ModelConfig {
378
+ const model = requireRecord(raw, configPath, pointer);
379
+ return {
380
+ contextWindow: requirePositiveNumber(
381
+ model.contextWindow,
382
+ configPath,
383
+ `${pointer}/contextWindow`,
384
+ ),
385
+ maxTokens: requirePositiveNumber(model.maxTokens, configPath, `${pointer}/maxTokens`),
386
+ input: requireInputArray(model.input, configPath, `${pointer}/input`),
387
+ reasoning: requireBoolean(model.reasoning, configPath, `${pointer}/reasoning`),
388
+ reasoningMap: requireReasoningMap(model.reasoningMap, configPath, `${pointer}/reasoningMap`),
389
+ thinkingKeep: requireThinkingKeep(model.thinkingKeep, configPath, `${pointer}/thinkingKeep`),
390
+ generation: requireGeneration(model.generation, configPath, `${pointer}/generation`),
391
+ };
86
392
  }
87
393
 
88
- export function loadKimiCodeConfig(options: LoadKimiCodeConfigOptions): KimiCodeConfig {
89
- const globalConfig = readConfigFile(getGlobalKimiCodeConfigPath(options.home));
90
- const projectConfig = readConfigFile(getProjectKimiCodeConfigPath(options.cwd));
91
- const merged = mergeConfig(globalConfig, projectConfig);
394
+ function validateToolsConfig(
395
+ raw: unknown,
396
+ configPath: string,
397
+ pointer: string,
398
+ ): KimiCodeConfig["tools"] {
399
+ const tools = requireRecord(raw, configPath, pointer);
400
+ const result = {} as KimiCodeConfig["tools"];
401
+ for (const name of KIMI_TOOL_NAMES) {
402
+ const tool = requireRecord(tools[name], configPath, `${pointer}/${name}`);
403
+ result[name] = {
404
+ enabled: requireBoolean(tool.enabled, configPath, `${pointer}/${name}/enabled`),
405
+ default_collapsed: requireBoolean(
406
+ tool.default_collapsed,
407
+ configPath,
408
+ `${pointer}/${name}/default_collapsed`,
409
+ ),
410
+ };
411
+ }
412
+ return result;
413
+ }
92
414
 
415
+ export function validateKimiCodeConfig(
416
+ raw: unknown,
417
+ configPath = "<kimi-code-config>",
418
+ ): KimiCodeConfig {
419
+ const config = requireRecord(raw, configPath, "");
93
420
  return {
94
- tools: {
95
- moonshot_search: {
96
- enabled: readEnabled(merged, "moonshot_search"),
97
- default_collapsed: readDefaultCollapsed(merged, "moonshot_search"),
98
- },
99
- moonshot_fetch: {
100
- enabled: readEnabled(merged, "moonshot_fetch"),
101
- default_collapsed: readDefaultCollapsed(merged, "moonshot_fetch"),
102
- },
421
+ model: validateModelConfig(config.model, configPath, "/model"),
422
+ tools: validateToolsConfig(config.tools, configPath, "/tools"),
423
+ uploads: {
424
+ thresholdBytes: requirePositiveNumber(
425
+ requireRecord(config.uploads, configPath, "/uploads").thresholdBytes,
426
+ configPath,
427
+ "/uploads/thresholdBytes",
428
+ ),
103
429
  },
430
+ protocol: requireProtocol(config.protocol, configPath, "/protocol"),
104
431
  };
105
432
  }
106
433
 
107
- export function loadProjectKimiCodeConfig(cwd: string): KimiCodeConfig {
108
- return loadKimiCodeConfigFile(getProjectKimiCodeConfigPath(cwd));
434
+ function hasPath(config: Record<string, unknown>, path: readonly string[]): boolean {
435
+ let current: unknown = config;
436
+ for (const key of path) {
437
+ if (!isRecord(current) || !Object.hasOwn(current, key)) return false;
438
+ current = current[key];
439
+ }
440
+ return true;
109
441
  }
110
442
 
111
- export function loadHomeKimiCodeConfig(home: string): KimiCodeConfig {
112
- return loadKimiCodeConfigFile(getGlobalKimiCodeConfigPath(home));
443
+ function sourceForPath(
444
+ layers: Array<{ source: KimiConfigSource; config: Record<string, unknown> }>,
445
+ path: readonly string[],
446
+ ): KimiConfigSource {
447
+ for (let i = layers.length - 1; i >= 0; i--) {
448
+ if (hasPath(layers[i].config, path)) return layers[i].source;
449
+ }
450
+ return "default";
113
451
  }
114
452
 
115
- function loadKimiCodeConfigFile(path: string): KimiCodeConfig {
116
- const projectConfig = readConfigFileQuiet(path);
453
+ function buildSources(
454
+ layers: Array<{ source: KimiConfigSource; config: Record<string, unknown> }>,
455
+ ): KimiCodeConfigSources {
456
+ const tools = {} as KimiCodeConfigSources["tools"];
457
+ for (const name of KIMI_TOOL_NAMES) {
458
+ tools[name] = {
459
+ enabled: sourceForPath(layers, ["tools", name, "enabled"]),
460
+ default_collapsed: sourceForPath(layers, ["tools", name, "default_collapsed"]),
461
+ };
462
+ }
117
463
  return {
118
- tools: {
119
- moonshot_search: {
120
- enabled: readEnabled(projectConfig, "moonshot_search"),
121
- default_collapsed: readDefaultCollapsed(projectConfig, "moonshot_search"),
122
- },
123
- moonshot_fetch: {
124
- enabled: readEnabled(projectConfig, "moonshot_fetch"),
125
- default_collapsed: readDefaultCollapsed(projectConfig, "moonshot_fetch"),
464
+ model: {
465
+ contextWindow: sourceForPath(layers, ["model", "contextWindow"]),
466
+ maxTokens: sourceForPath(layers, ["model", "maxTokens"]),
467
+ input: sourceForPath(layers, ["model", "input"]),
468
+ reasoning: sourceForPath(layers, ["model", "reasoning"]),
469
+ reasoningMap: sourceForPath(layers, ["model", "reasoningMap"]),
470
+ thinkingKeep: sourceForPath(layers, ["model", "thinkingKeep"]),
471
+ generation: {
472
+ temperature: sourceForPath(layers, ["model", "generation", "temperature"]),
473
+ topP: sourceForPath(layers, ["model", "generation", "topP"]),
474
+ maxCompletionTokens: sourceForPath(layers, ["model", "generation", "maxCompletionTokens"]),
126
475
  },
127
476
  },
477
+ tools,
478
+ uploads: { thresholdBytes: sourceForPath(layers, ["uploads", "thresholdBytes"]) },
479
+ protocol: sourceForPath(layers, ["protocol"]),
128
480
  };
129
481
  }
130
482
 
483
+ function loadLayers(
484
+ options: LoadKimiCodeConfigOptions,
485
+ ): Array<{ source: KimiConfigSource; config: Record<string, unknown> }> {
486
+ return [
487
+ { source: "home", config: readConfigFile(getGlobalKimiCodeConfigPath(options.home)) },
488
+ { source: "project", config: readConfigFile(getProjectKimiCodeConfigPath(options.cwd)) },
489
+ {
490
+ source: "env",
491
+ config: envConfigPatch(options.env ?? process.env) as Record<string, unknown>,
492
+ },
493
+ { source: "runtime", config: runtimeOverride as Record<string, unknown> },
494
+ ];
495
+ }
496
+
497
+ export function loadKimiCodeConfig(
498
+ options: LoadKimiCodeConfigOptions,
499
+ overrides?: KimiCodeConfigPatch,
500
+ ): KimiCodeConfig {
501
+ let merged = clone(DEFAULT_KIMI_CODE_CONFIG) as unknown as Record<string, unknown>;
502
+ for (const layer of loadLayers(options)) {
503
+ merged = mergeConfigPatch(merged, layer.config);
504
+ }
505
+ if (overrides) {
506
+ merged = mergeConfigPatch(merged, overrides as Record<string, unknown>);
507
+ }
508
+ return validateKimiCodeConfig(merged);
509
+ }
510
+
511
+ export function loadKimiCodeConfigSources(
512
+ options: LoadKimiCodeConfigOptions,
513
+ ): KimiCodeConfigSources {
514
+ return buildSources(loadLayers(options));
515
+ }
516
+
517
+ export function ensureKimiCodeConfig(pathOrHome: string): boolean {
518
+ const path = pathOrHome.endsWith(".json") ? pathOrHome : getGlobalKimiCodeConfigPath(pathOrHome);
519
+ if (existsSync(path)) return false;
520
+ mkdirSync(dirname(path), { recursive: true });
521
+ writeFileSync(path, `${JSON.stringify(DEFAULT_KIMI_CODE_CONFIG, null, 2)}\n`, "utf8");
522
+ return true;
523
+ }
524
+
525
+ export function loadProjectKimiCodeConfig(cwd: string): KimiCodeConfig {
526
+ return validateKimiCodeConfig(
527
+ mergeConfigPatch(
528
+ clone(DEFAULT_KIMI_CODE_CONFIG) as unknown as Record<string, unknown>,
529
+ readConfigFileQuiet(getProjectKimiCodeConfigPath(cwd)),
530
+ ),
531
+ );
532
+ }
533
+
534
+ export function loadHomeKimiCodeConfig(home: string): KimiCodeConfig {
535
+ return validateKimiCodeConfig(
536
+ mergeConfigPatch(
537
+ clone(DEFAULT_KIMI_CODE_CONFIG) as unknown as Record<string, unknown>,
538
+ readConfigFileQuiet(getGlobalKimiCodeConfigPath(home)),
539
+ ),
540
+ );
541
+ }
542
+
131
543
  export function saveProjectKimiCodeConfig(cwd: string, config: KimiCodeConfig): void {
132
544
  saveKimiCodeConfigFile(getProjectKimiCodeConfigPath(cwd), config);
133
545
  }
@@ -137,15 +549,6 @@ export function saveHomeKimiCodeConfig(home: string, config: KimiCodeConfig): vo
137
549
  }
138
550
 
139
551
  function saveKimiCodeConfigFile(path: string, config: KimiCodeConfig): void {
140
- const raw = readConfigFileQuiet(path);
141
- const next = {
142
- ...raw,
143
- tools: {
144
- ...(isRecord(raw.tools) ? raw.tools : {}),
145
- moonshot_search: { ...config.tools.moonshot_search },
146
- moonshot_fetch: { ...config.tools.moonshot_fetch },
147
- },
148
- };
149
552
  mkdirSync(dirname(path), { recursive: true });
150
- writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
553
+ writeFileSync(path, `${JSON.stringify(validateKimiCodeConfig(config, path), null, 2)}\n`, "utf8");
151
554
  }
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
  }