@ottocode/sdk 0.1.314 → 0.1.316

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.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/src/config/src/index.ts +2 -1
  3. package/src/config/src/manager.ts +1 -0
  4. package/src/core/src/providers/resolver.ts +3 -2
  5. package/src/core/src/tools/bin-manager.ts +88 -33
  6. package/src/core/src/tools/builtin/fs/edit.txt +1 -1
  7. package/src/core/src/tools/builtin/fs/index.ts +2 -0
  8. package/src/core/src/tools/builtin/fs/multiedit.txt +1 -1
  9. package/src/core/src/tools/builtin/glob.txt +4 -0
  10. package/src/core/src/tools/builtin/patch/indentation.ts +8 -1
  11. package/src/core/src/tools/builtin/patch/normalize.ts +4 -0
  12. package/src/core/src/tools/builtin/patch/repair.ts +42 -0
  13. package/src/core/src/tools/builtin/patch.txt +2 -0
  14. package/src/core/src/tools/builtin/search.txt +4 -0
  15. package/src/core/src/tools/builtin/shell.ts +161 -22
  16. package/src/core/src/tools/builtin/shell.txt +15 -1
  17. package/src/core/src/tools/loader.ts +8 -4
  18. package/src/index.ts +2 -5
  19. package/src/prompts/src/agents/build.txt +2 -2
  20. package/src/prompts/src/providers/{moonshot.txt → kimi.txt} +1 -1
  21. package/src/prompts/src/providers.ts +5 -5
  22. package/src/providers/src/catalog-manual.ts +101 -9
  23. package/src/providers/src/catalog.ts +74 -34
  24. package/src/providers/src/env.ts +4 -7
  25. package/src/providers/src/index.ts +3 -9
  26. package/src/providers/src/{moonshot-client.ts → kimi-client.ts} +131 -15
  27. package/src/providers/src/model-merge.ts +7 -1
  28. package/src/providers/src/pricing.ts +1 -1
  29. package/src/providers/src/registry.ts +8 -19
  30. package/src/providers/src/utils.ts +7 -8
  31. package/src/providers/src/zai-client.ts +3 -0
  32. package/src/types/src/config.ts +1 -0
  33. package/src/types/src/provider.ts +4 -4
@@ -3,7 +3,7 @@ import { AsyncLocalStorage } from 'node:async_hooks';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { z } from 'zod/v3';
5
5
  import DESCRIPTION from './shell.txt' with { type: 'text' };
6
- import { getShellExecutionConfig } from '../bin-manager.ts';
6
+ import { getShellExecutionConfig, type ShellEnvMode } from '../bin-manager.ts';
7
7
  import { createToolError, type ToolResponse } from '../error.ts';
8
8
  import {
9
9
  injectCoAuthorIntoGitCommit,
@@ -45,7 +45,18 @@ function killProcessTree(pid: number) {
45
45
  }
46
46
  }
47
47
 
48
+ function forceKillProcessTree(pid: number) {
49
+ try {
50
+ process.kill(-pid, 'SIGKILL');
51
+ } catch {
52
+ try {
53
+ process.kill(pid, 'SIGKILL');
54
+ } catch {}
55
+ }
56
+ }
57
+
48
58
  const REDIRECTED_SEARCH_COMMANDS = new Set(['grep', 'egrep', 'fgrep', 'rg']);
59
+ const REDIRECTED_GLOB_COMMANDS = new Set(['find', 'fd']);
49
60
 
50
61
  /**
51
62
  * Detect commands that start with a standalone grep-style search binary.
@@ -53,23 +64,78 @@ const REDIRECTED_SEARCH_COMMANDS = new Set(['grep', 'egrep', 'fgrep', 'rg']);
53
64
  * with grep/rg are redirected to the dedicated `search` tool.
54
65
  */
55
66
  export function findRedirectedSearchCommand(cmd: string): string | null {
67
+ return findRepositoryDiscoveryCommand(cmd, 'search')?.command ?? null;
68
+ }
69
+
70
+ function commandTokens(segment: string): string[] {
71
+ const tokens = segment.trim().split(/\s+/).filter(Boolean);
72
+ let index = 0;
73
+ while (
74
+ index < tokens.length &&
75
+ /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index] ?? '')
76
+ ) {
77
+ index++;
78
+ }
79
+ if (tokens[index] === 'command') index++;
80
+ return tokens.slice(index);
81
+ }
82
+
83
+ function findRepositoryDiscoveryCommand(
84
+ cmd: string,
85
+ kind?: 'search' | 'glob',
86
+ ): { command: string; tool: 'search' | 'glob' } | null {
56
87
  const segments = cmd.split(/&&|\|\||;|\n/);
57
88
  for (const segment of segments) {
58
- const tokens = segment.trim().split(/\s+/);
59
- let index = 0;
60
- while (
61
- index < tokens.length &&
62
- /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index] ?? '')
63
- ) {
64
- index++;
89
+ const tokens = commandTokens(segment);
90
+ const bin = tokens[0]?.split('/').pop() ?? '';
91
+ const second = tokens[1] ?? '';
92
+ if ((!kind || kind === 'search') && bin === 'git' && second === 'grep') {
93
+ return { command: 'git grep', tool: 'search' };
94
+ }
95
+ if ((!kind || kind === 'search') && REDIRECTED_SEARCH_COMMANDS.has(bin)) {
96
+ return { command: bin, tool: 'search' };
97
+ }
98
+ if ((!kind || kind === 'glob') && REDIRECTED_GLOB_COMMANDS.has(bin)) {
99
+ return { command: bin, tool: 'glob' };
100
+ }
101
+ if ((!kind || kind === 'glob') && bin === 'ls' && segment.includes('**')) {
102
+ return { command: 'ls **', tool: 'glob' };
65
103
  }
66
- if (tokens[index] === 'command') index++;
67
- const bin = tokens[index]?.split('/').pop() ?? '';
68
- if (REDIRECTED_SEARCH_COMMANDS.has(bin)) return bin;
69
104
  }
70
105
  return null;
71
106
  }
72
107
 
108
+ function repositoryDiscoveryHint(cmd: string): string | undefined {
109
+ const discovery = findRepositoryDiscoveryCommand(cmd);
110
+ if (!discovery) return undefined;
111
+ return discovery.tool === 'search'
112
+ ? `Tip: For repository content search, prefer the search tool instead of shelling out to ${discovery.command}. It is indexed, faster, and returns structured file:line matches.`
113
+ : `Tip: For repository file discovery, prefer the glob tool instead of shelling out to ${discovery.command}. It returns structured paths and skips common build/cache folders.`;
114
+ }
115
+
116
+ const SHELL_ENV_HINT =
117
+ 'This command may require environment from your login/interactive shell. If appropriate, retry with envMode: "login-cache" (or "login-fresh" after changing shell config).';
118
+
119
+ export function detectShellEnvHint(args: {
120
+ stdout: string;
121
+ stderr: string;
122
+ exitCode: number;
123
+ envMode?: ShellEnvMode;
124
+ }): string | undefined {
125
+ if (args.envMode && args.envMode !== 'fast') return undefined;
126
+ if (args.exitCode === 0) return undefined;
127
+ const text = `${args.stderr}\n${args.stdout}`;
128
+ const patterns = [
129
+ /\b[A-Z][A-Z0-9_]{2,}\b[^\n]*(?:not set|not defined|required|missing|must be set)/i,
130
+ /(?:missing|required|could not find)[^\n]*(?:api key|token|credential|credentials|secret|environment variable|env var)/i,
131
+ /(?:no credentials|credentials[^\n]*not found|not authenticated|authentication required|please log in|please login)/i,
132
+ /(?:asdf|nvm|mise|direnv|op|doppler)[^\n]*(?:not found|not loaded|command not found)/i,
133
+ ];
134
+ return patterns.some((pattern) => pattern.test(text))
135
+ ? SHELL_ENV_HINT
136
+ : undefined;
137
+ }
138
+
73
139
  export type ShellOutputMode = 'auto' | 'full' | 'tail';
74
140
 
75
141
  const DEFAULT_TAIL_LINES = 100;
@@ -136,6 +202,9 @@ type ShellResult = ToolResponse<{
136
202
  stderrTruncated?: boolean;
137
203
  stderrOriginalBytes?: number;
138
204
  stderrShownBytes?: number;
205
+ discoveryHint?: string;
206
+ envMode?: ShellEnvMode;
207
+ envHint?: string;
139
208
  }>;
140
209
 
141
210
  type ShellStreamChunk =
@@ -165,7 +234,7 @@ const shellInputSchema = z
165
234
  cmd: z
166
235
  .string()
167
236
  .describe(
168
- 'Non-interactive shell command to run using the user shell with login/interactive startup loaded',
237
+ 'Non-interactive shell command to run using the user shell. Login PATH is loaded, but interactive startup files are not sourced per command.',
169
238
  ),
170
239
  cwd: z
171
240
  .string()
@@ -181,6 +250,13 @@ const shellInputSchema = z
181
250
  .optional()
182
251
  .default(300000)
183
252
  .describe('Timeout in milliseconds (default: 300000 = 5 minutes)'),
253
+ envMode: z
254
+ .enum(['fast', 'login-cache', 'login-fresh'])
255
+ .optional()
256
+ .default('fast')
257
+ .describe(
258
+ 'Environment loading mode. "fast" is the default one-off shell env. "login-cache" reuses a cached environment captured from the user login/interactive shell for commands that need shell-managed credentials. "login-fresh" refreshes that cache.',
259
+ ),
184
260
  outputMode: z
185
261
  .enum(['auto', 'full', 'tail'])
186
262
  .optional()
@@ -236,6 +312,7 @@ export function buildShellTool(projectRoot: string): {
236
312
  cwd,
237
313
  allowNonZeroExit,
238
314
  timeout = 300000,
315
+ envMode = 'fast',
239
316
  outputMode = 'auto',
240
317
  tailLines = DEFAULT_TAIL_LINES,
241
318
  maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES,
@@ -263,6 +340,7 @@ export function buildShellTool(projectRoot: string): {
263
340
  cwd: absCwd,
264
341
  allowNonZeroExit,
265
342
  timeout,
343
+ envMode,
266
344
  outputMode,
267
345
  tailLines,
268
346
  maxOutputBytes,
@@ -271,7 +349,7 @@ export function buildShellTool(projectRoot: string): {
271
349
  ) as AsyncIterable<ShellStreamChunk> | ShellResult;
272
350
  }
273
351
 
274
- const shellConfig = getShellExecutionConfig(finalCmd);
352
+ const shellConfig = getShellExecutionConfig(finalCmd, { envMode });
275
353
  const proc = spawn(shellConfig.command, shellConfig.args, {
276
354
  cwd: absCwd,
277
355
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -284,8 +362,11 @@ export function buildShellTool(projectRoot: string): {
284
362
  let didTimeout = false;
285
363
  let didAbort = false;
286
364
  let settled = false;
365
+ let terminating = false;
287
366
  let done = false;
288
367
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
368
+ let killEscalationId: ReturnType<typeof setTimeout> | null = null;
369
+ let fallbackSettleId: ReturnType<typeof setTimeout> | null = null;
289
370
  const queue: ShellStreamChunk[] = [];
290
371
  let notify: (() => void) | null = null;
291
372
 
@@ -348,6 +429,8 @@ export function buildShellTool(projectRoot: string): {
348
429
  details.maxOutputBytes = maxOutputBytes;
349
430
  }
350
431
  if (timeoutId) clearTimeout(timeoutId);
432
+ if (killEscalationId) clearTimeout(killEscalationId);
433
+ if (fallbackSettleId) clearTimeout(fallbackSettleId);
351
434
  if (abortSignal) {
352
435
  abortSignal.removeEventListener('abort', onAbort);
353
436
  }
@@ -356,11 +439,54 @@ export function buildShellTool(projectRoot: string): {
356
439
  wake();
357
440
  };
358
441
 
442
+ const abortResult = () =>
443
+ createToolError(`Command aborted by user: ${cmd}`, 'abort', {
444
+ cmd,
445
+ stdout,
446
+ stderr,
447
+ envMode,
448
+ ...(outputMode === 'tail' || outputMode === 'auto'
449
+ ? { outputMode, tailLines, maxOutputBytes }
450
+ : { outputMode, maxOutputBytes }),
451
+ });
452
+
453
+ const timeoutResult = () =>
454
+ createToolError(
455
+ `Command timed out after ${timeout}ms: ${cmd}`,
456
+ 'timeout',
457
+ {
458
+ parameter: 'timeout',
459
+ value: timeout,
460
+ stdout,
461
+ stderr,
462
+ envMode,
463
+ ...(outputMode === 'tail' || outputMode === 'auto'
464
+ ? { outputMode, tailLines, maxOutputBytes }
465
+ : { outputMode, maxOutputBytes }),
466
+ suggestion: 'Increase timeout or optimize the command',
467
+ },
468
+ );
469
+
470
+ const terminate = (fallbackResult: () => ShellResult) => {
471
+ if (terminating) return;
472
+ terminating = true;
473
+ if (proc.pid) {
474
+ killProcessTree(proc.pid);
475
+ killEscalationId = setTimeout(() => {
476
+ if (proc.pid) forceKillProcessTree(proc.pid);
477
+ }, 1000);
478
+ } else {
479
+ proc.kill('SIGTERM');
480
+ }
481
+ fallbackSettleId = setTimeout(() => {
482
+ settle(fallbackResult());
483
+ }, 2000);
484
+ };
485
+
359
486
  const onAbort = () => {
360
487
  if (settled) return;
361
488
  didAbort = true;
362
- if (proc.pid) killProcessTree(proc.pid);
363
- else proc.kill('SIGTERM');
489
+ terminate(abortResult);
364
490
  };
365
491
 
366
492
  if (abortSignal) {
@@ -370,8 +496,7 @@ export function buildShellTool(projectRoot: string): {
370
496
  if (timeout > 0) {
371
497
  timeoutId = setTimeout(() => {
372
498
  didTimeout = true;
373
- if (proc.pid) killProcessTree(proc.pid);
374
- else proc.kill();
499
+ terminate(timeoutResult);
375
500
  }, timeout);
376
501
  }
377
502
 
@@ -394,12 +519,20 @@ export function buildShellTool(projectRoot: string): {
394
519
  });
395
520
 
396
521
  proc.on('close', (exitCode) => {
522
+ const resolvedExitCode = exitCode ?? 0;
523
+ const envHint = detectShellEnvHint({
524
+ stdout,
525
+ stderr,
526
+ exitCode: resolvedExitCode,
527
+ envMode,
528
+ });
397
529
  if (didAbort) {
398
530
  settle(
399
531
  createToolError(`Command aborted by user: ${cmd}`, 'abort', {
400
532
  cmd,
401
533
  stdout,
402
534
  stderr,
535
+ envMode,
403
536
  ...(outputMode === 'tail' || outputMode === 'auto'
404
537
  ? { outputMode, tailLines, maxOutputBytes }
405
538
  : { outputMode, maxOutputBytes }),
@@ -418,6 +551,7 @@ export function buildShellTool(projectRoot: string): {
418
551
  value: timeout,
419
552
  stdout,
420
553
  stderr,
554
+ envMode,
421
555
  ...(outputMode === 'tail' || outputMode === 'auto'
422
556
  ? { outputMode, tailLines, maxOutputBytes }
423
557
  : { outputMode, maxOutputBytes }),
@@ -428,15 +562,17 @@ export function buildShellTool(projectRoot: string): {
428
562
  return;
429
563
  }
430
564
 
431
- if (exitCode !== 0 && !allowNonZeroExit) {
565
+ if (resolvedExitCode !== 0 && !allowNonZeroExit) {
432
566
  const errorDetail = stderr.trim() || stdout.trim() || '';
433
- const errorMsg = `Command failed with exit code ${exitCode}${errorDetail ? `\n\n${errorDetail}` : ''}`;
567
+ const errorMsg = `Command failed with exit code ${resolvedExitCode}${errorDetail ? `\n\n${errorDetail}` : ''}`;
434
568
  settle(
435
569
  createToolError(errorMsg, 'execution', {
436
- exitCode,
570
+ exitCode: resolvedExitCode,
437
571
  stdout,
438
572
  stderr,
439
573
  cmd,
574
+ envMode,
575
+ ...(envHint ? { envHint } : {}),
440
576
  ...(outputMode === 'tail' || outputMode === 'auto'
441
577
  ? { outputMode, tailLines, maxOutputBytes }
442
578
  : { outputMode, maxOutputBytes }),
@@ -446,17 +582,20 @@ export function buildShellTool(projectRoot: string): {
446
582
  return;
447
583
  }
448
584
 
585
+ const discoveryHint = repositoryDiscoveryHint(finalCmd);
449
586
  settle({
450
587
  ok: true,
451
- exitCode: exitCode ?? 0,
588
+ exitCode: resolvedExitCode,
452
589
  stdout,
453
590
  stderr,
591
+ envMode,
592
+ ...(discoveryHint ? { discoveryHint } : {}),
593
+ ...(envHint ? { envHint } : {}),
454
594
  ...(outputMode === 'tail' || outputMode === 'auto'
455
595
  ? { outputMode, tailLines, maxOutputBytes }
456
596
  : { outputMode, maxOutputBytes }),
457
597
  });
458
598
  });
459
-
460
599
  proc.on('error', (err) => {
461
600
  settle(
462
601
  createToolError(
@@ -1,13 +1,27 @@
1
- - Execute a non-interactive shell command using the user's shell with login/interactive startup loaded
1
+ - Execute a non-interactive shell command using the user's shell
2
2
  - Returns `stdout`, `stderr`, and `exitCode`
3
3
  - `cwd` is relative to the project root and sandboxed within it
4
4
 
5
+ The shell tool loads the user's login PATH once for command lookup, but it does not source interactive startup files for every command. Use `terminal` when you need an interactive shell, a TTY, or a persistent process.
6
+
7
+ `envMode` controls extra environment loading:
8
+ - `fast` (default): fastest one-off shell environment plus cached login PATH.
9
+ - `login-cache`: reuses a cached environment captured from the user's login/interactive shell. Use this only when a prior fast command reports missing credentials/environment, or when the user explicitly asks to load shell startup environment.
10
+ - `login-fresh`: refreshes that login/interactive shell environment cache before running the command.
11
+
12
+ If a fast command fails with an `envHint`, retry once with `envMode: "login-cache"` when appropriate. Do not use login env modes for normal repository discovery, simple build/test commands, or as a blanket default.
13
+
5
14
  **Use `shell` for one-off, non-interactive commands.** These may be short-lived checks or long-running commands that finish on their own and do not require stdin. For commands that need interactive input, a TTY, or persistence across turns, use the `terminal` tool instead.
6
15
 
7
16
  For repository discovery, use `search` for content/code search and `glob` for filename/path discovery. Reserve `shell` for execution, builds, tests, diagnostics, and other command-line tasks.
8
17
 
9
18
  **Strongly prefer the `search` tool over `grep`/`rg`, and `glob` over `find`.** The `search` tool is indexed and faster, returns structured `file:line` matches, and supports regex, `glob` includes, `path` scoping, and `ignoreCase` — use it for all repository content search. Only fall back to `grep`/`rg` via shell for cases `search` cannot handle (e.g. gitignored files like node_modules or build output). Pipelines that filter program output (e.g. `ps aux | grep node`) are fine.
10
19
 
20
+ Mapping from common shell habits:
21
+ - `grep -r` / `rg` / `git grep` over repo files → use `search`.
22
+ - `find` / `fd` / recursive `ls` for filenames → use `glob`.
23
+ - Build, test, package manager, diagnostics, process inspection → use `shell`.
24
+
11
25
  ## Usage tips
12
26
 
13
27
  - Chain commands with `&&` to fail-fast.
@@ -142,7 +142,14 @@ async function discoverStaticProjectTools(
142
142
 
143
143
  const discoveryPromise = (async () => {
144
144
  const tools = new Map<string, Tool>();
145
- for (const { name, tool } of buildFsTools(projectRoot))
145
+ const fsTools = buildFsTools(projectRoot);
146
+ for (const { name, tool } of fsTools.filter(({ name }) => name === 'read'))
147
+ tools.set(name, tool);
148
+ // Put apply_patch before exact replacement tools so models see it as the
149
+ // default editing path after reading files.
150
+ const ap = buildApplyPatchTool(projectRoot);
151
+ tools.set(ap.name, ap.tool);
152
+ for (const { name, tool } of fsTools.filter(({ name }) => name !== 'read'))
146
153
  tools.set(name, tool);
147
154
  for (const { name, tool } of buildGitTools(projectRoot))
148
155
  tools.set(name, tool);
@@ -155,9 +162,6 @@ async function discoverStaticProjectTools(
155
162
  tools.set(search.name, search.tool);
156
163
  const glob = buildGlobTool(projectRoot);
157
164
  tools.set(glob.name, glob.tool);
158
- // Patch/apply
159
- const ap = buildApplyPatchTool(projectRoot);
160
- tools.set(ap.name, ap.tool);
161
165
  // Todo tracking
162
166
  tools.set('update_todos', updateTodosTool);
163
167
  // Web search
package/src/index.ts CHANGED
@@ -81,6 +81,7 @@ export type {
81
81
  } from './providers/src/index.ts';
82
82
  export {
83
83
  isBuiltInProviderId,
84
+ resolveBuiltInProviderCatalogId,
84
85
  getProviderSettings,
85
86
  getProviderDefinition,
86
87
  hasConfiguredProvider,
@@ -162,13 +163,9 @@ export { createOpencodeModel } from './providers/src/index.ts';
162
163
  export type { OpencodeProviderConfig } from './providers/src/index.ts';
163
164
  export {
164
165
  createKimiModel,
165
- createMoonshotModel,
166
166
  readKimiApiKeyFromEnv,
167
167
  } from './providers/src/index.ts';
168
- export type {
169
- KimiProviderConfig,
170
- MoonshotProviderConfig,
171
- } from './providers/src/index.ts';
168
+ export type { KimiProviderConfig } from './providers/src/index.ts';
172
169
  export { createMinimaxModel } from './providers/src/index.ts';
173
170
  export type { MinimaxProviderConfig } from './providers/src/index.ts';
174
171
  export {
@@ -8,8 +8,8 @@ You help with coding and build tasks.
8
8
 
9
9
  Pick the right tool for the job (each tool's description has its full contract):
10
10
 
11
- - Use the exact-replacement editing tools available to you for targeted changes in existing files.
12
- - Use patch-style editing for structural diffs, file add/delete/rename, or multi-file changes when that capability is available.
11
+ - Prefer `apply_patch` for code and text changes when it is available. It gives the clearest diff preview and handles targeted, structural, and multi-file edits.
12
+ - Use exact-replacement tools (`edit`/`multiedit`) when a patch would be awkward, when you have a precise replacement block, or after patch attempts fail.
13
13
  - Use `write` only for NEW files or >70% full-file rewrites. Never use it for targeted edits.
14
14
 
15
15
  **Always read a file immediately before editing it.** Memory and earlier context are not reliable — the file may have changed.
@@ -1,4 +1,4 @@
1
- You are Kimi, an agentic coding assistant by Moonshot AI operating in otto in Thinking mode. Precise, safe, helpful.
1
+ You are Kimi, an agentic coding assistant operating in otto in Thinking mode. Precise, safe, helpful.
2
2
 
3
3
  # Reasoning
4
4
 
@@ -14,7 +14,7 @@ import PROVIDER_ANTHROPIC from './providers/anthropic.txt' with {
14
14
  // eslint-disable-next-line @typescript-eslint/consistent-type-imports
15
15
  import PROVIDER_GOOGLE from './providers/google.txt' with { type: 'text' };
16
16
  // eslint-disable-next-line @typescript-eslint/consistent-type-imports
17
- import PROVIDER_MOONSHOT from './providers/moonshot.txt' with { type: 'text' };
17
+ import PROVIDER_KIMI from './providers/kimi.txt' with { type: 'text' };
18
18
  // eslint-disable-next-line @typescript-eslint/consistent-type-imports
19
19
  import PROVIDER_DEFAULT from './providers/default.txt' with { type: 'text' };
20
20
  // eslint-disable-next-line @typescript-eslint/consistent-type-imports
@@ -24,7 +24,7 @@ const FAMILY_PROMPTS: Record<string, string> = {
24
24
  openai: PROVIDER_OPENAI,
25
25
  anthropic: PROVIDER_ANTHROPIC,
26
26
  google: PROVIDER_GOOGLE,
27
- moonshot: PROVIDER_MOONSHOT,
27
+ kimi: PROVIDER_KIMI,
28
28
  glm: PROVIDER_GLM,
29
29
  minimax: PROVIDER_DEFAULT,
30
30
  };
@@ -142,9 +142,9 @@ export async function providerBasePrompt(
142
142
  const result = PROVIDER_GOOGLE.trim();
143
143
  return { prompt: result, resolvedType: 'google' };
144
144
  }
145
- if (id === 'moonshot') {
146
- const result = PROVIDER_MOONSHOT.trim();
147
- return { prompt: result, resolvedType: 'moonshot' };
145
+ if (id === 'kimi') {
146
+ const result = PROVIDER_KIMI.trim();
147
+ return { prompt: result, resolvedType: 'kimi' };
148
148
  }
149
149
  if (id === 'zai' || id === 'zai-coding') {
150
150
  const result = PROVIDER_GLM.trim();
@@ -13,6 +13,35 @@ type CatalogMap = Partial<Record<BuiltInProviderId, ProviderCatalogEntry>>;
13
13
 
14
14
  const OLLAMA_CLOUD_ID: BuiltInProviderId = 'ollama-cloud';
15
15
  const OTTOROUTER_ID: BuiltInProviderId = 'ottorouter';
16
+ const ZAI_CODING_ID: BuiltInProviderId = 'zai-coding';
17
+
18
+ const ZAI_CODING_MODEL_ORDER = [
19
+ 'glm-5.2',
20
+ 'glm-5.1',
21
+ 'glm-5-turbo',
22
+ 'glm-5',
23
+ 'glm-4.7',
24
+ 'glm-4.5-air',
25
+ 'glm-5v-turbo',
26
+ ];
27
+
28
+ const ZAI_CODING_MANUAL_MODELS: ModelInfo[] = [
29
+ {
30
+ id: 'glm-5',
31
+ ownedBy: 'zai',
32
+ label: 'GLM-5',
33
+ modalities: { input: ['text'], output: ['text'] },
34
+ toolCall: true,
35
+ reasoningText: true,
36
+ attachment: false,
37
+ temperature: true,
38
+ releaseDate: '2026-02-11',
39
+ lastUpdated: '2026-02-11',
40
+ openWeights: true,
41
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
42
+ limit: { context: 204_800, output: 131_072 },
43
+ },
44
+ ];
16
45
 
17
46
  const XAI_GROK_CLI_MODELS: ModelInfo[] = [
18
47
  {
@@ -46,7 +75,7 @@ const OWNER_NPM: Record<ModelOwner, string> = {
46
75
  google: '@ai-sdk/google',
47
76
  openrouter: '@openrouter/ai-sdk-provider',
48
77
  xai: '@ai-sdk/xai',
49
- moonshot: '@ai-sdk/openai-compatible',
78
+ kimi: '@ai-sdk/openai-compatible',
50
79
  qwen: '@ai-sdk/openai-compatible',
51
80
  zai: '@ai-sdk/openai-compatible',
52
81
  minimax: '@ai-sdk/anthropic',
@@ -162,26 +191,85 @@ const DEPRECATED_KIMI_MODEL_IDS = new Set([
162
191
  'kimi-k2-turbo-preview',
163
192
  ]);
164
193
 
194
+ const KIMI_MANUAL_MODELS: ModelInfo[] = [
195
+ {
196
+ id: 'kimi-k2.7-code-highspeed',
197
+ ownedBy: 'kimi',
198
+ label: 'Kimi K2.7 Code Highspeed',
199
+ modalities: { input: ['text', 'image', 'video'], output: ['text'] },
200
+ toolCall: true,
201
+ reasoningText: true,
202
+ attachment: true,
203
+ temperature: false,
204
+ knowledge: '2025-01',
205
+ openWeights: true,
206
+ cost: { input: 1.9, output: 8, cacheRead: 0.38 },
207
+ limit: { context: 262_144, output: 262_144 },
208
+ },
209
+ ];
210
+
165
211
  export function filterAvailableKimiModels(models: ModelInfo[]): ModelInfo[] {
166
212
  return models.filter((model) => !DEPRECATED_KIMI_MODEL_IDS.has(model.id));
167
213
  }
168
214
 
215
+ function appendKimiManualModels(models: ModelInfo[]): ModelInfo[] {
216
+ const manualById = new Map(
217
+ KIMI_MANUAL_MODELS.map((model) => [model.id, model]),
218
+ );
219
+ const mergedModels = models.map((model) => {
220
+ const override = manualById.get(model.id);
221
+ return override ? { ...model, ...override } : model;
222
+ });
223
+ const existingIds = new Set(mergedModels.map((model) => model.id));
224
+ const missingModels = KIMI_MANUAL_MODELS.filter(
225
+ (model) => !existingIds.has(model.id),
226
+ );
227
+ return missingModels.length
228
+ ? [...mergedModels, ...missingModels]
229
+ : mergedModels;
230
+ }
231
+
169
232
  export function applyOfficialKimiCatalogMetadata<
170
233
  T extends ProviderCatalogEntry,
171
234
  >(entry: T | undefined): T | undefined {
172
235
  if (!entry) return undefined;
173
- const env = Array.from(
174
- new Set(['KIMI_API_KEY', 'MOONSHOT_API_KEY', ...(entry.env ?? [])]),
175
- );
236
+ const env = Array.from(new Set(['KIMI_API_KEY', ...(entry.env ?? [])]));
176
237
  return {
177
238
  ...entry,
178
- models: filterAvailableKimiModels(entry.models),
179
- label: entry.label === 'Moonshot AI' ? 'Kimi' : entry.label,
239
+ models: appendKimiManualModels(filterAvailableKimiModels(entry.models)),
240
+ label: 'Kimi',
180
241
  env,
181
242
  doc: 'https://platform.kimi.ai/docs/api/overview.md',
182
243
  };
183
244
  }
184
245
 
246
+ export function applyZaiCodingCatalogMetadata<T extends ProviderCatalogEntry>(
247
+ entry: T | undefined,
248
+ ): T | undefined {
249
+ if (!entry) return undefined;
250
+ const order = new Map(
251
+ ZAI_CODING_MODEL_ORDER.map((modelId, index) => [modelId, index]),
252
+ );
253
+ const modelById = new Map(entry.models.map((model) => [model.id, model]));
254
+ for (const model of ZAI_CODING_MANUAL_MODELS) {
255
+ if (!modelById.has(model.id)) modelById.set(model.id, model);
256
+ }
257
+ const models = Array.from(modelById.values()).sort((a, b) => {
258
+ const orderA = order.get(a.id) ?? Number.MAX_SAFE_INTEGER;
259
+ const orderB = order.get(b.id) ?? Number.MAX_SAFE_INTEGER;
260
+ if (orderA !== orderB) return orderA - orderB;
261
+ return a.id.localeCompare(b.id);
262
+ });
263
+ return {
264
+ ...entry,
265
+ models,
266
+ label: 'Z.AI Coding Plan',
267
+ env: ['ZAI_CODING_API_KEY'],
268
+ api: 'https://api.z.ai/api/coding/paas/v4',
269
+ doc: 'https://docs.z.ai/devpack/overview',
270
+ };
271
+ }
272
+
185
273
  export function mergeManualCatalog(
186
274
  base: CatalogMap,
187
275
  ): Record<BuiltInProviderId, ProviderCatalogEntry> {
@@ -195,9 +283,13 @@ export function mergeManualCatalog(
195
283
  if (xaiEntry) {
196
284
  merged.xai = xaiEntry;
197
285
  }
198
- const moonshotEntry = applyOfficialKimiCatalogMetadata(merged.moonshot);
199
- if (moonshotEntry) {
200
- merged.moonshot = moonshotEntry;
286
+ const kimiEntry = applyOfficialKimiCatalogMetadata(merged.kimi);
287
+ if (kimiEntry) {
288
+ merged.kimi = kimiEntry;
289
+ }
290
+ const zaiCodingEntry = applyZaiCodingCatalogMetadata(merged[ZAI_CODING_ID]);
291
+ if (zaiCodingEntry) {
292
+ merged[ZAI_CODING_ID] = zaiCodingEntry;
201
293
  }
202
294
  if (manualEntry) {
203
295
  merged[OTTOROUTER_ID] = manualEntry;