@ottocode/sdk 0.1.313 → 0.1.315

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/config/src/index.ts +38 -8
  3. package/src/config/src/manager.ts +2 -0
  4. package/src/config/src/paths.ts +221 -3
  5. package/src/core/src/providers/resolver.ts +1 -2
  6. package/src/core/src/tools/builtin/fs/copy-attachment.ts +28 -13
  7. package/src/core/src/tools/builtin/fs/edit.txt +1 -1
  8. package/src/core/src/tools/builtin/fs/index.ts +2 -0
  9. package/src/core/src/tools/builtin/fs/multiedit.txt +1 -1
  10. package/src/core/src/tools/builtin/git-identity.ts +37 -3
  11. package/src/core/src/tools/builtin/git.ts +8 -2
  12. package/src/core/src/tools/builtin/glob.txt +4 -0
  13. package/src/core/src/tools/builtin/patch/indentation.ts +8 -1
  14. package/src/core/src/tools/builtin/patch/normalize.ts +4 -0
  15. package/src/core/src/tools/builtin/patch/repair.ts +42 -0
  16. package/src/core/src/tools/builtin/patch.txt +2 -0
  17. package/src/core/src/tools/builtin/search.txt +4 -0
  18. package/src/core/src/tools/builtin/shell.ts +54 -12
  19. package/src/core/src/tools/builtin/shell.txt +5 -0
  20. package/src/core/src/tools/builtin/terminal.ts +11 -3
  21. package/src/core/src/tools/loader.ts +8 -4
  22. package/src/index.ts +17 -5
  23. package/src/prompts/src/agents/build.txt +2 -2
  24. package/src/prompts/src/providers/{moonshot.txt → kimi.txt} +1 -1
  25. package/src/prompts/src/providers.ts +5 -5
  26. package/src/providers/src/catalog-manual.ts +53 -8
  27. package/src/providers/src/catalog.ts +74 -34
  28. package/src/providers/src/env.ts +4 -7
  29. package/src/providers/src/index.ts +3 -9
  30. package/src/providers/src/{moonshot-client.ts → kimi-client.ts} +131 -15
  31. package/src/providers/src/model-merge.ts +7 -1
  32. package/src/providers/src/pricing.ts +1 -1
  33. package/src/providers/src/registry.ts +8 -19
  34. package/src/providers/src/utils.ts +11 -8
  35. package/src/types/src/config.ts +12 -1
  36. package/src/types/src/provider.ts +4 -4
@@ -13,10 +13,30 @@ import {
13
13
 
14
14
  export function repairPatchContent(patch: string): string {
15
15
  patch = extractPatchFromWrappedJson(patch);
16
+ patch = extractEnvelopedPatchFromText(patch);
17
+ patch = stripTrailingMarkdownFenceBeforeMissingEndMarker(patch);
16
18
  patch = appendMissingEndMarker(patch);
19
+ patch = trimAfterEndMarker(patch);
17
20
  return patch;
18
21
  }
19
22
 
23
+ function looksLikeUnifiedPatch(patch: string): boolean {
24
+ const trimmed = patch.trimStart();
25
+ return (
26
+ trimmed.startsWith('diff --git ') ||
27
+ trimmed.startsWith('--- ') ||
28
+ trimmed.startsWith('Index: ')
29
+ );
30
+ }
31
+
32
+ function extractEnvelopedPatchFromText(patch: string): string {
33
+ const beginIndex = patch.indexOf(PATCH_BEGIN_MARKER);
34
+ if (beginIndex === -1) return patch;
35
+ if (beginIndex === patch.search(/\S/)) return patch;
36
+ if (looksLikeUnifiedPatch(patch)) return patch;
37
+ return patch.slice(beginIndex);
38
+ }
39
+
20
40
  function extractPatchFromWrappedJson(patch: string): string {
21
41
  if (patch.includes(PATCH_BEGIN_MARKER)) return patch;
22
42
 
@@ -61,3 +81,25 @@ function appendMissingEndMarker(patch: string): string {
61
81
 
62
82
  return patch;
63
83
  }
84
+
85
+ function stripTrailingMarkdownFenceBeforeMissingEndMarker(
86
+ patch: string,
87
+ ): string {
88
+ const trimmed = patch.trimEnd();
89
+ if (!trimmed.trimStart().startsWith(PATCH_BEGIN_MARKER)) return patch;
90
+ if (trimmed.includes(PATCH_END_MARKER)) return patch;
91
+ const lines = trimmed.split('\n');
92
+ const last = lines.at(-1)?.trim();
93
+ if (last !== '```') return patch;
94
+ return lines.slice(0, -1).join('\n');
95
+ }
96
+
97
+ function trimAfterEndMarker(patch: string): string {
98
+ if (!patch.trimStart().startsWith(PATCH_BEGIN_MARKER)) return patch;
99
+ const endIndex = patch.indexOf(PATCH_END_MARKER);
100
+ if (endIndex === -1) return patch;
101
+ const endOfMarker = endIndex + PATCH_END_MARKER.length;
102
+ const suffix = patch.slice(endOfMarker);
103
+ if (suffix.trim().length === 0) return patch;
104
+ return patch.slice(0, endOfMarker);
105
+ }
@@ -1,5 +1,7 @@
1
1
  Apply a patch to modify one or more files.
2
2
 
3
+ Prefer this as the first-choice editing tool for code and text changes when it is available. Use exact-replacement tools (`edit`/`multiedit`) when a patch would be awkward, when you only need a precise old/new string replacement, or after patch attempts fail.
4
+
3
5
  Use the **enveloped format** by default. Standard unified diffs (`---` / `+++`) are also accepted.
4
6
 
5
7
  ## Fastest / safest mode (recommended): Replace
@@ -5,6 +5,8 @@
5
5
 
6
6
  Use this for text/code search across the codebase. It is the primary tool for repository content discovery.
7
7
 
8
+ Think of `search` as the repository's fast local `rg`/`grep` replacement: use it for exact string and regex searches before reaching for shell commands.
9
+
8
10
  ## Usage tips
9
11
 
10
12
  - Narrow broad searches with `path` and `glob` values.
@@ -12,3 +14,5 @@ Use this for text/code search across the codebase. It is the primary tool for re
12
14
  - Batch independent searches (e.g. multiple function names) in a single turn for parallel execution.
13
15
  - Use `ignoreCase: true` for case-insensitive matching; pass `glob` patterns (e.g. `["*.ts", "*.tsx"]`) to limit file types.
14
16
  - For filename/path discovery, use `glob` first when you already know the file pattern.
17
+ - Instead of `grep -r "workspace:" packages apps`, call `search` with `query: "workspace:"`, `path: "."`, and `glob: ["**/package.json"]`.
18
+ - Instead of `rg "function foo" src`, call `search` with `query: "function foo"` and `path: "src"`.
@@ -5,7 +5,10 @@ import { z } from 'zod/v3';
5
5
  import DESCRIPTION from './shell.txt' with { type: 'text' };
6
6
  import { getShellExecutionConfig } from '../bin-manager.ts';
7
7
  import { createToolError, type ToolResponse } from '../error.ts';
8
- import { injectCoAuthorIntoGitCommit } from './git-identity.ts';
8
+ import {
9
+ injectCoAuthorIntoGitCommit,
10
+ shouldCoAuthorCommits,
11
+ } from './git-identity.ts';
9
12
 
10
13
  function normalizePath(p: string) {
11
14
  const normalized = p.replace(/\\/g, '/');
@@ -43,6 +46,7 @@ function killProcessTree(pid: number) {
43
46
  }
44
47
 
45
48
  const REDIRECTED_SEARCH_COMMANDS = new Set(['grep', 'egrep', 'fgrep', 'rg']);
49
+ const REDIRECTED_GLOB_COMMANDS = new Set(['find', 'fd']);
46
50
 
47
51
  /**
48
52
  * Detect commands that start with a standalone grep-style search binary.
@@ -50,23 +54,55 @@ const REDIRECTED_SEARCH_COMMANDS = new Set(['grep', 'egrep', 'fgrep', 'rg']);
50
54
  * with grep/rg are redirected to the dedicated `search` tool.
51
55
  */
52
56
  export function findRedirectedSearchCommand(cmd: string): string | null {
57
+ return findRepositoryDiscoveryCommand(cmd, 'search')?.command ?? null;
58
+ }
59
+
60
+ function commandTokens(segment: string): string[] {
61
+ const tokens = segment.trim().split(/\s+/).filter(Boolean);
62
+ let index = 0;
63
+ while (
64
+ index < tokens.length &&
65
+ /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index] ?? '')
66
+ ) {
67
+ index++;
68
+ }
69
+ if (tokens[index] === 'command') index++;
70
+ return tokens.slice(index);
71
+ }
72
+
73
+ function findRepositoryDiscoveryCommand(
74
+ cmd: string,
75
+ kind?: 'search' | 'glob',
76
+ ): { command: string; tool: 'search' | 'glob' } | null {
53
77
  const segments = cmd.split(/&&|\|\||;|\n/);
54
78
  for (const segment of segments) {
55
- const tokens = segment.trim().split(/\s+/);
56
- let index = 0;
57
- while (
58
- index < tokens.length &&
59
- /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index] ?? '')
60
- ) {
61
- index++;
79
+ const tokens = commandTokens(segment);
80
+ const bin = tokens[0]?.split('/').pop() ?? '';
81
+ const second = tokens[1] ?? '';
82
+ if ((!kind || kind === 'search') && bin === 'git' && second === 'grep') {
83
+ return { command: 'git grep', tool: 'search' };
84
+ }
85
+ if ((!kind || kind === 'search') && REDIRECTED_SEARCH_COMMANDS.has(bin)) {
86
+ return { command: bin, tool: 'search' };
87
+ }
88
+ if ((!kind || kind === 'glob') && REDIRECTED_GLOB_COMMANDS.has(bin)) {
89
+ return { command: bin, tool: 'glob' };
90
+ }
91
+ if ((!kind || kind === 'glob') && bin === 'ls' && segment.includes('**')) {
92
+ return { command: 'ls **', tool: 'glob' };
62
93
  }
63
- if (tokens[index] === 'command') index++;
64
- const bin = tokens[index]?.split('/').pop() ?? '';
65
- if (REDIRECTED_SEARCH_COMMANDS.has(bin)) return bin;
66
94
  }
67
95
  return null;
68
96
  }
69
97
 
98
+ function repositoryDiscoveryHint(cmd: string): string | undefined {
99
+ const discovery = findRepositoryDiscoveryCommand(cmd);
100
+ if (!discovery) return undefined;
101
+ return discovery.tool === 'search'
102
+ ? `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.`
103
+ : `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.`;
104
+ }
105
+
70
106
  export type ShellOutputMode = 'auto' | 'full' | 'tail';
71
107
 
72
108
  const DEFAULT_TAIL_LINES = 100;
@@ -133,6 +169,7 @@ type ShellResult = ToolResponse<{
133
169
  stderrTruncated?: boolean;
134
170
  stderrOriginalBytes?: number;
135
171
  stderrShownBytes?: number;
172
+ discoveryHint?: string;
136
173
  }>;
137
174
 
138
175
  type ShellStreamChunk =
@@ -248,7 +285,10 @@ export function buildShellTool(projectRoot: string): {
248
285
  }
249
286
 
250
287
  const absCwd = resolveSafePath(projectRoot, cwd || '.');
251
- const finalCmd = injectCoAuthorIntoGitCommit(cmd);
288
+ const finalCmd = injectCoAuthorIntoGitCommit(
289
+ cmd,
290
+ shouldCoAuthorCommits(projectRoot),
291
+ );
252
292
  const shellExecutor = shellExecutorContext.getStore();
253
293
  if (shellExecutor) {
254
294
  return shellExecutor(
@@ -440,11 +480,13 @@ export function buildShellTool(projectRoot: string): {
440
480
  return;
441
481
  }
442
482
 
483
+ const discoveryHint = repositoryDiscoveryHint(finalCmd);
443
484
  settle({
444
485
  ok: true,
445
486
  exitCode: exitCode ?? 0,
446
487
  stdout,
447
488
  stderr,
489
+ ...(discoveryHint ? { discoveryHint } : {}),
448
490
  ...(outputMode === 'tail' || outputMode === 'auto'
449
491
  ? { outputMode, tailLines, maxOutputBytes }
450
492
  : { outputMode, maxOutputBytes }),
@@ -8,6 +8,11 @@ For repository discovery, use `search` for content/code search and `glob` for fi
8
8
 
9
9
  **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
10
 
11
+ Mapping from common shell habits:
12
+ - `grep -r` / `rg` / `git grep` over repo files → use `search`.
13
+ - `find` / `fd` / recursive `ls` for filenames → use `glob`.
14
+ - Build, test, package manager, diagnostics, process inspection → use `shell`.
15
+
11
16
  ## Usage tips
12
17
 
13
18
  - Chain commands with `&&` to fail-fast.
@@ -5,7 +5,10 @@ import { createToolError } from '../error.ts';
5
5
  import type { TerminalManager } from '../../terminals/index.ts';
6
6
  import type { TerminalStatus } from '../../terminals/terminal.ts';
7
7
  import { normalizeTerminalLine } from '../../utils/ansi.ts';
8
- import { injectCoAuthorIntoGitCommit } from './git-identity.ts';
8
+ import {
9
+ injectCoAuthorIntoGitCommit,
10
+ shouldCoAuthorCommits,
11
+ } from './git-identity.ts';
9
12
 
10
13
  function shellQuote(segment: string): string {
11
14
  if (/^[a-zA-Z0-9._-]+$/.test(segment)) {
@@ -108,6 +111,7 @@ export function buildTerminalTool(
108
111
  execute: async (params) => {
109
112
  try {
110
113
  const { operation } = params;
114
+ const coAuthorCommits = shouldCoAuthorCommits(projectRoot);
111
115
 
112
116
  switch (operation) {
113
117
  case 'start': {
@@ -169,7 +173,9 @@ export function buildTerminalTool(
169
173
 
170
174
  if (initialCommand) {
171
175
  queueMicrotask(() => {
172
- term.write(`${injectCoAuthorIntoGitCommit(initialCommand)}\n`);
176
+ term.write(
177
+ `${injectCoAuthorIntoGitCommit(initialCommand, coAuthorCommits)}\n`,
178
+ );
173
179
  });
174
180
  }
175
181
 
@@ -243,7 +249,9 @@ export function buildTerminalTool(
243
249
  return createToolError(`Terminal ${params.terminalId} not found`);
244
250
  }
245
251
 
246
- term.write(injectCoAuthorIntoGitCommit(params.input));
252
+ term.write(
253
+ injectCoAuthorIntoGitCommit(params.input, coAuthorCommits),
254
+ );
247
255
 
248
256
  return {
249
257
  ok: true,
@@ -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 {
@@ -253,6 +250,20 @@ export type {
253
250
  export { loadConfig, read as readConfig } from './config/src/index.ts';
254
251
  export {
255
252
  getLocalDataDir,
253
+ getLegacyProjectDataDir,
254
+ getOttoHomeDir,
255
+ getProjectsStateRoot,
256
+ getProjectId,
257
+ getProjectConfigDir,
258
+ getProjectConfigPath,
259
+ getProjectStateDir,
260
+ getProjectDbPath,
261
+ getProjectAttachmentsDir,
262
+ getProjectDebugDir,
263
+ getProjectDebugDumpsDir,
264
+ getProjectLogsDir,
265
+ getProjectTmpDir,
266
+ getProjectCacheDir,
256
267
  getGlobalConfigDir,
257
268
  getGlobalConfigPath,
258
269
  getGlobalSkillsConfigPath,
@@ -334,6 +345,7 @@ export {
334
345
  export {
335
346
  appendCoAuthorTrailer,
336
347
  injectCoAuthorIntoGitCommit,
348
+ shouldCoAuthorCommits,
337
349
  OTTOCODE_BOT_NAME,
338
350
  OTTOCODE_BOT_EMAIL,
339
351
  OTTOCODE_CO_AUTHOR,
@@ -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();
@@ -46,7 +46,7 @@ const OWNER_NPM: Record<ModelOwner, string> = {
46
46
  google: '@ai-sdk/google',
47
47
  openrouter: '@openrouter/ai-sdk-provider',
48
48
  xai: '@ai-sdk/xai',
49
- moonshot: '@ai-sdk/openai-compatible',
49
+ kimi: '@ai-sdk/openai-compatible',
50
50
  qwen: '@ai-sdk/openai-compatible',
51
51
  zai: '@ai-sdk/openai-compatible',
52
52
  minimax: '@ai-sdk/anthropic',
@@ -154,16 +154,61 @@ export function appendXaiGrokCliModels<T extends { models: ModelInfo[] }>(
154
154
  return { ...entry, models: [...mergedModels, ...missingModels] };
155
155
  }
156
156
 
157
+ const DEPRECATED_KIMI_MODEL_IDS = new Set([
158
+ 'kimi-k2-0711-preview',
159
+ 'kimi-k2-0905-preview',
160
+ 'kimi-k2-thinking',
161
+ 'kimi-k2-thinking-turbo',
162
+ 'kimi-k2-turbo-preview',
163
+ ]);
164
+
165
+ const KIMI_MANUAL_MODELS: ModelInfo[] = [
166
+ {
167
+ id: 'kimi-k2.7-code-highspeed',
168
+ ownedBy: 'kimi',
169
+ label: 'Kimi K2.7 Code Highspeed',
170
+ modalities: { input: ['text', 'image', 'video'], output: ['text'] },
171
+ toolCall: true,
172
+ reasoningText: true,
173
+ attachment: true,
174
+ temperature: false,
175
+ knowledge: '2025-01',
176
+ openWeights: true,
177
+ cost: { input: 1.9, output: 8, cacheRead: 0.38 },
178
+ limit: { context: 262_144, output: 262_144 },
179
+ },
180
+ ];
181
+
182
+ export function filterAvailableKimiModels(models: ModelInfo[]): ModelInfo[] {
183
+ return models.filter((model) => !DEPRECATED_KIMI_MODEL_IDS.has(model.id));
184
+ }
185
+
186
+ function appendKimiManualModels(models: ModelInfo[]): ModelInfo[] {
187
+ const manualById = new Map(
188
+ KIMI_MANUAL_MODELS.map((model) => [model.id, model]),
189
+ );
190
+ const mergedModels = models.map((model) => {
191
+ const override = manualById.get(model.id);
192
+ return override ? { ...model, ...override } : model;
193
+ });
194
+ const existingIds = new Set(mergedModels.map((model) => model.id));
195
+ const missingModels = KIMI_MANUAL_MODELS.filter(
196
+ (model) => !existingIds.has(model.id),
197
+ );
198
+ return missingModels.length
199
+ ? [...mergedModels, ...missingModels]
200
+ : mergedModels;
201
+ }
202
+
157
203
  export function applyOfficialKimiCatalogMetadata<
158
204
  T extends ProviderCatalogEntry,
159
205
  >(entry: T | undefined): T | undefined {
160
206
  if (!entry) return undefined;
161
- const env = Array.from(
162
- new Set(['KIMI_API_KEY', 'MOONSHOT_API_KEY', ...(entry.env ?? [])]),
163
- );
207
+ const env = Array.from(new Set(['KIMI_API_KEY', ...(entry.env ?? [])]));
164
208
  return {
165
209
  ...entry,
166
- label: entry.label === 'Moonshot AI' ? 'Kimi' : entry.label,
210
+ models: appendKimiManualModels(filterAvailableKimiModels(entry.models)),
211
+ label: 'Kimi',
167
212
  env,
168
213
  doc: 'https://platform.kimi.ai/docs/api/overview.md',
169
214
  };
@@ -182,9 +227,9 @@ export function mergeManualCatalog(
182
227
  if (xaiEntry) {
183
228
  merged.xai = xaiEntry;
184
229
  }
185
- const moonshotEntry = applyOfficialKimiCatalogMetadata(merged.moonshot);
186
- if (moonshotEntry) {
187
- merged.moonshot = moonshotEntry;
230
+ const kimiEntry = applyOfficialKimiCatalogMetadata(merged.kimi);
231
+ if (kimiEntry) {
232
+ merged.kimi = kimiEntry;
188
233
  }
189
234
  if (manualEntry) {
190
235
  merged[OTTOROUTER_ID] = manualEntry;