Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/tolerate-invalid-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/kimi-code-sdk": patch
"@moonshot-ai/kimi-code": patch
---

Drop invalid config.toml sections with a warning instead of failing to start.
3 changes: 3 additions & 0 deletions apps/kimi-code/src/cli/run-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ export async function runPrompt(
try {
await harness.ensureConfigFile();
const config = await harness.getConfig();
for (const warning of (await harness.getConfigDiagnostics()).warnings) {
stderr.write(`Warning: ${warning}\n`);
}
const { session, resumed, restorePermission, telemetryModel, goalModel } =
await resolvePromptSession(
harness,
Expand Down
4 changes: 4 additions & 0 deletions apps/kimi-code/src/cli/run-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { loadTuiConfig, TuiConfigParseError } from '#/tui/config';
import { CHROME_GUTTER } from '#/tui/constant/rendering';
import { KimiTUI } from '#/tui/index';
import { currentTheme, getColorPalette } from '#/tui/theme';
import { combineStartupNotice } from '#/tui/utils/startup';

import type { CLIOptions } from './options';
import { createCliTelemetryBootstrap, initializeCliTelemetry } from './telemetry';
Expand Down Expand Up @@ -91,6 +92,9 @@ export async function runShell(
return;
}
const config = await harness.getConfig();
for (const warning of (await harness.getConfigDiagnostics()).warnings) {
configWarning = combineStartupNotice(configWarning, warning);
}
const configMs = Date.now() - configStartedAt;
const tui = new KimiTUI(harness, {
cliOptions: opts,
Expand Down
43 changes: 30 additions & 13 deletions apps/kimi-code/src/cli/sub/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,21 +410,34 @@ export function registerProviderCommand(parent: Command, deps?: Partial<Provider
.command('provider')
.description('Manage LLM providers non-interactively.');

// Last-resort boundary: handlers report expected failures themselves, but
// anything that escapes (e.g. a config write rejected because config.toml
// is invalid) must end as a one-line error + exit 1, not an unhandled
// rejection dumping a stack trace.
const runAction = async (resolved: ProviderDeps, run: () => Promise<void>): Promise<void> => {
try {
await run();
} catch (error) {
resolved.stderr.write(`${errorMessage(error)}\n`);
resolved.exit(1);
}
};

provider
.command('add <url>')
.description('Import every provider listed in a custom registry (api.json).')
.option('--api-key <key>', 'Registry API key. Falls back to KIMI_REGISTRY_API_KEY.')
.action(async (url: string, options: { apiKey?: string }) => {
const resolved = resolveDeps(deps);
await handleProviderAdd(resolved, url, { apiKey: options.apiKey });
await runAction(resolved, () => handleProviderAdd(resolved, url, { apiKey: options.apiKey }));
});

provider
.command('remove <providerId>')
.description('Remove a provider and every model alias that referenced it.')
.action(async (providerId: string) => {
const resolved = resolveDeps(deps);
await handleProviderRemove(resolved, providerId);
await runAction(resolved, () => handleProviderRemove(resolved, providerId));
});

provider
Expand All @@ -433,7 +446,7 @@ export function registerProviderCommand(parent: Command, deps?: Partial<Provider
.option('--json', 'Emit the raw providers/models config as JSON.', false)
.action(async (options: { json?: boolean }) => {
const resolved = resolveDeps(deps);
await handleProviderList(resolved, { json: options.json === true });
await runAction(resolved, () => handleProviderList(resolved, { json: options.json === true }));
});

const catalog = provider
Expand All @@ -452,11 +465,13 @@ export function registerProviderCommand(parent: Command, deps?: Partial<Provider
options: { filter?: string; url?: string; json?: boolean },
) => {
const resolved = resolveDeps(deps);
await handleCatalogList(resolved, providerId, {
json: options.json === true,
...(options.filter === undefined ? {} : { filter: options.filter }),
...(options.url === undefined ? {} : { url: options.url }),
});
await runAction(resolved, () =>
handleCatalogList(resolved, providerId, {
json: options.json === true,
...(options.filter === undefined ? {} : { filter: options.filter }),
...(options.url === undefined ? {} : { url: options.url }),
}),
);
},
);

Expand All @@ -472,11 +487,13 @@ export function registerProviderCommand(parent: Command, deps?: Partial<Provider
options: { apiKey?: string; defaultModel?: string; url?: string },
) => {
const resolved = resolveDeps(deps);
await handleCatalogAdd(resolved, providerId, {
...(options.apiKey === undefined ? {} : { apiKey: options.apiKey }),
...(options.defaultModel === undefined ? {} : { defaultModel: options.defaultModel }),
...(options.url === undefined ? {} : { url: options.url }),
});
await runAction(resolved, () =>
handleCatalogAdd(resolved, providerId, {
...(options.apiKey === undefined ? {} : { apiKey: options.apiKey }),
...(options.defaultModel === undefined ? {} : { defaultModel: options.defaultModel }),
...(options.url === undefined ? {} : { url: options.url }),
}),
);
},
);
}
Expand Down
24 changes: 16 additions & 8 deletions apps/kimi-code/src/tui/commands/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,14 @@ function buildProviderManagerOptions(host: SlashCommandHost): ProviderManagerOpt
providers: host.state.appState.availableProviders,
activeProviderId,
onAdd: () => {
void handleProviderAdd(host);
void handleProviderAdd(host).catch((error: unknown) => {
host.showError(`Add provider failed: ${formatErrorMessage(error)}`);
});
},
onDeleteSource: (providerIds) => {
void handleProviderManagerDeleteSource(host, providerIds);
void handleProviderManagerDeleteSource(host, providerIds).catch((error: unknown) => {
host.showError(`Remove provider failed: ${formatErrorMessage(error)}`);
});
},
onClose: () => {
host.restoreEditor();
Expand Down Expand Up @@ -233,7 +237,9 @@ async function handleCatalogProviderAdd(host: SlashCommandHost): Promise<void> {
initialTabId: providerId,
onSelect: ({ alias, thinking }) => {
host.restoreEditor();
void setDefaultModel(host, alias, thinking);
void setDefaultModel(host, alias, thinking).catch((error: unknown) => {
host.showError(`Set default model failed: ${formatErrorMessage(error)}`);
});
},
onCancel: () => {
host.restoreEditor();
Expand Down Expand Up @@ -269,8 +275,8 @@ async function handleCustomRegistryAddViaDialog(host: SlashCommandHost): Promise
let entries: Awaited<ReturnType<typeof fetchCustomRegistry>>;
try {
entries = await fetchCustomRegistry(source);
} catch (err) {
host.showError(`Failed to import registry: ${formatErrorMessage(err)}`);
} catch (error) {
host.showError(`Failed to import registry: ${formatErrorMessage(error)}`);
return false;
}

Expand All @@ -287,8 +293,8 @@ async function handleCustomRegistryAddViaDialog(host: SlashCommandHost): Promise
models: config.models,
});
await host.authFlow.refreshConfigAfterLogin();
} catch (err) {
host.showError(`Failed to apply registry: ${formatErrorMessage(err)}`);
} catch (error) {
host.showError(`Failed to apply registry: ${formatErrorMessage(error)}`);
return false;
}

Expand Down Expand Up @@ -321,7 +327,9 @@ async function handleCustomRegistryAddViaDialog(host: SlashCommandHost): Promise
initialTabId: firstNewProvider,
onSelect: ({ alias, thinking }) => {
host.restoreEditor();
void setDefaultModel(host, alias, thinking);
void setDefaultModel(host, alias, thinking).catch((error: unknown) => {
host.showError(`Set default model failed: ${formatErrorMessage(error)}`);
});
},
onCancel: () => {
host.restoreEditor();
Expand Down
13 changes: 13 additions & 0 deletions apps/kimi-code/src/tui/kimi-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1312,6 +1312,19 @@ export class KimiTUI {
this.sessionEventHandler.startSubscription();
this.clearTranscriptAndRedraw();
this.showStatus(`Started a new session (${session.id}).`);
void this.showConfigWarningsIfAny();
}

/** Surface config.toml load warnings (degraded or kept-previous config) in the status bar. */
private async showConfigWarningsIfAny(): Promise<void> {
try {
const { warnings } = await this.harness.getConfigDiagnostics();
for (const warning of warnings) {
this.showStatus(warning, 'warning');
}
} catch {
/* diagnostics are best-effort */
}
}

// =========================================================================
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/cli/goal-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => {
auth: { getCachedAccessToken: vi.fn() },
ensureConfigFile: vi.fn(),
getConfig: vi.fn(async () => ({ providers: {}, defaultModel: 'k2', telemetry: true })),
getConfigDiagnostics: vi.fn(async () => ({ warnings: [] as readonly string[] })),
getExperimentalFeatures: vi.fn(async () => mocks.experimentalFeatures),
createSession: vi.fn(async () => mocks.session),
resumeSession: vi.fn(async () => mocks.session),
Expand Down
24 changes: 24 additions & 0 deletions apps/kimi-code/test/cli/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,30 @@ describe('registerProviderCommand', () => {
expect(Object.keys(current().providers).toSorted()).toEqual(['kohub', 'kohub-responses']);
expect(stdout.join('')).toContain('Imported 2 providers');
});

it('reports write failures on stderr and exits 1 instead of crashing', async () => {
const { harness } = makeHarness({
providers: { kimi: { type: 'kimi' } },
} as unknown as KimiConfig);
// Simulate the strict write path rejecting because config.toml is invalid.
harness.removeProvider = async () => {
throw new Error(
'Cannot change settings while config.toml is invalid — fix it first (run `kimi doctor` for details).',
);
};
const { deps, stderr, exitCodes } = makeDeps(harness);

const program = new Command('kimi');
registerProviderCommand(program, deps);

await tryRun(() =>
program.parseAsync(['node', 'kimi', 'provider', 'remove', 'kimi'], { from: 'node' }),
);

expect(exitCodes).toEqual([1]);
expect(stderr.join('')).toContain('Cannot change settings');
expect(stderr.join('')).not.toContain(' at '); // no stack trace dump
});
});

describe('kimi provider catalog list', () => {
Expand Down
2 changes: 2 additions & 0 deletions apps/kimi-code/test/cli/run-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const mocks = vi.hoisted(() => {
telemetry: true,
}),
),
harnessGetConfigDiagnostics: vi.fn(async () => ({ warnings: [] as readonly string[] })),
harnessGetExperimentalFeatures: vi.fn(async () => []),
harnessCreateSession: vi.fn(async () => session),
harnessResumeSession: vi.fn(async () => session),
Expand Down Expand Up @@ -91,6 +92,7 @@ vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => {
auth: { getCachedAccessToken: mocks.harnessGetCachedAccessToken },
ensureConfigFile: mocks.harnessEnsureConfigFile,
getConfig: mocks.harnessGetConfig,
getConfigDiagnostics: mocks.harnessGetConfigDiagnostics,
getExperimentalFeatures: mocks.harnessGetExperimentalFeatures,
createSession: mocks.harnessCreateSession,
resumeSession: mocks.harnessResumeSession,
Expand Down
34 changes: 34 additions & 0 deletions apps/kimi-code/test/cli/run-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const mocks = vi.hoisted(() => {
defaultModel: 'k2',
telemetry: true,
})),
harnessGetConfigDiagnostics: vi.fn(async () => ({ warnings: [] as readonly string[] })),
harnessGetCachedAccessToken: vi.fn(),
harnessClose: vi.fn(),
detectPendingMigration: vi.fn<() => Promise<unknown>>(async () => null),
Expand Down Expand Up @@ -82,6 +83,7 @@ vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => {
},
ensureConfigFile: mocks.harnessEnsureConfigFile,
getConfig: mocks.harnessGetConfig,
getConfigDiagnostics: mocks.harnessGetConfigDiagnostics,
close: mocks.harnessClose,
track: mocks.harnessTrack,
};
Expand Down Expand Up @@ -483,6 +485,38 @@ describe('runShell', () => {
});
});

it('forwards config.toml diagnostics as startup notices', async () => {
mocks.loadTuiConfig.mockResolvedValue({
theme: 'dark',
editorCommand: null,
notifications: { enabled: true, condition: 'unfocused' },
});
mocks.harnessGetConfigDiagnostics.mockResolvedValue({
warnings: ['Ignored invalid config in config.toml: loop_control.'],
});
mocks.tuiStart.mockResolvedValue(undefined);

await runShell(
{
session: '',
continue: false,
yolo: false,
auto: false,
plan: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
skillsDirs: [],
},
'1.2.3-test',
);

const [, , startupInput] = mocks.kimiTuiConstructor.mock.calls[0]!;
expect(startupInput).toMatchObject({
startupNotice: 'Ignored invalid config in config.toml: loop_control.',
});
});

it('closes the harness when TUI startup fails', async () => {
mocks.loadTuiConfig.mockResolvedValue({
theme: 'dark',
Expand Down
Loading
Loading