Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/compaction-default-output-cap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Cap compaction output at 128k tokens by default to avoid provider max_tokens errors.
25 changes: 23 additions & 2 deletions packages/agent-core/src/agent/compaction/full.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down Expand Up @@ -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);
Expand Down
33 changes: 33 additions & 0 deletions packages/agent-core/test/agent/compaction/full.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading