mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
feat(agent-core): enable Preserved Thinking by default on the Anthropic provider (#1432)
* feat(agent-core): enable Preserved Thinking by default on the Anthropic provider Default thinking.keep to "all" for the Anthropic provider (Claude and Kimi in Anthropic-compatible mode) while Thinking is on, via a context_management clear_thinking_20251015 edit, mirroring the Kimi default. Reuses [thinking] keep and KIMI_MODEL_THINKING_KEEP (env > config > default "all"); off-values disable it. * feat(kosong): route Anthropic Preserved Thinking through the beta Messages API Force the beta endpoint (client.beta.messages.create) when thinking.keep is enabled, since clear_thinking_20251015 is only honored there. Also prepend clear_thinking to any existing context-management edits (for example clear_tool_uses) instead of replacing them, keeping it first as Anthropic requires when combining edits. * docs: clarify Anthropic beta endpoint and compaction keep behavior Note in code comments and bilingual docs that enabling Anthropic Preserved Thinking routes requests to the beta Messages API (client.beta.messages.create), with keep=off as the escape hatch back to the standard endpoint. Correct the resolveThinkingKeep comment to reflect that compaction shares ConfigState.provider and intentionally carries the same keep. * test(kosong): cover Anthropic beta endpoint (streaming and forced betaApi) Add a streaming beta-endpoint capture and a test that withThinkingKeep forces the beta endpoint even when constructed with betaApi: false, pinning down the documented behavior.
This commit is contained in:
parent
379bc57ef0
commit
25a655cf88
11 changed files with 365 additions and 13 deletions
5
.changeset/anthropic-preserved-thinking-by-default.md
Normal file
5
.changeset/anthropic-preserved-thinking-by-default.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": minor
|
||||
---
|
||||
|
||||
Preserve prior turns' thinking by default on the Anthropic provider (Claude and Kimi's Anthropic-compatible mode), matching the Kimi default. Disable with `[thinking] keep = "off"` or `KIMI_MODEL_THINKING_KEEP=off`.
|
||||
|
|
@ -169,7 +169,7 @@ You can also switch models temporarily without touching the config file — by s
|
|||
| --- | --- | --- | --- |
|
||||
| `enabled` | `boolean` | `true` | Whether Thinking is enabled by default for new sessions; set to `false` to force Thinking off |
|
||||
| `effort` | `string` | — | Thinking effort level (for example `low`, `medium`, `high`, `xhigh`, `max`); the levels actually available depend on the model's declared `support_efforts`, and unrecognized values are ignored by the provider |
|
||||
| `keep` | `string` | `"all"` | Moonshot Preserved Thinking passthrough (`thinking.keep`). `"all"` preserves previous turns' `reasoning_content`; set to an off-value (`false`/`0`/`no`/`off`/`none`/`null`) to disable. Overridden by `KIMI_MODEL_THINKING_KEEP`; applies to the `kimi` provider only, and only while Thinking is on |
|
||||
| `keep` | `string` | `"all"` | Preserved Thinking passthrough. On `kimi` it is sent as `thinking.keep`; on `anthropic` (Claude and Kimi's Anthropic-compatible mode) it is sent as a `context_management` `clear_thinking_20251015` edit (enabling keep routes Anthropic requests to the beta Messages API; an off-value disables keep and returns to the standard endpoint). `"all"` preserves prior turns' reasoning (`reasoning_content` / Anthropic thinking blocks); set to an off-value (`false`/`0`/`no`/`off`/`none`/`null`) to disable. Overridden by `KIMI_MODEL_THINKING_KEEP`; only injected while Thinking is on |
|
||||
|
||||
### Deprecated fields
|
||||
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ Switches that control the behavior of subsystems such as telemetry, background t
|
|||
| `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`); overrides `[thinking] keep` (which defaults to `"all"`); applies to the `kimi` provider only, and only while Thinking is on | A value the API accepts, e.g. `all`; an off-value (`false`/`0`/`no`/`off`/`none`/`null`) disables it |
|
||||
| `KIMI_MODEL_THINKING_KEEP` | Preserved-thinking passthrough; on `kimi` sent as `thinking.keep`, on `anthropic` (Claude and Kimi's Anthropic-compatible mode) sent as a `context_management` `clear_thinking_20251015` edit (enabling keep routes Anthropic requests to the beta Messages API); overrides `[thinking] keep` (which defaults to `"all"`); only injected while Thinking is on | A value the API accepts, e.g. `all`; an off-value (`false`/`0`/`no`/`off`/`none`/`null`) disables it |
|
||||
| `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 |
|
||||
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ display_name = "Kimi for Coding (custom)"
|
|||
| --- | --- | --- | --- |
|
||||
| `enabled` | `boolean` | `true` | 新会话是否默认开启 Thinking,设为 `false` 可强制关闭 |
|
||||
| `effort` | `string` | — | Thinking 强度(例如 `low`、`medium`、`high`、`xhigh`、`max`),实际可用等级取决于模型声明的 `support_efforts`,未识别的值会被供应商忽略 |
|
||||
| `keep` | `string` | `"all"` | Moonshot 保留思考透传(`thinking.keep`)。`"all"` 会保留历史轮次的 `reasoning_content`;传入关值(`false`/`0`/`no`/`off`/`none`/`null`)可禁用。可被 `KIMI_MODEL_THINKING_KEEP` 覆盖;仅对 `kimi` 供应商生效,且仅在 Thinking 开启时注入 |
|
||||
| `keep` | `string` | `"all"` | 保留思考透传。在 `kimi` 上以 `thinking.keep` 发送;在 `anthropic`(Claude 以及 Kimi 的 Anthropic 兼容模式)上以 `context_management` 的 `clear_thinking_20251015` 编辑发送(开启 keep 会让 Anthropic 请求走 beta Messages API;关值可禁用 keep 并回到标准端点)。`"all"` 会保留历史轮次的思考内容(`reasoning_content` / Anthropic thinking blocks);传入关值(`false`/`0`/`no`/`off`/`none`/`null`)可禁用。可被 `KIMI_MODEL_THINKING_KEEP` 覆盖;仅在 Thinking 开启时注入 |
|
||||
|
||||
### 已废弃字段
|
||||
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ kimi
|
|||
| `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`);覆盖 `[thinking] keep`(其默认值为 `"all"`);仅对 `kimi` 供应商生效,且仅在 Thinking 开启时注入 | API 接受的值,如 `all`;传入关值(`false`/`0`/`no`/`off`/`none`/`null`)可禁用 |
|
||||
| `KIMI_MODEL_THINKING_KEEP` | 保留思考透传;在 `kimi` 上以 `thinking.keep` 发送,在 `anthropic`(Claude 以及 Kimi 的 Anthropic 兼容模式)上以 `context_management` 的 `clear_thinking_20251015` 编辑发送(开启 keep 会让 Anthropic 请求走 beta Messages API);覆盖 `[thinking] keep`(其默认值为 `"all"`);仅在 Thinking 开启时注入 | API 接受的值,如 `all`;传入关值(`false`/`0`/`no`/`off`/`none`/`null`)可禁用 |
|
||||
| `KIMI_CODE_NO_AUTO_UPDATE` | 完全禁用更新预检——不检查、不后台安装、不提示。同时兼容旧名 `KIMI_CLI_NO_AUTO_UPDATE` | 真值:`1`/`true`/`yes`/`on` |
|
||||
| `KIMI_DISABLE_CRON` | 禁用定时任务工具(`CronCreate` 拒绝新计划,已有任务不触发) | `1` 表示禁用 |
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
} from '@moonshot-ai/kosong';
|
||||
|
||||
import {
|
||||
applyAnthropicThinkingKeep,
|
||||
applyKimiEnvSamplingParams,
|
||||
applyKimiEnvThinkingEffort,
|
||||
applyKimiEnvThinkingKeep,
|
||||
|
|
@ -122,16 +123,20 @@ export class ConfigState {
|
|||
// - 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: env KIMI_MODEL_THINKING_KEEP > config thinking.keep > default "all" (only while thinking is on)
|
||||
// - thinking.keep: env KIMI_MODEL_THINKING_KEEP > config thinking.keep > default "all"
|
||||
// (only while thinking is on). Drives Kimi's `thinking.keep` and, on the
|
||||
// Anthropic path, a `context_management` `clear_thinking_20251015` edit.
|
||||
const provider = createProvider(this.providerConfig).withThinking(this.thinkingEffort);
|
||||
const withSampling = applyKimiEnvSamplingParams(provider);
|
||||
const withEffort = applyKimiEnvThinkingEffort(withSampling, this.thinkingEffort);
|
||||
return applyKimiEnvThinkingKeep(
|
||||
const configKeep = this.agent.kimiConfig?.thinking?.keep;
|
||||
const withKimiKeep = applyKimiEnvThinkingKeep(
|
||||
withEffort,
|
||||
this.thinkingEffort,
|
||||
undefined,
|
||||
this.agent.kimiConfig?.thinking?.keep,
|
||||
configKeep,
|
||||
);
|
||||
return applyAnthropicThinkingKeep(withKimiKeep, this.thinkingEffort, undefined, configKeep);
|
||||
}
|
||||
|
||||
get model(): string {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
KimiChatProvider,
|
||||
type ThinkingEffort,
|
||||
} from '@moonshot-ai/kosong';
|
||||
import { AnthropicChatProvider } from '@moonshot-ai/kosong/providers/anthropic';
|
||||
|
||||
import { parseFloatEnv } from '#/config/resolve';
|
||||
|
||||
|
|
@ -77,12 +78,16 @@ function parseKeepValue(raw: string | undefined): KeepResolution {
|
|||
}
|
||||
|
||||
/**
|
||||
* Resolve the Moonshot Preserved Thinking passthrough (`thinking.keep`) with
|
||||
* precedence env (`KIMI_MODEL_THINKING_KEEP`) > config (`thinking.keep`) >
|
||||
* default `"all"`. Only meaningful while thinking is on — otherwise the API
|
||||
* would receive a `thinking.keep` with no accompanying `thinking.type` it
|
||||
* honors. (Compaction uses a raw provider with thinking off, so it correctly
|
||||
* resolves to `undefined`.)
|
||||
* Resolve the Preserved Thinking passthrough (Kimi `thinking.keep` / Anthropic
|
||||
* `context_management` `clear_thinking_20251015`) with precedence env
|
||||
* (`KIMI_MODEL_THINKING_KEEP`) > config (`thinking.keep`) > default `"all"`.
|
||||
* Only meaningful while thinking is on — otherwise the API would receive a keep
|
||||
* directive with no accompanying `thinking.type` it honors, so it resolves to
|
||||
* `undefined`. Applied via `ConfigState.provider`, which is shared by the main
|
||||
* loop AND full-history compaction, so compaction intentionally carries the
|
||||
* same keep (and, for Anthropic, the beta endpoint) when thinking is on;
|
||||
* `keep:"all"` prunes nothing and a consistent request shape maximizes KV-cache
|
||||
* reuse.
|
||||
*
|
||||
* Returns `undefined` when Preserved Thinking should be disabled.
|
||||
*/
|
||||
|
|
@ -115,3 +120,24 @@ export function applyKimiEnvThinkingKeep(
|
|||
if (keep === undefined) return provider;
|
||||
return provider.withExtraBody({ thinking: { keep } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the Anthropic equivalent of Preserved Thinking — a `context_management`
|
||||
* `clear_thinking_20251015` edit carrying `keep` — to an Anthropic chat
|
||||
* provider. See `resolveThinkingKeep` for precedence. Non-Anthropic providers
|
||||
* are returned unchanged. Applies to every Anthropic provider (Claude and
|
||||
* Kimi's Anthropic-compatible mode) while thinking is on; `keep: "all"` tells
|
||||
* the server to retain all prior thinking blocks (prune none), mirroring Kimi's
|
||||
* `thinking.keep`.
|
||||
*/
|
||||
export function applyAnthropicThinkingKeep(
|
||||
provider: ChatProvider,
|
||||
thinkingEffort: ThinkingEffort,
|
||||
env: Env = process.env,
|
||||
configKeep?: string,
|
||||
): ChatProvider {
|
||||
if (!(provider instanceof AnthropicChatProvider)) return provider;
|
||||
const keep = resolveThinkingKeep(env, configKeep, thinkingEffort);
|
||||
if (keep === undefined) return provider;
|
||||
return provider.withThinkingKeep(keep);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -398,4 +398,58 @@ describe('ConfigState.provider applies global KIMI_MODEL_* request config', () =
|
|||
vi.unstubAllEnvs();
|
||||
}
|
||||
});
|
||||
|
||||
function anthropicAgentWithThinkingKeep(keep: string | undefined) {
|
||||
const config: KimiConfig = {
|
||||
providers: { anthropic: { type: 'anthropic', apiKey: 'test-key' } },
|
||||
models: {
|
||||
'claude-sonnet-4-6': {
|
||||
provider: 'anthropic',
|
||||
model: 'claude-sonnet-4-6',
|
||||
maxContextSize: 200_000,
|
||||
capabilities: ['thinking', 'tool_use'],
|
||||
},
|
||||
},
|
||||
...(keep !== undefined ? { thinking: { keep } } : {}),
|
||||
};
|
||||
return testAgent({
|
||||
initialConfig: config,
|
||||
providerManager: new ProviderManager({ config }),
|
||||
});
|
||||
}
|
||||
|
||||
it('injects context_management clear_thinking keep into config.provider for anthropic when thinking is on', () => {
|
||||
vi.stubEnv('KIMI_MODEL_THINKING_KEEP', 'all');
|
||||
try {
|
||||
const ctx = anthropicAgentWithThinkingKeep(undefined);
|
||||
ctx.agent.config.update({ modelAlias: 'claude-sonnet-4-6', thinkingEffort: 'high' });
|
||||
|
||||
const provider = ctx.agent.config.provider;
|
||||
const gen = Reflect.get(provider as object, '_generationKwargs') as {
|
||||
contextManagement?: { edits: Array<{ type: string; keep?: string }> };
|
||||
betaFeatures?: string[];
|
||||
};
|
||||
expect(gen.contextManagement).toEqual({
|
||||
edits: [{ type: 'clear_thinking_20251015', keep: 'all' }],
|
||||
});
|
||||
expect(gen.betaFeatures).toContain('context-management-2025-06-27');
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
});
|
||||
|
||||
it('does NOT inject context_management for anthropic when thinking is off', () => {
|
||||
vi.stubEnv('KIMI_MODEL_THINKING_KEEP', 'all');
|
||||
try {
|
||||
const ctx = anthropicAgentWithThinkingKeep(undefined);
|
||||
ctx.agent.config.update({ modelAlias: 'claude-sonnet-4-6', thinkingEffort: 'off' });
|
||||
|
||||
const gen = Reflect.get(ctx.agent.config.provider as object, '_generationKwargs') as {
|
||||
contextManagement?: unknown;
|
||||
};
|
||||
expect(gen.contextManagement).toBeUndefined();
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { type ChatProvider, KimiChatProvider } from '@moonshot-ai/kosong';
|
||||
import { AnthropicChatProvider } from '@moonshot-ai/kosong/providers/anthropic';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
applyAnthropicThinkingKeep,
|
||||
applyKimiEnvSamplingParams,
|
||||
applyKimiEnvThinkingEffort,
|
||||
applyKimiEnvThinkingKeep,
|
||||
|
|
@ -151,3 +153,94 @@ describe('applyKimiEnvThinkingEffort', () => {
|
|||
).toBe(stub);
|
||||
});
|
||||
});
|
||||
|
||||
function anthropic(): AnthropicChatProvider {
|
||||
return new AnthropicChatProvider({ model: 'claude-sonnet-4-6', apiKey: 'k' });
|
||||
}
|
||||
|
||||
interface AnthropicKeepState {
|
||||
contextManagement?: { edits: Array<{ type: string; keep?: string }> };
|
||||
betaFeatures?: string[];
|
||||
}
|
||||
|
||||
function anthropicState(provider: ChatProvider): AnthropicKeepState {
|
||||
return Reflect.get(provider as object, '_generationKwargs') as AnthropicKeepState;
|
||||
}
|
||||
|
||||
describe('applyAnthropicThinkingKeep', () => {
|
||||
it('injects context_management keep="all" by default when thinking is on', () => {
|
||||
const out = applyAnthropicThinkingKeep(anthropic(), 'high', {});
|
||||
expect(anthropicState(out).contextManagement).toEqual({
|
||||
edits: [{ type: 'clear_thinking_20251015', keep: 'all' }],
|
||||
});
|
||||
expect(anthropicState(out).betaFeatures).toContain('context-management-2025-06-27');
|
||||
});
|
||||
|
||||
it('injects keep from env when thinking is on', () => {
|
||||
const out = applyAnthropicThinkingKeep(anthropic(), 'high', { KIMI_MODEL_THINKING_KEEP: 'all' });
|
||||
expect(anthropicState(out).contextManagement?.edits[0]?.keep).toBe('all');
|
||||
});
|
||||
|
||||
it('injects keep from config when env is unset', () => {
|
||||
const out = applyAnthropicThinkingKeep(anthropic(), 'high', {}, 'all');
|
||||
expect(anthropicState(out).contextManagement?.edits[0]?.keep).toBe('all');
|
||||
});
|
||||
|
||||
it('env takes precedence over config', () => {
|
||||
const out = applyAnthropicThinkingKeep(
|
||||
anthropic(),
|
||||
'high',
|
||||
{ KIMI_MODEL_THINKING_KEEP: 'all' },
|
||||
'off',
|
||||
);
|
||||
expect(anthropicState(out).contextManagement?.edits[0]?.keep).toBe('all');
|
||||
});
|
||||
|
||||
it.each(['off', 'false', '0', 'no', 'none', 'null', 'OFF', 'None'])(
|
||||
'env off-value %s disables keep even when config enables it',
|
||||
(off) => {
|
||||
const out = applyAnthropicThinkingKeep(
|
||||
anthropic(),
|
||||
'high',
|
||||
{ KIMI_MODEL_THINKING_KEEP: off },
|
||||
'all',
|
||||
);
|
||||
expect(anthropicState(out).contextManagement).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['off', 'none', 'null'])('config off-value %s disables keep by default', (off) => {
|
||||
const out = applyAnthropicThinkingKeep(anthropic(), 'high', {}, off);
|
||||
expect(anthropicState(out).contextManagement).toBeUndefined();
|
||||
});
|
||||
|
||||
it('blank env falls through to config', () => {
|
||||
const out = applyAnthropicThinkingKeep(
|
||||
anthropic(),
|
||||
'high',
|
||||
{ KIMI_MODEL_THINKING_KEEP: ' ' },
|
||||
'off',
|
||||
);
|
||||
expect(anthropicState(out).contextManagement).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does NOT inject context_management when thinking is off', () => {
|
||||
const out = applyAnthropicThinkingKeep(anthropic(), 'off', { KIMI_MODEL_THINKING_KEEP: 'all' });
|
||||
expect(anthropicState(out).contextManagement).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not duplicate the context-management beta on repeated calls', () => {
|
||||
const out = applyAnthropicThinkingKeep(
|
||||
applyAnthropicThinkingKeep(anthropic(), 'high', {}),
|
||||
'high',
|
||||
{},
|
||||
);
|
||||
const betas = anthropicState(out).betaFeatures ?? [];
|
||||
expect(betas.filter((b) => b === 'context-management-2025-06-27')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('leaves non-anthropic providers untouched', () => {
|
||||
const stub = { name: 'stub' } as unknown as ChatProvider;
|
||||
expect(applyAnthropicThinkingKeep(stub, 'high', { KIMI_MODEL_THINKING_KEEP: 'all' })).toBe(stub);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -114,6 +114,17 @@ interface AnthropicGenerationKwargs {
|
|||
thinking?: MessageCreateParams['thinking'] | undefined;
|
||||
output_config?: MessageCreateParams['output_config'] | undefined;
|
||||
betaFeatures?: string[] | undefined;
|
||||
contextManagement?: AnthropicContextManagement | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Anthropic beta context-management payload (`context-management-2025-06-27`).
|
||||
* Only the `clear_thinking_20251015` edit is emitted today, with `keep`
|
||||
* forwarded as a string (`"all"`); the `{ type, value }` turn-count form is
|
||||
* not used because the shared `[thinking] keep` config is a string.
|
||||
*/
|
||||
interface AnthropicContextManagement {
|
||||
edits: Array<{ type: string; keep?: unknown }>;
|
||||
}
|
||||
|
||||
// Anthropic's native effort values. `ThinkingEffort` is an open string, so after
|
||||
|
|
@ -122,6 +133,8 @@ interface AnthropicGenerationKwargs {
|
|||
type AnthropicEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
||||
|
||||
const INTERLEAVED_THINKING_BETA = 'interleaved-thinking-2025-05-14';
|
||||
const CONTEXT_MANAGEMENT_BETA = 'context-management-2025-06-27';
|
||||
const CLEAR_THINKING_EDIT = 'clear_thinking_20251015';
|
||||
const OPUS_VERSION_RE = /opus[.-](\d+)[.-](\d{1,2})(?!\d)/;
|
||||
const ADAPTIVE_MIN_VERSION = { major: 4, minor: 6 } as const;
|
||||
const ANTHROPIC_TOOL_CALL_ID_POLICY: ToolCallIdPolicy = {
|
||||
|
|
@ -1083,6 +1096,9 @@ export class AnthropicChatProvider implements ChatProvider {
|
|||
if (this._generationKwargs.output_config !== undefined) {
|
||||
kwargs['output_config'] = this._generationKwargs.output_config;
|
||||
}
|
||||
if (this._generationKwargs.contextManagement !== undefined) {
|
||||
kwargs['context_management'] = this._generationKwargs.contextManagement;
|
||||
}
|
||||
|
||||
// Build the beta feature list. On the standard Messages API these travel
|
||||
// via the `anthropic-beta` header; on the beta Messages API (`betaApi`) the
|
||||
|
|
@ -1289,6 +1305,35 @@ export class AnthropicChatProvider implements ChatProvider {
|
|||
return clone;
|
||||
}
|
||||
|
||||
withThinkingKeep(keep: string): AnthropicChatProvider {
|
||||
const current = this._generationKwargs.betaFeatures ?? [];
|
||||
const betaFeatures = current.includes(CONTEXT_MANAGEMENT_BETA)
|
||||
? current
|
||||
: [...current, CONTEXT_MANAGEMENT_BETA];
|
||||
// Preserve any existing context-management edits (e.g. clear_tool_uses) and
|
||||
// keep clear_thinking first, as Anthropic requires when combining edits. Drop
|
||||
// a previous clear_thinking edit so re-applying stays idempotent.
|
||||
const existingEdits = this._generationKwargs.contextManagement?.edits ?? [];
|
||||
const edits = [
|
||||
{ type: CLEAR_THINKING_EDIT, keep },
|
||||
...existingEdits.filter((edit) => edit.type !== CLEAR_THINKING_EDIT),
|
||||
];
|
||||
const clone = this._withGenerationKwargs({
|
||||
contextManagement: { edits },
|
||||
betaFeatures,
|
||||
});
|
||||
// clear_thinking_20251015 is honored only on the beta Messages API
|
||||
// (client.beta.messages.create), so enabling keep forces the beta endpoint
|
||||
// here even when the provider was constructed with betaApi: false. Setting
|
||||
// `[thinking] keep` to an off-value (or KIMI_MODEL_THINKING_KEEP=off) is the
|
||||
// escape hatch that disables keep and returns requests to the standard
|
||||
// endpoint. This also routes adaptive models (whose withThinking would
|
||||
// otherwise drop the interleaved-thinking beta and leave betaFeatures empty)
|
||||
// onto the beta endpoint with a body `betas=[context-management-...]`.
|
||||
clone._betaApi = true;
|
||||
return clone;
|
||||
}
|
||||
|
||||
withGenerationKwargs(kwargs: Partial<AnthropicGenerationKwargs>): AnthropicChatProvider {
|
||||
return this._withGenerationKwargs(kwargs);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ type AnthropicGenerationState = {
|
|||
| undefined;
|
||||
output_config?: { effort: string } | undefined;
|
||||
betaFeatures?: string[] | undefined;
|
||||
contextManagement?: { edits: Array<{ type: string; keep?: unknown }> } | undefined;
|
||||
};
|
||||
|
||||
function getGenerationState(provider: AnthropicChatProvider): AnthropicGenerationState {
|
||||
|
|
@ -218,6 +219,129 @@ describe('betaApi', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('withThinkingKeep (context_management)', () => {
|
||||
const history: Message[] = [
|
||||
{ role: 'user', content: [{ type: 'text', text: 'Hi' }], toolCalls: [] },
|
||||
];
|
||||
|
||||
it('forces the beta endpoint and emits context_management clear_thinking keep with the context-management beta', async () => {
|
||||
// betaApi is left at its default (false); withThinkingKeep must force it on.
|
||||
const provider = createProvider().withThinkingKeep('all');
|
||||
const body = await captureBetaRequestBody(provider, '', [], history);
|
||||
|
||||
expect(body['context_management']).toEqual({
|
||||
edits: [{ type: 'clear_thinking_20251015', keep: 'all' }],
|
||||
});
|
||||
expect(body['betas']).toContain('context-management-2025-06-27');
|
||||
expect(body['betas']).toContain('interleaved-thinking-2025-05-14');
|
||||
const headers = body['_extra_headers'] as Record<string, string> | undefined;
|
||||
expect(headers?.['anthropic-beta']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('prepends clear_thinking before existing context-management edits and keeps them', () => {
|
||||
const provider = createProvider()
|
||||
.withGenerationKwargs({
|
||||
contextManagement: {
|
||||
edits: [{ type: 'clear_tool_uses_20250919', keep: { type: 'tool_uses', value: 2 } }],
|
||||
},
|
||||
})
|
||||
.withThinkingKeep('all');
|
||||
const state = getGenerationState(provider);
|
||||
expect(state.contextManagement).toEqual({
|
||||
edits: [
|
||||
{ type: 'clear_thinking_20251015', keep: 'all' },
|
||||
{ type: 'clear_tool_uses_20250919', keep: { type: 'tool_uses', value: 2 } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('emits no context_management and stays off the beta endpoint when withThinkingKeep is not applied', async () => {
|
||||
const body = await captureRequestBody(createProvider(), '', [], history);
|
||||
expect(body['context_management']).toBeUndefined();
|
||||
const headers = body['_extra_headers'] as Record<string, string> | undefined;
|
||||
expect(headers?.['anthropic-beta']).not.toContain('context-management-2025-06-27');
|
||||
});
|
||||
|
||||
it('does not duplicate the context-management beta or the clear_thinking edit across repeated calls', () => {
|
||||
const provider = createProvider().withThinkingKeep('all').withThinkingKeep('all');
|
||||
const state = getGenerationState(provider);
|
||||
const betas = state.betaFeatures ?? [];
|
||||
expect(betas.filter((b) => b === 'context-management-2025-06-27')).toHaveLength(1);
|
||||
expect(state.contextManagement).toEqual({
|
||||
edits: [{ type: 'clear_thinking_20251015', keep: 'all' }],
|
||||
});
|
||||
});
|
||||
|
||||
// Capture a streaming (stream: true) beta-endpoint request by mocking
|
||||
// client.beta.messages.create to return a minimal valid stream.
|
||||
async function captureBetaStreamBody(
|
||||
provider: AnthropicChatProvider,
|
||||
history: Message[],
|
||||
): Promise<Record<string, unknown>> {
|
||||
let capturedParams: Record<string, unknown> | undefined;
|
||||
let capturedOptions: Record<string, unknown> | undefined;
|
||||
(provider as any)._client.beta.messages.create = vi
|
||||
.fn()
|
||||
.mockImplementation((params: unknown, options?: unknown) => {
|
||||
capturedParams = params as Record<string, unknown>;
|
||||
capturedOptions = options as Record<string, unknown> | undefined;
|
||||
return Promise.resolve(
|
||||
mockStream([
|
||||
{
|
||||
type: 'message_start',
|
||||
message: { id: 'm', usage: { input_tokens: 1, output_tokens: 0 } },
|
||||
},
|
||||
{ type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } },
|
||||
{ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'ok' } },
|
||||
{ type: 'content_block_stop', index: 0 },
|
||||
{
|
||||
type: 'message_delta',
|
||||
delta: { stop_reason: 'end_turn' },
|
||||
usage: { output_tokens: 1 },
|
||||
},
|
||||
{ type: 'message_stop' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
const stream = await provider.generate('', [], history);
|
||||
for await (const part of stream) void part;
|
||||
if (capturedParams === undefined) {
|
||||
throw new Error('Expected provider.generate() to call beta.messages.create');
|
||||
}
|
||||
const result = { ...capturedParams };
|
||||
if (capturedOptions !== undefined && capturedOptions['headers'] !== undefined) {
|
||||
result['_extra_headers'] = capturedOptions['headers'];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
it('emits context_management on the streaming beta endpoint too', async () => {
|
||||
const provider = createStreamProvider().withThinkingKeep('all');
|
||||
const body = await captureBetaStreamBody(provider, history);
|
||||
expect(body['context_management']).toEqual({
|
||||
edits: [{ type: 'clear_thinking_20251015', keep: 'all' }],
|
||||
});
|
||||
expect(body['betas']).toContain('context-management-2025-06-27');
|
||||
const headers = body['_extra_headers'] as Record<string, string> | undefined;
|
||||
expect(headers?.['anthropic-beta']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('forces the beta endpoint even when constructed with betaApi: false', async () => {
|
||||
const provider = new AnthropicChatProvider({
|
||||
model: 'kimi-for-coding',
|
||||
apiKey: 'test-key',
|
||||
defaultMaxTokens: 1024,
|
||||
stream: false,
|
||||
betaApi: false,
|
||||
}).withThinkingKeep('all');
|
||||
const body = await captureBetaRequestBody(provider, '', [], history);
|
||||
expect(body['context_management']).toEqual({
|
||||
edits: [{ type: 'clear_thinking_20251015', keep: 'all' }],
|
||||
});
|
||||
expect(body['betas']).toContain('context-management-2025-06-27');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AnthropicChatProvider', () => {
|
||||
it('does not read ANTHROPIC_API_KEY from process.env inside the adapter', () => {
|
||||
const previousApiKey = process.env['ANTHROPIC_API_KEY'];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue