mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
feat: add KIMI_MODEL_THINKING_EFFORT to force a thinking effort (#1275)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat: add KIMI_MODEL_THINKING_EFFORT to force a thinking effort Send thinking effort only when the model declares it in support_efforts, and add the KIMI_MODEL_THINKING_EFFORT environment variable as an escape hatch to force a specific effort regardless of declared support. * test: align thinking effort expectations with support_efforts gating Update the kimi adapter e2e and compaction tests that asserted the previous pass-through behavior on models without support_efforts.
This commit is contained in:
parent
54703d9457
commit
a5db546d77
10 changed files with 133 additions and 28 deletions
|
|
@ -130,6 +130,7 @@ Switches that control the behavior of subsystems such as telemetry, background t
|
|||
| `KIMI_MODEL_MAX_COMPLETION_TOKENS` | Hard cap on `max_completion_tokens` per LLM step; applies to the `kimi` provider only | Positive integer; `0` or negative disables clamping |
|
||||
| `KIMI_MODEL_TEMPERATURE` | Sampling temperature for every request; applies to the `kimi` provider only (global — independent of `KIMI_MODEL_NAME`) | Number, e.g. `0.3` |
|
||||
| `KIMI_MODEL_TOP_P` | Nucleus-sampling `top_p` for every request; applies to the `kimi` provider only (global) | Number, e.g. `0.95` |
|
||||
| `KIMI_MODEL_THINKING_EFFORT` | Force a specific thinking effort on the wire (`thinking.effort`), bypassing the model's declared `support_efforts`; applies to the `kimi` provider only, and only while Thinking is on | An effort value, e.g. `max` |
|
||||
| `KIMI_MODEL_THINKING_KEEP` | Moonshot preserved-thinking passthrough (`thinking.keep`); applies to the `kimi` provider only, and only while Thinking is on | A value the API accepts, e.g. `all` |
|
||||
| `KIMI_CODE_NO_AUTO_UPDATE` | Fully disable the update preflight — no check, background install, or prompt. Legacy alias `KIMI_CLI_NO_AUTO_UPDATE` is also honored | Truthy: `1`/`true`/`yes`/`on` |
|
||||
| `KIMI_DISABLE_CRON` | Disable the scheduled-task tool (`CronCreate` rejects new schedules; existing tasks do not fire) | `1` to disable |
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ kimi
|
|||
| `KIMI_MODEL_MAX_COMPLETION_TOKENS` | 单步 LLM 请求的 `max_completion_tokens` 硬上限,仅对 `kimi` 供应商生效 | 正整数;`0` 或负数禁用 clamp |
|
||||
| `KIMI_MODEL_TEMPERATURE` | 每次请求的采样温度,仅对 `kimi` 供应商生效(全局生效,不依赖 `KIMI_MODEL_NAME`) | 数字,如 `0.3` |
|
||||
| `KIMI_MODEL_TOP_P` | 每次请求的核采样 `top_p`,仅对 `kimi` 供应商生效(全局生效) | 数字,如 `0.95` |
|
||||
| `KIMI_MODEL_THINKING_EFFORT` | 在线上强制使用指定的思考强度(`thinking.effort`),绕过模型声明的 `support_efforts`;仅对 `kimi` 供应商生效,且仅在 Thinking 开启时注入 | 思考强度值,如 `max` |
|
||||
| `KIMI_MODEL_THINKING_KEEP` | Moonshot 保留思考透传(`thinking.keep`),仅对 `kimi` 供应商生效,且仅在 Thinking 开启时注入 | API 接受的值,如 `all` |
|
||||
| `KIMI_CODE_NO_AUTO_UPDATE` | 完全禁用更新预检——不检查、不后台安装、不提示。同时兼容旧名 `KIMI_CLI_NO_AUTO_UPDATE` | 真值:`1`/`true`/`yes`/`on` |
|
||||
| `KIMI_DISABLE_CRON` | 禁用定时任务工具(`CronCreate` 拒绝新计划,已有任务不触发) | `1` 表示禁用 |
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import {
|
|||
type ProviderConfig,
|
||||
} from '@moonshot-ai/kosong';
|
||||
|
||||
import { applyKimiEnvSamplingParams, applyKimiEnvThinkingKeep } from '#/config/kimi-env-params';
|
||||
import {
|
||||
applyKimiEnvSamplingParams,
|
||||
applyKimiEnvThinkingEffort,
|
||||
applyKimiEnvThinkingKeep,
|
||||
} from '#/config/kimi-env-params';
|
||||
|
||||
import type { Agent } from '..';
|
||||
import { ErrorCodes, KimiError } from '../../errors';
|
||||
|
|
@ -117,9 +121,12 @@ export class ConfigState {
|
|||
// from config.provider — the main loop AND full-history compaction — carries it:
|
||||
// - withThinking: preserve thinking during compaction (#464)
|
||||
// - sampling params: KIMI_MODEL_TEMPERATURE / KIMI_MODEL_TOP_P
|
||||
// - thinking.effort: KIMI_MODEL_THINKING_EFFORT (forces an effort, only while thinking is on)
|
||||
// - thinking.keep: KIMI_MODEL_THINKING_KEEP (only while thinking is on)
|
||||
const provider = createProvider(this.providerConfig).withThinking(this.thinkingEffort);
|
||||
return applyKimiEnvThinkingKeep(applyKimiEnvSamplingParams(provider), this.thinkingEffort);
|
||||
const withSampling = applyKimiEnvSamplingParams(provider);
|
||||
const withEffort = applyKimiEnvThinkingEffort(withSampling, this.thinkingEffort);
|
||||
return applyKimiEnvThinkingKeep(withEffort, this.thinkingEffort);
|
||||
}
|
||||
|
||||
get model(): string {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,27 @@ export function applyKimiEnvSamplingParams(
|
|||
return Object.keys(kwargs).length > 0 ? provider.withGenerationKwargs(kwargs) : provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a specific thinking effort via `KIMI_MODEL_THINKING_EFFORT`, bypassing
|
||||
* the model's declared `support_efforts`. Applied in `ConfigState.provider`
|
||||
* after `withThinking`, and only while thinking is on — effort has no meaning
|
||||
* when thinking is disabled. The value is forwarded verbatim as
|
||||
* `thinking.effort`, so callers can target a model that accepts an effort but
|
||||
* does not advertise one via `support_efforts`.
|
||||
*
|
||||
* Non-Kimi providers — and an unset/blank value — are returned unchanged.
|
||||
*/
|
||||
export function applyKimiEnvThinkingEffort(
|
||||
provider: ChatProvider,
|
||||
thinkingEffort: ThinkingEffort,
|
||||
env: Env = process.env,
|
||||
): ChatProvider {
|
||||
if (!(provider instanceof KimiChatProvider)) return provider;
|
||||
const effort = env['KIMI_MODEL_THINKING_EFFORT']?.trim();
|
||||
if (effort === undefined || effort.length === 0 || thinkingEffort === 'off') return provider;
|
||||
return provider.withExtraBody({ thinking: { effort } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the Moonshot preserved-thinking passthrough (`KIMI_MODEL_THINKING_KEEP`
|
||||
* -> `thinking.keep`) to a chat provider. Applied in `ConfigState.provider` after
|
||||
|
|
|
|||
|
|
@ -1874,10 +1874,10 @@ describe('FullCompaction', () => {
|
|||
|
||||
expect(callCount).toBe(3);
|
||||
// The catalogued model declares no supportEfforts, so the kimi provider
|
||||
// passes the requested effort through verbatim. The agent's stored
|
||||
// thinkingEffort ('high') is also carried across the compaction (see the
|
||||
// record assertion below).
|
||||
expect(providerThinkingEfforts).toEqual(['high', 'high', 'high']);
|
||||
// normalizes to boolean thinking and reports 'on' rather than the
|
||||
// requested 'high'. The agent's stored thinkingEffort ('high') is still
|
||||
// carried across the compaction (see the record assertion below).
|
||||
expect(providerThinkingEfforts).toEqual(['on', 'on', 'on']);
|
||||
expect(records).toContainEqual({
|
||||
event: 'compaction_finished',
|
||||
properties: expect.objectContaining({
|
||||
|
|
|
|||
|
|
@ -289,4 +289,36 @@ describe('ConfigState.provider applies global KIMI_MODEL_* request config', () =
|
|||
vi.unstubAllEnvs();
|
||||
}
|
||||
});
|
||||
|
||||
it('injects KIMI_MODEL_THINKING_EFFORT into config.provider when thinking is on', () => {
|
||||
vi.stubEnv('KIMI_MODEL_THINKING_EFFORT', 'max');
|
||||
try {
|
||||
const ctx = kimiAgent();
|
||||
ctx.agent.config.update({ modelAlias: 'kimi-code', thinkingEffort: 'high' });
|
||||
|
||||
const provider = ctx.agent.config.provider;
|
||||
const gen = Reflect.get(provider as object, '_generationKwargs') as {
|
||||
extra_body?: { thinking?: { type?: string; effort?: string } };
|
||||
};
|
||||
expect(gen.extra_body?.thinking).toEqual({ type: 'enabled', effort: 'max' });
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
});
|
||||
|
||||
it('does NOT inject KIMI_MODEL_THINKING_EFFORT into config.provider when thinking is off', () => {
|
||||
vi.stubEnv('KIMI_MODEL_THINKING_EFFORT', 'max');
|
||||
try {
|
||||
const ctx = kimiAgent();
|
||||
ctx.agent.config.update({ modelAlias: 'kimi-code', thinkingEffort: 'off' });
|
||||
|
||||
const provider = ctx.agent.config.provider;
|
||||
const gen = Reflect.get(provider as object, '_generationKwargs') as {
|
||||
extra_body?: { thinking?: { effort?: string } };
|
||||
};
|
||||
expect(gen.extra_body?.thinking?.effort).toBeUndefined();
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { type ChatProvider, KimiChatProvider } from '@moonshot-ai/kosong';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { applyKimiEnvSamplingParams, applyKimiEnvThinkingKeep } from '../../src/config/kimi-env-params';
|
||||
import {
|
||||
applyKimiEnvSamplingParams,
|
||||
applyKimiEnvThinkingEffort,
|
||||
applyKimiEnvThinkingKeep,
|
||||
} from '../../src/config/kimi-env-params';
|
||||
import { KimiError } from '../../src/errors';
|
||||
|
||||
function kimi(): KimiChatProvider {
|
||||
|
|
@ -11,7 +15,7 @@ function kimi(): KimiChatProvider {
|
|||
interface KimiGenerationState {
|
||||
temperature?: number;
|
||||
top_p?: number;
|
||||
extra_body?: { thinking?: { keep?: unknown } };
|
||||
extra_body?: { thinking?: { type?: string; effort?: string; keep?: unknown } };
|
||||
}
|
||||
|
||||
function genState(provider: ChatProvider): KimiGenerationState {
|
||||
|
|
@ -78,3 +82,44 @@ describe('applyKimiEnvThinkingKeep', () => {
|
|||
expect(applyKimiEnvThinkingKeep(stub, 'high', { KIMI_MODEL_THINKING_KEEP: 'all' })).toBe(stub);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyKimiEnvThinkingEffort', () => {
|
||||
it('injects thinking.effort when thinking is on', () => {
|
||||
const out = applyKimiEnvThinkingEffort(kimi(), 'high', {
|
||||
KIMI_MODEL_THINKING_EFFORT: 'max',
|
||||
});
|
||||
expect(genState(out).extra_body?.thinking?.effort).toBe('max');
|
||||
});
|
||||
|
||||
it('forces the effort even when the model does not declare it', () => {
|
||||
// kimi() has no support_efforts, so withThinking('high') carries no effort;
|
||||
// the env var injects one anyway, bypassing the support_efforts gate.
|
||||
const provider = kimi().withThinking('high');
|
||||
const out = applyKimiEnvThinkingEffort(provider, 'high', {
|
||||
KIMI_MODEL_THINKING_EFFORT: 'max',
|
||||
});
|
||||
expect(genState(out).extra_body?.thinking).toEqual({ type: 'enabled', effort: 'max' });
|
||||
});
|
||||
|
||||
it('does NOT inject thinking.effort when thinking is off', () => {
|
||||
const out = applyKimiEnvThinkingEffort(kimi(), 'off', {
|
||||
KIMI_MODEL_THINKING_EFFORT: 'max',
|
||||
});
|
||||
expect(genState(out).extra_body).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns the same provider when the env var is unset or blank', () => {
|
||||
const provider = kimi();
|
||||
expect(applyKimiEnvThinkingEffort(provider, 'high', {})).toBe(provider);
|
||||
expect(
|
||||
applyKimiEnvThinkingEffort(provider, 'high', { KIMI_MODEL_THINKING_EFFORT: ' ' }),
|
||||
).toBe(provider);
|
||||
});
|
||||
|
||||
it('leaves non-kimi providers untouched', () => {
|
||||
const stub = { name: 'stub' } as unknown as ChatProvider;
|
||||
expect(
|
||||
applyKimiEnvThinkingEffort(stub, 'high', { KIMI_MODEL_THINKING_EFFORT: 'max' }),
|
||||
).toBe(stub);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -513,17 +513,12 @@ export class KimiChatProvider implements ChatProvider {
|
|||
if (effort === 'off') {
|
||||
thinking = { type: 'disabled' };
|
||||
} else {
|
||||
// When `support_efforts` is present, it is the source of truth: only
|
||||
// declared values are sent as effort. When it is absent, pass the
|
||||
// requested effort through verbatim.
|
||||
const hasDeclaredEfforts = this._supportEfforts.length > 0;
|
||||
const declared = hasDeclaredEfforts
|
||||
? this._supportEfforts.includes(effort)
|
||||
? effort
|
||||
: undefined
|
||||
: effort;
|
||||
thinking =
|
||||
declared !== undefined ? { type: 'enabled', effort: declared } : { type: 'enabled' };
|
||||
// Only efforts the model explicitly declares via `support_efforts` are
|
||||
// sent on the wire. When `support_efforts` is absent/empty, or the
|
||||
// requested effort is not declared, only thinking.type is sent.
|
||||
thinking = this._supportEfforts.includes(effort)
|
||||
? { type: 'enabled', effort }
|
||||
: { type: 'enabled' };
|
||||
}
|
||||
// Replace extra_body.thinking wholesale so a stale `effort` from a previous
|
||||
// withThinking call can never linger on a disabled or non-effort thinking
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ describe('e2e: kimi adapter', () => {
|
|||
model: 'kimi-k2-turbo-preview',
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
thinking: { type: 'enabled', effort: 'high' },
|
||||
thinking: { type: 'enabled' },
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are helpful.' },
|
||||
{ role: 'user', content: 'Check the weather.' },
|
||||
|
|
|
|||
|
|
@ -679,7 +679,7 @@ describe('KimiChatProvider', () => {
|
|||
|
||||
expect(getGenerationState(provider)).toEqual({
|
||||
extra_body: {
|
||||
thinking: { type: 'enabled', effort: 'high' },
|
||||
thinking: { type: 'enabled' },
|
||||
},
|
||||
max_tokens: 512,
|
||||
});
|
||||
|
|
@ -716,7 +716,7 @@ describe('KimiChatProvider', () => {
|
|||
});
|
||||
|
||||
describe('with thinking', () => {
|
||||
it('model without support_efforts passes the requested effort through', async () => {
|
||||
it('model without support_efforts omits effort', async () => {
|
||||
const provider = createProvider().withThinking('high');
|
||||
const history: Message[] = [
|
||||
{ role: 'user', content: [{ type: 'text', text: 'Think' }], toolCalls: [] },
|
||||
|
|
@ -724,7 +724,7 @@ describe('KimiChatProvider', () => {
|
|||
const body = await captureRequestBody(provider, '', [], history);
|
||||
|
||||
expect(body['reasoning_effort']).toBeUndefined();
|
||||
expect(body['thinking']).toEqual({ type: 'enabled', effort: 'high' });
|
||||
expect(body['thinking']).toEqual({ type: 'enabled' });
|
||||
expect(body['extra_body']).toBeUndefined();
|
||||
});
|
||||
|
||||
|
|
@ -785,10 +785,13 @@ describe('KimiChatProvider', () => {
|
|||
expect(provider.withThinking('off').thinkingEffort).toBe('off');
|
||||
});
|
||||
|
||||
it('thinkingEffort reflects the requested effort when support_efforts is absent', () => {
|
||||
it("thinkingEffort falls back to 'on' when support_efforts is absent", () => {
|
||||
const provider = createProvider();
|
||||
expect(provider.withThinking('high').thinkingEffort).toBe('high');
|
||||
// Without declared efforts the wire object carries no `effort`, so the
|
||||
// getter reports boolean-thinking ("on") for any non-off effort.
|
||||
expect(provider.withThinking('high').thinkingEffort).toBe('on');
|
||||
expect(provider.withThinking('on').thinkingEffort).toBe('on');
|
||||
expect(provider.withThinking('off').thinkingEffort).toBe('off');
|
||||
});
|
||||
|
||||
it('replaces the previous thinking effort when called again', () => {
|
||||
|
|
@ -1338,7 +1341,7 @@ describe('KimiChatProvider', () => {
|
|||
.withThinking('high');
|
||||
|
||||
expect(getGenerationState(provider).extra_body).toEqual({
|
||||
thinking: { type: 'enabled', effort: 'high', keep: 'all' },
|
||||
thinking: { type: 'enabled', keep: 'all' },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1351,7 +1354,7 @@ describe('KimiChatProvider', () => {
|
|||
];
|
||||
const body = await captureRequestBody(provider, '', [], history);
|
||||
|
||||
expect(body['thinking']).toEqual({ type: 'enabled', effort: 'high' });
|
||||
expect(body['thinking']).toEqual({ type: 'enabled' });
|
||||
});
|
||||
|
||||
it('treats empty thinking patch as noop, preserving prior withThinking', async () => {
|
||||
|
|
@ -1361,7 +1364,7 @@ describe('KimiChatProvider', () => {
|
|||
];
|
||||
const body = await captureRequestBody(provider, '', [], history);
|
||||
|
||||
expect(body['thinking']).toEqual({ type: 'enabled', effort: 'high' });
|
||||
expect(body['thinking']).toEqual({ type: 'enabled' });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue