Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/fix-adaptive-thinking-opt-out.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Honor adaptive_thinking = false on Anthropic-compatible models by limiting thinking efforts to the legacy budget set and omitting the effort parameter from requests.
5 changes: 5 additions & 0 deletions .changeset/fix-anthropic-effort-profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Apply official Anthropic effort profiles and a 128k output fallback for unknown models. Preserve compatible-provider thinking history across session resumes and model switches, normalize incomplete stream events, and warn on unlisted efforts.
5 changes: 5 additions & 0 deletions .changeset/fix-anthropic-provider-effort-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix custom-named models on Anthropic-compatible providers starting new sessions with thinking effort off instead of the model default, and not showing the thinking control in ACP clients.
32 changes: 26 additions & 6 deletions apps/kimi-code/src/tui/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ function currentTuiConfig(host: SlashCommandHost): TuiConfig {
};
}

function effectiveModelForHost(host: SlashCommandHost, model: ModelAlias): ModelAlias {
const providerType = host.state.appState.availableProviders[model.provider]?.type;
return effectiveModelAlias(model, (model.protocol ?? providerType) === 'anthropic');
}

export async function handlePlanCommand(host: SlashCommandHost, args: string): Promise<void> {
const session = host.session;
if (session === undefined) {
Expand Down Expand Up @@ -234,18 +239,27 @@ export async function handleEffortCommand(host: SlashCommandHost, args: string):
host.showError('No model selected. Run /model to select one first.');
return;
}
const effective = effectiveModelAlias(model);
const effective = effectiveModelForHost(host, model);
const segments = segmentsFor(effective);
const arg = args.trim().toLowerCase();
if (arg.length === 0) {
showEffortPicker(host, effective, segments);
return;
}
if (!segments.includes(arg)) {
host.showError(
`Unsupported thinking effort "${arg}" for ${alias}. Available: ${segments.join(', ')}`,
const providerType = host.state.appState.availableProviders[effective.provider]?.type;
const protocol = effective.protocol ?? providerType;
if (protocol !== 'anthropic') {
host.showError(
`Unsupported thinking effort "${arg}" for ${alias}. Available: ${segments.join(', ')}`,
);
return;
}
const knownEfforts = effective.supportEfforts?.join(', ') ?? 'none declared';
host.showStatus(
`Thinking effort "${arg}" is not listed for ${alias} (known: ${knownEfforts}). Sending "${arg}" unchanged; the configured provider will validate it.`,
'warning',
);
return;
}
await performModelSwitch(host, alias, arg, true);
}
Expand Down Expand Up @@ -358,7 +372,13 @@ async function applyEditorChoice(host: SlashCommandHost, value: string): Promise
}

export function showModelPicker(host: SlashCommandHost, selectedValue: string = host.state.appState.model): void {
const entries = Object.entries(host.state.appState.availableModels);
const models = Object.fromEntries(
Object.entries(host.state.appState.availableModels).map(([alias, model]) => [
alias,
effectiveModelForHost(host, model),
]),
);
const entries = Object.entries(models);
if (entries.length === 0) {
host.showNotice(
'No models configured',
Expand All @@ -368,7 +388,7 @@ export function showModelPicker(host: SlashCommandHost, selectedValue: string =
}
host.mountEditorReplacement(
new TabbedModelSelectorComponent({
models: host.state.appState.availableModels,
models,
currentValue: host.state.appState.model,
selectedValue,
currentThinkingEffort: host.state.appState.thinkingEffort,
Expand Down
49 changes: 49 additions & 0 deletions apps/kimi-code/test/tui/components/dialogs/model-selector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,55 @@ describe('ModelSelectorComponent', () => {
expect(out).toContain('Thinking (←→ to switch)');
});

it('derives official Anthropic effort segments from the model name', () => {
const onSelect = vi.fn();
const picker = new ModelSelectorComponent({
models: {
opus: {
provider: 'anthropic',
model: 'claude-opus-4-6',
maxContextSize: 200000,
},
},
currentValue: 'opus',
currentThinkingEffort: 'high',
onSelect,
onCancel: vi.fn(),
});

const out = text(picker);
expect(out).toContain('Low');
expect(out).toContain('[ High ]');
expect(out).toContain('Max');
expect(out).toContain('Off');
expect(out).not.toContain('Xhigh');

picker.handleInput(RIGHT);
picker.handleInput('\r');
expect(onSelect).toHaveBeenCalledWith({ alias: 'opus', thinking: 'max' });
});

it('derives official always-on Anthropic models without an Off segment', () => {
const picker = new ModelSelectorComponent({
models: {
fable: {
provider: 'anthropic',
model: 'claude-fable-5',
maxContextSize: 200000,
},
},
currentValue: 'fable',
currentThinkingEffort: 'high',
onSelect: vi.fn(),
onCancel: vi.fn(),
});

const out = text(picker);
expect(out).toContain('Xhigh');
expect(out).toContain('Max');
expect(out).not.toContain('Off');
});

it('cycles efforts with Left/Right and clamps at the ends', () => {
const onSelect = vi.fn();
const picker = new ModelSelectorComponent({
Expand Down
82 changes: 76 additions & 6 deletions apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { ApprovalRequest, ApprovalResponse, Event } from '@moonshot-ai/kimi
import { afterEach, describe, expect, it, vi } from 'vitest';

import { ApprovalPanelComponent } from '#/tui/components/dialogs/approval-panel';
import { EffortSelectorComponent } from '#/tui/components/dialogs/effort-selector';
import { KIMI_CODE_PLUGIN_MARKETPLACE_URL } from '#/constant/app';
import { MOON_SPINNER_FRAMES } from '#/tui/constant/rendering';
import {
Expand Down Expand Up @@ -5161,16 +5162,20 @@ describe('/model status displayName override', () => {
});

describe('/effort support_efforts override', () => {
it('rejects efforts hidden by support_efforts override', async () => {
it('warns and applies efforts hidden by an Anthropic support_efforts override', async () => {
const session = makeSession();
const { driver } = await makeDriver(session, {
getConfig: vi.fn(async () => ({
providers: {
compatible: { type: 'kimi', apiKey: 'test-key' },
},
models: {
k2: {
provider: 'managed:kimi-code',
model: 'kimi-k2',
provider: 'compatible',
model: 'compatible-model',
protocol: 'anthropic',
maxContextSize: 100,
displayName: 'Kimi K2',
displayName: 'Compatible Model',
capabilities: ['thinking'],
supportEfforts: ['low', 'high', 'max'],
overrides: { supportEfforts: ['low', 'high'] },
Expand All @@ -5184,8 +5189,73 @@ describe('/effort support_efforts override', () => {
driver.handleUserInput('/effort max');

await vi.waitFor(() => {
expect(renderTranscript(driver)).toContain('Unsupported thinking effort "max" for k2. Available: off, low, high');
expect(session.setThinking).toHaveBeenCalledWith('max');
});
await vi.waitFor(() => {
expect(renderTranscript(driver)).toContain('Thinking set to max.');
});
const transcript = renderTranscript(driver).replaceAll(/\s+/g, ' ');
expect(transcript).toContain(
'Thinking effort "max" is not listed for k2 (known: low, high). Sending "max" unchanged; the configured provider will validate it.',
);
expect(transcript).toContain('Thinking set to max.');
});

it('offers the latest Opus efforts for an unknown Anthropic-compatible model', async () => {
const { driver } = await makeDriver(makeSession(), {
getConfig: vi.fn(async () => ({
providers: {
compatible: { type: 'kimi', apiKey: 'test-key' },
},
models: {
k2: {
provider: 'compatible',
model: 'compatible-model',
protocol: 'anthropic',
maxContextSize: 100,
},
},
defaultModel: 'k2',
})),
});

driver.handleUserInput('/effort');

await vi.waitFor(() => {
expect(driver.state.editorContainer.children[0]).toBeInstanceOf(EffortSelectorComponent);
});
const picker = driver.state.editorContainer.children[0] as EffortSelectorComponent;
expect(picker.render(80).join('\n')).toContain('Max');
});

it('keeps rejecting efforts hidden by a Kimi support_efforts override', async () => {
const session = makeSession();
const { driver } = await makeDriver(session, {
getConfig: vi.fn(async () => ({
providers: {
kimi: { type: 'kimi', apiKey: 'test-key' },
},
models: {
k2: {
provider: 'kimi',
model: 'kimi-model',
maxContextSize: 100,
capabilities: ['thinking'],
supportEfforts: ['low', 'high'],
},
},
defaultModel: 'k2',
thinking: { enabled: true, effort: 'low' },
})),
});

driver.handleUserInput('/effort max');

await vi.waitFor(() => {
expect(renderTranscript(driver)).toContain(
'Unsupported thinking effort "max" for k2. Available: off, low, high',
);
});
expect(renderTranscript(driver)).not.toContain('Switched to Kimi K2 with thinking max.');
expect(session.setThinking).not.toHaveBeenCalled();
});
});
63 changes: 49 additions & 14 deletions packages/acp-adapter/src/model-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,20 @@
* `for model_key, model in models.items()`.
*
* `thinkingSupported` is true if any of:
* 1. the alias's declared `capabilities` array contains `'thinking'`, or
* 1. the alias's declared `capabilities` array contains `'thinking'`
* (including the capability inferred from the Anthropic wire protocol —
* see the `anthropicCompatible` context below), or
* 2. the underlying model name matches `/thinking|reason/i`
* (always-thinking variants), or
* 3. the underlying model name is on the {@link TOGGLEABLE_THINKING_MODELS}
* allow-list (mirrors `kimi-cli/src/kimi_cli/llm.py:derive_model_capabilities`).
*
* The runtime resolves a model's wire protocol from
* `alias.protocol ?? provider.type` (see
* `ProviderManager.resolveProviderConfig`). The derive helpers below take the
* provider-derived `anthropicCompatible` flag as an optional second argument
* so the catalog agrees with the runtime about Anthropic profiles even when
* the alias itself does not declare `protocol`.
*/

import { effectiveModelAlias } from '@moonshot-ai/agent-core';
Expand Down Expand Up @@ -55,8 +64,8 @@ export interface AcpModelEntry {
*/
const TOGGLEABLE_THINKING_MODELS = new Set(['kimi-for-coding', 'kimi-code']);

export function deriveThinkingSupported(alias: ModelAlias): boolean {
const effective = effectiveModelAlias(alias);
export function deriveThinkingSupported(alias: ModelAlias, anthropicCompatible = false): boolean {
const effective = effectiveModelAlias(alias, anthropicCompatible);
const declared = effective.capabilities ?? [];
if (declared.includes('thinking') || declared.includes('always_thinking')) return true;
const lower = effective.model.toLowerCase();
Expand All @@ -72,17 +81,22 @@ export function deriveThinkingSupported(alias: ModelAlias): boolean {
* `thinkingSupported`, but only an explicit (server-derived) declaration
* may remove the off option from the client.
*/
export function deriveAlwaysThinking(alias: ModelAlias): boolean {
return (effectiveModelAlias(alias).capabilities ?? []).includes('always_thinking');
export function deriveAlwaysThinking(alias: ModelAlias, anthropicCompatible = false): boolean {
return (effectiveModelAlias(alias, anthropicCompatible).capabilities ?? []).includes(
'always_thinking',
);
}

/**
* The effort a boolean "thinking on" toggle maps to for this model: declared
* `default_effort`, else the middle `support_efforts` entry, else `'on'` for
* boolean models (no `support_efforts`).
*/
export function deriveDefaultThinkingEffort(alias: ModelAlias): string {
const effective = effectiveModelAlias(alias);
export function deriveDefaultThinkingEffort(
alias: ModelAlias,
anthropicCompatible = false,
): string {
const effective = effectiveModelAlias(alias, anthropicCompatible);
const efforts = effective.supportEfforts;
if (efforts !== undefined && efforts.length > 0) {
return effective.defaultEffort ?? efforts[Math.floor(efforts.length / 2)]!;
Expand All @@ -102,24 +116,45 @@ export async function listModelsFromHarness(
harness: KimiHarness,
): Promise<readonly AcpModelEntry[]> {
if (typeof harness.getConfig !== 'function') return [];
let models: Record<string, ModelAlias> | undefined;
let config: Awaited<ReturnType<KimiHarness['getConfig']>>;
try {
const config = await harness.getConfig();
models = config.models;
config = await harness.getConfig();
} catch {
return [];
}
const models = config.models;
if (models === undefined) return [];
const out: AcpModelEntry[] = [];
for (const [id, alias] of Object.entries(models)) {
const effective = effectiveModelAlias(alias);
const anthropicCompatible = usesAnthropicProvider(alias, config);
const effective = effectiveModelAlias(alias, anthropicCompatible);
out.push({
id,
name: effective.displayName ?? effective.model ?? id,
thinkingSupported: deriveThinkingSupported(alias),
alwaysThinking: deriveAlwaysThinking(alias),
defaultThinkingEffort: deriveDefaultThinkingEffort(alias),
thinkingSupported: deriveThinkingSupported(alias, anthropicCompatible),
alwaysThinking: deriveAlwaysThinking(alias, anthropicCompatible),
defaultThinkingEffort: deriveDefaultThinkingEffort(alias, anthropicCompatible),
});
}
return out;
}

/**
* Provider-level Anthropic context for an alias, mirroring how
* `ProviderManager.resolveProviderConfig` resolves the wire protocol: the
* alias's provider (falling back to the configured default provider) decides
* when the alias itself does not declare `protocol`. Without this the catalog
* would mark a custom-named model on an `type = "anthropic"` provider as not
* thinking-capable while the runtime infers the latest Anthropic profile.
*/
function usesAnthropicProvider(
alias: ModelAlias,
config: {
providers?: Record<string, { type?: string } | undefined>;
defaultProvider?: string | undefined;
},
): boolean {
const providerName = alias.provider ?? config.defaultProvider;
if (providerName === undefined) return false;
return config.providers?.[providerName]?.type === 'anthropic';
}
Loading
Loading