mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
* feat: support multi-level thinking effort switching
- kimi provider: emit thinking.effort in the new wire format; keep reasoning_effort mirrored during the transition
- model catalog: thread support_efforts / default_effort from oauth through to /models
- config schema: add supportEfforts / defaultEffort on model aliases
- TUI: multi-segment thinking control in /model, new /effort command, footer effort display
- switch status uses displayName and distinguishes model vs effort-only changes
* docs: add thinking effort design plans
- thinking-effort-switching.md: implemented multi-level effort switching
- thinking-model-overhaul.md: follow-up refactor plan for the thinking state model
* docs: collapse thinking overhaul plan into a single PR
* refactor!: overhaul thinking config and effort resolution
Replace default_thinking and thinking.mode with a single [thinking] enabled/effort table. ThinkingEffort is now an open string ('off' | 'on' | model-declared effort); effort levels come from each model's support_efforts instead of a fixed enum.
Centralize default and always_thinking clamp logic in resolveThinkingEffort/defaultThinkingEffortFor, and honor an explicitly configured effort when an always_thinking model is forced back on.
TUI keeps a single thinkingEffort field instead of the boolean + level pair; 'on' is normalized to the model default at the UI boundary.
BREAKING CHANGE: default_thinking and thinking.mode are removed from config; migrate to [thinking] enabled/effort.
* refactor: rename residual thinking level wording to effort
Rename comments, error messages, parameter names, the SetThinkingPayload wire field (level -> effort), and TUI local variables so the thinking effort naming is consistent throughout. No behavior change.
* refactor: rename remaining camelCase thinking level identifiers to effort
Rename liveLevel/prevLevel/levelChanged/commitLevel/effectiveLevel to liveEffort/prevEffort/effortChanged/commitEffort/effectiveEffort in the TUI model picker and config commands.
* refactor: eliminate remaining thinking level wording in comments and tests
Rename levelLabel -> effortLabel, EffortSelectorOptions.levels -> efforts, and 'effort level(s)' / 'default level' / 'requested level' wording in comments, error messages, slash-command description, and test titles to effort. Also restore the withThinking(effort) parameter rename in the Kimi provider that was accidentally reverted.
* fix: address codex review feedback on thinking effort handling
- OpenAI thinkingEffortToReasoningEffort and Anthropic clampEffort now normalize 'on' / unrecognized efforts instead of throwing, so boolean non-Kimi models no longer crash on session start.
- ACP resolveCurrentThinkingEnabled treats a non-empty thinking.effort as enabled, matching agent-core's resolveThinkingEffort.
- REST promptThinkingSchema accepts any non-empty effort string so model-declared efforts are not rejected at the API boundary.
* test: align kimi e2e expectations with supportEfforts-gated reasoning_effort
The kimi provider now sends reasoning_effort only when the model declares support_efforts; boolean models (no support_efforts) send only thinking.type. Update the kimi e2e tests to drop the stale reasoning_effort expectation for the boolean test model.
* test: cover [thinking] effort parsing in config.test
Add effort = "high" to the documented [thinking] table in the config parse test and assert config.thinking.effort is resolved, so the new [thinking] effort field has direct parse coverage.
* docs: add thinking test coverage gap analysis
Capture the explore agent's test coverage review for the thinking overhaul PR, including P1/P2 gaps and the two open design questions, for follow-up test additions.
* feat(oauth): parse nested think_efforts from /models response
The /models endpoint now returns effort levels under a nested think_efforts object ({ support, valid_efforts, default_effort }). Parse it preferentially in both managed-kimi-code and open-platform model parsing, falling back to the legacy flat support_efforts / default_effort fields for older servers.
* refactor(oauth): only read nested think_efforts; gate on support=true
Drop the legacy flat support_efforts / default_effort fallback. The think_efforts object is now the single source, and its support flag gates the whole object — when support is not true, valid_efforts and default_effort are ignored entirely.
* chore: remove unused parseStringArray import in open-platform
* docs: finalize thinking effort release notes
Downgrade the changeset to minor with an English summary, drop the version-specific 'added in 1.0.0' info block, and present the deprecated config fields as a table (field / deprecated in 0.21.0 / description).
* refactor: drop temporary refresh toggles and kimi reasoning_effort mirror
Remove the always-true REFRESH_MODELS_ON_PICKER_OPEN / REFRESH_PROVIDER_MODELS_ON_STARTUP toggles and their stale re-enable TODOs, and stop sending reasoning_effort from the kimi provider (thinking.effort is the only wire field now).
* fix(tui): avoid persisting "on" as thinking effort
* fix: preserve persisted thinking effort across login and provider setup
* fix(tui): show actual thinking effort in /status and footer
* test(tui): align message-flow expectations with effort persistence and /status display
* fix(vis): rename thinkingLevel to thinkingEffort in config.update analysis
149 lines
4.5 KiB
TypeScript
149 lines
4.5 KiB
TypeScript
import type { KimiConfig } from '@moonshot-ai/agent-core';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
applyCatalogProvider,
|
|
catalogModelToAlias,
|
|
catalogProviderModels,
|
|
CatalogFetchError,
|
|
fetchCatalog,
|
|
type CatalogModel,
|
|
} from '../src/catalog';
|
|
|
|
function catalogResponse(body: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
const model: CatalogModel = {
|
|
id: 'm1',
|
|
name: 'M1',
|
|
maxOutputSize: 64000,
|
|
capability: {
|
|
image_in: true,
|
|
video_in: false,
|
|
audio_in: false,
|
|
thinking: true,
|
|
tool_use: true,
|
|
max_context_tokens: 200000,
|
|
},
|
|
};
|
|
|
|
describe('fetchCatalog', () => {
|
|
it('fetches and returns the catalog map', async () => {
|
|
const catalog = { anthropic: { id: 'anthropic', models: { x: { id: 'x', limit: { context: 1000 } } } } };
|
|
const fetchMock = vi.fn(async () => catalogResponse(catalog));
|
|
const result = await fetchCatalog('https://x/api.json', undefined, fetchMock as unknown as typeof fetch);
|
|
expect(result).toEqual(catalog);
|
|
});
|
|
|
|
it('throws CatalogFetchError on HTTP error', async () => {
|
|
const fetchMock = vi.fn(async () => catalogResponse('no', 500));
|
|
await expect(
|
|
fetchCatalog('https://x', undefined, fetchMock as unknown as typeof fetch),
|
|
).rejects.toBeInstanceOf(CatalogFetchError);
|
|
});
|
|
|
|
it('throws on a non-object payload', async () => {
|
|
const fetchMock = vi.fn(async () => catalogResponse([1, 2]));
|
|
await expect(
|
|
fetchCatalog('https://x', undefined, fetchMock as unknown as typeof fetch),
|
|
).rejects.toThrow(/Unexpected catalog response/);
|
|
});
|
|
});
|
|
|
|
describe('catalogModelToAlias', () => {
|
|
it('flattens a catalog model capability into alias fields', () => {
|
|
expect(catalogModelToAlias('anthropic', model)).toEqual({
|
|
provider: 'anthropic',
|
|
model: 'm1',
|
|
maxContextSize: 200000,
|
|
maxOutputSize: 64000,
|
|
capabilities: ['image_in', 'thinking', 'tool_use'],
|
|
displayName: 'M1',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('applyCatalogProvider', () => {
|
|
it('writes provider, model aliases, and defaults', () => {
|
|
const config = { providers: {} } as KimiConfig;
|
|
const result = applyCatalogProvider(config, {
|
|
providerId: 'anthropic',
|
|
wire: 'anthropic',
|
|
baseUrl: 'https://api.anthropic.com',
|
|
apiKey: 'sk',
|
|
models: [model],
|
|
selectedModelId: 'm1',
|
|
thinking: true,
|
|
});
|
|
|
|
expect(result.defaultModel).toBe('anthropic/m1');
|
|
expect(config.providers['anthropic']).toMatchObject({ type: 'anthropic', apiKey: 'sk' });
|
|
expect(config.models?.['anthropic/m1']).toMatchObject({
|
|
provider: 'anthropic',
|
|
model: 'm1',
|
|
maxContextSize: 200000,
|
|
});
|
|
expect(config.defaultModel).toBe('anthropic/m1');
|
|
expect(config.thinking?.enabled).toBe(true);
|
|
});
|
|
|
|
it('writes interleaved reasoning key from a catalog-selected model alias', () => {
|
|
const models = catalogProviderModels({
|
|
id: 'deepseek',
|
|
models: {
|
|
'deepseek-v4-pro': {
|
|
id: 'deepseek-v4-pro',
|
|
name: 'DeepSeek V4 Pro',
|
|
family: 'deepseek-thinking',
|
|
limit: { context: 1000000, output: 384000 },
|
|
reasoning: true,
|
|
tool_call: true,
|
|
interleaved: { field: 'reasoning_content' },
|
|
},
|
|
},
|
|
});
|
|
const config = { providers: {} } as KimiConfig;
|
|
|
|
applyCatalogProvider(config, {
|
|
providerId: 'deepseek',
|
|
wire: 'openai',
|
|
baseUrl: 'https://api.deepseek.com',
|
|
apiKey: 'sk',
|
|
models,
|
|
selectedModelId: 'deepseek-v4-pro',
|
|
thinking: true,
|
|
});
|
|
|
|
expect(config.models?.['deepseek/deepseek-v4-pro']).toMatchObject({
|
|
provider: 'deepseek',
|
|
model: 'deepseek-v4-pro',
|
|
reasoningKey: 'reasoning_content',
|
|
});
|
|
});
|
|
|
|
it('clears stale aliases for the same provider but keeps others', () => {
|
|
const config = {
|
|
providers: { anthropic: { type: 'anthropic', apiKey: 'old' } },
|
|
models: {
|
|
'anthropic/stale': { provider: 'anthropic', model: 'stale', maxContextSize: 1 },
|
|
'other/keep': { provider: 'other', model: 'keep', maxContextSize: 1 },
|
|
},
|
|
} as unknown as KimiConfig;
|
|
|
|
applyCatalogProvider(config, {
|
|
providerId: 'anthropic',
|
|
wire: 'anthropic',
|
|
apiKey: 'new',
|
|
models: [model],
|
|
selectedModelId: 'm1',
|
|
thinking: false,
|
|
});
|
|
|
|
expect(config.models?.['anthropic/stale']).toBeUndefined();
|
|
expect(config.models?.['other/keep']).toBeDefined();
|
|
});
|
|
});
|