diff --git a/.changeset/acp-model-catalog-guard.md b/.changeset/acp-model-catalog-guard.md new file mode 100644 index 000000000..16e6d3878 --- /dev/null +++ b/.changeset/acp-model-catalog-guard.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Return an empty ACP model catalog when `config.models` is not a model map. diff --git a/packages/acp-adapter/src/model-catalog.ts b/packages/acp-adapter/src/model-catalog.ts index e126362b4..796f7da63 100644 --- a/packages/acp-adapter/src/model-catalog.ts +++ b/packages/acp-adapter/src/model-catalog.ts @@ -69,26 +69,33 @@ export function deriveAlwaysThinking(alias: ModelAlias): boolean { /** * Project `harness.getConfig().models` into a flat catalog. Returns an - * empty array when the harness has no models configured, when - * `getConfig` is missing on the harness (partial test stubs), or when - * `getConfig` throws — letting the caller decide how to surface a - * degenerate config without forcing every test stub to provide every - * field. + * empty array when the harness has no models configured, when the + * `models` field is malformed, when `getConfig` is missing on the + * harness (partial test stubs), or when `getConfig` throws — letting + * the caller decide how to surface a degenerate config without forcing + * every test stub to provide every field. */ export async function listModelsFromHarness( harness: KimiHarness, ): Promise { if (typeof harness.getConfig !== 'function') return []; - let models: Record | undefined; + let models: unknown; try { const config = await harness.getConfig(); models = config.models; } catch { return []; } - if (models === undefined) return []; + if ( + models === undefined || + models === null || + typeof models !== 'object' || + Array.isArray(models) + ) { + return []; + } const out: AcpModelEntry[] = []; - for (const [id, alias] of Object.entries(models)) { + for (const [id, alias] of Object.entries(models as Record)) { out.push({ id, name: alias.displayName ?? alias.model ?? id, diff --git a/packages/acp-adapter/test/model-catalog-list.test.ts b/packages/acp-adapter/test/model-catalog-list.test.ts new file mode 100644 index 000000000..cb56f94cf --- /dev/null +++ b/packages/acp-adapter/test/model-catalog-list.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; + +import type { KimiHarness, ModelAlias } from '@moonshot-ai/kimi-code-sdk'; + +import { listModelsFromHarness } from '../src/model-catalog'; + +function harnessWithModels(models: unknown): KimiHarness { + return { + getConfig: async () => ({ models }), + } as unknown as KimiHarness; +} + +describe('listModelsFromHarness', () => { + it('projects configured model aliases into ACP model entries', async () => { + const models: Record = { + coder: { + model: 'kimi-code', + displayName: 'Kimi Code', + capabilities: ['thinking'], + } as ModelAlias, + }; + + await expect(listModelsFromHarness(harnessWithModels(models))).resolves.toEqual([ + { + id: 'coder', + name: 'Kimi Code', + thinkingSupported: true, + alwaysThinking: false, + }, + ]); + }); + + it('returns an empty catalog for malformed models values', async () => { + await expect(listModelsFromHarness(harnessWithModels(null))).resolves.toEqual([]); + await expect(listModelsFromHarness(harnessWithModels('bad-models'))).resolves.toEqual([]); + await expect(listModelsFromHarness(harnessWithModels(['coder']))).resolves.toEqual([]); + }); +});