Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/model-alias-overrides.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add model alias overrides so manual thinking effort levels and model metadata survive provider catalog refreshes. Set them under `[models."<alias>".overrides]`.
20 changes: 11 additions & 9 deletions apps/kimi-code/src/tui/commands/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type {
ExperimentalFeatureState,
FlagId,
ModelAlias,
PermissionMode,
Session,
ThinkingEffort,
import {
effectiveModelAlias,
type ExperimentalFeatureState,
type FlagId,
type ModelAlias,
type PermissionMode,
type Session,
type ThinkingEffort,
} from '@moonshot-ai/kimi-code-sdk';

import { EditorSelectorComponent } from '../components/dialogs/editor-selector';
Expand Down Expand Up @@ -224,10 +225,11 @@ export async function handleEffortCommand(host: SlashCommandHost, args: string):
host.showError('No model selected. Run /model to select one first.');
return;
}
const segments = segmentsFor(model);
const effective = effectiveModelAlias(model);
const segments = segmentsFor(effective);
const arg = args.trim().toLowerCase();
if (arg.length === 0) {
showEffortPicker(host, model, segments);
showEffortPicker(host, effective, segments);
return;
}
if (!segments.includes(arg)) {
Expand Down
7 changes: 5 additions & 2 deletions apps/kimi-code/src/tui/components/chrome/footer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import type { Component } from '@earendil-works/pi-tui';
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
import chalk from 'chalk';
import { effectiveModelAlias } from '@moonshot-ai/kimi-code-sdk';

import { ALL_TIPS, type ToolbarTip } from '#/tui/constant/tips';
import { isRainbowDancing, renderDanceFooterModel } from '#/tui/easter-eggs/dance';
Expand Down Expand Up @@ -132,7 +133,8 @@ function formatBadgeElapsed(ms: number): string {

function modelDisplayName(state: AppState): string {
const model = state.availableModels[state.model];
return model?.displayName ?? model?.model ?? state.model;
const effective = model === undefined ? undefined : effectiveModelAlias(model);
return effective?.displayName ?? effective?.model ?? state.model;
}

function shortenCwd(path: string): string {
Expand Down Expand Up @@ -263,7 +265,8 @@ export class FooterComponent implements Component {
const model = modelDisplayName(state);
if (model) {
const effort = state.thinkingEffort;
const currentModel = state.availableModels[state.model];
const rawCurrentModel = state.availableModels[state.model];
const currentModel = rawCurrentModel === undefined ? undefined : effectiveModelAlias(rawCurrentModel);
// Only effort-capable models (those declaring support_efforts) show the
// concrete effort; legacy boolean models keep the plain "thinking" suffix.
const hasEfforts = (currentModel?.supportEfforts?.length ?? 0) > 0;
Expand Down
7 changes: 5 additions & 2 deletions apps/kimi-code/src/tui/components/chrome/welcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type { Component } from '@earendil-works/pi-tui';
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
import chalk from 'chalk';

import { effectiveModelAlias } from '@moonshot-ai/kimi-code-sdk';

import { isRainbowDancing, renderDanceWelcomeHeader } from '#/tui/easter-eggs/dance';
import type { AppState } from '#/tui/types';
import { currentTheme } from '#/tui/theme';
Expand All @@ -25,6 +27,7 @@ export class WelcomeComponent implements Component {
const primary = (s: string): string => chalk.hex(currentTheme.palette.primary)(s);
const isLoggedOut = !this.state.model;
const activeModel = this.state.availableModels[this.state.model];
const effectiveActiveModel = activeModel === undefined ? undefined : effectiveModelAlias(activeModel);

if (safeWidth < 24) {
const title = chalk.bold.hex(currentTheme.palette.primary)('Welcome to Kimi Code!');
Expand All @@ -33,7 +36,7 @@ export class WelcomeComponent implements Component {
: chalk.hex(currentTheme.palette.textDim)('Send /help for help information.');
const model = isLoggedOut
? chalk.hex(currentTheme.palette.warning)('not set, run /login or /provider')
: (activeModel?.displayName ?? activeModel?.model ?? this.state.model);
: (effectiveActiveModel?.displayName ?? effectiveActiveModel?.model ?? this.state.model);
return ['', title, prompt, `Model: ${model}`].map((line) =>
truncateToWidth(line, safeWidth, '…'),
);
Expand Down Expand Up @@ -71,7 +74,7 @@ export class WelcomeComponent implements Component {

const modelValue = isLoggedOut
? chalk.hex(currentTheme.palette.warning)('not set, run /login or /provider')
: (activeModel?.displayName ?? activeModel?.model ?? this.state.model);
: (effectiveActiveModel?.displayName ?? effectiveActiveModel?.model ?? this.state.model);

const infoLines = [
labelStyle('Directory: ') + this.state.workDir,
Expand Down
23 changes: 14 additions & 9 deletions apps/kimi-code/src/tui/components/dialogs/model-selector.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ModelAlias, ThinkingEffort } from '@moonshot-ai/kimi-code-sdk';
import { effectiveModelAlias, type ModelAlias, type ThinkingEffort } from '@moonshot-ai/kimi-code-sdk';
import {
Container,
Key,
Expand Down Expand Up @@ -37,7 +37,8 @@ export interface ModelSelection {
}

export function modelDisplayName(alias: string, model: ModelAlias | undefined): string {
return model?.displayName ?? model?.model ?? alias;
const effective = model === undefined ? undefined : effectiveModelAlias(model);
return effective?.displayName ?? effective?.model ?? alias;
}

export function providerDisplayName(provider: string): string {
Expand All @@ -49,10 +50,13 @@ export function providerDisplayName(provider: string): string {
export function createModelChoiceOptions(
models: Record<string, ModelAlias>,
): readonly ChoiceOption[] {
return Object.entries(models).map(([alias, cfg]) => ({
value: alias,
label: `${modelDisplayName(alias, cfg)} (${providerDisplayName(cfg.provider)})`,
}));
return Object.entries(models).map(([alias, cfg]) => {
const effective = effectiveModelAlias(cfg);
return {
value: alias,
label: `${modelDisplayName(alias, effective)} (${providerDisplayName(effective.provider)})`,
};
});
}

export interface ModelSelectorOptions {
Expand All @@ -78,9 +82,10 @@ export interface ModelSelectorOptions {

function createModelChoices(models: Record<string, ModelAlias>): readonly ModelChoice[] {
return Object.entries(models).map(([alias, cfg]) => {
const name = modelDisplayName(alias, cfg);
const provider = providerDisplayName(cfg.provider);
return { alias, model: cfg, name, provider, label: `${name} (${provider})` };
const effective = effectiveModelAlias(cfg);
Comment thread
liruifengv marked this conversation as resolved.
const name = modelDisplayName(alias, effective);
const provider = providerDisplayName(effective.provider);
return { alias, model: effective, name, provider, label: `${name} (${provider})` };
});
}

Expand Down
11 changes: 9 additions & 2 deletions apps/kimi-code/src/tui/components/messages/status-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
* separate from the TUI orchestration layer.
*/

import type { ModelAlias, PermissionMode, SessionStatus, ThinkingEffort } from '@moonshot-ai/kimi-code-sdk';
import {
effectiveModelAlias,
type ModelAlias,
type PermissionMode,
type SessionStatus,
type ThinkingEffort,
} from '@moonshot-ai/kimi-code-sdk';

import { PRODUCT_NAME } from '#/constant/app';
import { currentTheme } from '#/tui/theme';
Expand Down Expand Up @@ -47,7 +53,8 @@ type Colorize = (text: string) => string;

function displayModelName(alias: string, models: Record<string, ModelAlias>): string {
const model = models[alias];
return model?.displayName ?? model?.model ?? alias;
const effective = model === undefined ? undefined : effectiveModelAlias(model);
return effective?.displayName ?? effective?.model ?? alias;
}

function formatModelStatus(options: StatusReportOptions): string {
Expand Down
43 changes: 43 additions & 0 deletions apps/kimi-code/test/tui/components/chrome/footer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,46 @@ describe('FooterComponent', () => {
expect(rendered).not.toContain('thinking:high');
});
});

describe('FooterComponent overrides', () => {
it('shows the overridden effort list', () => {
const effortModelWithOverride: ModelAlias = {
provider: 'managed:kimi-code',
model: 'kimi-k2',
maxContextSize: 262144,
supportEfforts: ['low', 'high', 'max'],
defaultEffort: 'max',
overrides: { supportEfforts: ['low', 'high'], defaultEffort: 'high' },
};
const state: AppState = {
...appState,
thinkingEffort: 'high',
availableModels: { 'kimi-k2': effortModelWithOverride },
};
const footer = new FooterComponent(state);

expect(footer.render(120).join('\n')).toContain('thinking: high');
});
});

describe('FooterComponent displayName override', () => {
it('renders the overridden display name', () => {
const state: AppState = {
...appState,
model: 'kimi-k2',
availableModels: {
'kimi-k2': {
provider: 'managed:kimi-code',
model: 'kimi-k2',
maxContextSize: 262144,
displayName: 'Remote Name',
overrides: { displayName: 'Custom Name' },
},
},
};
const footer = new FooterComponent(state);

expect(footer.render(120).join('\n')).toContain('Custom Name');
expect(footer.render(120).join('\n')).not.toContain('Remote Name');
});
});
22 changes: 22 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 @@ -422,3 +422,25 @@ describe('ModelSelectorComponent', () => {
expect(text(picker)).toContain('[ Medium ]');
});
});

describe('ModelSelectorComponent overrides', () => {
it('uses overridden support_efforts for selectable efforts', () => {
const picker = new ModelSelectorComponent({
models: {
kimi: {
...effortModel('Kimi K2', ['low', 'high', 'max'], 'max'),
overrides: { supportEfforts: ['low', 'high'] },
},
},
currentValue: 'kimi',
currentThinkingEffort: 'max',
onSelect: vi.fn(),
onCancel: vi.fn(),
});

const out = text(picker);
expect(out).toContain('Low');
expect(out).toContain('High');
expect(out).not.toContain('Max');
});
});
78 changes: 78 additions & 0 deletions apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4684,3 +4684,81 @@ command = "vim"
expect(transcript).not.toContain('<hook_result');
});
});

describe('/model status displayName override', () => {
it('shows the overridden display name in the switch status', async () => {
const session = makeSession();
const setConfig = vi.fn(async () => ({ providers: {} }));
const { driver } = await makeDriver(session, {
getConfig: vi.fn(async () => ({
models: {
k2: {
provider: 'managed:kimi-code',
model: 'kimi-k2',
maxContextSize: 100,
displayName: 'Kimi K2',
capabilities: ['thinking'],
},
turbo: {
provider: 'managed:kimi-code',
model: 'kimi-turbo',
maxContextSize: 100,
displayName: 'Remote Turbo',
capabilities: ['thinking'],
overrides: { displayName: 'Custom Turbo' },
},
},
defaultModel: 'k2',
thinking: { enabled: false },
})),
setConfig,
});

driver.handleUserInput('/model turbo');

await vi.waitFor(() => {
expect(driver.state.editorContainer.children[0]).toBeInstanceOf(TabbedModelSelectorComponent);
});
(driver.state.editorContainer.children[0] as TabbedModelSelectorComponent).handleInput('\r');

await vi.waitFor(() => {
expect(setConfig).toHaveBeenCalledWith({
defaultModel: 'turbo',
thinking: { enabled: true },
});
});

expect(renderTranscript(driver)).toContain('Switched to Custom Turbo with thinking on.');
expect(renderTranscript(driver)).not.toContain('Remote Turbo');
});
});

describe('/effort support_efforts override', () => {
it('rejects efforts hidden by support_efforts override', async () => {
const session = makeSession();
const { driver } = await makeDriver(session, {
getConfig: vi.fn(async () => ({
models: {
k2: {
provider: 'managed:kimi-code',
model: 'kimi-k2',
maxContextSize: 100,
displayName: 'Kimi K2',
capabilities: ['thinking'],
supportEfforts: ['low', 'high', 'max'],
overrides: { 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.');
});
});
21 changes: 21 additions & 0 deletions docs/en/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ Each entry in the `models` table defines a model alias (the name used in `defaul
| `max_context_size` | `integer` | Yes | Maximum context length in tokens; must be at least 1 |
| `max_output_size` | `integer` | No | Per-request output token cap (maps to `max_tokens`). Currently only the `anthropic` provider honors it; recognized Claude models are automatically clamped to the server-side maximum |
| `capabilities` | `array<string>` | No | Capability tags to add explicitly: `thinking`, `image_in`, `video_in`, `audio_in`, `tool_use`. Unioned with the capabilities auto-detected by the provider — entries can only be added, never removed |
| `support_efforts` | `array<string>` | No | Thinking effort levels declared by the model catalog. Managed and open-platform refreshes may rewrite this field; to pin it manually, set `[models."<alias>".overrides] support_efforts` instead |
| `default_effort` | `string` | No | Default thinking effort for the model. Managed and open-platform refreshes may rewrite this field; to pin it manually, set `[models."<alias>".overrides] default_effort` instead |
| `display_name` | `string` | No | Name shown in the UI; falls back to `model` when unset |
| `reasoning_key` | `string` | No | `openai` provider only. Override the field name used for reasoning content when the gateway returns it under a non-standard name; by default `reasoning_content`, `reasoning_details`, and `reasoning` are auto-detected |
| `adaptive_thinking` | `boolean` | No | `anthropic` provider only. Force adaptive thinking on or off, overriding the version inference based on the model name. Omit to infer automatically (Claude ≥ 4.6 uses adaptive) |
Expand All @@ -140,6 +142,25 @@ model = "gpt-4.1"
max_context_size = 1047576
```

### Model overrides

Use `[models."<alias>".overrides]` for user overrides that must survive provider-model refreshes. Runtime consumers read the effective value: the override when present, otherwise the top-level field.

```toml
[models."kimi-code/kimi-k2"]
provider = "managed:kimi-code"
model = "kimi-k2"
max_context_size = 262144
support_efforts = ["low", "high", "max"]
default_effort = "max"

[models."kimi-code/kimi-k2".overrides]
support_efforts = ["low", "high"]
default_effort = "high"
```

`[models."<alias>".overrides]` accepts ordinary model fields such as `max_context_size`, `max_output_size`, `capabilities`, `display_name`, `reasoning_key`, `adaptive_thinking`, `support_efforts`, and `default_effort`. It does not accept identity / routing fields: `provider`, `model`, `protocol`, and `beta_api`.

You can also switch models temporarily without touching the config file — by setting `KIMI_MODEL_*` environment variables, the CLI synthesizes a temporary provider in memory that does not persist after restart. See [Define a model from environment variables](./env-vars.md#define-a-model-from-environment-variables-kimi_model).

## `thinking`
Expand Down
21 changes: 21 additions & 0 deletions docs/zh/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ KIMI_BASE_URL = "https://api.moonshot.ai/v1"
| `max_context_size` | `integer` | 是 | 最大上下文长度(token 数),必须 ≥ 1 |
| `max_output_size` | `integer` | 否 | 单次请求的输出 token 上限(对应 `max_tokens`)。目前仅 `anthropic` 供应商读取;已识别的 Claude 系列会自动限制在服务端允许的最大值内 |
| `capabilities` | `array<string>` | 否 | 显式追加的能力标签:`thinking`、`image_in`、`video_in`、`audio_in`、`tool_use`。与供应商自动识别的能力取并集,只能追加不能移除 |
| `support_efforts` | `array<string>` | 否 | 模型目录声明的 Thinking 档位。managed 和 open-platform 刷新可能会改写该字段;如需手动固定,请改用 `[models."<alias>".overrides] support_efforts` |
| `default_effort` | `string` | 否 | 模型的默认 Thinking 档位。managed 和 open-platform 刷新可能会改写该字段;如需手动固定,请改用 `[models."<alias>".overrides] default_effort` |
| `display_name` | `string` | 否 | UI 中显示的名称,未设时回退到 `model` |
| `reasoning_key` | `string` | 否 | 仅 `openai` 供应商。当网关用非标准字段名返回推理内容时才需要设置;默认自动识别 `reasoning_content` / `reasoning_details` / `reasoning` |
| `adaptive_thinking` | `boolean` | 否 | 仅 `anthropic` 供应商。强制开启或关闭 adaptive thinking,覆盖按模型名推断的逻辑。省略时自动推断(Claude ≥ 4.6 使用 adaptive) |
Expand All @@ -140,6 +142,25 @@ model = "gpt-4.1"
max_context_size = 1047576
```

### 模型覆盖项

如果某些用户覆盖需要在 provider-model 刷新后保留,请写到 `[models."<alias>".overrides]`。运行时读取的是 effective 值:有 override 时用 override,否则用顶层字段。

```toml
[models."kimi-code/kimi-k2"]
provider = "managed:kimi-code"
model = "kimi-k2"
max_context_size = 262144
support_efforts = ["low", "high", "max"]
default_effort = "max"

[models."kimi-code/kimi-k2".overrides]
support_efforts = ["low", "high"]
default_effort = "high"
```

`[models."<alias>".overrides]` 接受普通模型字段,例如 `max_context_size`、`max_output_size`、`capabilities`、`display_name`、`reasoning_key`、`adaptive_thinking`、`support_efforts` 和 `default_effort`。不接受身份 / 路由字段:`provider`、`model`、`protocol` 和 `beta_api`。

无需修改配置文件也可以临时切换模型——通过 `KIMI_MODEL_*` 环境变量在内存里合成一个临时供应商,详见[用环境变量定义模型](./env-vars.md#用环境变量定义模型-kimi-model)。

## `thinking`
Expand Down
Loading
Loading