Skip to content

Commit bfbd522

Browse files
authored
fix: widen Kimi completion budget (#17)
1 parent 2004aed commit bfbd522

7 files changed

Lines changed: 95 additions & 108 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@moonshot-ai/agent-core": patch
3+
"@moonshot-ai/kimi-code": patch
4+
---
5+
6+
Let Kimi requests use the remaining context window for completion tokens by default while keeping explicit environment limits as hard caps.

docs/en/configuration/env-vars.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ When neither `KIMI_CODE_OAUTH_HOST` nor `KIMI_OAUTH_HOST` is set, the OAuth auth
7474
| `KIMI_DISABLE_TELEMETRY` | Disable telemetry reporting | `1`, `true`, `t`, `yes`, `y` (case-insensitive) |
7575
| `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` | Override `[background].keep_alive_on_exit`, controlling whether still-running background tasks are kept when the session closes | True values: `1`, `true`, `yes`, `on`; false values: `0`, `false`, `no`, `off`; when unset, reads `config.toml`, then falls back to `true` |
7676
| `KIMI_SHELL_PATH` | Override the absolute path to Git Bash (`bash.exe`) on Windows; only needed when auto-detection fails on Windows | None |
77-
| `KIMI_MODEL_MAX_COMPLETION_TOKENS` | Desired budget for `max_completion_tokens` in a single-step LLM request (the actual value is further clamped by the context window and input size); set to `0` or a negative value to disable clamping entirely. **Currently effective only for providers of type `kimi`**; for Anthropic and other providers, use `[models.<alias>].max_output_size` instead (see [Config files](./config-files.md#models)) | Defaults to 32000, influenced by `loop_control.reserved_context_size` |
77+
| `KIMI_MODEL_MAX_COMPLETION_TOKENS` | Explicit hard cap for `max_completion_tokens` in a single-step LLM request. When unset, Kimi Code uses the safe remaining context window for models with a known context size. Set to `0` or a negative value to disable clamping entirely. **Currently effective only for providers of type `kimi`**; for Anthropic and other providers, use `[models.<alias>].max_output_size` instead (see [Config files](./config-files.md#models)) | Unset: computed from remaining context; unknown context falls back to `loop_control.reserved_context_size`, then 32000 |
7878

7979
For example, to disable telemetry on a shared host:
8080

docs/zh/configuration/env-vars.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ OAuth 流程默认连接 Kimi 官方的认证与托管端点,下列变量可
7474
| `KIMI_DISABLE_TELEMETRY` | 关闭遥测上报 | `1``true``t``yes``y`(不区分大小写) |
7575
| `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` | 覆盖 `[background].keep_alive_on_exit`,控制会话关闭时是否保留仍在运行的后台任务 | 真值:`1``true``yes``on`;假值:`0``false``no``off`;未设置时读取 `config.toml`,再回退到 `true` |
7676
| `KIMI_SHELL_PATH` | 覆盖 Windows 上 Git Bash (`bash.exe`) 的绝对路径,仅在 Windows 自动探测失败时需要 ||
77-
| `KIMI_MODEL_MAX_COMPLETION_TOKENS` | 单步 LLM 请求 `max_completion_tokens` 的期望预算(实际值按上下文窗口与输入大小再做 clamp);设为 `0` 或负数则完全禁用 clamp。**目前只对 `kimi` 类型的供应商生效**;Anthropic 等其它供应商请改用 `[models.<alias>].max_output_size`(详见 [配置文件](./config-files.md#models)| 默认 32000,受 `loop_control.reserved_context_size` 影响 |
77+
| `KIMI_MODEL_MAX_COMPLETION_TOKENS` | 单步 LLM 请求 `max_completion_tokens` 的显式硬上限。未设置时,对于已知上下文窗口的模型,Kimi Code 会使用安全的剩余上下文窗口;设为 `0` 或负数则完全禁用 clamp。**目前只对 `kimi` 类型的供应商生效**;Anthropic 等其它供应商请改用 `[models.<alias>].max_output_size`(详见 [配置文件](./config-files.md#models)| 未设置:按剩余上下文计算;未知上下文窗口时回退到 `loop_control.reserved_context_size`,再回退到 32000 |
7878

7979
例如在共享主机上禁用遥测:
8080

packages/agent-core/src/agent/turn/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ export class TurnFlow {
366366
const model = this.agent.config.model;
367367
const provider = this.agent.config.provider.withThinking(this.agent.config.thinkingLevel);
368368
const loopControl = this.agent.providerManager?.config.loopControl;
369-
const completionBudget = resolveCompletionBudget({
369+
const completionBudgetConfig = resolveCompletionBudget({
370370
reservedContextSize: loopControl?.reservedContextSize,
371371
});
372372

@@ -380,7 +380,7 @@ export class TurnFlow {
380380
systemPrompt: this.agent.config.systemPrompt,
381381
capability: this.agent.config.modelCapabilities,
382382
generate: this.agent.generate,
383-
completionBudget,
383+
completionBudgetConfig,
384384
}),
385385
buildMessages: () => this.agent.context.messages,
386386
dispatchEvent: this.buildDispatchEvent(turnId),

packages/agent-core/src/agent/turn/kosong-llm.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
import type { LLM, LLMChatParams, LLMChatResponse, LLMRequestLogContext } from '../../loop';
3333
import {
3434
applyCompletionBudget,
35-
type CompletionBudget,
35+
type CompletionBudgetConfig,
3636
} from '../../utils/completion-budget';
3737

3838
export const GENERATE_REQUEST_LOG_CONTEXT = '__kimiRequestLogContext';
@@ -56,12 +56,10 @@ export interface KosongLLMConfig {
5656
*/
5757
readonly generate?: GenerateFn | undefined;
5858
/**
59-
* Per-request completion-token budget. When set, each `chat()` call
60-
* clones the configured provider with a clamped `max_completion_tokens`
61-
* derived from the current input size and model context window. The
62-
* clone is local to the call and never replaces `this.provider`.
59+
* Completion budget config resolved from agent/provider settings. The
60+
* final cap is computed per request from the current messages and tools.
6361
*/
64-
readonly completionBudget?: CompletionBudget | undefined;
62+
readonly completionBudgetConfig?: CompletionBudgetConfig | undefined;
6563
}
6664

6765
export class KosongLLM implements LLM {
@@ -71,15 +69,15 @@ export class KosongLLM implements LLM {
7169

7270
private readonly provider: ChatProvider;
7371
private readonly generate: GenerateFn;
74-
private readonly completionBudget: CompletionBudget | undefined;
72+
private readonly completionBudgetConfig: CompletionBudgetConfig | undefined;
7573

7674
constructor(config: KosongLLMConfig) {
7775
this.provider = config.provider;
7876
this.modelName = config.modelName;
7977
this.systemPrompt = config.systemPrompt;
8078
this.capability = config.capability;
8179
this.generate = config.generate ?? kosongGenerate;
82-
this.completionBudget = config.completionBudget;
80+
this.completionBudgetConfig = config.completionBudgetConfig;
8381
}
8482

8583
async chat(params: LLMChatParams): Promise<LLMChatResponse> {
@@ -98,7 +96,7 @@ export class KosongLLM implements LLM {
9896
// context can still slip past the limit.
9997
const effectiveProvider = applyCompletionBudget({
10098
provider: this.provider,
101-
budget: this.completionBudget,
99+
budget: this.completionBudgetConfig,
102100
capability: this.capability,
103101
messages: params.messages,
104102
systemPrompt: this.systemPrompt,

packages/agent-core/src/utils/completion-budget.ts

Lines changed: 34 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -11,61 +11,41 @@ import {
1111
estimateTokensForTools,
1212
} from './tokens';
1313

14-
/**
15-
* Desired completion-token budget for the next LLM step.
16-
*
17-
* The budget is a request, not a guarantee: it is clamped against the
18-
* current input size and the model's context window before being applied
19-
* to the provider. This avoids two failure modes for Kimi reasoning
20-
* models:
21-
* 1. A small cap can return HTTP 200 with empty `content` because the
22-
* whole budget was spent on `reasoning_content`.
23-
* 2. A large cap may exceed the remaining context window and trigger
24-
* `Invalid request: Your request exceeded model token limit`.
25-
*/
26-
export interface CompletionBudget {
27-
/** Desired completion budget when the model context window allows it. */
28-
readonly desired: number;
29-
/**
30-
* Safety margin reserved between current input and the context limit,
31-
* to absorb tokenizer estimation error and provider-side overhead.
32-
*/
33-
readonly safetyMargin?: number | undefined;
14+
/** Completion-token budget for the next LLM request. */
15+
export interface CompletionBudgetConfig {
16+
/** Explicit user-configured maximum. */
17+
readonly hardCap?: number;
18+
/** Conservative cap for providers/models whose context window is unknown. */
19+
readonly fallback?: number;
20+
/** Tokens kept out of the output budget to absorb estimation drift. */
21+
readonly safetyMargin?: number;
3422
}
3523

3624
const MIN_FLOOR = 1;
3725
const DEFAULT_SAFETY_MARGIN = 1024;
38-
const DEFAULT_DESIRED_BUDGET = 32000;
26+
const DEFAULT_UNKNOWN_CONTEXT_FALLBACK = 32000;
3927

4028
/**
41-
* Resolve the completion budget for a turn from configuration and Kimi
42-
* environment variables.
43-
*
44-
* Priority (first wins): `KIMI_MODEL_MAX_COMPLETION_TOKENS`,
45-
* `KIMI_MODEL_MAX_TOKENS` (legacy alias), `reservedContextSize`,
46-
* `DEFAULT_DESIRED_BUDGET` (32000, preserves pre-PR-2332 behavior).
47-
*
48-
* Operators can opt out of clamping entirely by setting the env var to
49-
* `0` or a negative integer; in that case this function returns
50-
* `undefined`, which `applyCompletionBudget` treats as a no-op.
29+
* Resolve configured completion budget. Env values are explicit hard caps;
30+
* non-positive env values disable clamping.
5131
*/
5232
export function resolveCompletionBudget(args: {
53-
readonly reservedContextSize?: number | undefined;
54-
readonly env?: NodeJS.ProcessEnv | undefined;
55-
}): CompletionBudget | undefined {
33+
readonly reservedContextSize?: number;
34+
readonly env?: NodeJS.ProcessEnv;
35+
}): CompletionBudgetConfig | undefined {
5636
const env = args.env ?? process.env;
5737
const fromNew = parseEnvBudget(env['KIMI_MODEL_MAX_COMPLETION_TOKENS']);
5838
if (fromNew !== 'absent') {
59-
return fromNew === 'disabled' ? undefined : { desired: fromNew };
39+
return fromNew === 'disabled' ? undefined : { hardCap: fromNew };
6040
}
6141
const fromLegacy = parseEnvBudget(env['KIMI_MODEL_MAX_TOKENS']);
6242
if (fromLegacy !== 'absent') {
63-
return fromLegacy === 'disabled' ? undefined : { desired: fromLegacy };
43+
return fromLegacy === 'disabled' ? undefined : { hardCap: fromLegacy };
6444
}
6545
if (args.reservedContextSize !== undefined && args.reservedContextSize > 0) {
66-
return { desired: args.reservedContextSize };
46+
return { fallback: args.reservedContextSize };
6747
}
68-
return { desired: DEFAULT_DESIRED_BUDGET };
48+
return { fallback: DEFAULT_UNKNOWN_CONTEXT_FALLBACK };
6949
}
7050

7151
type EnvBudget = number | 'disabled' | 'absent';
@@ -79,37 +59,23 @@ function parseEnvBudget(raw: string | undefined): EnvBudget {
7959
}
8060

8161
/**
82-
* Compute the effective `max_completion_tokens` cap for the next request.
83-
*
84-
* cap = clamp(desired, MIN_FLOOR, max_context_tokens - input - safetyMargin)
85-
*
86-
* `input` accounts for everything the provider will actually serialize:
87-
* the conversation history, the system prompt, and the tool schemas.
88-
* Counting only `messages` underestimates by enough to push a near-limit
89-
* request past the model context window.
90-
*
91-
* When the model context size is unknown, the desired value is returned
92-
* unchanged (floored at `MIN_FLOOR`).
93-
*
94-
* When the remaining window is non-positive (input already at or above
95-
* the limit), `MIN_FLOOR` is returned — we can't honor a meaningful cap
96-
* and the API will surface the overflow on its own.
97-
*
98-
* Note: the floor never exceeds `remaining`, so a near-full context
99-
* cannot be pushed past the limit by `MIN_FLOOR` itself.
62+
* Compute the effective `max_completion_tokens` cap. Known-context requests
63+
* use the remaining window unless a hard cap is configured.
10064
*/
10165
export function computeCompletionBudgetCap(args: {
102-
readonly budget: CompletionBudget;
66+
readonly budget: CompletionBudgetConfig;
10367
readonly capability: ModelCapability | undefined;
10468
readonly messages: readonly Message[];
105-
readonly systemPrompt?: string | undefined;
106-
readonly tools?: readonly Tool[] | undefined;
69+
readonly systemPrompt?: string;
70+
readonly tools?: readonly Tool[];
10771
}): number {
108-
const desired = args.budget.desired;
10972
const safetyMargin = args.budget.safetyMargin ?? DEFAULT_SAFETY_MARGIN;
11073
const maxCtx = args.capability?.max_context_tokens ?? 0;
11174
if (maxCtx <= 0) {
112-
return Math.max(MIN_FLOOR, desired);
75+
return Math.max(
76+
MIN_FLOOR,
77+
args.budget.hardCap ?? args.budget.fallback ?? DEFAULT_UNKNOWN_CONTEXT_FALLBACK,
78+
);
11379
}
11480
const input =
11581
estimateTokensForMessages([...args.messages]) +
@@ -119,7 +85,10 @@ export function computeCompletionBudgetCap(args: {
11985
if (remaining <= 0) {
12086
return MIN_FLOOR;
12187
}
122-
return Math.max(MIN_FLOOR, Math.min(desired, remaining));
88+
if (args.budget.hardCap === undefined) {
89+
return Math.max(MIN_FLOOR, remaining);
90+
}
91+
return Math.max(MIN_FLOOR, Math.min(args.budget.hardCap, remaining));
12392
}
12493

12594
/**
@@ -134,11 +103,11 @@ export function computeCompletionBudgetCap(args: {
134103
*/
135104
export function applyCompletionBudget(args: {
136105
readonly provider: ChatProvider;
137-
readonly budget: CompletionBudget | undefined;
106+
readonly budget: CompletionBudgetConfig | undefined;
138107
readonly capability: ModelCapability | undefined;
139108
readonly messages: readonly Message[];
140-
readonly systemPrompt?: string | undefined;
141-
readonly tools?: readonly Tool[] | undefined;
109+
readonly systemPrompt?: string;
110+
readonly tools?: readonly Tool[];
142111
}): ChatProvider {
143112
if (args.budget === undefined) return args.provider;
144113
if (args.provider.withMaxCompletionTokens === undefined) return args.provider;

0 commit comments

Comments
 (0)