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
9 changes: 9 additions & 0 deletions .changeset/catalog-followup-correctness.md
Original file line number Diff line number Diff line change
@@ -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: 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.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
}
Expand Down
4 changes: 3 additions & 1 deletion packages/agent-core-v2/src/agent/profile/profileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment thread
RealKai42 marked this conversation as resolved.
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
RealKai42 marked this conversation as resolved.
: (caps.max_input_tokens ?? caps.max_context_tokens ?? 0);
Comment thread
RealKai42 marked this conversation as resolved.
const tokens = contextSize.get().size;
const planData = await plan.status();

Expand All @@ -175,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,
};
}

Expand Down Expand Up @@ -204,7 +209,8 @@ function resolveDefaultModelContextTokens(agent: IAgentScopeHandle): number {
const defaultModel = agent.accessor.get(IConfigService).get<string>('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;
}
Expand Down
23 changes: 23 additions & 0 deletions packages/agent-core-v2/src/kosong/model/inspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,20 @@ export function attributeEffectiveFields(
const before = (base as Record<string, unknown>)[key];
const after = (effective as Record<string, unknown>)[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;
Expand Down Expand Up @@ -321,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',
Expand Down Expand Up @@ -486,6 +501,14 @@ function attributeCapabilities(
kind: 'synthesized',
detail: 'forced to the resolved maxContextSize',
});
const maxInputSource = sources.get('model.effective.maxInputSize');
sources.set(
'resolved.capabilities.max_input_tokens',
Comment thread
RealKai42 marked this conversation as resolved.
maxInputSource ?? {
kind: 'none',
detail: 'no declared input limit — the total window applies',
},
);
}

function attributeHeaders(
Expand Down
8 changes: 7 additions & 1 deletion packages/agent-core-v2/src/kosong/model/modelAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,13 @@ export function effectiveModelConfig(
) {
delete effective.defaultEffort;
}
return withAnthropicProfile(effective, providerType);
const clamped =
effective.maxInputSize !== undefined &&
effective.maxContextSize !== undefined &&
effective.maxInputSize > effective.maxContextSize
? { ...effective, maxInputSize: effective.maxContextSize }
: effective;
return withAnthropicProfile(clamped, providerType);
}

function withAnthropicProfile(model: ModelRecord, providerType?: string): ModelRecord {
Expand Down
4 changes: 2 additions & 2 deletions packages/agent-core-v2/src/kosong/model/thinking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2217,6 +2217,63 @@ 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();

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) => {
Expand Down
14 changes: 14 additions & 0 deletions packages/agent-core-v2/test/agent/profile/thinking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
import {
defaultThinkingEffortForModel,
modelSupportsThinkingEffort,
resolveForcedThinkingEffort,
resolveThinkingEffortForModel,
} from '#/kosong/model/thinking';

Expand Down Expand Up @@ -158,6 +159,19 @@ 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('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',
Expand Down
25 changes: 25 additions & 0 deletions packages/agent-core-v2/test/app/model/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,31 @@ 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 without mutating the source', () => {
const record = {
provider: 'custom',
model: 'gpt-5',
maxContextSize: 128000,
maxInputSize: 272000,
};

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', () => {
expect(
effectiveModelConfig({
Expand Down
71 changes: 71 additions & 0 deletions packages/agent-core-v2/test/kosong/model/catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Expand Down Expand Up @@ -586,6 +589,74 @@ 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 },
},
clampedOverride: {
provider: 'kimi',
model: 'kimi-k2',
maxContextSize: 400000,
overrides: { maxContextSize: 128000, maxInputSize: 272000 },
},
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 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',
});
expect(clampedOverrideView.sources['resolved.maxInputSize']).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: {
Expand Down
11 changes: 8 additions & 3 deletions packages/agent-core/src/agent/config/thinking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,18 @@ 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; 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;
} 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) {
Expand All @@ -119,8 +124,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);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core/src/config/kimi-env-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading
Loading