Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -1298,6 +1298,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