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
6 changes: 6 additions & 0 deletions .changeset/thinking-model-overhaul.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/kimi-code": major
"@moonshot-ai/kimi-code-sdk": major
---

Consolidate thinking configuration into `[thinking] enabled` / `effort`, removing the top-level `default_thinking` field and `thinking.mode`. Migrate: `default_thinking = true` → `[thinking] enabled = true`; `default_thinking = false` or `mode = "off"` → `enabled = false`; `mode = "on"` / `mode = "auto"` → delete the line. Effort levels now come from each model's declared `support_efforts` instead of a fixed enum.
18 changes: 9 additions & 9 deletions apps/kimi-code/src/cli/sub/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,15 +340,15 @@ export async function handleCatalogAdd(
// already-configured provider would lose the user's previously-set default
// even when `--default-model` is not supplied.
const previousDefaultModel = config.defaultModel;
const previousDefaultThinking = config.defaultThinking;
const previousThinking = config.thinking;

if (config.providers[providerId] !== undefined) {
config = await harness.removeProvider(providerId);
}

const baseUrl = catalogBaseUrl(entry, wire);
// `applyCatalogProvider` always overwrites both `defaultModel` and
// `defaultThinking`. The values we pass here are temporary; we restore
// `[thinking]`. The values we pass here are temporary; we restore
// a consistent state in the post-apply block below.
applyCatalogProvider(config, {
providerId,
Expand All @@ -373,18 +373,18 @@ export async function handleCatalogAdd(
config.defaultModel = stillResolves ? previousDefaultModel : undefined;
}

// Always restore `defaultThinking` from what was there before — including
// `undefined`. Persisting `false` when the user never set it would make
// `resolveThinkingLevel` (agent-core/src/agent/config/thinking.ts) treat
// it as an explicit "off" request and silently disable thinking, even
// for thinking-capable models.
config.defaultThinking = previousDefaultThinking;
// Always restore `[thinking]` from what was there before — including
// `undefined`. Persisting `enabled: false` when the user never set it would
// make `resolveThinkingEffort` (agent-core/src/agent/config/thinking.ts) treat
// it as an explicit "off" request and silently disable thinking, even for
// thinking-capable models.
config.thinking = previousThinking;

await harness.setConfig({
providers: config.providers,
models: config.models,
defaultModel: config.defaultModel,
defaultThinking: config.defaultThinking,
thinking: config.thinking,
});

const displayName = entry.name ?? providerId;
Expand Down
4 changes: 2 additions & 2 deletions apps/kimi-code/src/tui/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,15 @@ async function handleOpenPlatformLogin(
platform,
models,
selectedModel: selection.model,
thinking: selection.thinking,
thinking: selection.thinking !== 'off',
apiKey,
});

await host.harness.setConfig({
providers: config.providers,
models: config.models,
defaultModel: config.defaultModel,
defaultThinking: config.defaultThinking,
thinking: config.thinking,
});

await host.authFlow.refreshConfigAfterLogin();
Expand Down
116 changes: 95 additions & 21 deletions apps/kimi-code/src/tui/commands/config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import type {
ExperimentalFeatureState,
FlagId,
ModelAlias,
PermissionMode,
Session,
ThinkingEffort,
} from '@moonshot-ai/kimi-code-sdk';

import { EditorSelectorComponent } from '../components/dialogs/editor-selector';
import { EffortSelectorComponent } from '../components/dialogs/effort-selector';
import {
ExperimentsSelectorComponent,
type ExperimentalFeatureDraftChange,
} from '../components/dialogs/experiments-selector';
import { modelDisplayName, segmentsFor } from '../components/dialogs/model-selector';
import { TabbedModelSelectorComponent } from '../components/dialogs/tabbed-model-selector';
import { PermissionSelectorComponent } from '../components/dialogs/permission-selector';
import { SettingsSelectorComponent, type SettingsSelection } from '../components/dialogs/settings-selector';
Expand All @@ -20,6 +24,7 @@ import type { ThemeName } from '#/tui/theme';
import { currentTheme, isBuiltInTheme, lightColors, loadCustomThemeMerged } from '#/tui/theme';
import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui';
import { formatErrorMessage } from '../utils/event-payload';
import { thinkingEffortToConfig } from '../utils/thinking-config';
import { showUsage } from './info';
import { setExperimentalFeatures } from './experimental-flags';
import type { SlashCommandHost } from './dispatch';
Expand All @@ -29,6 +34,7 @@ import type { SlashCommandHost } from './dispatch';
// ---------------------------------------------------------------------------

const MODEL_PICKER_REFRESH_TIMEOUT_MS = 2_000;
const REFRESH_MODELS_ON_PICKER_OPEN = true;

export async function handlePlanCommand(host: SlashCommandHost, args: string): Promise<void> {
const session = host.session;
Expand Down Expand Up @@ -212,6 +218,55 @@ export async function handleModelCommand(host: SlashCommandHost, args: string):
showModelPicker(host, alias);
}

export async function handleEffortCommand(host: SlashCommandHost, args: string): Promise<void> {
const alias = host.state.appState.model;
const model = host.state.appState.availableModels[alias];
if (model === undefined) {
host.showError('No model selected. Run /model to select one first.');
return;
}
const segments = segmentsFor(model);
const arg = args.trim().toLowerCase();
if (arg.length === 0) {
showEffortPicker(host, model, segments);
return;
}
if (!segments.includes(arg)) {
host.showError(
`Unsupported thinking effort "${arg}" for ${alias}. Available: ${segments.join(', ')}`,
);
return;
}
await performModelSwitch(host, alias, arg, true);
}

function showEffortPicker(
host: SlashCommandHost,
model: ModelAlias,
segments: readonly string[],
): void {
const liveEffort = host.state.appState.thinkingEffort;
const currentValue = segments.includes(liveEffort) ? liveEffort : (segments[0] ?? 'off');
const alias = host.state.appState.model;
host.mountEditorReplacement(
new EffortSelectorComponent({
efforts: segments,
currentValue,
onSelect: (effort) => {
host.restoreEditor();
void performModelSwitch(host, alias, effort, true);
},
onSessionOnlySelect: (effort) => {
host.restoreEditor();
void performModelSwitch(host, alias, effort, false);
},
onCancel: () => {
host.restoreEditor();
},
}),
);
}

// ---------------------------------------------------------------------------
// Pickers & config apply
// ---------------------------------------------------------------------------
Expand All @@ -233,6 +288,10 @@ function showEditorPicker(host: SlashCommandHost): void {
}

async function refreshModelsForPicker(host: SlashCommandHost): Promise<void> {
// TODO: re-enable once refreshing the model catalog no longer rebuilds (and
// thus overwrites) manually configured model capabilities such as
// support_efforts / default_effort on every picker open.
if (!REFRESH_MODELS_ON_PICKER_OPEN) return;
try {
const result = await withTimeout(
host.authFlow.refreshOAuthProviderModels(),
Expand Down Expand Up @@ -308,7 +367,7 @@ export function showModelPicker(host: SlashCommandHost, selectedValue: string =
models: host.state.appState.availableModels,
currentValue: host.state.appState.model,
selectedValue,
currentThinking: host.state.appState.thinking,
currentThinkingEffort: host.state.appState.thinkingEffort,
onSelect: ({ alias, thinking }) => {
host.restoreEditor();
void performModelSwitch(host, alias, thinking, true);
Expand All @@ -327,29 +386,31 @@ export function showModelPicker(host: SlashCommandHost, selectedValue: string =
async function performModelSwitch(
host: SlashCommandHost,
alias: string,
thinking: boolean,
effort: ThinkingEffort,
persist: boolean,
): Promise<void> {
if (host.state.appState.streamingPhase !== 'idle') {
host.showError('Cannot switch models while streaming — press Esc or Ctrl-C first.');
return;
}

const level = thinking ? 'on' : 'off';
const prevModel = host.state.appState.model;
const prevThinking = host.state.appState.thinking;
const runtimeChanged = alias !== prevModel || thinking !== prevThinking;
const prevEffort = host.state.appState.thinkingEffort;
const modelChanged = alias !== prevModel;
const effortChanged = effort !== prevEffort;
const runtimeChanged = modelChanged || effortChanged;
const displayName = modelDisplayName(alias, host.state.appState.availableModels[alias]);

const session = host.session;
try {
if (session === undefined && runtimeChanged) {
await host.authFlow.activateModelAfterLogin(alias, thinking);
await host.authFlow.activateModelAfterLogin(alias, effort);
} else if (session !== undefined) {
if (alias !== prevModel) {
await session.setModel(alias);
}
if (thinking !== prevThinking) {
await session.setThinking(level);
if (effort !== prevEffort) {
await session.setThinking(effort);
}
}
} catch (error) {
Expand All @@ -358,48 +419,61 @@ async function performModelSwitch(
return;
}

host.setAppState({ model: alias, thinking });
host.setAppState({ model: alias, thinkingEffort: effort });
if (session === undefined && runtimeChanged) {
if (alias !== prevModel) {
host.track('model_switch', { model: alias });
}
if (thinking !== prevThinking) {
host.track('thinking_toggle', { enabled: thinking });
if (effort !== prevEffort) {
host.track('thinking_toggle', { effort });
}
}

let persisted = false;
if (persist) {
try {
persisted = await persistModelSelection(host, alias, thinking);
persisted = await persistModelSelection(host, alias, effort);
} catch (error) {
const msg = formatErrorMessage(error);
host.showError(`Switched to ${alias}, but failed to save default: ${msg}`);
host.showError(`Switched to ${displayName}, but failed to save default: ${msg}`);
return;
}
}

let status: string;
if (runtimeChanged) {
if (modelChanged) {
status = persist
? `Switched to ${alias} with thinking ${level}.`
: `Switched to ${alias} with thinking ${level} for this session only.`;
? `Switched to ${displayName} with thinking ${effort}.`
: `Switched to ${displayName} with thinking ${effort} for this session only.`;
} else if (effortChanged) {
status = persist
? `Thinking set to ${effort}.`
: `Thinking set to ${effort} for this session only.`;
} else if (persist && persisted) {
status = `Saved ${alias} with thinking ${level} as default.`;
status = `Saved ${displayName} with thinking ${effort} as default.`;
} else {
status = `Already using ${alias} with thinking ${level}.`;
status = `Already using ${displayName} with thinking ${effort}.`;
}
host.showStatus(status, 'success');
}

async function persistModelSelection(host: SlashCommandHost, alias: string, thinking: boolean): Promise<boolean> {
async function persistModelSelection(
host: SlashCommandHost,
alias: string,
effort: ThinkingEffort,
): Promise<boolean> {
const config = await host.harness.getConfig({ reload: true });
if (config.defaultModel === alias && config.defaultThinking === thinking) {
const patch = thinkingEffortToConfig(effort);
if (
config.defaultModel === alias &&
config.thinking?.enabled === patch.enabled &&
config.thinking?.effort === patch.effort
) {
return false;
}
await host.harness.setConfig({
defaultModel: alias,
defaultThinking: thinking,
thinking: patch,
});
return true;
}
Expand Down
5 changes: 5 additions & 0 deletions apps/kimi-code/src/tui/commands/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
handleAutoCommand,
handleCompactCommand,
handleEditorCommand,
handleEffortCommand,
handleModelCommand,
handlePlanCommand,
handleThemeCommand,
Expand Down Expand Up @@ -65,6 +66,7 @@ export {
handleAutoCommand,
handleCompactCommand,
handleEditorCommand,
handleEffortCommand,
handleModelCommand,
handlePlanCommand,
handleThemeCommand,
Expand Down Expand Up @@ -270,6 +272,9 @@ async function handleBuiltInSlashCommand(
case 'model':
await handleModelCommand(host, args);
return;
case 'effort':
await handleEffortCommand(host, args);
return;
case 'provider':
await handleProviderCommand(host);
return;
Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-code/src/tui/commands/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export async function showStatusReport(host: SlashCommandHost): Promise<void> {
workDir: appState.workDir,
sessionId: appState.sessionId,
sessionTitle: appState.sessionTitle,
thinking: appState.thinking,
thinkingEffort: appState.thinkingEffort,
permissionMode: appState.permissionMode,
planMode: appState.planMode,
contextUsage: appState.contextUsage,
Expand Down
9 changes: 5 additions & 4 deletions apps/kimi-code/src/tui/commands/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type Catalog,
type CatalogModel,
type ModelAlias,
type ThinkingEffort,
} from '@moonshot-ai/kimi-code-sdk';
import { capabilitiesForModel } from '@moonshot-ai/kimi-code-oauth';
import type {
Expand Down Expand Up @@ -166,7 +167,7 @@ export async function promptModelSelectionForOpenPlatform(
host: SlashCommandHost,
models: ManagedKimiCodeModelInfo[],
platform: OpenPlatformDefinition,
): Promise<{ model: ManagedKimiCodeModelInfo; thinking: boolean } | undefined> {
): Promise<{ model: ManagedKimiCodeModelInfo; thinking: ThinkingEffort } | undefined> {
const modelDict: Record<string, ModelAlias> = {};
for (const m of models) {
modelDict[`${platform.id}/${m.id}`] = {
Expand All @@ -187,7 +188,7 @@ export async function promptModelSelectionForCatalog(
host: SlashCommandHost,
providerId: string,
models: CatalogModel[],
): Promise<{ model: CatalogModel; thinking: boolean } | undefined> {
): Promise<{ model: CatalogModel; thinking: ThinkingEffort } | undefined> {
const modelDict: Record<string, ModelAlias> = {};
for (const m of models) {
modelDict[`${providerId}/${m.id}`] = catalogModelToAlias(providerId, m);
Expand All @@ -201,15 +202,15 @@ export async function promptModelSelectionForCatalog(
export function runModelSelector(
host: SlashCommandHost,
modelDict: Record<string, ModelAlias>,
): Promise<{ alias: string; thinking: boolean } | undefined> {
): Promise<{ alias: string; thinking: ThinkingEffort } | undefined> {
return new Promise((resolve) => {
const firstAlias = Object.keys(modelDict)[0] ?? '';
const caps = modelDict[firstAlias]?.capabilities ?? [];
const initialThinking = caps.includes('always_thinking') || caps.includes('thinking');
const selector = new ModelSelectorComponent({
models: modelDict,
currentValue: firstAlias,
currentThinking: initialThinking,
currentThinkingEffort: initialThinking ? 'on' : 'off',
searchable: true,
onSelect: ({ alias, thinking }) => {
host.restoreEditor();
Expand Down
Loading
Loading