Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/preserve-bundled-catalog-metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Preserve model status, reasoning options, and provider overrides in the bundled model catalog.
35 changes: 23 additions & 12 deletions apps/kimi-code/scripts/update-catalog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@

import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { pathToFileURL } from "node:url";

const scriptDir = import.meta.dirname;
const outFile = resolveOutputFile(process.argv.slice(2));
const modelsUrl = process.env.MODELS_DEV_URL || "https://models.dev/api.json";

const KEEP_PROVIDER = new Set(["id", "name", "api", "env", "npm", "type", "models"]);
const KEEP_MODEL = new Set([
Expand All @@ -22,6 +21,9 @@ const KEEP_MODEL = new Set([
"limit",
"tool_call",
"reasoning",
"reasoning_options",
"status",
"provider",
"interleaved",
"modalities",
// Message-level tool declarations capability — kosong's
Expand Down Expand Up @@ -71,30 +73,39 @@ function stripProvider(provider) {
return result;
}

export function stripCatalog(raw) {
const stripped = {};
for (const [k, v] of Object.entries(raw)) {
const p = stripProvider(v);
if (p !== undefined && Object.keys(p).length > 0) stripped[k] = p;
}
return stripped;
}

async function fetchCatalog(url) {
const res = await fetch(url, { headers: { Accept: "application/json" } });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const raw = await res.json();
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
throw new Error("invalid payload shape");
}
const stripped = {};
for (const [k, v] of Object.entries(raw)) {
const p = stripProvider(v);
if (p !== undefined && Object.keys(p).length > 0) stripped[k] = p;
}
return JSON.stringify(stripped);
return JSON.stringify(stripCatalog(raw));
}

async function main() {
const outFile = resolveOutputFile(process.argv.slice(2));
const modelsUrl = process.env.MODELS_DEV_URL || "https://models.dev/api.json";
console.log(`Fetching ${modelsUrl} ...`);
const json = await fetchCatalog(modelsUrl);
mkdirSync(dirname(outFile), { recursive: true });
writeFileSync(outFile, json, "utf-8");
console.log(`Wrote ${outFile} (${(json.length / 1024).toFixed(0)} KB JSON)`);
}

main().catch((error) => {
console.error(error.message);
process.exit(1);
});
const isMain = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
if (isMain) {
main().catch((error) => {
console.error(error.message);
process.exit(1);
});
}
152 changes: 152 additions & 0 deletions apps/kimi-code/test/scripts/fixtures/models-dev-api-2026-07-25.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
{
"anyapi": {
"id": "anyapi",
"env": [
"ANYAPI_API_KEY"
],
"npm": "@ai-sdk/openai-compatible",
"api": "https://api.anyapi.ai/v1",
"name": "AnyAPI",
"doc": "https://docs.anyapi.ai",
"models": {
"anthropic/claude-sonnet-4-6": {
"id": "anthropic/claude-sonnet-4-6",
"name": "Claude Sonnet 4.6",
"description": "Claude workhorse for coding agents, careful analysis, and production cost control",
"family": "claude-sonnet",
"attachment": true,
"reasoning": true,
"reasoning_options": [
{
"type": "toggle"
},
{
"type": "effort",
"values": [
"low",
"medium",
"high"
]
},
{
"type": "budget_tokens",
"min": 1024,
"max": 63999
}
],
"tool_call": true,
"temperature": true,
"knowledge": "2025-08-31",
"release_date": "2026-02-17",
"last_updated": "2026-03-13",
"modalities": {
"input": [
"text",
"image",
"pdf"
],
"output": [
"text"
]
},
"open_weights": false,
"limit": {
"context": 1000000,
"output": 64000
}
},
"mistralai/devstral-2512": {
"id": "mistralai/devstral-2512",
"name": "Devstral 2",
"description": "Mistral's coding-agent model for repository work, terminal tasks, and software fixes",
"family": "devstral",
"attachment": false,
"reasoning": false,
"tool_call": true,
"temperature": true,
"knowledge": "2025-12",
"release_date": "2025-12-09",
"last_updated": "2025-12-09",
"modalities": {
"input": [
"text"
],
"output": [
"text"
]
},
"open_weights": true,
"limit": {
"context": 262144,
"output": 262144
},
"status": "deprecated"
}
}
},
"opencode": {
"id": "opencode",
"env": [
"OPENCODE_API_KEY"
],
"npm": "@ai-sdk/openai-compatible",
"api": "https://opencode.ai/zen/v1",
"name": "OpenCode Zen",
"doc": "https://opencode.ai/docs/zen",
"models": {
"claude-sonnet-4-6": {
"id": "claude-sonnet-4-6",
"name": "Claude Sonnet 4.6",
"description": "Balanced Claude model for coding, analysis, agent workflows, and cost control",
"family": "claude-sonnet",
"attachment": true,
"reasoning": true,
"reasoning_options": [
{
"type": "effort",
"values": [
"low",
"medium",
"high",
"max"
]
},
{
"type": "budget_tokens",
"min": 1024
}
],
"tool_call": true,
"interleaved": true,
"temperature": true,
"knowledge": "2025-08-31",
"release_date": "2026-02-17",
"last_updated": "2026-02-17",
"modalities": {
"input": [
"text",
"image",
"pdf"
],
"output": [
"text"
]
},
"open_weights": false,
"limit": {
"context": 1000000,
"output": 64000
},
"provider": {
"npm": "@ai-sdk/anthropic"
},
"cost": {
"input": 3,
"output": 15,
"cache_read": 0.3,
"cache_write": 3.75
}
}
}
}
}
57 changes: 57 additions & 0 deletions apps/kimi-code/test/scripts/update-catalog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Scenario: pruning the bundled models.dev catalog.
* Responsibilities: preserve the model semantics observed by Kimi Code's catalog consumer.
* Wiring: the real pruning function and public catalog consumer with a pinned raw catalog fixture.
* Fixture: models.dev/api.json fetched 2026-07-25, reduced to three real model entries.
* Run: pnpm --filter @moonshot-ai/kimi-code exec vitest run test/scripts/update-catalog.test.ts
*/

import { readFileSync } from 'node:fs';

import { catalogProviderModels, type Catalog } from '@moonshot-ai/kimi-code-sdk';
import { describe, expect, it } from 'vitest';

import { stripCatalog } from '../../scripts/update-catalog.mjs';

const fixtureUrl = new URL('./fixtures/models-dev-api-2026-07-25.json', import.meta.url);

function loadFixture(): Catalog {
return JSON.parse(readFileSync(fixtureUrl, 'utf8')) as Catalog;
}

function consumeCatalog(catalog: Catalog): Record<string, unknown> {
return Object.fromEntries(
Object.entries(catalog).map(([id, provider]) => [id, catalogProviderModels(provider)]),
);
}

describe('catalog snapshot pruning', () => {
it('preserves consumer output when a release snapshot is pruned', () => {
const rawCatalog = loadFixture();

const strippedCatalog = stripCatalog(rawCatalog) as Catalog;

expect(consumeCatalog(strippedCatalog)).toEqual(consumeCatalog(rawCatalog));
});

it.each([
{
field: 'reasoning_options',
providerId: 'anyapi',
modelId: 'anthropic/claude-sonnet-4-6',
},
{ field: 'status', providerId: 'anyapi', modelId: 'mistralai/devstral-2512' },
{ field: 'provider', providerId: 'opencode', modelId: 'claude-sonnet-4-6' },
] as const)('exercises $field through observable consumer output', ({ field, providerId, modelId }) => {
const rawCatalog = loadFixture();
const withoutField = structuredClone(rawCatalog) as unknown as Record<
string,
{ models: Record<string, Record<string, unknown>> }
>;
delete withoutField[providerId]!.models[modelId]![field];

expect(consumeCatalog(withoutField as unknown as Catalog)).not.toEqual(
consumeCatalog(rawCatalog),
);
});
});