diff --git a/docs/en/configuration/env-vars.md b/docs/en/configuration/env-vars.md index 3d57e29a7..e084c326e 100644 --- a/docs/en/configuration/env-vars.md +++ b/docs/en/configuration/env-vars.md @@ -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 | diff --git a/docs/zh/configuration/env-vars.md b/docs/zh/configuration/env-vars.md index 130010a6a..19bce1e73 100644 --- a/docs/zh/configuration/env-vars.md +++ b/docs/zh/configuration/env-vars.md @@ -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` 表示禁用 | diff --git a/packages/agent-core/src/agent/config/index.ts b/packages/agent-core/src/agent/config/index.ts index 990f17232..b97623de5 100644 --- a/packages/agent-core/src/agent/config/index.ts +++ b/packages/agent-core/src/agent/config/index.ts @@ -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 { diff --git a/packages/agent-core/src/config/kimi-env-params.ts b/packages/agent-core/src/config/kimi-env-params.ts index b12dd79d1..446dd8aa7 100644 --- a/packages/agent-core/src/config/kimi-env-params.ts +++ b/packages/agent-core/src/config/kimi-env-params.ts @@ -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 diff --git a/packages/agent-core/test/agent/compaction/full.test.ts b/packages/agent-core/test/agent/compaction/full.test.ts index 0975c952d..fb2b8894e 100644 --- a/packages/agent-core/test/agent/compaction/full.test.ts +++ b/packages/agent-core/test/agent/compaction/full.test.ts @@ -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({ diff --git a/packages/agent-core/test/agent/config-state.test.ts b/packages/agent-core/test/agent/config-state.test.ts index ff15defb9..9ab9a01c6 100644 --- a/packages/agent-core/test/agent/config-state.test.ts +++ b/packages/agent-core/test/agent/config-state.test.ts @@ -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(); + } + }); }); diff --git a/packages/agent-core/test/config/kimi-env-params.test.ts b/packages/agent-core/test/config/kimi-env-params.test.ts index 723679bff..46ba04786 100644 --- a/packages/agent-core/test/config/kimi-env-params.test.ts +++ b/packages/agent-core/test/config/kimi-env-params.test.ts @@ -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); + }); +}); diff --git a/packages/kosong/src/providers/kimi.ts b/packages/kosong/src/providers/kimi.ts index 93bbc52e9..e3a5a05e9 100644 --- a/packages/kosong/src/providers/kimi.ts +++ b/packages/kosong/src/providers/kimi.ts @@ -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 diff --git a/packages/kosong/test/e2e/kimi-adapter-e2e.test.ts b/packages/kosong/test/e2e/kimi-adapter-e2e.test.ts index 20091e3c7..5c8018a6e 100644 --- a/packages/kosong/test/e2e/kimi-adapter-e2e.test.ts +++ b/packages/kosong/test/e2e/kimi-adapter-e2e.test.ts @@ -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.' }, diff --git a/packages/kosong/test/kimi.test.ts b/packages/kosong/test/kimi.test.ts index 7a5eb54e6..17bee33d4 100644 --- a/packages/kosong/test/kimi.test.ts +++ b/packages/kosong/test/kimi.test.ts @@ -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' }); }); });