Skip to content

Commit 401125f

Browse files
committed
fix(kimi-code): inject built-in catalog at release time
1 parent 082f09d commit 401125f

13 files changed

Lines changed: 196 additions & 46 deletions

File tree

.changeset/connect-model-catalog.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,4 @@
44
"@moonshot-ai/kimi-code": minor
55
---
66

7-
Add a `/connect` command that configures a provider and model from a model catalog. By default it reads the catalog from models.dev and fills in model metadata (context window, output limit, and capabilities) automatically, so models no longer need to be written by hand in config. Pass `--url` to point at a custom catalog endpoint that uses the same format. When connecting an Anthropic-compatible provider whose catalog base URL already includes a version segment, the request path no longer duplicates that segment, so connections that previously failed with a not-found error now succeed.
8-
9-
When the network is unavailable, the CLI falls back to a pruned catalog snapshot that is inlined at build time, so the `/connect` command can still be used offline.
7+
Add a `/connect` command that configures a provider and model from a model catalog. By default it reads from a pruned catalog snapshot bundled with the CLI, so the command works offline and is not gated by models.dev availability. Model metadata (context window, output limit, and capabilities) is filled in automatically, so models no longer need to be written by hand in config. Pass `--refresh` to fetch the latest catalog from models.dev (falling back to the bundled snapshot on failure), or `--url` to point at a custom catalog endpoint that uses the same format. When connecting an Anthropic-compatible provider whose catalog base URL already includes a version segment, the request path no longer duplicates that segment, so connections that previously failed with a not-found error now succeed.

.github/workflows/_native-build.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ jobs:
7777
certificate-p12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
7878
certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
7979

80+
- name: Generate built-in catalog (release artifacts)
81+
if: inputs.sign-macos
82+
shell: bash
83+
run: |
84+
CATALOG_FILE="$RUNNER_TEMP/kimi-code-built-in-catalog.json"
85+
node apps/kimi-code/scripts/update-catalog.mjs --out "$CATALOG_FILE"
86+
echo "KIMI_CODE_BUILT_IN_CATALOG_FILE=$CATALOG_FILE" >> "$GITHUB_ENV"
87+
8088
- name: Build native executable (release profile, macOS signed)
8189
if: runner.os == 'macOS' && inputs.sign-macos
8290
run: pnpm --filter @moonshot-ai/kimi-code run build:native:release

.github/workflows/release.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ jobs:
4141
- name: Install dependencies
4242
run: pnpm install --frozen-lockfile
4343

44+
- name: Generate Kimi Code built-in catalog
45+
shell: bash
46+
run: |
47+
CATALOG_FILE="$RUNNER_TEMP/kimi-code-built-in-catalog.json"
48+
node apps/kimi-code/scripts/update-catalog.mjs --out "$CATALOG_FILE"
49+
echo "KIMI_CODE_BUILT_IN_CATALOG_FILE=$CATALOG_FILE" >> "$GITHUB_ENV"
50+
4451
- name: Build packages
4552
run: pnpm build
4653

apps/kimi-code/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
},
4545
"scripts": {
4646
"build": "tsdown",
47+
"catalog:update": "node scripts/update-catalog.mjs --out dist/built-in-catalog.json",
4748
"smoke": "node scripts/smoke.mjs",
4849
"build:native:js": "node scripts/native/01-bundle.mjs",
4950
"build:native:sea": "node scripts/native/build.mjs --profile=local",
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { readFileSync } from 'node:fs';
2+
3+
export const BUILT_IN_CATALOG_ENV = 'KIMI_CODE_BUILT_IN_CATALOG_FILE';
4+
export const BUILT_IN_CATALOG_DEFINE = '__KIMI_CODE_BUILT_IN_CATALOG__';
5+
6+
export function builtInCatalogDefine(env = process.env) {
7+
const file = env[BUILT_IN_CATALOG_ENV];
8+
if (file === undefined || file.length === 0) return 'undefined';
9+
return JSON.stringify(readFileSync(file, 'utf-8'));
10+
}

apps/kimi-code/scripts/native/build.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import { resolve } from 'node:path';
12
import { parseArgs } from 'node:util';
23

34
import { runBundleStep } from './01-bundle.mjs';
45
import { runInjectStep } from './03-inject.mjs';
56
import { runSeaBlobStep } from './02-sea-blob.mjs';
67
import { runSignStep } from './04-sign.mjs';
78
import { runVerifyStep } from './05-verify.mjs';
9+
import { run } from './exec.mjs';
10+
import { appRoot, nativeIntermediatesDir } from './paths.mjs';
11+
import { BUILT_IN_CATALOG_ENV } from '../built-in-catalog.mjs';
812

913
const { values } = parseArgs({
1014
options: {
@@ -31,6 +35,12 @@ function ensureNodeVersion() {
3135
ensureNodeVersion();
3236
console.log(`==> Native build (profile=${profile})`);
3337

38+
if (profile === 'release' && process.env[BUILT_IN_CATALOG_ENV] === undefined) {
39+
const catalogPath = resolve(nativeIntermediatesDir(), 'built-in-catalog.json');
40+
await run(process.execPath, [resolve(appRoot, 'scripts/update-catalog.mjs'), '--out', catalogPath]);
41+
process.env[BUILT_IN_CATALOG_ENV] = catalogPath;
42+
}
43+
3444
await runBundleStep();
3545
await runSeaBlobStep();
3646
await runInjectStep();

apps/kimi-code/scripts/update-catalog.mjs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,34 @@
11
#!/usr/bin/env node
22
/**
3-
* Fetches models.dev/api.json, strips fields not needed by kimi-code,
4-
* and writes the result into src/built-in-catalog.ts as a TS constant.
3+
* Fetches models.dev/api.json, strips fields not needed by kimi-code, and
4+
* writes the result as raw JSON for release builds to inline.
55
*
6-
* Run before release builds so the CLI ships an offline-capable catalog.
6+
* This script intentionally does not write into src/. The source tree keeps a
7+
* placeholder so the generated catalog is not committed.
78
*/
89

9-
import { writeFileSync } from "node:fs";
10-
import { resolve } from "node:path";
10+
import { mkdirSync, writeFileSync } from "node:fs";
11+
import { dirname, resolve } from "node:path";
1112

1213
const scriptDir = import.meta.dirname;
13-
const outFile = resolve(scriptDir, "../src/built-in-catalog.ts");
14+
const outFile = resolveOutputFile(process.argv.slice(2));
1415
const modelsUrl = process.env.MODELS_DEV_URL || "https://models.dev/api.json";
1516

1617
const KEEP_PROVIDER = new Set(["id", "name", "api", "env", "npm", "type", "models"]);
1718
const KEEP_MODEL = new Set(["id", "name", "family", "limit", "tool_call", "reasoning", "modalities"]);
1819

20+
function resolveOutputFile(args) {
21+
const index = args.indexOf("--out");
22+
if (index !== -1) {
23+
const value = args[index + 1];
24+
if (value === undefined || value.length === 0) {
25+
throw new Error("Missing value for --out");
26+
}
27+
return resolve(process.cwd(), value);
28+
}
29+
return resolve(scriptDir, "../dist/built-in-catalog.json");
30+
}
31+
1932
function stripModel(model) {
2033
if (typeof model !== "object" || model === null) return undefined;
2134
const result = {};
@@ -63,10 +76,8 @@ async function fetchCatalog(url) {
6376
async function main() {
6477
console.log(`Fetching ${modelsUrl} ...`);
6578
const json = await fetchCatalog(modelsUrl);
66-
const content = `// Auto-generated by scripts/update-catalog.mjs. Do not edit manually.
67-
export const BUILT_IN_CATALOG_JSON: string | undefined = ${JSON.stringify(json)};
68-
`;
69-
writeFileSync(outFile, content, "utf-8");
79+
mkdirSync(dirname(outFile), { recursive: true });
80+
writeFileSync(outFile, json, "utf-8");
7081
console.log(`Wrote ${outFile} (${(json.length / 1024).toFixed(0)} KB JSON)`);
7182
}
7283

apps/kimi-code/src/built-in-catalog.ts

Lines changed: 8 additions & 2 deletions
Large diffs are not rendered by default.

apps/kimi-code/src/tui/kimi-tui.ts

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5065,40 +5065,54 @@ export class KimiTUI {
50655065
// (context size, capabilities) comes from the catalog, so users do not
50665066
// hand-write it.
50675067
private async handleConnectCommand(args: string): Promise<void> {
5068-
const { url, allowBuiltInFallback } = resolveConnectCatalogRequest(args);
5069-
5070-
const controller = new AbortController();
5071-
const cancel = (): void => {
5072-
controller.abort();
5073-
};
5074-
this.cancelInFlight = cancel;
5068+
const { url, preferBuiltIn, allowBuiltInFallback } = resolveConnectCatalogRequest(args);
50755069

50765070
let catalog: Catalog | undefined;
5077-
const spinner = this.showLoginProgressSpinner(`Fetching catalog from ${url}`);
5078-
try {
5079-
catalog = await fetchCatalog(url, controller.signal);
5080-
spinner.stop({ ok: true, label: 'Catalog loaded.' });
5081-
} catch (error) {
5082-
if (controller.signal.aborted) {
5083-
spinner.stop({ ok: false, label: 'Aborted.' });
5084-
} else {
5085-
const hint = error instanceof CatalogFetchError ? ` (HTTP ${error.status})` : '';
5086-
if (!allowBuiltInFallback) {
5087-
spinner.stop({ ok: false, label: 'Failed to load catalog.' });
5088-
this.showError(`Failed to fetch catalog${hint}: ${formatErrorMessage(error)}`);
5071+
5072+
// Default path: serve the bundled catalog so /connect works without a
5073+
// live network and is not gated by models.dev availability. The source
5074+
// placeholder is undefined in dev builds, so dev falls through to fetch.
5075+
if (preferBuiltIn) {
5076+
const builtIn = loadBuiltInCatalog(BUILT_IN_CATALOG_JSON);
5077+
if (builtIn !== undefined) {
5078+
this.showStatus('Loaded built-in catalog. Run /connect --refresh for the latest.');
5079+
catalog = builtIn;
5080+
}
5081+
}
5082+
5083+
if (catalog === undefined) {
5084+
const controller = new AbortController();
5085+
const cancel = (): void => {
5086+
controller.abort();
5087+
};
5088+
this.cancelInFlight = cancel;
5089+
5090+
const spinner = this.showLoginProgressSpinner(`Fetching catalog from ${url}`);
5091+
try {
5092+
catalog = await fetchCatalog(url, controller.signal);
5093+
spinner.stop({ ok: true, label: 'Catalog loaded.' });
5094+
} catch (error) {
5095+
if (controller.signal.aborted) {
5096+
spinner.stop({ ok: false, label: 'Aborted.' });
50895097
} else {
5090-
const fallback = loadBuiltInCatalog(BUILT_IN_CATALOG_JSON);
5091-
if (fallback !== undefined) {
5092-
spinner.stop({ ok: true, label: 'Using built-in catalog (offline mode).' });
5093-
catalog = fallback;
5094-
} else {
5098+
const hint = error instanceof CatalogFetchError ? ` (HTTP ${error.status})` : '';
5099+
if (!allowBuiltInFallback) {
50955100
spinner.stop({ ok: false, label: 'Failed to load catalog.' });
50965101
this.showError(`Failed to fetch catalog${hint}: ${formatErrorMessage(error)}`);
5102+
} else {
5103+
const fallback = loadBuiltInCatalog(BUILT_IN_CATALOG_JSON);
5104+
if (fallback !== undefined) {
5105+
spinner.stop({ ok: true, label: 'Using built-in catalog (offline mode).' });
5106+
catalog = fallback;
5107+
} else {
5108+
spinner.stop({ ok: false, label: 'Failed to load catalog.' });
5109+
this.showError(`Failed to fetch catalog${hint}: ${formatErrorMessage(error)}`);
5110+
}
50975111
}
50985112
}
5113+
} finally {
5114+
if (this.cancelInFlight === cancel) this.cancelInFlight = undefined;
50995115
}
5100-
} finally {
5101-
if (this.cancelInFlight === cancel) this.cancelInFlight = undefined;
51025116
}
51035117

51045118
if (catalog === undefined) return;
Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { DEFAULT_CATALOG_URL } from '@moonshot-ai/kimi-code-sdk';
22

33
const CATALOG_URL_FLAG_RE = /--url(?:=|\s+)(\S+)/;
4+
const REFRESH_FLAG_RE = /(?:^|\s)--refresh(?=\s|$)/;
45
const BARE_HTTP_URL_RE = /^https?:\/\/\S+$/;
56

67
export interface ConnectCatalogRequest {
78
readonly url: string;
9+
readonly preferBuiltIn: boolean;
810
readonly allowBuiltInFallback: boolean;
911
}
1012

@@ -14,8 +16,18 @@ export function resolveConnectCatalogRequest(args: string): ConnectCatalogReques
1416
const bareUrl = BARE_HTTP_URL_RE.test(trimmed) ? trimmed : undefined;
1517
const explicitUrl = urlMatch?.[1] ?? bareUrl;
1618

19+
if (explicitUrl !== undefined) {
20+
return {
21+
url: explicitUrl,
22+
preferBuiltIn: false,
23+
allowBuiltInFallback: false,
24+
};
25+
}
26+
27+
const refreshRequested = REFRESH_FLAG_RE.test(trimmed);
1728
return {
18-
url: explicitUrl ?? DEFAULT_CATALOG_URL,
19-
allowBuiltInFallback: explicitUrl === undefined,
29+
url: DEFAULT_CATALOG_URL,
30+
preferBuiltIn: !refreshRequested,
31+
allowBuiltInFallback: true,
2032
};
2133
}

0 commit comments

Comments
 (0)