diff --git a/.changeset/session-thinking-effort.md b/.changeset/session-thinking-effort.md new file mode 100644 index 0000000000..51d5ac6b74 --- /dev/null +++ b/.changeset/session-thinking-effort.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Add the `--effort` flag for session-level thinking overrides. Pass `--effort ` when starting or resuming a session. diff --git a/apps/kimi-code/src/cli/commands.ts b/apps/kimi-code/src/cli/commands.ts index a090df4d0f..8d318a5f6c 100644 --- a/apps/kimi-code/src/cli/commands.ts +++ b/apps/kimi-code/src/cli/commands.ts @@ -54,6 +54,7 @@ export function createProgram( 'LLM model alias to use for this invocation. Defaults to default_model in config.toml.', ), ) + .option('--effort ', 'Set the thinking effort for this session') .addOption( new Option( '-p, --prompt ', @@ -157,6 +158,7 @@ export function createProgram( auto: autoValue, plan: raw['plan'] as boolean, model: raw['model'] as string | undefined, + effort: raw['effort'] as string | undefined, outputFormat: raw['outputFormat'] as CLIOptions['outputFormat'], prompt: raw['prompt'] as string | undefined, skillsDirs: raw['skillsDir'] as string[], diff --git a/apps/kimi-code/src/cli/options.ts b/apps/kimi-code/src/cli/options.ts index 004fd7cabd..1292de098a 100644 --- a/apps/kimi-code/src/cli/options.ts +++ b/apps/kimi-code/src/cli/options.ts @@ -41,6 +41,7 @@ export interface CLIOptions { auto: boolean; plan: boolean; model: string | undefined; + effort: string | undefined; outputFormat: PromptOutputFormat | undefined; prompt: string | undefined; skillsDirs: string[]; @@ -73,6 +74,9 @@ export function validateOptions( if (opts.model !== undefined && opts.model.trim().length === 0) { throw new OptionConflictError('Model cannot be empty.'); } + if (opts.effort !== undefined && opts.effort.trim().length === 0) { + throw new OptionConflictError('Effort cannot be empty.'); + } if (!promptMode && opts.outputFormat !== undefined) { throw new OptionConflictError('Output format is only supported in prompt mode.'); } diff --git a/apps/kimi-code/src/cli/prompt-session.ts b/apps/kimi-code/src/cli/prompt-session.ts index e4b4410af9..157722e182 100644 --- a/apps/kimi-code/src/cli/prompt-session.ts +++ b/apps/kimi-code/src/cli/prompt-session.ts @@ -53,6 +53,7 @@ export interface PromptSession { getStatus(): Promise; setModel(model: string): Promise; + setThinking(effort: string): Promise; setPermission(mode: PermissionMode): Promise; setApprovalHandler(handler: ApprovalHandler | undefined): void; setQuestionHandler(handler: QuestionHandler | undefined): void; diff --git a/apps/kimi-code/src/cli/run-prompt.ts b/apps/kimi-code/src/cli/run-prompt.ts index cd519b223e..342305d347 100644 --- a/apps/kimi-code/src/cli/run-prompt.ts +++ b/apps/kimi-code/src/cli/run-prompt.ts @@ -288,6 +288,15 @@ interface ResolvedPromptSession { readonly goalModel?: string; } +async function applySessionOverrides(session: PromptSession, opts: CLIOptions): Promise { + if (opts.model !== undefined) { + await session.setModel(opts.model); + } + if (opts.effort !== undefined) { + await session.setThinking(opts.effort); + } +} + async function resolvePromptSession( harness: PromptHarness, opts: CLIOptions, @@ -326,9 +335,7 @@ async function resolvePromptSession( status.permission, setRestorePermission, ); - if (opts.model !== undefined) { - await session.setModel(opts.model); - } + await applySessionOverrides(session, opts); installHeadlessHandlers(session); return { session, @@ -353,9 +360,7 @@ async function resolvePromptSession( status.permission, setRestorePermission, ); - if (opts.model !== undefined) { - await session.setModel(opts.model); - } + await applySessionOverrides(session, opts); installHeadlessHandlers(session); return { session, @@ -373,12 +378,18 @@ async function resolvePromptSession( const session = await harness.createSession({ workDir, model, + thinking: opts.effort, permission: 'auto', additionalDirs: opts.addDirs?.length ? opts.addDirs : undefined, agentProfile, agentFiles: opts.agentFiles?.length ? opts.agentFiles : undefined, drainAgentTasksOnStop: true, }); + // Session creation preserves the SDK's lenient compatibility semantics. + // Explicit CLI input must additionally use the strict, model-aware setter. + if (opts.effort !== undefined) { + await session.setThinking(opts.effort); + } installHeadlessHandlers(session); return { session, diff --git a/apps/kimi-code/src/cli/v2/run-v2-print.ts b/apps/kimi-code/src/cli/v2/run-v2-print.ts index 6112d8bf67..b49ae533a4 100644 --- a/apps/kimi-code/src/cli/v2/run-v2-print.ts +++ b/apps/kimi-code/src/cli/v2/run-v2-print.ts @@ -299,13 +299,16 @@ async function resolveNativeSession( } // `--agent` / `--agent-file` are creation-only: validateOptions rejects them - // together with --session/--continue, so resume paths only apply an - // explicitly requested model — the bound profile is restored by the engine. - const applyModelOverride = async ( + // together with --session/--continue, so resume paths only apply explicit + // model/thinking overrides — the bound profile is restored by the engine. + // Model must be applied first because effort is validated against it. + const applyProfileOverrides = async ( profile: IAgentProfileService, model: string | undefined, + effort: string | undefined, ): Promise => { if (model !== undefined) await profile.setModel(model); + if (effort !== undefined) profile.setThinking(effort); }; const resumeById = async (id: string): Promise => { @@ -344,7 +347,7 @@ async function resolveNativeSession( const session = await resumeById(opts.session); const agent = await ensureMainAgent(session); const profile = agent.accessor.get(IAgentProfileService); - await applyModelOverride(profile, opts.model); + await applyProfileOverrides(profile, opts.model, opts.effort); const currentModel = profile.getModel(); const { restorePermission } = forceAuto(agent); return { @@ -363,7 +366,7 @@ async function resolveNativeSession( const session = await resumeById(previous.id); const agent = await ensureMainAgent(session); const profile = agent.accessor.get(IAgentProfileService); - await applyModelOverride(profile, opts.model); + await applyProfileOverrides(profile, opts.model, opts.effort); const currentModel = profile.getModel(); const { restorePermission } = forceAuto(agent); return { @@ -385,6 +388,8 @@ async function resolveNativeSession( mainAgentBinding: { profile: agentProfileName ?? 'agent', model, + thinking: opts.effort, + strictThinking: opts.effort !== undefined, }, }); const agent = await ensureMainAgent(session); diff --git a/apps/kimi-code/src/main.ts b/apps/kimi-code/src/main.ts index 7c4e1040b0..15944ff663 100644 --- a/apps/kimi-code/src/main.ts +++ b/apps/kimi-code/src/main.ts @@ -133,6 +133,7 @@ const MIGRATE_CLI_OPTIONS: CLIOptions = { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], diff --git a/apps/kimi-code/src/tui/controllers/auth-flow.ts b/apps/kimi-code/src/tui/controllers/auth-flow.ts index 67fac913c2..6c44cdd845 100644 --- a/apps/kimi-code/src/tui/controllers/auth-flow.ts +++ b/apps/kimi-code/src/tui/controllers/auth-flow.ts @@ -76,7 +76,11 @@ export class AuthFlowController { this.host.setStartupReady(); } - async activateModelAfterLogin(model: string, effort?: string): Promise { + async activateModelAfterLogin( + model: string, + effort?: string, + strictThinking = false, + ): Promise { const { host } = this; if (host.session !== undefined) { await host.session.setModel(model); @@ -121,6 +125,9 @@ export class AuthFlowController { options.additionalDirs = [...host.state.appState.additionalDirs]; } const session = await host.harness.createSession(options); + if (strictThinking && effort !== undefined) { + await session.setThinking(effort); + } await host.setSession(session); host.setAppState({ sessionId: session.id, @@ -164,7 +171,12 @@ export class AuthFlowController { return; } - await this.activateModelAfterLogin(defaultModel, thinkingEffortFromConfig(config.thinking)); + const startupEffort = host.options.startup.effort; + await this.activateModelAfterLogin( + defaultModel, + startupEffort ?? thinkingEffortFromConfig(config.thinking), + startupEffort !== undefined, + ); if (host.session === undefined && host.engineV2) { // Session-less v2: also hydrate permission/plan defaults from the // refreshed config, same as startup. diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 845c48bf7e..d8ffc8406f 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -14,6 +14,7 @@ import type { PromptPart, Session, SkillSummary, + ThinkingEffort, WorkspaceTrustInfo, } from '@moonshot-ai/kimi-code-sdk'; import type { MigrationPlan } from '@moonshot-ai/migration-legacy'; @@ -404,6 +405,7 @@ export class KimiTUI { auto: startupInput.cliOptions.auto, plan: startupInput.cliOptions.plan, model: startupInput.cliOptions.model, + effort: startupInput.cliOptions.effort, agentProfile: startupInput.agentProfile, agentFiles: startupInput.cliOptions.agentFiles, startupNotice: startupInput.startupNotice, @@ -810,10 +812,12 @@ export class KimiTUI { const { workDir } = this.state.appState; let session: Session | undefined; let shouldReplayHistory = false; + let createdFreshSession = false; const isResumeStartup = startup.sessionFlag !== undefined || startup.continueLast; const createSessionOptions: MutableCreateSessionOptions = { workDir, model: startup.model, + thinking: startup.effort, permission: startup.auto ? 'auto' : startup.yolo ? 'yolo' : undefined, planMode: startup.plan ? true : undefined, // --agent/--agent-file bind the startup session only; sessions created @@ -872,6 +876,7 @@ export class KimiTUI { shouldReplayHistory = true; } else { session = await this.harness.createSession(createSessionOptions); + createdFreshSession = true; this.startupNotice = combineStartupNotice( this.startupNotice, `No sessions to continue under "${workDir}"; starting a fresh session.`, @@ -888,12 +893,14 @@ export class KimiTUI { this.appendStartupNotice(SESSIONLESS_STARTUP_NOTICE); } else { session = await this.harness.createSession(createSessionOptions); + createdFreshSession = true; } if (session !== undefined && shouldReplayHistory) { - await this.applyStartupModesToResumedSession(session); - if (startup.model !== undefined) { - await session.setModel(startup.model); - } + await this.applyStartupOverridesToResumedSession(session); + } else if (session !== undefined && createdFreshSession && startup.effort !== undefined) { + // createSession intentionally keeps lenient SDK compatibility; an + // explicit CLI effort must use the strict, model-aware setter too. + await session.setThinking(startup.effort); } } catch (error) { if (!isOAuthLoginRequiredError(error)) throw error; @@ -1694,18 +1701,23 @@ export class KimiTUI { if (!startup.plan) { patch.planMode = config.defaultPlanMode === true; } - const effort = thinkingEffortFromConfig(config.thinking); - if (effort !== undefined) { - patch.thinkingEffort = effort; - } else if (startupModel !== undefined) { - // No concrete effort configured: mirror the engine, which resolves the - // model's default effort at createSession time. - const raw = config.models?.[startupModel]; - if (raw !== undefined) { - const providerType = config.providers?.[raw.provider]?.type; - patch.thinkingEffort = defaultThinkingEffortFor( - effectiveModelAlias(raw, providerType ?? raw.protocol), - ); + if (startup.effort !== undefined) { + patch.thinkingEffort = startup.effort as ThinkingEffort; + patch.lazySessionThinking = startup.effort as ThinkingEffort; + } else { + const effort = thinkingEffortFromConfig(config.thinking); + if (effort !== undefined) { + patch.thinkingEffort = effort; + } else if (startupModel !== undefined) { + // No concrete effort configured: mirror the engine, which resolves the + // model's default effort at createSession time. + const raw = config.models?.[startupModel]; + if (raw !== undefined) { + const providerType = config.providers?.[raw.provider]?.type; + patch.thinkingEffort = defaultThinkingEffortFor( + effectiveModelAlias(raw, providerType ?? raw.protocol), + ); + } } } if (startup.agentProfile !== undefined || startup.agentFiles !== undefined) { @@ -1796,6 +1808,11 @@ export class KimiTUI { let session: Session; try { session = await this.createSessionFromCurrentState(true); + if (this.options.startup.effort !== undefined) { + // createSession remains lenient for SDK compatibility. An explicit + // CLI effort must also pass the strict, model-aware session setter. + await session.setThinking(this.options.startup.effort); + } } catch (error) { const msg = formatErrorMessage(error); this.showError(`Failed to start a session: ${msg}`); @@ -1856,11 +1873,12 @@ export class KimiTUI { this.syncAdditionalDirs(session); } - // Apply --auto/--yolo/--plan startup flags to a resumed session. The resumed + // Apply CLI startup overrides to a resumed session. The resumed // session may already be in plan mode from its persisted records, and // re-entering plan mode throws, so only enable it when it is not active yet. - // setPermission is idempotent and needs no such guard. - private async applyStartupModesToResumedSession(session: Session): Promise { + // setPermission is idempotent and needs no such guard. Model must be applied + // before effort because the engine validates effort against the active model. + private async applyStartupOverridesToResumedSession(session: Session): Promise { const { startup } = this.options; if (startup.auto) { await session.setPermission('auto'); @@ -1873,6 +1891,12 @@ export class KimiTUI { await session.setPlanMode(true); } } + if (startup.model !== undefined) { + await session.setModel(startup.model); + } + if (startup.effort !== undefined) { + await session.setThinking(startup.effort); + } } // Re-apply startup flags that the user explicitly passed on the command line. @@ -3305,7 +3329,9 @@ export class KimiTUI { const switched = await this.resumeSession(session.id); if (!switched) return; if (applyStartupModes) { - await this.applyStartupModesToResumedSession(this.requireSession()); + const resumedSession = this.requireSession(); + await this.applyStartupOverridesToResumedSession(resumedSession); + await this.syncRuntimeState(resumedSession); this.applyStartupPermissionAndPlanToAppState(); } this.hideSessionPicker(); diff --git a/apps/kimi-code/src/tui/types.ts b/apps/kimi-code/src/tui/types.ts index 8ff0041a04..08fdd515fc 100644 --- a/apps/kimi-code/src/tui/types.ts +++ b/apps/kimi-code/src/tui/types.ts @@ -263,6 +263,7 @@ export interface TUIStartupOptions { readonly auto: boolean; readonly plan: boolean; readonly model?: string; + readonly effort?: string; /** Resolved profile name from --agent/--agent-file; bound to the startup session only. */ readonly agentProfile?: string; /** Raw --agent-file paths, passed to session creation alongside `agentProfile`. */ diff --git a/apps/kimi-code/test/cli/goal-prompt.test.ts b/apps/kimi-code/test/cli/goal-prompt.test.ts index 8f600525e0..7be9490dfd 100644 --- a/apps/kimi-code/test/cli/goal-prompt.test.ts +++ b/apps/kimi-code/test/cli/goal-prompt.test.ts @@ -152,6 +152,7 @@ function opts(overrides: Partial[0]> = {}) { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: '/goal Ship feature X', skillsDirs: [], diff --git a/apps/kimi-code/test/cli/main.test.ts b/apps/kimi-code/test/cli/main.test.ts index 8e058068a4..e90356cdb3 100644 --- a/apps/kimi-code/test/cli/main.test.ts +++ b/apps/kimi-code/test/cli/main.test.ts @@ -148,6 +148,7 @@ function defaultOpts(): CLIOptions { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], diff --git a/apps/kimi-code/test/cli/options.test.ts b/apps/kimi-code/test/cli/options.test.ts index 95936fe5c1..3748acaba3 100644 --- a/apps/kimi-code/test/cli/options.test.ts +++ b/apps/kimi-code/test/cli/options.test.ts @@ -45,6 +45,7 @@ describe('CLI options parsing', () => { expect(opts.continue).toBe(false); expect(opts.session).toBeUndefined(); expect(opts.model).toBeUndefined(); + expect(opts.effort).toBeUndefined(); expect(opts.outputFormat).toBeUndefined(); expect(opts.prompt).toBeUndefined(); expect(opts.skillsDirs).toEqual([]); @@ -241,6 +242,27 @@ describe('CLI options parsing', () => { }); }); + describe('--effort', () => { + it('parses a space-separated effort override', () => { + expect(parse(['--effort', 'high']).effort).toBe('high'); + }); + + it('parses an equals-separated effort override', () => { + expect(parse(['--effort=high']).effort).toBe('high'); + }); + + it('accepts a custom effort name for model-level validation', () => { + const opts = parse(['--effort=custom-tier']); + expect(validateOptions(opts).options.effort).toBe('custom-tier'); + }); + + it('rejects an empty effort value before reaching the SDK', () => { + const opts = parse(['--effort=']); + expect(() => validateOptions(opts)).toThrow(OptionConflictError); + expect(() => validateOptions(opts)).toThrow('Effort cannot be empty.'); + }); + }); + describe('--prompt / -p', () => { it('parses -p as prompt mode', () => { const opts = parse(['-p', 'explain this repo']); diff --git a/apps/kimi-code/test/cli/run-prompt.test.ts b/apps/kimi-code/test/cli/run-prompt.test.ts index 726a83e607..fa045a1d73 100644 --- a/apps/kimi-code/test/cli/run-prompt.test.ts +++ b/apps/kimi-code/test/cli/run-prompt.test.ts @@ -28,6 +28,7 @@ const mocks = vi.hoisted(() => { const session = { id: 'ses_prompt', setModel: vi.fn(), + setThinking: vi.fn(), setPermission: vi.fn(), setApprovalHandler: vi.fn(), setQuestionHandler: vi.fn(), @@ -191,6 +192,7 @@ function opts(overrides: Partial[0]> = {}) { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: 'say hello', skillsDirs: [], @@ -275,6 +277,7 @@ describe('runPrompt', () => { expect(mocks.harnessCreateSession).toHaveBeenCalledWith({ workDir: process.cwd(), model: 'k2', + thinking: undefined, permission: 'auto', additionalDirs: undefined, drainAgentTasksOnStop: true, @@ -430,6 +433,7 @@ describe('runPrompt', () => { expect(mocks.harnessCreateSession).toHaveBeenCalledWith({ workDir: process.cwd(), model: 'kimi-code/k2.5', + thinking: undefined, permission: 'auto', additionalDirs: undefined, drainAgentTasksOnStop: true, @@ -439,6 +443,31 @@ describe('runPrompt', () => { ); }); + it('validates the CLI effort through the session setter after creating a fresh prompt session', async () => { + await runPrompt(opts({ effort: 'high' }), '1.2.3-test', { + stdout: writer(), + stderr: writer(), + }); + + expect(mocks.harnessCreateSession).toHaveBeenCalledWith( + expect.objectContaining({ thinking: 'high' }), + ); + expect(mocks.session.setThinking).toHaveBeenCalledWith('high'); + }); + + it('rejects a fresh prompt session when the engine rejects the CLI effort', async () => { + mocks.session.setThinking.mockRejectedValueOnce(new Error('unsupported thinking effort')); + + await expect( + runPrompt(opts({ effort: 'custom' }), '1.2.3-test', { + stdout: writer(), + stderr: writer(), + }), + ).rejects.toThrow('unsupported thinking effort'); + + expect(mocks.session.prompt).not.toHaveBeenCalled(); + }); + it('passes the CLI additional directory when creating a fresh prompt session', async () => { await runPrompt(opts({ addDirs: ['../shared', '/tmp/extra'] }), '1.2.3-test', { stdout: { write: vi.fn(() => true) }, @@ -448,6 +477,7 @@ describe('runPrompt', () => { expect(mocks.harnessCreateSession).toHaveBeenCalledWith({ workDir: process.cwd(), model: 'k2', + thinking: undefined, permission: 'auto', additionalDirs: ['../shared', '/tmp/extra'], drainAgentTasksOnStop: true, @@ -711,6 +741,36 @@ describe('runPrompt', () => { ); }); + it('applies the CLI effort override to resumed prompt sessions', async () => { + await runPrompt(opts({ session: 'ses_existing', effort: 'low' }), '1.2.3-test', { + stdout: writer(), + stderr: writer(), + }); + + expect(mocks.session.setThinking).toHaveBeenCalledWith('low'); + }); + + it('applies the model before effort when both resume overrides are provided', async () => { + await runPrompt( + opts({ session: 'ses_existing', model: 'kimi-code/k2.5', effort: 'high' }), + '1.2.3-test', + { stdout: writer(), stderr: writer() }, + ); + + expect(mocks.session.setModel.mock.invocationCallOrder[0]).toBeLessThan( + mocks.session.setThinking.mock.invocationCallOrder[0]!, + ); + }); + + it('keeps the resumed thinking setting when no effort override is provided', async () => { + await runPrompt(opts({ session: 'ses_existing' }), '1.2.3-test', { + stdout: writer(), + stderr: writer(), + }); + + expect(mocks.session.setThinking).not.toHaveBeenCalled(); + }); + it('writes stream-json output as assistant JSONL with resume meta without transcript bullets', async () => { const stdout = writer(); const stderr = writer(); diff --git a/apps/kimi-code/test/cli/run-shell.test.ts b/apps/kimi-code/test/cli/run-shell.test.ts index c4e95c1d1b..bb686c204f 100644 --- a/apps/kimi-code/test/cli/run-shell.test.ts +++ b/apps/kimi-code/test/cli/run-shell.test.ts @@ -185,6 +185,7 @@ describe('runShell', () => { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], @@ -274,6 +275,7 @@ describe('runShell', () => { auto: false, plan: true, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], @@ -355,6 +357,7 @@ describe('runShell', () => { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], @@ -384,6 +387,7 @@ describe('runShell', () => { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: ['/skills'], @@ -419,6 +423,7 @@ describe('runShell', () => { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], @@ -461,6 +466,7 @@ describe('runShell', () => { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], @@ -506,6 +512,7 @@ describe('runShell', () => { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], @@ -541,6 +548,7 @@ describe('runShell', () => { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], @@ -594,6 +602,7 @@ describe('runShell', () => { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], @@ -634,6 +643,7 @@ describe('runShell', () => { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], @@ -673,6 +683,7 @@ describe('runShell', () => { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], @@ -724,6 +735,7 @@ describe('runShell', () => { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], @@ -768,6 +780,7 @@ describe('runShell', () => { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], @@ -807,6 +820,7 @@ describe('runShell', () => { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], @@ -863,6 +877,7 @@ describe('runShell', () => { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], @@ -911,6 +926,7 @@ describe('runShell', () => { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], diff --git a/apps/kimi-code/test/cli/v2-run-print.test.ts b/apps/kimi-code/test/cli/v2-run-print.test.ts index c7b76db423..9e18bbc0dd 100644 --- a/apps/kimi-code/test/cli/v2-run-print.test.ts +++ b/apps/kimi-code/test/cli/v2-run-print.test.ts @@ -111,6 +111,7 @@ function opts(overrides: Record = {}) { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: 'say hello', skillsDirs: [], @@ -133,6 +134,7 @@ function makeFakeHarness() { { bind: vi.fn(async () => {}), setModel: vi.fn(async () => ({ model: 'k2' })), + setThinking: vi.fn(async () => ({ thinking: 'high' })), getModel: () => 'k2', data: () => ({ profileName: profileState.profileName }), }, @@ -348,7 +350,12 @@ describe('runV2Print', () => { expect(lifecycle.create).toHaveBeenCalledWith({ workDir: process.cwd(), additionalDirs: undefined, - mainAgentBinding: { profile: 'reviewer', model: 'k2' }, + mainAgentBinding: { + profile: 'reviewer', + model: 'k2', + thinking: undefined, + strictThinking: false, + }, }); const profile = agentServices.get(IAgentProfileService) as { bind: ReturnType }; expect(profile.bind).not.toHaveBeenCalled(); @@ -382,7 +389,12 @@ describe('runV2Print', () => { expect(lifecycle.create).toHaveBeenCalledWith({ workDir: process.cwd(), additionalDirs: undefined, - mainAgentBinding: { profile: 'file-reviewer', model: 'k2' }, + mainAgentBinding: { + profile: 'file-reviewer', + model: 'k2', + thinking: undefined, + strictThinking: false, + }, }); const profile = agentServices.get(IAgentProfileService) as { bind: ReturnType }; expect(profile.bind).not.toHaveBeenCalled(); @@ -440,6 +452,26 @@ describe('runV2Print', () => { expect(input.args?.agentFiles ?? []).toEqual([]); }); + it('marks the CLI effort as strict in a fresh main-agent binding', async () => { + const stdout = writer(); + const stderr = writer(); + const { app, agent, handlerServices } = makeFakeHarness(); + + mocks.bootstrap.mockReturnValue({ app }); + mocks.ensureMainAgent.mockResolvedValue(agent); + + await runV2Print(opts({ effort: 'high' }) as never, '1.2.3-test', { stdout, stderr }); + + const lifecycle = handlerServices.get(ISessionLifecycleService) as { + create: ReturnType; + }; + expect(lifecycle.create).toHaveBeenCalledWith( + expect.objectContaining({ + mainAgentBinding: expect.objectContaining({ thinking: 'high', strictThinking: true }), + }), + ); + }); + it('passes --agent-file paths through unresolved so the engine can expand ~', async () => { const stdout = writer(); const stderr = writer(); @@ -508,4 +540,31 @@ describe('runV2Print', () => { expect(profile.bind).not.toHaveBeenCalled(); expect(profile.setModel).toHaveBeenCalledWith('new-model'); }); + + it('applies the model before effort when both resumed profile overrides are provided', async () => { + const stdout = writer(); + const stderr = writer(); + const { app, agent, agentServices, appServices } = makeFakeHarness(); + + const index = appServices.get(ISessionIndex) as { get: ReturnType }; + index.get.mockResolvedValue({ id: 'ses_1', cwd: process.cwd() }); + + mocks.bootstrap.mockReturnValue({ app }); + mocks.ensureMainAgent.mockResolvedValue(agent); + + await runV2Print( + opts({ session: 'ses_1', model: 'new-model', effort: 'low' }) as never, + '1.2.3-test', + { stdout, stderr }, + ); + + const profile = agentServices.get(IAgentProfileService) as { + setModel: ReturnType; + setThinking: ReturnType; + }; + expect(profile.setThinking).toHaveBeenCalledWith('low'); + expect(profile.setModel.mock.invocationCallOrder[0]).toBeLessThan( + profile.setThinking.mock.invocationCallOrder[0]!, + ); + }); }); diff --git a/apps/kimi-code/test/tui/activity-pane.test.ts b/apps/kimi-code/test/tui/activity-pane.test.ts index 3d8b2ed382..cc0da234b6 100644 --- a/apps/kimi-code/test/tui/activity-pane.test.ts +++ b/apps/kimi-code/test/tui/activity-pane.test.ts @@ -23,6 +23,7 @@ function makeStartupInput(): KimiTUIStartupInput { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], diff --git a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts index 25ba20a06f..5c97e9d33c 100644 --- a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts @@ -138,6 +138,7 @@ function makeStartupInput(): KimiTUIStartupInput { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], @@ -813,6 +814,31 @@ describe('KimiTUI message flow', () => { expect(driver.state.appState.lazySessionThinking).toBeUndefined(); }); + it('strictly applies the CLI effort to the lazy-created session (v2 engine)', async () => { + const session = makeSession({ id: 'ses-lazy' }); + const startupInput: KimiTUIStartupInput = { + ...makeStartupInput(), + engineV2: true, + cliOptions: { + ...makeStartupInput().cliOptions, + model: 'k2', + effort: 'custom-effort', + }, + }; + const { driver, harness } = await makeDriver(session, {}, startupInput); + + driver.handleUserInput('hello'); + + await vi.waitFor(() => { + expect(session.prompt).toHaveBeenCalledWith('hello'); + }); + expect(harness.createSession).toHaveBeenCalledWith( + expect.objectContaining({ model: 'k2', thinking: 'custom-effort' }), + ); + expect(session.setThinking).toHaveBeenCalledWith('custom-effort'); + expect(driver.state.appState.thinkingEffort).toBe('custom-effort'); + }); + it('does not pass the config default plan mode into the lazy-created session (v2 engine)', async () => { const session = makeSession({ id: 'ses-lazy' }); const startupInput: KimiTUIStartupInput = { diff --git a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts index fe816442b6..c2f6ac8406 100644 --- a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts @@ -82,6 +82,7 @@ function makeStartupInput( auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], @@ -820,11 +821,25 @@ describe('KimiTUI startup', () => { expect(harness.createSession).toHaveBeenCalledWith({ workDir: '/tmp/proj-a', model: 'kimi-code/k2.5', + thinking: undefined, permission: undefined, planMode: undefined, }); }); + it('validates the CLI effort through the session setter after creating a fresh startup session', async () => { + const session = makeSession(); + const harness = makeHarness(session); + const driver = makeDriver(harness, makeStartupInput({ effort: 'high' })); + + await expect(driver.init()).resolves.toBe(false); + + expect(harness.createSession).toHaveBeenCalledWith( + expect.objectContaining({ thinking: 'high' }), + ); + expect(session.setThinking).toHaveBeenCalledWith('high'); + }); + it('applies the CLI model override when resuming a startup session', async () => { let model = 'k2'; const session = makeSession({ @@ -855,6 +870,53 @@ describe('KimiTUI startup', () => { expect(driver.state.appState.model).toBe('kimi-code/k2.5'); }); + it('syncs the engine-normalized effort after resuming a startup session', async () => { + let thinkingEffort = 'off'; + const session = makeSession({ + setThinking: vi.fn(async () => { + thinkingEffort = 'high'; + }), + getStatus: vi.fn(async () => ({ + model: 'k2', + thinkingEffort, + permission: 'manual', + planMode: false, + contextTokens: 10, + maxContextTokens: 100, + contextUsage: 0.1, + })), + }); + const harness = makeHarness(session, { + listSessions: vi.fn(async () => [{ id: 'ses-latest' }]), + }); + const driver = makeDriver( + harness, + makeStartupInput({ continue: true, effort: 'off' }), + ); + + await expect(driver.init()).resolves.toBe(true); + + expect(session.setThinking).toHaveBeenCalledWith('off'); + expect(driver.state.appState.thinkingEffort).toBe('high'); + }); + + it('applies the model before effort when both resume overrides are provided', async () => { + const session = makeSession(); + const harness = makeHarness(session, { + listSessions: vi.fn(async () => [{ id: 'ses-latest' }]), + }); + const driver = makeDriver( + harness, + makeStartupInput({ continue: true, model: 'kimi-code/k2.5', effort: 'low' }), + ); + + await expect(driver.init()).resolves.toBe(true); + + expect(session.setModel.mock.invocationCallOrder[0]).toBeLessThan( + session.setThinking.mock.invocationCallOrder[0]!, + ); + }); + it('enters picker startup for bare --session without creating a session', async () => { const harness = makeHarness(); const driver = makeDriver(harness, makeStartupInput({ session: '' })); @@ -1553,6 +1615,50 @@ describe('KimiTUI startup', () => { }); }); + it('preserves the CLI effort when OAuth login recreates the startup session', async () => { + let thinkingEffort = 'off'; + const session = makeSession({ + setThinking: vi.fn(async (effort: string) => { + thinkingEffort = effort; + }), + getStatus: vi.fn(async () => ({ + model: 'k2', + thinkingEffort, + permission: 'manual', + planMode: false, + contextTokens: 10, + maxContextTokens: 100, + contextUsage: 0.1, + })), + }); + const createSession = vi + .fn() + .mockRejectedValueOnce(loginRequiredError()) + .mockResolvedValueOnce(session); + const harness = makeHarness(session, { + getConfig: vi.fn(async () => ({ + defaultModel: 'k2', + thinking: { enabled: false }, + models: { + k2: { model: 'moonshot-v1', maxContextSize: 100 }, + }, + })), + createSession, + }); + const driver = makeDriver(harness, makeStartupInput({ effort: 'high' })); + + await expect(driver.init()).resolves.toBe(false); + vi.mocked(promptPlatformSelection).mockResolvedValue('kimi-code'); + await handleLoginCommand(driver as any); + + expect(createSession).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ model: 'k2', thinking: 'high' }), + ); + expect(session.setThinking).toHaveBeenCalledWith('high'); + expect(driver.state.appState.thinkingEffort).toBe('high'); + }); + it('carries the agent binding into the post-login startup session', async () => { const session = makeSession(); const createSession = vi diff --git a/apps/kimi-code/test/tui/message-replay.test.ts b/apps/kimi-code/test/tui/message-replay.test.ts index d944a485f8..a373f3beaf 100644 --- a/apps/kimi-code/test/tui/message-replay.test.ts +++ b/apps/kimi-code/test/tui/message-replay.test.ts @@ -53,6 +53,7 @@ function makeStartupInput(): KimiTUIStartupInput { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], diff --git a/apps/kimi-code/test/tui/signal-handlers.test.ts b/apps/kimi-code/test/tui/signal-handlers.test.ts index 0cf9c76c81..ddcb01deb8 100644 --- a/apps/kimi-code/test/tui/signal-handlers.test.ts +++ b/apps/kimi-code/test/tui/signal-handlers.test.ts @@ -19,6 +19,7 @@ function makeStartupInput(): KimiTUIStartupInput { auto: false, plan: false, model: undefined, + effort: undefined, outputFormat: undefined, prompt: undefined, skillsDirs: [], diff --git a/docs/en/configuration/overrides.md b/docs/en/configuration/overrides.md index c59a1b28d2..5fea0a2ab0 100644 --- a/docs/en/configuration/overrides.md +++ b/docs/en/configuration/overrides.md @@ -3,7 +3,7 @@ Kimi Code CLI has three places where runtime parameters can be influenced: the config file, command-line options, and environment variables. They are not a simple "whoever has higher priority wins" relationship — the three serve different scenarios and have non-overlapping scopes: - **Config file** stores long-term preferences (model, keys, loop control, etc.); takes effect on every startup -- **Command-line options** make one-off changes for the current startup; discarded after exit +- **Command-line options** apply to the session selected at startup without changing the global config file; session-level values can remain with that session - **Environment variables** primarily handle data directory location, OAuth endpoint switching, and a small number of runtime switches — **not a general fallback mechanism for config fields** This distinction matters: many users run `export KIMI_API_KEY=xxx` in the shell expecting the CLI to pick it up automatically, but it does not. See [Provider credentials](#provider-credentials) below for why. @@ -20,7 +20,7 @@ Environment variables fall into three categories by function and cannot be colla For ordinary runtime parameters such as model alias, Plan mode, yolo mode, and Skills directories, priority from highest to lowest is: -1. **Command-line options** (`-m`, `--plan`, `--yolo`, etc.): apply only to the current startup +1. **Command-line options** (`-m`, `--effort`, `--plan`, `--yolo`, etc.): apply to the session selected at startup without rewriting `config.toml` 2. **User config file** (`~/.kimi-code/config.toml`): stores long-term preferences A small number of environment variables explicitly override specific config file fields — for example, `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` has higher priority than `[background].keep_alive_on_exit`. These exceptions are noted in [Environment variables](./env-vars.md) and in the relevant field descriptions in [Configuration files](./config-files.md). @@ -59,10 +59,15 @@ Options passed at startup have the highest priority and apply only to the curren | `--auto` | Start in auto permission mode: fully autonomous, the agent will not ask questions | | `--plan` | Start in Plan mode | | `-m, --model ` | Use a specific model alias for this session | +| `--effort ` | Set the thinking effort for this session; supported values depend on the selected model | | `-p, --prompt ` | Run in non-interactive mode: execute a single prompt and exit | | `--output-format ` | Output format for `-p` mode: `text` or `stream-json` | | `--skills-dir ` | Replace auto-discovered Skills directories (repeatable; applies to this session only) | +`--effort` accepts any non-empty string and leaves model-specific validation to agent-core. For a new session, the value is written into the session configuration; for a resumed session, it replaces that session's current effort. Omitting the flag keeps the existing configuration or model default. If `--model` is also present, the model is applied first so the effort is validated against the target model. + +This is a session-level override and never edits `config.toml`. The operational `KIMI_MODEL_THINKING_EFFORT` environment override is different: for supported Kimi-provider requests while Thinking is on, it can force the effort sent on the wire and therefore take precedence over the CLI session value. See [Environment variables](./env-vars.md#define-a-model-from-environment-variables-kimi-model). + Mutual exclusion rules (startup fails if violated): - `--output-format` can only be used with `-p` @@ -101,6 +106,12 @@ kimi --yolo -p "Batch rename the following files..." kimi --plan ``` +**Set a session's thinking effort without changing global configuration**: + +```sh +kimi --session 01HZ...XYZ --model kimi-code/kimi-for-coding --effort high +``` + ## Next steps - [Configuration files](./config-files.md) — complete reference for all configurable fields diff --git a/docs/en/reference/kimi-command.md b/docs/en/reference/kimi-command.md index 36480081d2..3178422a92 100644 --- a/docs/en/reference/kimi-command.md +++ b/docs/en/reference/kimi-command.md @@ -18,6 +18,7 @@ All flags are optional — run `kimi` directly to enter an interactive session: | `--session [id]` | `-S` | Resume a session. With an ID, opens that session directly; without an ID, enters an interactive selector | | `--continue` | `-c` | Continue the most recent session in the current working directory, without specifying an ID manually | | `--model ` | `-m` | Specify a model alias for this launch. When omitted, new sessions use `default_model` from the config file | +| `--effort ` | | Set the thinking effort for this session. Supported values depend on the selected model | | `--prompt ` | `-p` | Run a single prompt non-interactively and stream the Assistant output to stdout. This mode does not open the TUI | | `--output-format ` | | Set the non-interactive output format; supports `text` and `stream-json`. Can only be used with `--prompt`; defaults to `text` | | `--yolo` | `-y` | Auto-approve regular tool calls, skipping approval requests | @@ -45,6 +46,8 @@ The following combinations are rejected at startup: When resuming a session, you can override its saved permission or plan mode by adding `--auto`, `--yolo`, or `--plan`. For example, `kimi --continue --auto` resumes the latest session and switches it to auto permission mode. +`--effort` accepts any non-empty string because each model can expose different effort levels; agent-core validates the value against the selected model. The flag works in the interactive TUI and `--prompt` mode. It initializes the setting for a new session or replaces the current setting when a session is resumed, without changing `config.toml`. When `--model` and `--effort` are both present, Kimi Code switches models before applying the effort. + ## Common Usage Start a new session directly: @@ -66,6 +69,16 @@ kimi --session kimi --session 01HZ...XYZ ``` +Set the thinking effort for a new session, a one-shot prompt, or a resumed session: + +```sh +kimi --effort high +kimi -p "Analyze this project" --effort high +kimi --session 01HZ...XYZ --model kimi-code/kimi-for-coding --effort low +``` + +The operational `KIMI_MODEL_THINKING_EFFORT` environment override can still force the effort sent to a supported Kimi provider while Thinking is on, taking precedence over the session value when it applies. See [Environment variables](../configuration/env-vars.md#define-a-model-from-environment-variables-kimi-model). + Skip approval prompts — suitable for batch tasks that are known to be safe: ```sh diff --git a/docs/zh/configuration/overrides.md b/docs/zh/configuration/overrides.md index e3fed310e4..ddb7f7afb8 100644 --- a/docs/zh/configuration/overrides.md +++ b/docs/zh/configuration/overrides.md @@ -3,7 +3,7 @@ Kimi Code CLI 有三个地方可以影响运行参数:配置文件、命令行选项、环境变量。它们不是简单的"谁优先级高谁赢"——三者面向不同场景,作用范围互不相同: - **配置文件** 保存长期偏好(模型、密钥、循环控制等),每次启动都生效 -- **命令行选项** 做本次启动的临时切换,退出后失效 +- **命令行选项** 对启动时选中的会话生效,不修改全局配置文件;会话级取值可以随该会话保留 - **环境变量** 主要负责数据目录定位、OAuth 端点切换,以及少数运行时开关——**不是配置字段的通用后备来源** 这个区别很关键:很多人会在 shell 里 `export KIMI_API_KEY=xxx`,以为 CLI 会自动取到,但实际上不会。原因见下文[供应商凭证](#供应商凭证)。 @@ -20,7 +20,7 @@ Kimi Code CLI 有三个地方可以影响运行参数:配置文件、命令行 对模型别名、Plan 模式、yolo 模式、Skills 目录等普通运行参数,优先级从高到低: -1. **命令行选项**(`-m`、`--plan`、`--yolo` 等):仅对本次启动生效 +1. **命令行选项**(`-m`、`--effort`、`--plan`、`--yolo` 等):对启动时选中的会话生效,不会重写 `config.toml` 2. **用户配置文件**(`~/.kimi-code/config.toml`):保存长期偏好 少数环境变量明确覆盖特定配置字段,例如 `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` 的优先级高于 `[background].keep_alive_on_exit`。这类例外在[环境变量](./env-vars.md)和[配置文件](./config-files.md)对应字段里都有标注。 @@ -59,10 +59,15 @@ Kimi Code CLI 有三个地方可以影响运行参数:配置文件、命令行 | `--auto` | 以 auto 权限模式启动:完全自主,Agent 不会向用户提问 | | `--plan` | 以 Plan 模式启动 | | `-m, --model ` | 指定本次使用的模型别名 | +| `--effort ` | 设置本次会话的 thinking effort;支持值取决于所选模型 | | `-p, --prompt ` | 非交互模式:执行单条提示词后退出 | | `--output-format ` | `-p` 模式的输出格式:`text` 或 `stream-json` | | `--skills-dir ` | 替换自动发现的 Skills 目录(可重复,仅本次生效) | +`--effort` 接受任意非空字符串,具体模型支持校验由 agent-core 完成。新建会话时,该值写入会话配置;恢复会话时,它会替换该会话当前的 effort。省略该 flag 时,继续使用已有配置或模型默认值。同时传入 `--model` 时,会先应用模型,确保 effort 按目标模型校验。 + +这是会话级覆盖,绝不会修改 `config.toml`。运维侧的 `KIMI_MODEL_THINKING_EFFORT` 环境变量语义不同:Thinking 开启且请求使用支持的 Kimi 供应商时,它可以强制覆盖最终发送到线上请求的 effort,因此优先级可能高于 CLI 会话值。详见[环境变量](./env-vars.md#用环境变量定义模型-kimi-model)。 + 互斥规则(违反时启动报错): - `--output-format` 只能配合 `-p` 使用 @@ -101,6 +106,12 @@ kimi --yolo -p "批量重命名以下文件..." kimi --plan ``` +**设置会话的 thinking effort,不修改全局配置**: + +```sh +kimi --session 01HZ...XYZ --model kimi-code/kimi-for-coding --effort high +``` + ## 下一步 - [配置文件](./config-files.md) — 所有可配置字段的完整参考 diff --git a/docs/zh/reference/kimi-command.md b/docs/zh/reference/kimi-command.md index 642026c799..ec13621e62 100644 --- a/docs/zh/reference/kimi-command.md +++ b/docs/zh/reference/kimi-command.md @@ -18,6 +18,7 @@ kimi [options] | `--session [id]` | `-S` | 恢复一个会话。带 ID 时直接打开指定会话;不带 ID 时进入交互式选择器 | | `--continue` | `-c` | 继续当前工作目录下最近一次的会话,无需手动指定 ID | | `--model ` | `-m` | 为本次启动指定模型别名。省略时新会话使用配置文件中的 `default_model` | +| `--effort ` | | 设置本次会话的 thinking effort。支持值取决于所选模型 | | `--prompt ` | `-p` | 非交互执行单次 prompt,并把 Assistant 输出流式写到 stdout。该模式不会打开 TUI | | `--output-format ` | | 设置非交互输出格式,支持 `text` 与 `stream-json`。仅可与 `--prompt` 一起使用,默认 `text` | | `--yolo` | `-y` | 自动批准普通工具调用,跳过审批请求 | @@ -45,6 +46,8 @@ kimi [options] 恢复会话时,可以通过 `--auto`、`--yolo` 或 `--plan` 覆盖原会话保存的权限或计划模式。例如,`kimi --continue --auto` 会恢复最近会话并切换到 auto 权限模式。 +`--effort` 接受任意非空字符串,因为不同模型可以提供不同的 effort 档位;agent-core 会根据所选模型校验该值。该 flag 同时支持交互式 TUI 和 `--prompt` 模式。新建会话时,它用于初始化会话设置;恢复会话时,它会替换该会话当前的设置,但不会修改 `config.toml`。同时传入 `--model` 和 `--effort` 时,Kimi Code 会先切换模型,再应用 effort。 + ## 典型用法 直接运行开启新会话: @@ -66,6 +69,16 @@ kimi --session kimi --session 01HZ...XYZ ``` +为新会话、单次 prompt 或恢复的会话设置 thinking effort: + +```sh +kimi --effort high +kimi -p "分析这个项目" --effort high +kimi --session 01HZ...XYZ --model kimi-code/kimi-for-coding --effort low +``` + +运维侧的 `KIMI_MODEL_THINKING_EFFORT` 环境变量仍可在 Thinking 开启时强制覆盖支持的 Kimi 供应商最终收到的 effort;适用时,它的优先级高于会话值。详见[环境变量](../configuration/env-vars.md#用环境变量定义模型-kimi-model)。 + 跳过审批确认,适合已知安全的批处理任务: ```sh