From 30f887f864323555aa9dbf7c59291699e51c8f16 Mon Sep 17 00:00:00 2001 From: NarwhalChen Date: Sat, 25 Jul 2026 14:14:29 +0800 Subject: [PATCH] fix(catalog): preserve bundled model metadata --- .../preserve-bundled-catalog-metadata.md | 5 + apps/kimi-code/scripts/update-catalog.mjs | 35 ++-- .../fixtures/models-dev-api-2026-07-25.json | 152 ++++++++++++++++++ .../test/scripts/update-catalog.test.ts | 57 +++++++ 4 files changed, 237 insertions(+), 12 deletions(-) create mode 100644 .changeset/preserve-bundled-catalog-metadata.md create mode 100644 apps/kimi-code/test/scripts/fixtures/models-dev-api-2026-07-25.json create mode 100644 apps/kimi-code/test/scripts/update-catalog.test.ts diff --git a/.changeset/preserve-bundled-catalog-metadata.md b/.changeset/preserve-bundled-catalog-metadata.md new file mode 100644 index 0000000000..bd037226a8 --- /dev/null +++ b/.changeset/preserve-bundled-catalog-metadata.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Preserve model status, reasoning options, and provider overrides in the bundled model catalog. diff --git a/apps/kimi-code/scripts/update-catalog.mjs b/apps/kimi-code/scripts/update-catalog.mjs index ee43eeafc1..1c97a9bbda 100644 --- a/apps/kimi-code/scripts/update-catalog.mjs +++ b/apps/kimi-code/scripts/update-catalog.mjs @@ -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([ @@ -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 @@ -71,6 +73,15 @@ 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}`); @@ -78,15 +89,12 @@ async function fetchCatalog(url) { 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 }); @@ -94,7 +102,10 @@ async function main() { 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); + }); +} diff --git a/apps/kimi-code/test/scripts/fixtures/models-dev-api-2026-07-25.json b/apps/kimi-code/test/scripts/fixtures/models-dev-api-2026-07-25.json new file mode 100644 index 0000000000..d71d09b7ca --- /dev/null +++ b/apps/kimi-code/test/scripts/fixtures/models-dev-api-2026-07-25.json @@ -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 + } + } + } + } +} diff --git a/apps/kimi-code/test/scripts/update-catalog.test.ts b/apps/kimi-code/test/scripts/update-catalog.test.ts new file mode 100644 index 0000000000..b6e6b37270 --- /dev/null +++ b/apps/kimi-code/test/scripts/update-catalog.test.ts @@ -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 { + 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> } + >; + delete withoutField[providerId]!.models[modelId]![field]; + + expect(consumeCatalog(withoutField as unknown as Catalog)).not.toEqual( + consumeCatalog(rawCatalog), + ); + }); +});