Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
aadef41
feat: support multi-level thinking effort switching
liruifengv Jun 26, 2026
22cc9f5
docs: add thinking effort design plans
liruifengv Jun 26, 2026
922d3e7
docs: collapse thinking overhaul plan into a single PR
liruifengv Jun 26, 2026
04f3a6a
refactor!: overhaul thinking config and effort resolution
liruifengv Jun 26, 2026
45c26b8
refactor: rename residual thinking level wording to effort
liruifengv Jun 26, 2026
23c2dc2
refactor: rename remaining camelCase thinking level identifiers to ef…
liruifengv Jun 26, 2026
2d3de56
refactor: eliminate remaining thinking level wording in comments and …
liruifengv Jun 26, 2026
930bb68
fix: address codex review feedback on thinking effort handling
liruifengv Jun 26, 2026
f93d164
test: align kimi e2e expectations with supportEfforts-gated reasoning…
liruifengv Jun 26, 2026
8bbdd83
test: cover [thinking] effort parsing in config.test
liruifengv Jun 26, 2026
a5d9df4
docs: add thinking test coverage gap analysis
liruifengv Jun 26, 2026
5a808a6
feat(oauth): parse nested think_efforts from /models response
liruifengv Jun 26, 2026
24764d9
refactor(oauth): only read nested think_efforts; gate on support=true
liruifengv Jun 26, 2026
69fe4b6
chore: remove unused parseStringArray import in open-platform
liruifengv Jun 26, 2026
bcea992
Merge branch 'main' into support-effort
liruifengv Jun 26, 2026
602d4cf
Merge branch 'main' into support-effort
liruifengv Jun 29, 2026
dadba10
Merge branch 'main' into support-effort
liruifengv Jun 29, 2026
ba9b141
Merge branch 'main' into support-effort
liruifengv Jun 29, 2026
4d4e675
docs: finalize thinking effort release notes
liruifengv Jun 29, 2026
c98af6e
Merge remote-tracking branch 'origin/support-effort' into support-effort
liruifengv Jun 29, 2026
ba0a347
refactor: drop temporary refresh toggles and kimi reasoning_effort mi…
liruifengv Jun 29, 2026
79a4065
fix(tui): avoid persisting "on" as thinking effort
liruifengv Jun 30, 2026
0fd834d
fix: preserve persisted thinking effort across login and provider setup
liruifengv Jun 30, 2026
2edf4e6
fix(tui): show actual thinking effort in /status and footer
liruifengv Jun 30, 2026
053ca8e
Merge remote-tracking branch 'origin/main' into support-effort
liruifengv Jun 30, 2026
780cd27
test(tui): align message-flow expectations with effort persistence an…
liruifengv Jun 30, 2026
73a1ce6
Merge remote-tracking branch 'origin/main' into support-effort
liruifengv Jun 30, 2026
6504c17
fix(vis): rename thinkingLevel to thinkingEffort in config.update ana…
liruifengv Jun 30, 2026
9cd8f19
Merge remote-tracking branch 'origin/main' into support-effort
liruifengv Jun 30, 2026
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": minor
"@moonshot-ai/kimi-code-sdk": minor
---

Refactor the thinking effort system
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
8 changes: 6 additions & 2 deletions apps/kimi-code/src/tui/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,19 @@ async function handleOpenPlatformLogin(
platform,
models,
selectedModel: selection.model,
thinking: selection.thinking,
thinking: selection.thinking !== 'off',
effort:
selection.thinking !== 'off' && selection.thinking !== 'on'
? selection.thinking
: undefined,
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
111 changes: 90 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 Down Expand Up @@ -212,6 +217,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 Down Expand Up @@ -308,7 +362,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 +381,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 +414,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 @@ -292,6 +294,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