From 1dfad4ceda639e30e9b695fcc8f0b886699fec3d Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Wed, 22 Jul 2026 01:42:38 +0800 Subject: [PATCH 1/5] fix: five correctness follow-ups to the catalog metadata work - Normalize a configured effort value (case/whitespace) before thinking resolution on both engines, so "OFF" is read as off instead of being sent upstream as an invalid effort. - Clamp a declared input cap to the effective context window in the effective model resolution, so an override lowering max_context_size cannot leave a stale larger cap behind. - Report context usage against the same effective cap (max_input_tokens ?? max_context_tokens) in the SDK getStatus and the v2 legacy status projections, matching the session-event and service surfaces. - Preserve concretely declared per-model endpoints when the override npm is unrecognized, via the same OpenAI-compatible fallback used for top-level entries. - Attribute resolved.maxInputSize and resolved.capabilities .max_input_tokens in the model inspector to their config, override, or clamp provenance. --- .changeset/catalog-followup-correctness.md | 9 ++++ .../app/sessionLegacy/sessionLegacyService.ts | 9 +++- .../src/kosong/model/inspection.ts | 22 ++++++++ .../src/kosong/model/modelAuth.ts | 7 +++ .../src/kosong/model/thinking.ts | 2 +- .../test/agent/profile/thinking.test.ts | 8 +++ .../test/app/model/model.test.ts | 13 +++++ .../test/kosong/model/catalog.test.ts | 52 +++++++++++++++++++ .../agent-core/src/agent/config/thinking.ts | 9 ++-- packages/agent-core/src/config/model.ts | 9 ++++ .../test/agent/config/thinking.test.ts | 9 ++++ .../test/config/model-overrides.test.ts | 14 +++++ packages/kosong/src/catalog.ts | 6 ++- packages/kosong/test/catalog.test.ts | 35 +++++++++++++ packages/node-sdk/src/rpc.ts | 3 +- 15 files changed, 198 insertions(+), 9 deletions(-) create mode 100644 .changeset/catalog-followup-correctness.md diff --git a/.changeset/catalog-followup-correctness.md b/.changeset/catalog-followup-correctness.md new file mode 100644 index 0000000000..cd46505a8e --- /dev/null +++ b/.changeset/catalog-followup-correctness.md @@ -0,0 +1,9 @@ +--- +"@moonshot-ai/kosong": patch +"@moonshot-ai/kimi-code-sdk": patch +"@moonshot-ai/agent-core": patch +"@moonshot-ai/agent-core-v2": patch +"@moonshot-ai/kimi-code": patch +--- + +Fix a set of small correctness issues on top of the catalog metadata work: a configured `effort = "OFF"` is now normalized instead of being sent upstream as an invalid effort; a model's declared input limit can no longer exceed its effective context window (overrides are clamped); context-usage percentages are now consistent across all status endpoints (SDK, REST, and session events); per-model endpoints declared with an unrecognized override SDK are preserved via the OpenAI-compatible fallback instead of being dropped; and the model inspector attributes the new input-limit fields to their actual config, override, or clamp provenance. diff --git a/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts b/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts index 13870e9bee..836340214b 100644 --- a/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts +++ b/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts @@ -160,9 +160,14 @@ export class SessionLegacyService implements ISessionLegacyService { const swarm = agent.accessor.get(IAgentSwarmService); const model = profile.getModel(); - const caps = profile.getModelCapabilities() as { max_context_tokens?: number }; + const caps = profile.getModelCapabilities() as { + max_context_tokens?: number; + max_input_tokens?: number; + }; const maxTokens = - model === '' ? resolveDefaultModelContextTokens(agent) : (caps.max_context_tokens ?? 0); + model === '' + ? resolveDefaultModelContextTokens(agent) + : (caps.max_input_tokens ?? caps.max_context_tokens ?? 0); const tokens = contextSize.get().size; const planData = await plan.status(); diff --git a/packages/agent-core-v2/src/kosong/model/inspection.ts b/packages/agent-core-v2/src/kosong/model/inspection.ts index 346f404828..9b03082c45 100644 --- a/packages/agent-core-v2/src/kosong/model/inspection.ts +++ b/packages/agent-core-v2/src/kosong/model/inspection.ts @@ -195,6 +195,17 @@ export function attributeEffectiveFields( }); continue; } + if ( + key === 'maxInputSize' && + before !== undefined && + JSON.stringify(before) !== JSON.stringify(after) + ) { + trace.record(path, { + kind: 'synthesized', + detail: 'clamped to the effective max_context_size', + }); + continue; + } const profileTouched = (key === 'capabilities' || key === 'supportEfforts' || key === 'defaultEffort') && profileDetail !== undefined && @@ -486,6 +497,17 @@ function attributeCapabilities( kind: 'synthesized', detail: 'forced to the resolved maxContextSize', }); + // The capability-level input cap mirrors the field-level provenance of + // maxInputSize (config / override / clamp); without a declaration the + // total window is the only ceiling. + const maxInputSource = sources.get('model.effective.maxInputSize'); + sources.set( + 'resolved.capabilities.max_input_tokens', + maxInputSource ?? { + kind: 'none', + detail: 'no declared input limit — the total window applies', + }, + ); } function attributeHeaders( diff --git a/packages/agent-core-v2/src/kosong/model/modelAuth.ts b/packages/agent-core-v2/src/kosong/model/modelAuth.ts index 3a33b9bb3a..61f5df08a1 100644 --- a/packages/agent-core-v2/src/kosong/model/modelAuth.ts +++ b/packages/agent-core-v2/src/kosong/model/modelAuth.ts @@ -119,6 +119,13 @@ export function effectiveModelConfig( ) { delete effective.defaultEffort; } + if ( + effective.maxInputSize !== undefined && + effective.maxContextSize !== undefined && + effective.maxInputSize > effective.maxContextSize + ) { + effective.maxInputSize = effective.maxContextSize; + } return withAnthropicProfile(effective, providerType); } diff --git a/packages/agent-core-v2/src/kosong/model/thinking.ts b/packages/agent-core-v2/src/kosong/model/thinking.ts index 18b22a57bc..c77d1432a0 100644 --- a/packages/agent-core-v2/src/kosong/model/thinking.ts +++ b/packages/agent-core-v2/src/kosong/model/thinking.ts @@ -257,7 +257,7 @@ export function resolveThinkingEffortForModel( model: ModelThinkingMetadata | undefined, strictValidation = false, ): ThinkingEffort { - const configured = nonEmpty(defaults?.effort) as ThinkingEffort | undefined; + const configured = normalizeRequestedThinkingEffort(defaults?.effort); const normalized = normalizeRequestedThinkingEffort(requested); let effort: ThinkingEffort; if (normalized !== undefined) { diff --git a/packages/agent-core-v2/test/agent/profile/thinking.test.ts b/packages/agent-core-v2/test/agent/profile/thinking.test.ts index 0728dbe84d..55d3495882 100644 --- a/packages/agent-core-v2/test/agent/profile/thinking.test.ts +++ b/packages/agent-core-v2/test/agent/profile/thinking.test.ts @@ -158,6 +158,14 @@ describe('resolveThinkingEffortForModel', () => { expect(resolveThinkingEffortForModel('off', undefined, alwaysThinkingModel)).toBe('on'); }); + it('normalizes a configured off value (case/whitespace) instead of sending it upstream', () => { + expect(resolveThinkingEffortForModel(undefined, { effort: ' OFF ' }, effortModel)).toBe('off'); + expect(resolveThinkingEffortForModel(undefined, { effort: 'Off' }, booleanModel)).toBe('off'); + expect( + resolveThinkingEffortForModel(undefined, { enabled: false, effort: ' OFF ' }, alwaysThinkingEffortModel), + ).toBe('high'); + }); + it('treats a configured off as absent when clamping always-thinking models', () => { expect(resolveThinkingEffortForModel(undefined, { effort: 'off' }, alwaysThinkingEffortModel)).toBe( 'high', diff --git a/packages/agent-core-v2/test/app/model/model.test.ts b/packages/agent-core-v2/test/app/model/model.test.ts index 471ae03437..264638538e 100644 --- a/packages/agent-core-v2/test/app/model/model.test.ts +++ b/packages/agent-core-v2/test/app/model/model.test.ts @@ -28,6 +28,19 @@ import '#/kosong/provider/providers/kimi/kimi.contrib'; import '#/kosong/provider/providers/standard.contrib'; describe('effectiveModelConfig', () => { + it('clamps the input cap to the effective total window', () => { + const effective = effectiveModelConfig({ + provider: 'custom', + model: 'gpt-5', + maxContextSize: 400000, + maxInputSize: 272000, + overrides: { maxContextSize: 128000 }, + }); + + expect(effective.maxContextSize).toBe(128000); + expect(effective.maxInputSize).toBe(128000); + }); + it('derives the official effort metadata from a Claude model name', () => { expect( effectiveModelConfig({ diff --git a/packages/agent-core-v2/test/kosong/model/catalog.test.ts b/packages/agent-core-v2/test/kosong/model/catalog.test.ts index c51ac33db8..6ca6876233 100644 --- a/packages/agent-core-v2/test/kosong/model/catalog.test.ts +++ b/packages/agent-core-v2/test/kosong/model/catalog.test.ts @@ -508,6 +508,9 @@ describe('ModelCatalog inspect', () => { expect(view.sources['resolved.capabilities.max_context_tokens']).toMatchObject({ kind: 'synthesized', }); + expect(view.sources['resolved.capabilities.max_input_tokens']).toMatchObject({ + kind: 'none', + }); expect(view.sources['resolved']).toMatchObject({ kind: 'synthesized' }); // Kimi's definition capability is UNKNOWN — nothing is detected. expect(view.sources['resolved.capabilities.tool_use']).toMatchObject({ kind: 'none' }); @@ -586,6 +589,55 @@ describe('ModelCatalog inspect', () => { } }); + it('attributes the input cap to config, its clamp, and its absence', () => { + const { host, catalog } = createHost({ + providers: { + kimi: { type: 'kimi', apiKey: 'sk', baseUrl: 'https://api.example.test/v1' }, + }, + models: { + declared: { + provider: 'kimi', + model: 'kimi-k2', + maxContextSize: 400000, + maxInputSize: 272000, + }, + clamped: { + provider: 'kimi', + model: 'kimi-k2', + maxContextSize: 400000, + maxInputSize: 272000, + overrides: { maxContextSize: 128000 }, + }, + plain: { provider: 'kimi', model: 'kimi-k2', maxContextSize: 100 }, + }, + }); + try { + const declaredView = catalog.inspect('declared'); + expect(declaredView.resolved.maxInputSize).toBe(272000); + expect(declaredView.sources['model.effective.maxInputSize']).toMatchObject({ kind: 'config' }); + expect(declaredView.sources['resolved.capabilities.max_input_tokens']).toMatchObject({ + kind: 'config', + }); + + const clampedView = catalog.inspect('clamped'); + expect(clampedView.resolved.maxInputSize).toBe(128000); + expect(clampedView.sources['model.effective.maxInputSize']).toMatchObject({ + kind: 'synthesized', + detail: expect.stringContaining('clamped'), + }); + expect(clampedView.sources['resolved.capabilities.max_input_tokens']).toMatchObject({ + kind: 'synthesized', + }); + + const plainView = catalog.inspect('plain'); + expect(plainView.sources['resolved.capabilities.max_input_tokens']).toMatchObject({ + kind: 'none', + }); + } finally { + host.dispose(); + } + }); + it('attributes env-bag credentials and endpoints by env-var name', () => { const { host, catalog } = createHost({ providers: { diff --git a/packages/agent-core/src/agent/config/thinking.ts b/packages/agent-core/src/agent/config/thinking.ts index c54e6b515a..00171c3a35 100644 --- a/packages/agent-core/src/agent/config/thinking.ts +++ b/packages/agent-core/src/agent/config/thinking.ts @@ -104,13 +104,16 @@ export function resolveThinkingEffort( kimiProtocol = false, ): ThinkingEffort { const effectiveModel = model === undefined ? undefined : effectiveModelAlias(model); + // Normalize the configured value once: 'OFF' / ' off ' must be read as off + // on every path, not passed upstream as a concrete effort. + const configured = config?.effort === undefined ? undefined : config.effort.trim().toLowerCase(); let effort: ThinkingEffort; if (requested !== undefined) { effort = requested; } else if (config?.enabled === false) { effort = 'off'; } else { - effort = config?.effort ?? defaultThinkingEffortFor(effectiveModel); + effort = configured ?? defaultThinkingEffortFor(effectiveModel); } if (effort === 'off' && effectiveModel?.capabilities?.includes('always_thinking') === true) { @@ -119,8 +122,8 @@ export function resolveThinkingEffort( // disable, it should not also discard a chosen effort. A configured // 'off' is treated as absent: the model default applies instead. effort = - config?.effort !== undefined && config.effort.trim().toLowerCase() !== 'off' - ? config.effort + configured !== undefined && configured !== 'off' + ? configured : defaultThinkingEffortFor(effectiveModel); } diff --git a/packages/agent-core/src/config/model.ts b/packages/agent-core/src/config/model.ts index 2a64842e94..0c159a23dd 100644 --- a/packages/agent-core/src/config/model.ts +++ b/packages/agent-core/src/config/model.ts @@ -22,6 +22,15 @@ export function effectiveModelAlias( delete effective.defaultEffort; } + // The input cap can never exceed the effective total window (an override + // lowering max_context_size must not leave a stale, larger cap behind). + if ( + effective.maxInputSize !== undefined && + effective.maxInputSize > effective.maxContextSize + ) { + effective.maxInputSize = effective.maxContextSize; + } + return withAnthropicProfile(effective, providerType); } diff --git a/packages/agent-core/test/agent/config/thinking.test.ts b/packages/agent-core/test/agent/config/thinking.test.ts index 154088a7b0..10a96a1b96 100644 --- a/packages/agent-core/test/agent/config/thinking.test.ts +++ b/packages/agent-core/test/agent/config/thinking.test.ts @@ -140,6 +140,15 @@ describe('resolveThinkingEffort', () => { expect(resolveThinkingEffort('off', undefined, alwaysThinkingModel, false)).toBe('on'); }); + it('normalizes a configured off value (case/whitespace) instead of sending it upstream', () => { + expect(resolveThinkingEffort(undefined, { effort: ' OFF ' }, effortModel, false)).toBe('off'); + expect(resolveThinkingEffort(undefined, { effort: 'Off' }, booleanModel, false)).toBe('off'); + // … and inside the always-on clamp it is treated as absent, not as an effort. + expect( + resolveThinkingEffort(undefined, { enabled: false, effort: ' OFF ' }, alwaysThinkingEffortModel, false), + ).toBe('high'); + }); + it('treats a configured off as absent when clamping always-thinking models', () => { expect(resolveThinkingEffort(undefined, { effort: 'off' }, alwaysThinkingEffortModel, false)).toBe( 'high', diff --git a/packages/agent-core/test/config/model-overrides.test.ts b/packages/agent-core/test/config/model-overrides.test.ts index 5e878a1c49..6f140c18e2 100644 --- a/packages/agent-core/test/config/model-overrides.test.ts +++ b/packages/agent-core/test/config/model-overrides.test.ts @@ -16,6 +16,20 @@ function alias(overrides?: ModelAlias['overrides']): ModelAlias { } describe('effectiveModelAlias', () => { + it('clamps the input cap to the effective total window', () => { + const model: ModelAlias = { + provider: 'custom', + model: 'gpt-5', + maxContextSize: 400000, + maxInputSize: 272000, + overrides: { maxContextSize: 128000 }, + }; + + const effective = effectiveModelAlias(model); + expect(effective.maxContextSize).toBe(128000); + expect(effective.maxInputSize).toBe(128000); + }); + it('returns the alias unchanged when there are no overrides', () => { const model = alias(); diff --git a/packages/kosong/src/catalog.ts b/packages/kosong/src/catalog.ts index 2e3467f97a..2d62018fe4 100644 --- a/packages/kosong/src/catalog.ts +++ b/packages/kosong/src/catalog.ts @@ -435,9 +435,11 @@ function applyModelProviderOverride( const override = raw.provider; if (override === undefined) return model; // An api-only override keeps the provider's wire; an npm override points at - // a (possibly different) one. Unidentified npm keeps the benefit of doubt. + // a (possibly different) one. Unrecognized npm gets the same + // OpenAI-compatible fallback as top-level entries so a concretely declared + // endpoint is not silently dropped. const overrideWire = - typeof override.npm === 'string' ? inferOverrideWire(override.npm) : providerWire; + typeof override.npm === 'string' ? (inferOverrideWire(override.npm) ?? 'openai') : providerWire; if (overrideWire === undefined) return model; const rawApi = override.api; const api = rawApi ?? entry.api; diff --git a/packages/kosong/test/catalog.test.ts b/packages/kosong/test/catalog.test.ts index 0cfb8cf2de..987c53d8cd 100644 --- a/packages/kosong/test/catalog.test.ts +++ b/packages/kosong/test/catalog.test.ts @@ -699,4 +699,39 @@ describe('catalogProviderModels', () => { }); expect(models).toHaveLength(0); }); + + it('falls back to the OpenAI wire for unrecognized override SDKs, preserving a concrete endpoint', () => { + // xai-flavored model on an OpenAI-compatible gateway: the npm is unknown + // but the endpoint is concrete — carry it (same-wire), do not drop it. + const models = catalogProviderModels({ + id: 'gateway', + npm: '@ai-sdk/openai-compatible', + api: 'https://gateway.example.test/v1', + models: { + 'tenant-model': { + id: 'tenant-model', + limit: { context: 1000 }, + provider: { npm: '@ai-sdk/xai', api: 'https://tenant.example.test/v1' }, + }, + }, + }); + expect(models[0]).toMatchObject({ baseUrl: 'https://tenant.example.test/v1' }); + expect(models[0]?.protocol).toBeUndefined(); + }); + + it('skips unrecognized-wire overrides when the provider speaks another wire', () => { + const models = catalogProviderModels({ + id: 'claude-gw', + npm: '@ai-sdk/anthropic', + api: 'https://gw.example.test', + models: { + 'vendor-model': { + id: 'vendor-model', + limit: { context: 1000 }, + provider: { npm: '@ai-sdk/xai', api: 'https://tenant.example.test/v1' }, + }, + }, + }); + expect(models).toHaveLength(0); + }); }); diff --git a/packages/node-sdk/src/rpc.ts b/packages/node-sdk/src/rpc.ts index 41abb8e277..7cc7b918a3 100644 --- a/packages/node-sdk/src/rpc.ts +++ b/packages/node-sdk/src/rpc.ts @@ -578,7 +578,8 @@ export abstract class SDKRpcClientBase { sessionId: input.sessionId, agentId, }); - const maxContextTokens = config.modelCapabilities?.max_context_tokens ?? 0; + const capability = config.modelCapabilities; + const maxContextTokens = capability?.max_input_tokens ?? capability?.max_context_tokens ?? 0; const contextTokens = context.tokenCount; const contextUsage = maxContextTokens > 0 ? contextTokens / maxContextTokens : 0; const hasUsage = From 82ffdaf386386986668d40eea6594d980f946b16 Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Wed, 22 Jul 2026 02:02:35 +0800 Subject: [PATCH 2/5] fix: honor observed context caps and publish the effective cap in v2 status - The overflow-learned provider window is now written into both max_context_tokens and max_input_tokens of the effective compaction context, so the strategy cannot bypass it by re-selecting the raw catalog input cap during overflow recovery. - agent.status.updated publishes max_input_tokens ?? max_context_tokens, matching the other v1/v2 status surfaces. --- .changeset/catalog-followup-correctness.md | 2 +- .../fullCompaction/fullCompactionService.ts | 4 +- .../src/agent/profile/profileService.ts | 4 +- .../fullCompaction/fullCompaction.test.ts | 60 +++++++++++++++++++ 4 files changed, 67 insertions(+), 3 deletions(-) diff --git a/.changeset/catalog-followup-correctness.md b/.changeset/catalog-followup-correctness.md index cd46505a8e..05f7c9bbc9 100644 --- a/.changeset/catalog-followup-correctness.md +++ b/.changeset/catalog-followup-correctness.md @@ -6,4 +6,4 @@ "@moonshot-ai/kimi-code": patch --- -Fix a set of small correctness issues on top of the catalog metadata work: a configured `effort = "OFF"` is now normalized instead of being sent upstream as an invalid effort; a model's declared input limit can no longer exceed its effective context window (overrides are clamped); context-usage percentages are now consistent across all status endpoints (SDK, REST, and session events); per-model endpoints declared with an unrecognized override SDK are preserved via the OpenAI-compatible fallback instead of being dropped; and the model inspector attributes the new input-limit fields to their actual config, override, or clamp provenance. +Fix a set of small correctness issues on top of the catalog metadata work: a configured `effort = "OFF"` is now normalized instead of being sent upstream as an invalid effort; a model's declared input limit can no longer exceed its effective context window (overrides are clamped); context-usage percentages are now consistent across all status endpoints (SDK, REST, and session events), and a provider-observed smaller context window now actually wins over the catalog's declared input cap during overflow recovery; per-model endpoints declared with an unrecognized override SDK are preserved via the OpenAI-compatible fallback instead of being dropped; and the model inspector attributes the new input-limit fields to their actual config, override, or clamp provenance. diff --git a/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts b/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts index d375a2e5c7..e89c49a6ab 100644 --- a/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts +++ b/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts @@ -184,11 +184,13 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull private resolveModelContextWithEffectiveMax(): ProfileModelContext { const resolved = this.profile.resolveModelContext(); + const effectiveMax = this.getEffectiveMaxContextTokens(); return { ...resolved, modelCapabilities: { ...resolved.modelCapabilities, - max_context_tokens: this.getEffectiveMaxContextTokens(), + max_context_tokens: effectiveMax, + max_input_tokens: effectiveMax, }, }; } diff --git a/packages/agent-core-v2/src/agent/profile/profileService.ts b/packages/agent-core-v2/src/agent/profile/profileService.ts index b2afbd8863..c285d8bcdb 100644 --- a/packages/agent-core-v2/src/agent/profile/profileService.ts +++ b/packages/agent-core-v2/src/agent/profile/profileService.ts @@ -593,7 +593,9 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ thinkingEffort: includeThinkingEffort ? this.getEffectiveThinkingLevel() : undefined, - maxContextTokens: this.getModelCapabilities().max_context_tokens, + maxContextTokens: + this.getModelCapabilities().max_input_tokens ?? + this.getModelCapabilities().max_context_tokens, }); } diff --git a/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts b/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts index 3a279047a9..9f809c610c 100644 --- a/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts +++ b/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts @@ -2217,6 +2217,66 @@ describe('FullCompaction', () => { await ctx.expectResumeMatches(); }); + it('honors the observed provider window over a declared input cap', async () => { + let callCount = 0; + const generate: GenerateFn = async (_provider, _system, _tools, _history, callbacks) => { + callCount += 1; + if (callCount === 1) { + throw new APIContextOverflowError(400, 'Context length exceeded', 'req-observed-window'); + } + if (callCount === 2) { + return textResult('Observed recovery summary.'); + } + if (callCount === 3) { + await callbacks?.onMessagePart?.({ + type: 'text', + text: 'Recovered after observed overflow.', + }); + return textResult('Recovered after observed overflow.'); + } + if (callCount === 4) { + return textResult('Observed preemptive summary.'); + } + if (callCount === 5) { + await callbacks?.onMessagePart?.({ + type: 'text', + text: 'Answered after observed-window precompaction.', + }); + return textResult('Answered after observed-window precompaction.'); + } + throw new Error(`Unexpected generate call ${String(callCount)}`); + }; + const ctx = testAgent({ generate }); + ctx.configure({ + provider: CATALOGUED_PROVIDER, + modelCapabilities: { + ...CATALOGUED_MODEL_CAPABILITIES, + max_context_tokens: 200_000, + max_input_tokens: 150_000, + }, + tools: SNAPSHOT_VISIBLE_TOOLS, + }); + ctx.appendExchange(1, 'old user one', 'old assistant one', 20); + ctx.newEvents(); + + await ctx.rpc.prompt({ input: [{ type: 'text', text: 'learn observed window' }] }); + await ctx.untilTurnEnd(); + expect(callCount).toBe(3); + + ctx.appendExchange(2, 'near observed user', 'near observed assistant', 120_000); + ctx.newEvents(); + await ctx.rpc.prompt({ input: [{ type: 'text', text: 'use observed window' }] }); + const events = await ctx.untilTurnEnd(); + + // Without the observed cap being written into max_input_tokens, the + // strategy would keep sizing against the raw 150k input cap and never + // preemptively compact the 120k context the provider already rejected. + expect(callCount).toBe(5); + expect(eventIndex(events, 'compaction.started')).toBeLessThan( + eventIndex(events, 'turn.step.started'), + ); + }); + it('recovers from plain 413 when estimated request is over effective max', async () => { let callCount = 0; const generate: GenerateFn = async (_provider, _system, _tools, _history, callbacks) => { From 8faae90434f7960d9fe3f7c42d0c509df8b89fca Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Wed, 22 Jul 2026 02:11:37 +0800 Subject: [PATCH 3/5] fix: never mutate config on clamp, cap usage ratio, and refuse proprietary override SDKs - effectiveModelAlias / effectiveModelConfig now build a copy before clamping maxInputSize to the effective window instead of rewriting the caller's config record in place. - context_usage is clamped to 1 in the v2 legacy status, the v1 session service, and the SDK getStatus; the default-model status fallback also resolves the input cap. - Overrides naming known proprietary SDKs (Bedrock, Cohere) are refused before the OpenAI-compatible fallback, matching top-level import behavior. - The inspector attributes a clamped maxInputSize to the clamp even when the raw value came from models.*.overrides. --- .changeset/catalog-followup-correctness.md | 2 +- .../app/sessionLegacy/sessionLegacyService.ts | 5 ++-- .../src/kosong/model/inspection.ts | 28 +++++++++---------- .../src/kosong/model/modelAuth.ts | 9 +++--- .../test/app/model/model.test.ts | 24 ++++++++++++---- .../test/kosong/model/catalog.test.ts | 18 ++++++++++++ packages/agent-core/src/config/model.ts | 13 ++++----- .../src/services/session/sessionService.ts | 2 +- .../test/config/model-overrides.test.ts | 12 +++++++- packages/kosong/src/catalog.ts | 15 +++++++--- packages/kosong/test/catalog.test.ts | 18 ++++++++++++ packages/node-sdk/src/rpc.ts | 2 +- 12 files changed, 106 insertions(+), 42 deletions(-) diff --git a/.changeset/catalog-followup-correctness.md b/.changeset/catalog-followup-correctness.md index 05f7c9bbc9..a66f6d171e 100644 --- a/.changeset/catalog-followup-correctness.md +++ b/.changeset/catalog-followup-correctness.md @@ -6,4 +6,4 @@ "@moonshot-ai/kimi-code": patch --- -Fix a set of small correctness issues on top of the catalog metadata work: a configured `effort = "OFF"` is now normalized instead of being sent upstream as an invalid effort; a model's declared input limit can no longer exceed its effective context window (overrides are clamped); context-usage percentages are now consistent across all status endpoints (SDK, REST, and session events), and a provider-observed smaller context window now actually wins over the catalog's declared input cap during overflow recovery; per-model endpoints declared with an unrecognized override SDK are preserved via the OpenAI-compatible fallback instead of being dropped; and the model inspector attributes the new input-limit fields to their actual config, override, or clamp provenance. +Fix a set of small correctness issues on top of the catalog metadata work: a configured `effort = "OFF"` is now normalized instead of being sent upstream as an invalid effort; a model's declared input limit can no longer exceed its effective context window, and the clamp now copies the record instead of mutating the user's config in place; context-usage percentages are consistent across all status endpoints and clamped to 1, with the default-model fallback also using the input cap; a provider-observed smaller context window now actually wins over the catalog's declared input cap during overflow recovery; per-model endpoints declared with an unrecognized override SDK are preserved via the OpenAI-compatible fallback, while known proprietary SDKs stay refused; and the model inspector attributes input-limit fields to their actual config, override, or clamp provenance. diff --git a/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts b/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts index 836340214b..bf03d35159 100644 --- a/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts +++ b/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts @@ -180,7 +180,7 @@ export class SessionLegacyService implements ISessionLegacyService { swarm_mode: swarm.isActive, context_tokens: tokens, max_context_tokens: maxTokens, - context_usage: maxTokens > 0 ? tokens / maxTokens : 0, + context_usage: maxTokens > 0 ? Math.min(1, tokens / maxTokens) : 0, }; } @@ -209,7 +209,8 @@ function resolveDefaultModelContextTokens(agent: IAgentScopeHandle): number { const defaultModel = agent.accessor.get(IConfigService).get('defaultModel'); if (typeof defaultModel !== 'string' || defaultModel.length === 0) return 0; try { - return agent.accessor.get(IModelCatalog).get(defaultModel).capabilities.max_context_tokens; + const capabilities = agent.accessor.get(IModelCatalog).get(defaultModel).capabilities; + return capabilities.max_input_tokens ?? capabilities.max_context_tokens; } catch { return 0; } diff --git a/packages/agent-core-v2/src/kosong/model/inspection.ts b/packages/agent-core-v2/src/kosong/model/inspection.ts index 9b03082c45..72dcf3dc19 100644 --- a/packages/agent-core-v2/src/kosong/model/inspection.ts +++ b/packages/agent-core-v2/src/kosong/model/inspection.ts @@ -184,6 +184,20 @@ export function attributeEffectiveFields( const before = (base as Record)[key]; const after = (effective as Record)[key]; if (before === undefined && after === undefined) continue; + if (key === 'maxInputSize') { + const rawValue = (overridden.has(key) ? overrides?.[key] : before) as number | undefined; + if ( + rawValue !== undefined && + effective.maxContextSize !== undefined && + rawValue > effective.maxContextSize + ) { + trace.record(path, { + kind: 'synthesized', + detail: 'clamped to the effective max_context_size', + }); + continue; + } + } if (overridden.has(key)) { trace.record(path, { kind: 'override', detail: 'models.*.overrides' }); continue; @@ -195,17 +209,6 @@ export function attributeEffectiveFields( }); continue; } - if ( - key === 'maxInputSize' && - before !== undefined && - JSON.stringify(before) !== JSON.stringify(after) - ) { - trace.record(path, { - kind: 'synthesized', - detail: 'clamped to the effective max_context_size', - }); - continue; - } const profileTouched = (key === 'capabilities' || key === 'supportEfforts' || key === 'defaultEffort') && profileDetail !== undefined && @@ -497,9 +500,6 @@ function attributeCapabilities( kind: 'synthesized', detail: 'forced to the resolved maxContextSize', }); - // The capability-level input cap mirrors the field-level provenance of - // maxInputSize (config / override / clamp); without a declaration the - // total window is the only ceiling. const maxInputSource = sources.get('model.effective.maxInputSize'); sources.set( 'resolved.capabilities.max_input_tokens', diff --git a/packages/agent-core-v2/src/kosong/model/modelAuth.ts b/packages/agent-core-v2/src/kosong/model/modelAuth.ts index 61f5df08a1..6ce82b6c41 100644 --- a/packages/agent-core-v2/src/kosong/model/modelAuth.ts +++ b/packages/agent-core-v2/src/kosong/model/modelAuth.ts @@ -119,14 +119,13 @@ export function effectiveModelConfig( ) { delete effective.defaultEffort; } - if ( + const clamped = effective.maxInputSize !== undefined && effective.maxContextSize !== undefined && effective.maxInputSize > effective.maxContextSize - ) { - effective.maxInputSize = effective.maxContextSize; - } - return withAnthropicProfile(effective, providerType); + ? { ...effective, maxInputSize: effective.maxContextSize } + : effective; + return withAnthropicProfile(clamped, providerType); } function withAnthropicProfile(model: ModelRecord, providerType?: string): ModelRecord { diff --git a/packages/agent-core-v2/test/app/model/model.test.ts b/packages/agent-core-v2/test/app/model/model.test.ts index 264638538e..8891e61865 100644 --- a/packages/agent-core-v2/test/app/model/model.test.ts +++ b/packages/agent-core-v2/test/app/model/model.test.ts @@ -28,17 +28,29 @@ import '#/kosong/provider/providers/kimi/kimi.contrib'; import '#/kosong/provider/providers/standard.contrib'; describe('effectiveModelConfig', () => { - it('clamps the input cap to the effective total window', () => { - const effective = effectiveModelConfig({ + it('clamps the input cap to the effective total window without mutating the source', () => { + const record = { provider: 'custom', model: 'gpt-5', - maxContextSize: 400000, + maxContextSize: 128000, maxInputSize: 272000, - overrides: { maxContextSize: 128000 }, - }); + }; - expect(effective.maxContextSize).toBe(128000); + const effective = effectiveModelConfig(record); expect(effective.maxInputSize).toBe(128000); + expect(record.maxInputSize).toBe(272000); + + const withOverrides = { + provider: 'custom', + model: 'gpt-5', + maxContextSize: 400000, + maxInputSize: 272000, + overrides: { maxContextSize: 128000 }, + }; + const effectiveOverride = effectiveModelConfig(withOverrides); + expect(effectiveOverride.maxContextSize).toBe(128000); + expect(effectiveOverride.maxInputSize).toBe(128000); + expect(withOverrides.maxInputSize).toBe(272000); }); it('derives the official effort metadata from a Claude model name', () => { diff --git a/packages/agent-core-v2/test/kosong/model/catalog.test.ts b/packages/agent-core-v2/test/kosong/model/catalog.test.ts index 6ca6876233..e7a9dadab5 100644 --- a/packages/agent-core-v2/test/kosong/model/catalog.test.ts +++ b/packages/agent-core-v2/test/kosong/model/catalog.test.ts @@ -608,6 +608,12 @@ describe('ModelCatalog inspect', () => { maxInputSize: 272000, overrides: { maxContextSize: 128000 }, }, + clampedOverride: { + provider: 'kimi', + model: 'kimi-k2', + maxContextSize: 400000, + overrides: { maxContextSize: 128000, maxInputSize: 272000 }, + }, plain: { provider: 'kimi', model: 'kimi-k2', maxContextSize: 100 }, }, }); @@ -629,6 +635,18 @@ describe('ModelCatalog inspect', () => { kind: 'synthesized', }); + // An override value itself above the window is attributed to the clamp, + // not to models.*.overrides. + const clampedOverrideView = catalog.inspect('clampedOverride'); + expect(clampedOverrideView.resolved.maxInputSize).toBe(128000); + expect(clampedOverrideView.sources['model.effective.maxInputSize']).toMatchObject({ + kind: 'synthesized', + detail: expect.stringContaining('clamped'), + }); + expect(clampedOverrideView.sources['model.effective.maxInputSize']).not.toMatchObject({ + kind: 'override', + }); + const plainView = catalog.inspect('plain'); expect(plainView.sources['resolved.capabilities.max_input_tokens']).toMatchObject({ kind: 'none', diff --git a/packages/agent-core/src/config/model.ts b/packages/agent-core/src/config/model.ts index 0c159a23dd..f65fbf20e1 100644 --- a/packages/agent-core/src/config/model.ts +++ b/packages/agent-core/src/config/model.ts @@ -24,14 +24,13 @@ export function effectiveModelAlias( // The input cap can never exceed the effective total window (an override // lowering max_context_size must not leave a stale, larger cap behind). - if ( - effective.maxInputSize !== undefined && - effective.maxInputSize > effective.maxContextSize - ) { - effective.maxInputSize = effective.maxContextSize; - } + // Build a copy for the clamp — never rewrite the caller's config record. + const clamped = + effective.maxInputSize !== undefined && effective.maxInputSize > effective.maxContextSize + ? { ...effective, maxInputSize: effective.maxContextSize } + : effective; - return withAnthropicProfile(effective, providerType); + return withAnthropicProfile(clamped, providerType); } function withAnthropicProfile(model: ModelAlias, providerType?: ProviderType): ModelAlias { diff --git a/packages/agent-core/src/services/session/sessionService.ts b/packages/agent-core/src/services/session/sessionService.ts index fa0b64adfa..8424f30457 100644 --- a/packages/agent-core/src/services/session/sessionService.ts +++ b/packages/agent-core/src/services/session/sessionService.ts @@ -475,7 +475,7 @@ export class SessionService extends Disposable implements ISessionService { const capability = config.modelCapabilities; const maxContextTokens = capability?.max_input_tokens ?? capability?.max_context_tokens ?? 0; const contextTokens = context.tokenCount; - const contextUsage = maxContextTokens > 0 ? contextTokens / maxContextTokens : 0; + const contextUsage = maxContextTokens > 0 ? Math.min(1, contextTokens / maxContextTokens) : 0; const agentState = this.promptService.getAgentStateSnapshot(id); diff --git a/packages/agent-core/test/config/model-overrides.test.ts b/packages/agent-core/test/config/model-overrides.test.ts index 6f140c18e2..1af415067a 100644 --- a/packages/agent-core/test/config/model-overrides.test.ts +++ b/packages/agent-core/test/config/model-overrides.test.ts @@ -16,7 +16,7 @@ function alias(overrides?: ModelAlias['overrides']): ModelAlias { } describe('effectiveModelAlias', () => { - it('clamps the input cap to the effective total window', () => { + it('clamps the input cap to the effective total window without mutating the source', () => { const model: ModelAlias = { provider: 'custom', model: 'gpt-5', @@ -28,6 +28,16 @@ describe('effectiveModelAlias', () => { const effective = effectiveModelAlias(model); expect(effective.maxContextSize).toBe(128000); expect(effective.maxInputSize).toBe(128000); + expect(model.maxInputSize).toBe(272000); + + const noOverrides: ModelAlias = { + provider: 'custom', + model: 'gpt-5', + maxContextSize: 128000, + maxInputSize: 272000, + }; + expect(effectiveModelAlias(noOverrides).maxInputSize).toBe(128000); + expect(noOverrides.maxInputSize).toBe(272000); }); it('returns the alias unchanged when there are no overrides', () => { diff --git a/packages/kosong/src/catalog.ts b/packages/kosong/src/catalog.ts index 2d62018fe4..bce6e9da7b 100644 --- a/packages/kosong/src/catalog.ts +++ b/packages/kosong/src/catalog.ts @@ -435,11 +435,18 @@ function applyModelProviderOverride( const override = raw.provider; if (override === undefined) return model; // An api-only override keeps the provider's wire; an npm override points at - // a (possibly different) one. Unrecognized npm gets the same - // OpenAI-compatible fallback as top-level entries so a concretely declared - // endpoint is not silently dropped. + // a (possibly different) one. Known proprietary SDKs are refused like at + // top level; other unrecognized npm gets the same OpenAI-compatible + // fallback so a concretely declared endpoint is not silently dropped. + const overrideNpm = typeof override.npm === 'string' ? override.npm.toLowerCase() : undefined; + if ( + overrideNpm !== undefined && + (overrideNpm.includes('amazon-bedrock') || overrideNpm.includes('cohere')) + ) { + return undefined; + } const overrideWire = - typeof override.npm === 'string' ? (inferOverrideWire(override.npm) ?? 'openai') : providerWire; + overrideNpm !== undefined ? (inferOverrideWire(overrideNpm) ?? 'openai') : providerWire; if (overrideWire === undefined) return model; const rawApi = override.api; const api = rawApi ?? entry.api; diff --git a/packages/kosong/test/catalog.test.ts b/packages/kosong/test/catalog.test.ts index 987c53d8cd..319886cf82 100644 --- a/packages/kosong/test/catalog.test.ts +++ b/packages/kosong/test/catalog.test.ts @@ -700,6 +700,24 @@ describe('catalogProviderModels', () => { expect(models).toHaveLength(0); }); + it('refuses known proprietary override SDKs instead of falling back to OpenAI', () => { + for (const npm of ['@ai-sdk/cohere', '@ai-sdk/amazon-bedrock']) { + const models = catalogProviderModels({ + id: 'gateway', + npm: '@ai-sdk/openai-compatible', + api: 'https://gateway.example.test/v1', + models: { + 'vendor-model': { + id: 'vendor-model', + limit: { context: 1000 }, + provider: { npm, api: 'https://tenant.example.test/v1' }, + }, + }, + }); + expect(models).toHaveLength(0); + } + }); + it('falls back to the OpenAI wire for unrecognized override SDKs, preserving a concrete endpoint', () => { // xai-flavored model on an OpenAI-compatible gateway: the npm is unknown // but the endpoint is concrete — carry it (same-wire), do not drop it. diff --git a/packages/node-sdk/src/rpc.ts b/packages/node-sdk/src/rpc.ts index 7cc7b918a3..a966f42cba 100644 --- a/packages/node-sdk/src/rpc.ts +++ b/packages/node-sdk/src/rpc.ts @@ -581,7 +581,7 @@ export abstract class SDKRpcClientBase { const capability = config.modelCapabilities; const maxContextTokens = capability?.max_input_tokens ?? capability?.max_context_tokens ?? 0; const contextTokens = context.tokenCount; - const contextUsage = maxContextTokens > 0 ? contextTokens / maxContextTokens : 0; + const contextUsage = maxContextTokens > 0 ? Math.min(1, contextTokens / maxContextTokens) : 0; const hasUsage = usage.byModel !== undefined || usage.total !== undefined || usage.currentTurn !== undefined; return { From d4ba1443edb2d67104db816c6016ca6432d71f30 Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Wed, 22 Jul 2026 02:25:32 +0800 Subject: [PATCH 4/5] fix: normalize forced efforts, prefer input cap in WS status, keep raw event ratio - Whitespace-only configured efforts now read as absent, and the KIMI_MODEL_THINKING_EFFORT override is lowercased on both engines. - kap-server's WS legacy status publishes max_input_tokens ?? max_context_tokens like the other status surfaces. - SDK getStatus keeps the ratio unclamped on purpose (>100% is the documented overflow signal on that path); schema-bounded REST status stays clamped to 1. - Pin the provider-observed window beating a declared input cap with a v1 full-compaction regression test. --- .changeset/catalog-followup-correctness.md | 2 +- .../src/kosong/model/thinking.ts | 2 +- .../test/agent/profile/thinking.test.ts | 6 ++ .../agent-core/src/agent/config/thinking.ts | 6 +- .../agent-core/src/config/kimi-env-params.ts | 2 +- .../test/agent/compaction/full.test.ts | 59 +++++++++++++++++++ .../test/agent/config/thinking.test.ts | 5 ++ .../test/config/kimi-env-params.test.ts | 8 +++ .../src/services/legacyStatus/legacyStatus.ts | 3 +- packages/node-sdk/src/rpc.ts | 5 +- 10 files changed, 91 insertions(+), 7 deletions(-) diff --git a/.changeset/catalog-followup-correctness.md b/.changeset/catalog-followup-correctness.md index a66f6d171e..569e577c84 100644 --- a/.changeset/catalog-followup-correctness.md +++ b/.changeset/catalog-followup-correctness.md @@ -6,4 +6,4 @@ "@moonshot-ai/kimi-code": patch --- -Fix a set of small correctness issues on top of the catalog metadata work: a configured `effort = "OFF"` is now normalized instead of being sent upstream as an invalid effort; a model's declared input limit can no longer exceed its effective context window, and the clamp now copies the record instead of mutating the user's config in place; context-usage percentages are consistent across all status endpoints and clamped to 1, with the default-model fallback also using the input cap; a provider-observed smaller context window now actually wins over the catalog's declared input cap during overflow recovery; per-model endpoints declared with an unrecognized override SDK are preserved via the OpenAI-compatible fallback, while known proprietary SDKs stay refused; and the model inspector attributes input-limit fields to their actual config, override, or clamp provenance. +Fix a set of small correctness issues on top of the catalog metadata work: configured efforts (config or the KIMI_MODEL_THINKING_EFFORT env override) are now normalized instead of being sent upstream as invalid values; a model's declared input limit can no longer exceed its effective context window, and the clamp now copies the record instead of mutating the user's config in place; context-usage percentages share one denominator (the effective input cap) across status endpoints, clamped to 1 where the wire schema bounds it while event streams keep the documented raw overflow signal; a provider-observed smaller context window now actually wins over the catalog's declared input cap during overflow recovery; per-model endpoints declared with an unrecognized override SDK are preserved via the OpenAI-compatible fallback, while known proprietary SDKs stay refused; and the model inspector attributes input-limit fields to their actual config, override, or clamp provenance. diff --git a/packages/agent-core-v2/src/kosong/model/thinking.ts b/packages/agent-core-v2/src/kosong/model/thinking.ts index c77d1432a0..3534eb3fd4 100644 --- a/packages/agent-core-v2/src/kosong/model/thinking.ts +++ b/packages/agent-core-v2/src/kosong/model/thinking.ts @@ -155,7 +155,7 @@ export function resolveForcedThinkingEffort( traitDriven: boolean, ): ThinkingEffort | undefined { if (!traitDriven || effective === 'off') return undefined; - return nonEmpty(forced) as ThinkingEffort | undefined; + return nonEmpty(forced)?.toLowerCase() as ThinkingEffort | undefined; } function hasCapability( diff --git a/packages/agent-core-v2/test/agent/profile/thinking.test.ts b/packages/agent-core-v2/test/agent/profile/thinking.test.ts index 55d3495882..2eec72bf4d 100644 --- a/packages/agent-core-v2/test/agent/profile/thinking.test.ts +++ b/packages/agent-core-v2/test/agent/profile/thinking.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'; import { defaultThinkingEffortForModel, modelSupportsThinkingEffort, + resolveForcedThinkingEffort, resolveThinkingEffortForModel, } from '#/kosong/model/thinking'; @@ -166,6 +167,11 @@ describe('resolveThinkingEffortForModel', () => { ).toBe('high'); }); + it('normalizes the env-forced effort (case/whitespace)', () => { + expect(resolveForcedThinkingEffort(' MAX ', 'high', true)).toBe('max'); + expect(resolveForcedThinkingEffort(' ', 'high', true)).toBeUndefined(); + }); + it('treats a configured off as absent when clamping always-thinking models', () => { expect(resolveThinkingEffortForModel(undefined, { effort: 'off' }, alwaysThinkingEffortModel)).toBe( 'high', diff --git a/packages/agent-core/src/agent/config/thinking.ts b/packages/agent-core/src/agent/config/thinking.ts index 00171c3a35..a25776ca93 100644 --- a/packages/agent-core/src/agent/config/thinking.ts +++ b/packages/agent-core/src/agent/config/thinking.ts @@ -105,8 +105,10 @@ export function resolveThinkingEffort( ): ThinkingEffort { const effectiveModel = model === undefined ? undefined : effectiveModelAlias(model); // Normalize the configured value once: 'OFF' / ' off ' must be read as off - // on every path, not passed upstream as a concrete effort. - const configured = config?.effort === undefined ? undefined : config.effort.trim().toLowerCase(); + // on every path, not passed upstream as a concrete effort; whitespace-only + // reads as absent. + const configuredRaw = config?.effort?.trim().toLowerCase(); + const configured = configuredRaw === undefined || configuredRaw === '' ? undefined : configuredRaw; let effort: ThinkingEffort; if (requested !== undefined) { effort = requested; diff --git a/packages/agent-core/src/config/kimi-env-params.ts b/packages/agent-core/src/config/kimi-env-params.ts index 10a8e3d9ce..eb8f479392 100644 --- a/packages/agent-core/src/config/kimi-env-params.ts +++ b/packages/agent-core/src/config/kimi-env-params.ts @@ -51,7 +51,7 @@ export function resolveKimiEnvThinkingEffort( env: Env = process.env, ): ThinkingEffort | undefined { if (!kimiProvider || thinkingEffort === 'off') return undefined; - const effort = env['KIMI_MODEL_THINKING_EFFORT']?.trim(); + const effort = env['KIMI_MODEL_THINKING_EFFORT']?.trim().toLowerCase(); return effort === undefined || effort.length === 0 ? undefined : effort; } diff --git a/packages/agent-core/test/agent/compaction/full.test.ts b/packages/agent-core/test/agent/compaction/full.test.ts index 9668f18c51..3e14319e23 100644 --- a/packages/agent-core/test/agent/compaction/full.test.ts +++ b/packages/agent-core/test/agent/compaction/full.test.ts @@ -1846,6 +1846,65 @@ describe('FullCompaction', () => { await ctx.expectResumeMatches(); }); + it('honors the observed provider window over a declared input cap', async () => { + let callCount = 0; + const generate: GenerateFn = async (_provider, _system, _tools, _history, callbacks) => { + callCount += 1; + if (callCount === 1) { + throw new APIContextOverflowError(400, 'Context length exceeded', 'req-observed-window'); + } + if (callCount === 2) { + return textResult('Observed recovery summary.'); + } + if (callCount === 3) { + await callbacks?.onMessagePart?.({ + type: 'text', + text: 'Recovered after observed overflow.', + }); + return textResult('Recovered after observed overflow.'); + } + if (callCount === 4) { + return textResult('Observed preemptive summary.'); + } + if (callCount === 5) { + await callbacks?.onMessagePart?.({ + type: 'text', + text: 'Answered after observed-window precompaction.', + }); + return textResult('Answered after observed-window precompaction.'); + } + throw new Error(`Unexpected generate call ${String(callCount)}`); + }; + const ctx = testAgent({ generate }); + ctx.configure({ + provider: CATALOGUED_PROVIDER, + modelCapabilities: { + ...CATALOGUED_MODEL_CAPABILITIES, + max_context_tokens: 200_000, + max_input_tokens: 150_000, + }, + }); + ctx.appendExchange(1, 'old user one', 'old assistant one', 20); + ctx.newEvents(); + + await ctx.rpc.prompt({ input: [{ type: 'text', text: 'learn observed window' }] }); + await ctx.untilTurnEnd(); + expect(callCount).toBe(3); + + ctx.appendExchange(2, 'near observed user', 'near observed assistant', 120_000); + ctx.newEvents(); + await ctx.rpc.prompt({ input: [{ type: 'text', text: 'use observed window' }] }); + const events = await ctx.untilTurnEnd(); + + // The strategy must size against the provider-learned window (~108k), + // not the raw 150k declared input cap — otherwise it never preemptively + // compacts the 120k context the provider already rejected. + expect(callCount).toBe(5); + expect(events).toContainEqual( + expect.objectContaining({ event: 'compaction.started' }), + ); + }); + it('stops repeated provider-overflow compactions when the compacted context still overflows', async () => { let callCount = 0; const generate: GenerateFn = async (_provider, _system, _tools, history) => { diff --git a/packages/agent-core/test/agent/config/thinking.test.ts b/packages/agent-core/test/agent/config/thinking.test.ts index 10a96a1b96..349c28feba 100644 --- a/packages/agent-core/test/agent/config/thinking.test.ts +++ b/packages/agent-core/test/agent/config/thinking.test.ts @@ -149,6 +149,11 @@ describe('resolveThinkingEffort', () => { ).toBe('high'); }); + it('reads a whitespace-only configured effort as absent, not as an empty effort', () => { + expect(resolveThinkingEffort(undefined, { effort: ' ' }, effortModel, false)).toBe('medium'); + expect(resolveThinkingEffort(undefined, { effort: ' ' }, alwaysThinkingModel, false)).toBe('on'); + }); + it('treats a configured off as absent when clamping always-thinking models', () => { expect(resolveThinkingEffort(undefined, { effort: 'off' }, alwaysThinkingEffortModel, false)).toBe( 'high', 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 edf8d8fb97..3d2b672a16 100644 --- a/packages/agent-core/test/config/kimi-env-params.test.ts +++ b/packages/agent-core/test/config/kimi-env-params.test.ts @@ -122,6 +122,14 @@ describe('resolveKimiEnvThinkingEffort', () => { ).toBe('max'); }); + it('lowercases the force override', () => { + expect( + resolveKimiEnvThinkingEffort('high', true, { + KIMI_MODEL_THINKING_EFFORT: ' MAX ', + }), + ).toBe('max'); + }); + it('does not override an explicit off effort', () => { expect( resolveKimiEnvThinkingEffort('off', true, { diff --git a/packages/kap-server/src/services/legacyStatus/legacyStatus.ts b/packages/kap-server/src/services/legacyStatus/legacyStatus.ts index 21a5aea6d5..c70057c034 100644 --- a/packages/kap-server/src/services/legacyStatus/legacyStatus.ts +++ b/packages/kap-server/src/services/legacyStatus/legacyStatus.ts @@ -133,7 +133,8 @@ export function readLegacyStatus(agent: IAgentScopeHandle): LegacyStatusSnapshot // rebases the measured model first, so the max only wins in that window. const measured = wire.getModel(ContextSizeModel); const contextTokens = Math.max(contextSize.get().size, measured.tokens); - const maxContextTokens = profile.getModelCapabilities().max_context_tokens; + const capabilities = profile.getModelCapabilities(); + const maxContextTokens = capabilities.max_input_tokens ?? capabilities.max_context_tokens; const model = profile.getModel(); return { usage, contextTokens, maxContextTokens, model }; } diff --git a/packages/node-sdk/src/rpc.ts b/packages/node-sdk/src/rpc.ts index a966f42cba..1315812c0b 100644 --- a/packages/node-sdk/src/rpc.ts +++ b/packages/node-sdk/src/rpc.ts @@ -581,7 +581,10 @@ export abstract class SDKRpcClientBase { const capability = config.modelCapabilities; const maxContextTokens = capability?.max_input_tokens ?? capability?.max_context_tokens ?? 0; const contextTokens = context.tokenCount; - const contextUsage = maxContextTokens > 0 ? Math.min(1, contextTokens / maxContextTokens) : 0; + // Deliberately unclamped: >100% is the documented overflow signal on this + // path (see acp-adapter's formatContextUsage), unlike the schema-bounded + // REST status surfaces which clamp to 1. + const contextUsage = maxContextTokens > 0 ? contextTokens / maxContextTokens : 0; const hasUsage = usage.byModel !== undefined || usage.total !== undefined || usage.currentTurn !== undefined; return { From 7c3d774095bbc1776de0d85875e60f23c4e356a4 Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Wed, 22 Jul 2026 02:30:48 +0800 Subject: [PATCH 5/5] fix: attribute resolved.maxInputSize provenance and drop test-body comments in v2 --- packages/agent-core-v2/src/kosong/model/inspection.ts | 1 + .../test/agent/fullCompaction/fullCompaction.test.ts | 3 --- packages/agent-core-v2/test/kosong/model/catalog.test.ts | 5 +++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/agent-core-v2/src/kosong/model/inspection.ts b/packages/agent-core-v2/src/kosong/model/inspection.ts index 72dcf3dc19..fa5bcd96eb 100644 --- a/packages/agent-core-v2/src/kosong/model/inspection.ts +++ b/packages/agent-core-v2/src/kosong/model/inspection.ts @@ -335,6 +335,7 @@ export function assembleModelInspection(args: { // Mirror the effective-field sources onto their resolved counterparts. for (const field of [ 'maxContextSize', + 'maxInputSize', 'maxOutputSize', 'displayName', 'reasoningKey', diff --git a/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts b/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts index 9f809c610c..0667272fd7 100644 --- a/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts +++ b/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts @@ -2268,9 +2268,6 @@ describe('FullCompaction', () => { await ctx.rpc.prompt({ input: [{ type: 'text', text: 'use observed window' }] }); const events = await ctx.untilTurnEnd(); - // Without the observed cap being written into max_input_tokens, the - // strategy would keep sizing against the raw 150k input cap and never - // preemptively compact the 120k context the provider already rejected. expect(callCount).toBe(5); expect(eventIndex(events, 'compaction.started')).toBeLessThan( eventIndex(events, 'turn.step.started'), diff --git a/packages/agent-core-v2/test/kosong/model/catalog.test.ts b/packages/agent-core-v2/test/kosong/model/catalog.test.ts index e7a9dadab5..a02ba10cd4 100644 --- a/packages/agent-core-v2/test/kosong/model/catalog.test.ts +++ b/packages/agent-core-v2/test/kosong/model/catalog.test.ts @@ -635,8 +635,6 @@ describe('ModelCatalog inspect', () => { kind: 'synthesized', }); - // An override value itself above the window is attributed to the clamp, - // not to models.*.overrides. const clampedOverrideView = catalog.inspect('clampedOverride'); expect(clampedOverrideView.resolved.maxInputSize).toBe(128000); expect(clampedOverrideView.sources['model.effective.maxInputSize']).toMatchObject({ @@ -646,6 +644,9 @@ describe('ModelCatalog inspect', () => { expect(clampedOverrideView.sources['model.effective.maxInputSize']).not.toMatchObject({ kind: 'override', }); + expect(clampedOverrideView.sources['resolved.maxInputSize']).toMatchObject({ + kind: 'synthesized', + }); const plainView = catalog.inspect('plain'); expect(plainView.sources['resolved.capabilities.max_input_tokens']).toMatchObject({