diff --git a/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts b/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts index e89c49a6ab..4fd66d470d 100644 --- a/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts +++ b/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts @@ -33,6 +33,7 @@ import { isRetryableGenerateError, } from '#/kosong/contract/errors'; import { createUserMessage, type Message } from '#/kosong/contract/message'; +import { getModelInputTokenLimit } from '#/kosong/contract/capability'; import type { Tool } from '#/kosong/contract/tool'; import { inputTotal, type TokenUsage } from '#/kosong/contract/usage'; import { IEventBus } from '#/app/event/eventBus'; @@ -173,7 +174,7 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull private getEffectiveMaxContextTokens(): number { const capability = this.profile.data().modelCapabilities; - const configured = capability.max_input_tokens ?? capability.max_context_tokens; + const configured = getModelInputTokenLimit(capability); const modelAlias = this.profile.data().modelAlias; const observed = modelAlias === undefined ? undefined : this.observedMaxContextTokensByModel.get(modelAlias); diff --git a/packages/agent-core-v2/src/agent/fullCompaction/strategy.ts b/packages/agent-core-v2/src/agent/fullCompaction/strategy.ts index c8ebb8e579..31b5d88235 100644 --- a/packages/agent-core-v2/src/agent/fullCompaction/strategy.ts +++ b/packages/agent-core-v2/src/agent/fullCompaction/strategy.ts @@ -1,4 +1,5 @@ import type { Message } from '#/kosong/contract/message'; +import { getModelInputTokenLimit } from '#/kosong/contract/capability'; import type { ProfileModelContext } from '#/agent/profile/profile'; import type { CompactionSource } from './types'; import { estimateTokensForMessage } from '#/kosong/contract/tokens'; @@ -71,14 +72,14 @@ export class RuntimeCompactionStrategy implements CompactionStrategy { private delegate(): DefaultCompactionStrategy { const model = this.context(); return new DefaultCompactionStrategy( - () => model.modelCapabilities.max_input_tokens ?? model.modelCapabilities.max_context_tokens, + () => getModelInputTokenLimit(model.modelCapabilities), this.config(model), ); } private windowDelegate(): DefaultCompactionStrategy { return new DefaultCompactionStrategy( - () => this.context().modelCapabilities.max_input_tokens ?? this.context().modelCapabilities.max_context_tokens, + () => getModelInputTokenLimit(this.context().modelCapabilities), DEFAULT_COMPACTION_CONFIG, ); } diff --git a/packages/agent-core-v2/src/agent/profile/profileService.ts b/packages/agent-core-v2/src/agent/profile/profileService.ts index 2dcd419555..f518982833 100644 --- a/packages/agent-core-v2/src/agent/profile/profileService.ts +++ b/packages/agent-core-v2/src/agent/profile/profileService.ts @@ -49,7 +49,11 @@ import { InstantiationType } from '#/_base/di/extensions'; import { Disposable } from '#/_base/di/lifecycle'; import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; -import { UNKNOWN_CAPABILITY, type ModelCapability } from '#/kosong/contract/capability'; +import { + UNKNOWN_CAPABILITY, + getModelInputTokenLimit, + type ModelCapability, +} from '#/kosong/contract/capability'; import { type SamplingOptions, type ThinkingEffort } from '#/kosong/contract/provider'; import { IModelCatalog, type Model } from '#/kosong/model/catalog'; import { type ModelOverrides } from '#/kosong/model/model.types'; @@ -587,9 +591,7 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ thinkingEffort: includeThinkingEffort ? this.getEffectiveThinkingLevel() : undefined, - maxContextTokens: - this.getModelCapabilities().max_input_tokens ?? - this.getModelCapabilities().max_context_tokens, + maxContextTokens: getModelInputTokenLimit(this.getModelCapabilities()), }); } diff --git a/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts b/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts index bf03d35159..69a7afa56d 100644 --- a/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts +++ b/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts @@ -28,6 +28,7 @@ import { IAgentProfileService } from '#/agent/profile/profile'; import { IAgentSwarmService } from '#/agent/swarm/swarm'; import { IConfigService } from '#/app/config/config'; import { IModelCatalog } from '#/kosong/model/catalog'; +import { getModelInputTokenLimit } from '#/kosong/contract/capability'; import { ISessionLifecycleService } from '#/app/sessionLifecycle/sessionLifecycle'; import { ErrorCodes, Error2 } from '#/errors'; import { ensureMainAgent } from '#/session/agentLifecycle/mainAgent'; @@ -160,14 +161,11 @@ export class SessionLegacyService implements ISessionLegacyService { const swarm = agent.accessor.get(IAgentSwarmService); const model = profile.getModel(); - const caps = profile.getModelCapabilities() as { - max_context_tokens?: number; - max_input_tokens?: number; - }; + const caps = profile.getModelCapabilities(); const maxTokens = model === '' ? resolveDefaultModelContextTokens(agent) - : (caps.max_input_tokens ?? caps.max_context_tokens ?? 0); + : getModelInputTokenLimit(caps); const tokens = contextSize.get().size; const planData = await plan.status(); @@ -210,7 +208,7 @@ function resolveDefaultModelContextTokens(agent: IAgentScopeHandle): number { if (typeof defaultModel !== 'string' || defaultModel.length === 0) return 0; try { const capabilities = agent.accessor.get(IModelCatalog).get(defaultModel).capabilities; - return capabilities.max_input_tokens ?? capabilities.max_context_tokens; + return getModelInputTokenLimit(capabilities); } catch { return 0; } diff --git a/packages/agent-core-v2/src/kosong/contract/capability.ts b/packages/agent-core-v2/src/kosong/contract/capability.ts index fa75b8187e..273a02f06a 100644 --- a/packages/agent-core-v2/src/kosong/contract/capability.ts +++ b/packages/agent-core-v2/src/kosong/contract/capability.ts @@ -5,9 +5,10 @@ * so callers can gate requests against what the model accepts without * dispatching the request and watching it fail upstream. * - * `UNKNOWN_CAPABILITY` is the marker value returned when nothing is known - * about a model: `max_context_tokens: 0` means "unknown"; callers that do - * not gate on context length can ignore the field. + * `getModelInputTokenLimit` resolves the prompt/input ceiling before callers + * make compaction or status decisions. `UNKNOWN_CAPABILITY` is the marker + * value returned when nothing is known about a model: + * `max_context_tokens: 0` means "unknown". */ export interface ModelCapability { @@ -21,6 +22,10 @@ export interface ModelCapability { readonly dynamically_loaded_tools?: boolean; } +export function getModelInputTokenLimit(capability: ModelCapability | undefined): number { + return capability?.max_input_tokens ?? capability?.max_context_tokens ?? 0; +} + const UNKNOWN_CAPABILITY_MARKER = Symbol.for('moonshot-ai.kosong.UNKNOWN_CAPABILITY'); export const UNKNOWN_CAPABILITY: ModelCapability = Object.freeze( diff --git a/packages/agent-core-v2/test/kosong/provider/composition.test.ts b/packages/agent-core-v2/test/kosong/provider/composition.test.ts index e51b436967..0a0fd5c374 100644 --- a/packages/agent-core-v2/test/kosong/provider/composition.test.ts +++ b/packages/agent-core-v2/test/kosong/provider/composition.test.ts @@ -40,7 +40,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { isUnknownCapability, UNKNOWN_CAPABILITY } from '#/kosong/contract/capability'; +import { + getModelInputTokenLimit, + isUnknownCapability, + UNKNOWN_CAPABILITY, + type ModelCapability, +} from '#/kosong/contract/capability'; import { APIConnectionError } from '#/kosong/contract/errors'; import type { Message } from '#/kosong/contract/message'; import type { @@ -300,6 +305,25 @@ describe('resolveCapability', () => { }); }); +describe('getModelInputTokenLimit', () => { + const capability: ModelCapability = { + image_in: false, + video_in: false, + audio_in: false, + thinking: false, + tool_use: true, + max_context_tokens: 128_000, + }; + + it.each([ + ['the declared input cap', { ...capability, max_input_tokens: 64_000 }, 64_000], + ['the total context window when no input cap is declared', capability, 128_000], + ['zero when the capability is unavailable', undefined, 0], + ])('resolves %s', (_label, value, expected) => { + expect(getModelInputTokenLimit(value)).toBe(expected); + }); +}); + describe('explainCapability', () => { it('reports the definition level when the pair declares a capability', () => { const { capability, source } = registry.explainCapability('openai', 'gpt-4o', 'kimi'); diff --git a/packages/agent-core/src/agent/compaction/full.ts b/packages/agent-core/src/agent/compaction/full.ts index f56c7bc919..efeccdf00f 100644 --- a/packages/agent-core/src/agent/compaction/full.ts +++ b/packages/agent-core/src/agent/compaction/full.ts @@ -16,6 +16,7 @@ import { APIRequestTooLargeError, APIStatusError, createUserMessage, + getModelInputTokenLimit, isImageFormatError, } from '@moonshot-ai/kosong'; @@ -125,7 +126,7 @@ export class FullCompaction { getEffectiveMaxContextTokens(): number { const capability = this.agent.config.modelCapabilities; - const configured = capability.max_input_tokens ?? capability.max_context_tokens; + const configured = getModelInputTokenLimit(capability); const modelAlias = this.agent.config.modelAlias; const observed = modelAlias === undefined ? undefined : this.observedMaxContextTokensByModel.get(modelAlias); diff --git a/packages/agent-core/src/agent/context/index.ts b/packages/agent-core/src/agent/context/index.ts index 2ac5096e76..e2f2716375 100644 --- a/packages/agent-core/src/agent/context/index.ts +++ b/packages/agent-core/src/agent/context/index.ts @@ -1,4 +1,9 @@ -import { createToolMessage, type ContentPart, type Message } from '@moonshot-ai/kosong'; +import { + createToolMessage, + getModelInputTokenLimit, + type ContentPart, + type Message, +} from '@moonshot-ai/kosong'; import type { Agent } from '..'; import { ErrorCodes, KimiError } from '../../errors'; @@ -220,7 +225,7 @@ export class ContextMemory { const importTokenCount = estimateTokensForMessages([message]); const totalTokenCount = currentTokenCount + importTokenCount; const capability = this.agent.config.modelCapabilities; - const maxContextTokens = capability.max_input_tokens ?? capability.max_context_tokens; + const maxContextTokens = getModelInputTokenLimit(capability); if (maxContextTokens > 0 && totalTokenCount > maxContextTokens) { throw new KimiError( ErrorCodes.CONTEXT_OVERFLOW, diff --git a/packages/agent-core/src/agent/index.ts b/packages/agent-core/src/agent/index.ts index 26a2efe99c..09b7eba284 100644 --- a/packages/agent-core/src/agent/index.ts +++ b/packages/agent-core/src/agent/index.ts @@ -6,7 +6,7 @@ import { ErrorCodes, KimiError, makeErrorPayload } from '#/errors'; import { log } from '#/logging/logger'; import type { Logger } from '#/logging/types'; import type { AgentAPI, AgentEvent, KimiConfig, SDKAgentRPC, UsageStatus } from '#/rpc'; -import { generate, type ChatProvider } from '@moonshot-ai/kosong'; +import { generate, getModelInputTokenLimit, type ChatProvider } from '@moonshot-ai/kosong'; import type { EnabledPluginSessionStart, PluginCommandDef } from '#/plugin'; import { expandCommandArguments } from '../plugin/commands'; @@ -674,7 +674,7 @@ export class Agent { const contextTokens = this.context.tokenCount; const capability = this.config.modelCapabilities; - const maxContextTokens = capability.max_input_tokens ?? capability.max_context_tokens; + const maxContextTokens = getModelInputTokenLimit(capability); const contextUsage = maxContextTokens !== undefined && maxContextTokens > 0 ? contextTokens / maxContextTokens diff --git a/packages/agent-core/src/index.ts b/packages/agent-core/src/index.ts index 46b97172a5..2efff073f0 100644 --- a/packages/agent-core/src/index.ts +++ b/packages/agent-core/src/index.ts @@ -20,6 +20,7 @@ export { export { resolveLoggingConfig } from './logging/resolve-config'; export type { ResolveLoggingInput } from './logging/resolve-config'; export { installGlobalProxyDispatcher } from './utils/proxy'; +export { getModelInputTokenLimit } from '@moonshot-ai/kosong'; export type { LogContext, LogEntry, diff --git a/packages/agent-core/src/services/session/sessionService.ts b/packages/agent-core/src/services/session/sessionService.ts index 8424f30457..7902267356 100644 --- a/packages/agent-core/src/services/session/sessionService.ts +++ b/packages/agent-core/src/services/session/sessionService.ts @@ -23,6 +23,7 @@ import { type UndoSessionRequest, type UndoSessionResponse, } from '@moonshot-ai/protocol'; +import { getModelInputTokenLimit } from '@moonshot-ai/kosong'; import { IApprovalService } from '../approval/approval'; import { ICoreProcessService } from '../coreProcess/coreProcess'; @@ -473,7 +474,7 @@ export class SessionService extends Disposable implements ISessionService { ]); const capability = config.modelCapabilities; - const maxContextTokens = capability?.max_input_tokens ?? capability?.max_context_tokens ?? 0; + const maxContextTokens = getModelInputTokenLimit(capability); const contextTokens = context.tokenCount; const contextUsage = maxContextTokens > 0 ? Math.min(1, contextTokens / maxContextTokens) : 0; diff --git a/packages/kap-server/src/services/legacyStatus/legacyStatus.ts b/packages/kap-server/src/services/legacyStatus/legacyStatus.ts index c70057c034..1c8819fe8a 100644 --- a/packages/kap-server/src/services/legacyStatus/legacyStatus.ts +++ b/packages/kap-server/src/services/legacyStatus/legacyStatus.ts @@ -20,6 +20,7 @@ import { IAgentProfileService, IAgentUsageService, IWireService, + getModelInputTokenLimit, type IAgentScopeHandle, type UsageStatus, } from '@moonshot-ai/agent-core-v2'; @@ -134,7 +135,7 @@ export function readLegacyStatus(agent: IAgentScopeHandle): LegacyStatusSnapshot const measured = wire.getModel(ContextSizeModel); const contextTokens = Math.max(contextSize.get().size, measured.tokens); const capabilities = profile.getModelCapabilities(); - const maxContextTokens = capabilities.max_input_tokens ?? capabilities.max_context_tokens; + const maxContextTokens = getModelInputTokenLimit(capabilities); const model = profile.getModel(); return { usage, contextTokens, maxContextTokens, model }; } diff --git a/packages/kosong/src/capability.ts b/packages/kosong/src/capability.ts index f2d8249908..99c44c7d31 100644 --- a/packages/kosong/src/capability.ts +++ b/packages/kosong/src/capability.ts @@ -33,6 +33,11 @@ export interface ModelCapability { readonly dynamically_loaded_tools?: boolean; } +/** Return the model's prompt/input limit, or 0 when the limit is unknown. */ +export function getModelInputTokenLimit(capability: ModelCapability | undefined): number { + return capability?.max_input_tokens ?? capability?.max_context_tokens ?? 0; +} + const UNKNOWN_CAPABILITY_MARKER = Symbol.for('moonshot-ai.kosong.UNKNOWN_CAPABILITY'); /** diff --git a/packages/kosong/src/index.ts b/packages/kosong/src/index.ts index d999a02bc6..f68c078cec 100644 --- a/packages/kosong/src/index.ts +++ b/packages/kosong/src/index.ts @@ -34,7 +34,7 @@ export { KimiChatProvider } from './providers/kimi'; export type { ExtraBody, GenerationKwargs, KimiOptions, ThinkingConfig } from './providers/kimi'; // Model capability matrix -export { isUnknownCapability, UNKNOWN_CAPABILITY } from './capability'; +export { getModelInputTokenLimit, isUnknownCapability, UNKNOWN_CAPABILITY } from './capability'; export type { ModelCapability } from './capability'; // Model catalog (models.dev-style) metadata diff --git a/packages/kosong/test/capability.test.ts b/packages/kosong/test/capability.test.ts index 93846103b0..e7df01ea64 100644 --- a/packages/kosong/test/capability.test.ts +++ b/packages/kosong/test/capability.test.ts @@ -1,13 +1,35 @@ /** - * `ModelCapability` type and `UNKNOWN_CAPABILITY` default — pure + * `ModelCapability` type, input-limit resolution, and `UNKNOWN_CAPABILITY` default — pure * type/helper layer. Per-wire `getModelCapability` tables live in * `capability-providers.test.ts`. */ -import { UNKNOWN_CAPABILITY, isUnknownCapability, type ModelCapability } from '#/capability'; +import { + UNKNOWN_CAPABILITY, + getModelInputTokenLimit, + isUnknownCapability, + type ModelCapability, +} from '#/capability'; import { describe, expect, it } from 'vitest'; describe('ModelCapability / UNKNOWN_CAPABILITY', () => { + const capability: ModelCapability = { + image_in: false, + video_in: false, + audio_in: false, + thinking: false, + tool_use: true, + max_context_tokens: 128_000, + }; + + it.each([ + ['the declared input cap', { ...capability, max_input_tokens: 64_000 }, 64_000], + ['the total context window when no input cap is declared', capability, 128_000], + ['zero when the capability is unavailable', undefined, 0], + ])('resolves %s', (_label, value, expected) => { + expect(getModelInputTokenLimit(value)).toBe(expected); + }); + it('UNKNOWN_CAPABILITY has all boolean fields false', () => { expect(UNKNOWN_CAPABILITY.image_in).toBe(false); expect(UNKNOWN_CAPABILITY.video_in).toBe(false); diff --git a/packages/node-sdk/src/rpc.ts b/packages/node-sdk/src/rpc.ts index 1315812c0b..4d80915f45 100644 --- a/packages/node-sdk/src/rpc.ts +++ b/packages/node-sdk/src/rpc.ts @@ -2,6 +2,7 @@ import { AsyncLocalStorage } from 'node:async_hooks'; import { ErrorCodes, + getModelInputTokenLimit, makeErrorPayload, type AgentContextData, type ApprovalRequest, @@ -579,7 +580,7 @@ export abstract class SDKRpcClientBase { agentId, }); const capability = config.modelCapabilities; - const maxContextTokens = capability?.max_input_tokens ?? capability?.max_context_tokens ?? 0; + const maxContextTokens = getModelInputTokenLimit(capability); const contextTokens = context.tokenCount; // Deliberately unclamped: >100% is the documented overflow signal on this // path (see acp-adapter's formatContextUsage), unlike the schema-bounded