fix(agent-core): cap compaction output at 128k by default (#1156)

This commit is contained in:
qer 2026-06-27 23:24:57 +08:00 committed by GitHub
parent 54baf5d07f
commit 794db55538
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 61 additions and 2 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Cap compaction output at 128k tokens by default to avoid provider max_tokens errors.

View file

@ -39,6 +39,15 @@ import {
export const MAX_COMPACTION_RETRY_ATTEMPTS = 5;
/**
* Default hard cap on compaction output tokens when `maxOutputSize` is not
* configured on the model alias. Without this, compaction falls back to the
* full context window size, which exceeds the `max_tokens` ceiling enforced
* by many OpenAI-compatible providers. 128k matches the chat-completions
* ceiling applied by the OpenAI Legacy provider.
*/
const DEFAULT_COMPACTION_MAX_COMPLETION_TOKENS = 128 * 1024;
class CompactionTruncatedError extends Error {
constructor() {
super('Compaction response was truncated before producing a complete summary.');
@ -261,13 +270,25 @@ export class FullCompaction {
await this.triggerPreCompactHook(data, tokensBefore, signal);
const model = this.agent.config.model;
const capability = this.agent.config.modelCapabilities;
const maxContextTokens = capability.max_context_tokens;
// When the model's context window is known and the user has not set
// `maxOutputSize`, cap compaction output to a safe default so a large
// context window does not push `max_tokens` past the provider's ceiling.
// When the window is unknown (maxContextTokens === 0), leave
// `maxOutputSize` unset so `resolveCompletionBudget` falls back to the
// conservative unknown-context fallback.
const defaultCompactionCap =
maxContextTokens > 0
? Math.min(maxContextTokens, DEFAULT_COMPACTION_MAX_COMPLETION_TOKENS)
: undefined;
const provider = applyCompletionBudget({
provider: this.agent.config.provider,
budget: resolveCompletionBudget({
maxOutputSize: this.agent.config.maxOutputSize,
maxOutputSize: this.agent.config.maxOutputSize ?? defaultCompactionCap,
reservedContextSize: this.agent.kimiConfig?.loopControl?.reservedContextSize,
}),
capability: this.agent.config.modelCapabilities,
capability,
});
const delays = retryBackoffDelays(MAX_COMPACTION_RETRY_ATTEMPTS);

View file

@ -1811,6 +1811,39 @@ describe('FullCompaction', () => {
expect(compactionMaxCompletionTokens).toEqual([384000]);
});
it('uses default 128k hardCap when maxOutputSize is not configured', async () => {
let callCount = 0;
const compactionMaxCompletionTokens: unknown[] = [];
const generate: GenerateFn = async (provider, _system, _tools, _history, callbacks) => {
callCount += 1;
if (callCount === 1) {
throw new APIContextOverflowError(400, 'Context length exceeded', 'req-default-cap');
}
if (callCount === 2) {
compactionMaxCompletionTokens.push(providerMaxCompletionTokens(provider));
return textResult('Default cap compacted summary.');
}
await callbacks?.onMessagePart?.({
type: 'text',
text: 'Recovered with default cap.',
});
return textResult('Recovered with default cap.');
};
const ctx = testAgent({ generate });
ctx.configure({
provider: CATALOGUED_PROVIDER,
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
});
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
ctx.newEvents();
await ctx.rpc.prompt({ input: [{ type: 'text', text: 'Retry with default cap' }] });
await ctx.untilTurnEnd();
expect(callCount).toBe(3);
expect(compactionMaxCompletionTokens).toEqual([128 * 1024]);
});
it('ignores filtered assistant placeholders when checking the retained overflow suffix', async () => {
let callCount = 0;
const generate: GenerateFn = async (_provider, _system, _tools, _history, callbacks) => {