Skip to content

Commit ec88d35

Browse files
authored
fix: five correctness follow-ups to the catalog metadata work (MoonshotAI#2030)
* 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. * 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. * 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. * 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. * fix: attribute resolved.maxInputSize provenance and drop test-body comments in v2
1 parent b5efba7 commit ec88d35

23 files changed

Lines changed: 419 additions & 19 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@moonshot-ai/kosong": patch
3+
"@moonshot-ai/kimi-code-sdk": patch
4+
"@moonshot-ai/agent-core": patch
5+
"@moonshot-ai/agent-core-v2": patch
6+
"@moonshot-ai/kimi-code": patch
7+
---
8+
9+
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.

‎packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,13 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
184184

185185
private resolveModelContextWithEffectiveMax(): ProfileModelContext {
186186
const resolved = this.profile.resolveModelContext();
187+
const effectiveMax = this.getEffectiveMaxContextTokens();
187188
return {
188189
...resolved,
189190
modelCapabilities: {
190191
...resolved.modelCapabilities,
191-
max_context_tokens: this.getEffectiveMaxContextTokens(),
192+
max_context_tokens: effectiveMax,
193+
max_input_tokens: effectiveMax,
192194
},
193195
};
194196
}

‎packages/agent-core-v2/src/agent/profile/profileService.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,9 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ
593593
thinkingEffort: includeThinkingEffort
594594
? this.getEffectiveThinkingLevel()
595595
: undefined,
596-
maxContextTokens: this.getModelCapabilities().max_context_tokens,
596+
maxContextTokens:
597+
this.getModelCapabilities().max_input_tokens ??
598+
this.getModelCapabilities().max_context_tokens,
597599
});
598600
}
599601

‎packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,14 @@ export class SessionLegacyService implements ISessionLegacyService {
160160
const swarm = agent.accessor.get(IAgentSwarmService);
161161

162162
const model = profile.getModel();
163-
const caps = profile.getModelCapabilities() as { max_context_tokens?: number };
163+
const caps = profile.getModelCapabilities() as {
164+
max_context_tokens?: number;
165+
max_input_tokens?: number;
166+
};
164167
const maxTokens =
165-
model === '' ? resolveDefaultModelContextTokens(agent) : (caps.max_context_tokens ?? 0);
168+
model === ''
169+
? resolveDefaultModelContextTokens(agent)
170+
: (caps.max_input_tokens ?? caps.max_context_tokens ?? 0);
166171
const tokens = contextSize.get().size;
167172
const planData = await plan.status();
168173

@@ -175,7 +180,7 @@ export class SessionLegacyService implements ISessionLegacyService {
175180
swarm_mode: swarm.isActive,
176181
context_tokens: tokens,
177182
max_context_tokens: maxTokens,
178-
context_usage: maxTokens > 0 ? tokens / maxTokens : 0,
183+
context_usage: maxTokens > 0 ? Math.min(1, tokens / maxTokens) : 0,
179184
};
180185
}
181186

@@ -204,7 +209,8 @@ function resolveDefaultModelContextTokens(agent: IAgentScopeHandle): number {
204209
const defaultModel = agent.accessor.get(IConfigService).get<string>('defaultModel');
205210
if (typeof defaultModel !== 'string' || defaultModel.length === 0) return 0;
206211
try {
207-
return agent.accessor.get(IModelCatalog).get(defaultModel).capabilities.max_context_tokens;
212+
const capabilities = agent.accessor.get(IModelCatalog).get(defaultModel).capabilities;
213+
return capabilities.max_input_tokens ?? capabilities.max_context_tokens;
208214
} catch {
209215
return 0;
210216
}

‎packages/agent-core-v2/src/kosong/model/inspection.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,20 @@ export function attributeEffectiveFields(
184184
const before = (base as Record<string, unknown>)[key];
185185
const after = (effective as Record<string, unknown>)[key];
186186
if (before === undefined && after === undefined) continue;
187+
if (key === 'maxInputSize') {
188+
const rawValue = (overridden.has(key) ? overrides?.[key] : before) as number | undefined;
189+
if (
190+
rawValue !== undefined &&
191+
effective.maxContextSize !== undefined &&
192+
rawValue > effective.maxContextSize
193+
) {
194+
trace.record(path, {
195+
kind: 'synthesized',
196+
detail: 'clamped to the effective max_context_size',
197+
});
198+
continue;
199+
}
200+
}
187201
if (overridden.has(key)) {
188202
trace.record(path, { kind: 'override', detail: 'models.*.overrides' });
189203
continue;
@@ -321,6 +335,7 @@ export function assembleModelInspection(args: {
321335
// Mirror the effective-field sources onto their resolved counterparts.
322336
for (const field of [
323337
'maxContextSize',
338+
'maxInputSize',
324339
'maxOutputSize',
325340
'displayName',
326341
'reasoningKey',
@@ -486,6 +501,14 @@ function attributeCapabilities(
486501
kind: 'synthesized',
487502
detail: 'forced to the resolved maxContextSize',
488503
});
504+
const maxInputSource = sources.get('model.effective.maxInputSize');
505+
sources.set(
506+
'resolved.capabilities.max_input_tokens',
507+
maxInputSource ?? {
508+
kind: 'none',
509+
detail: 'no declared input limit — the total window applies',
510+
},
511+
);
489512
}
490513

491514
function attributeHeaders(

‎packages/agent-core-v2/src/kosong/model/modelAuth.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,13 @@ export function effectiveModelConfig(
119119
) {
120120
delete effective.defaultEffort;
121121
}
122-
return withAnthropicProfile(effective, providerType);
122+
const clamped =
123+
effective.maxInputSize !== undefined &&
124+
effective.maxContextSize !== undefined &&
125+
effective.maxInputSize > effective.maxContextSize
126+
? { ...effective, maxInputSize: effective.maxContextSize }
127+
: effective;
128+
return withAnthropicProfile(clamped, providerType);
123129
}
124130

125131
function withAnthropicProfile(model: ModelRecord, providerType?: string): ModelRecord {

‎packages/agent-core-v2/src/kosong/model/thinking.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export function resolveForcedThinkingEffort(
155155
traitDriven: boolean,
156156
): ThinkingEffort | undefined {
157157
if (!traitDriven || effective === 'off') return undefined;
158-
return nonEmpty(forced) as ThinkingEffort | undefined;
158+
return nonEmpty(forced)?.toLowerCase() as ThinkingEffort | undefined;
159159
}
160160

161161
function hasCapability(
@@ -257,7 +257,7 @@ export function resolveThinkingEffortForModel(
257257
model: ModelThinkingMetadata | undefined,
258258
strictValidation = false,
259259
): ThinkingEffort {
260-
const configured = nonEmpty(defaults?.effort) as ThinkingEffort | undefined;
260+
const configured = normalizeRequestedThinkingEffort(defaults?.effort);
261261
const normalized = normalizeRequestedThinkingEffort(requested);
262262
let effort: ThinkingEffort;
263263
if (normalized !== undefined) {

‎packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2217,6 +2217,63 @@ describe('FullCompaction', () => {
22172217
await ctx.expectResumeMatches();
22182218
});
22192219

2220+
it('honors the observed provider window over a declared input cap', async () => {
2221+
let callCount = 0;
2222+
const generate: GenerateFn = async (_provider, _system, _tools, _history, callbacks) => {
2223+
callCount += 1;
2224+
if (callCount === 1) {
2225+
throw new APIContextOverflowError(400, 'Context length exceeded', 'req-observed-window');
2226+
}
2227+
if (callCount === 2) {
2228+
return textResult('Observed recovery summary.');
2229+
}
2230+
if (callCount === 3) {
2231+
await callbacks?.onMessagePart?.({
2232+
type: 'text',
2233+
text: 'Recovered after observed overflow.',
2234+
});
2235+
return textResult('Recovered after observed overflow.');
2236+
}
2237+
if (callCount === 4) {
2238+
return textResult('Observed preemptive summary.');
2239+
}
2240+
if (callCount === 5) {
2241+
await callbacks?.onMessagePart?.({
2242+
type: 'text',
2243+
text: 'Answered after observed-window precompaction.',
2244+
});
2245+
return textResult('Answered after observed-window precompaction.');
2246+
}
2247+
throw new Error(`Unexpected generate call ${String(callCount)}`);
2248+
};
2249+
const ctx = testAgent({ generate });
2250+
ctx.configure({
2251+
provider: CATALOGUED_PROVIDER,
2252+
modelCapabilities: {
2253+
...CATALOGUED_MODEL_CAPABILITIES,
2254+
max_context_tokens: 200_000,
2255+
max_input_tokens: 150_000,
2256+
},
2257+
tools: SNAPSHOT_VISIBLE_TOOLS,
2258+
});
2259+
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
2260+
ctx.newEvents();
2261+
2262+
await ctx.rpc.prompt({ input: [{ type: 'text', text: 'learn observed window' }] });
2263+
await ctx.untilTurnEnd();
2264+
expect(callCount).toBe(3);
2265+
2266+
ctx.appendExchange(2, 'near observed user', 'near observed assistant', 120_000);
2267+
ctx.newEvents();
2268+
await ctx.rpc.prompt({ input: [{ type: 'text', text: 'use observed window' }] });
2269+
const events = await ctx.untilTurnEnd();
2270+
2271+
expect(callCount).toBe(5);
2272+
expect(eventIndex(events, 'compaction.started')).toBeLessThan(
2273+
eventIndex(events, 'turn.step.started'),
2274+
);
2275+
});
2276+
22202277
it('recovers from plain 413 when estimated request is over effective max', async () => {
22212278
let callCount = 0;
22222279
const generate: GenerateFn = async (_provider, _system, _tools, _history, callbacks) => {

‎packages/agent-core-v2/test/agent/profile/thinking.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
33
import {
44
defaultThinkingEffortForModel,
55
modelSupportsThinkingEffort,
6+
resolveForcedThinkingEffort,
67
resolveThinkingEffortForModel,
78
} from '#/kosong/model/thinking';
89

@@ -158,6 +159,19 @@ describe('resolveThinkingEffortForModel', () => {
158159
expect(resolveThinkingEffortForModel('off', undefined, alwaysThinkingModel)).toBe('on');
159160
});
160161

162+
it('normalizes a configured off value (case/whitespace) instead of sending it upstream', () => {
163+
expect(resolveThinkingEffortForModel(undefined, { effort: ' OFF ' }, effortModel)).toBe('off');
164+
expect(resolveThinkingEffortForModel(undefined, { effort: 'Off' }, booleanModel)).toBe('off');
165+
expect(
166+
resolveThinkingEffortForModel(undefined, { enabled: false, effort: ' OFF ' }, alwaysThinkingEffortModel),
167+
).toBe('high');
168+
});
169+
170+
it('normalizes the env-forced effort (case/whitespace)', () => {
171+
expect(resolveForcedThinkingEffort(' MAX ', 'high', true)).toBe('max');
172+
expect(resolveForcedThinkingEffort(' ', 'high', true)).toBeUndefined();
173+
});
174+
161175
it('treats a configured off as absent when clamping always-thinking models', () => {
162176
expect(resolveThinkingEffortForModel(undefined, { effort: 'off' }, alwaysThinkingEffortModel)).toBe(
163177
'high',

‎packages/agent-core-v2/test/app/model/model.test.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,31 @@ import '#/kosong/provider/providers/kimi/kimi.contrib';
2828
import '#/kosong/provider/providers/standard.contrib';
2929

3030
describe('effectiveModelConfig', () => {
31+
it('clamps the input cap to the effective total window without mutating the source', () => {
32+
const record = {
33+
provider: 'custom',
34+
model: 'gpt-5',
35+
maxContextSize: 128000,
36+
maxInputSize: 272000,
37+
};
38+
39+
const effective = effectiveModelConfig(record);
40+
expect(effective.maxInputSize).toBe(128000);
41+
expect(record.maxInputSize).toBe(272000);
42+
43+
const withOverrides = {
44+
provider: 'custom',
45+
model: 'gpt-5',
46+
maxContextSize: 400000,
47+
maxInputSize: 272000,
48+
overrides: { maxContextSize: 128000 },
49+
};
50+
const effectiveOverride = effectiveModelConfig(withOverrides);
51+
expect(effectiveOverride.maxContextSize).toBe(128000);
52+
expect(effectiveOverride.maxInputSize).toBe(128000);
53+
expect(withOverrides.maxInputSize).toBe(272000);
54+
});
55+
3156
it('derives the official effort metadata from a Claude model name', () => {
3257
expect(
3358
effectiveModelConfig({

0 commit comments

Comments
 (0)