1
/**2
* Load-time validation and routed-model policy resolution for compaction-basic.3
*4
* @module @deepseek-ai/dsh-compaction-basic/config5
*/7
import type { LlmCallConfig } from '@deepseek-ai/dsh-llm'8
import { deepFreeze } from '@deepseek-ai/dsh-util-values'9
import type {10
BasicCompactionConfig,11
CompactionPolicyConfig,12
ModelCompactPolicyConfig,13
ResolvedCompactSpec,14
ResolvedConfig,15
ResolvedRetention,16
ResolvedTargetPolicy,17
} from './types.ts'19
/** Default request-pressure fraction for every routed model. */20
const DEFAULT_THRESHOLD_RATIO = 0.822
/** Default verbatim-tail fraction for every routed model. */23
const DEFAULT_RETAIN_RATIO = 0.1625
/** Fields shared by top-level defaults and exact-target overrides. */26
const POLICY_CONFIG_KEYS = [27
'thresholdRatio',28
'headroomTokens',29
'retainRatio',30
'retainTokens',31
'summarizationProvider',32
'summarizationModel',33
'maxTokens',34
'compactionRetries',35
'maxOverflowRetries',36
] as const38
/** Complete public top-level configuration key set. */39
const BASIC_COMPACT_CONFIG_KEYS: ReadonlySet<string> = new Set([40
...POLICY_CONFIG_KEYS,41
'modelPolicies',42
'auto',43
])45
/** Complete exact-target override key set. */46
const MODEL_POLICY_KEYS: ReadonlySet<string> = new Set([47
'provider',48
'model',49
...POLICY_CONFIG_KEYS,50
])52
/** Target-specific pressure configuration failure eligible for warning suppression. */53
export class TargetPressureConfigError extends Error {54
/**55
* @param targetKey - exact provider/model route used as the warning key.56
* @param message - actionable configuration failure detail.57
*/58
constructor(readonly targetKey: string, message: string) {59
super(message)60
}61
}63
/**64
* Resolve and validate service defaults plus exact-target partial overrides.65
* @param config - untrusted plugin configuration after Loader normalization.66
* @returns detached immutable defaults and validated exact-target overrides.67
*/68
export function resolveConfig(config: BasicCompactionConfig = {}): ResolvedConfig {69
validateKeys(config, BASIC_COMPACT_CONFIG_KEYS, 'BasicCompactionConfig')70
validatePolicy(config, 'BasicCompactionConfig')71
if (config.auto !== undefined && typeof config.auto !== 'boolean') {72
throw new Error('BasicCompactionConfig: auto must be a boolean')73
}75
const headroomTokens = config.headroomTokens ?? 65_53676
const maxTokens = config.maxTokens ?? headroomTokens77
assertPositiveInteger('BasicCompactionConfig.maxTokens (explicit or from headroomTokens)', maxTokens)78
const thresholdRatio = config.thresholdRatio ?? DEFAULT_THRESHOLD_RATIO79
const retention = resolveRetention(config, { retainRatio: DEFAULT_RETAIN_RATIO })80
validateRatioRetention(thresholdRatio, retention, 'BasicCompactionConfig')81
const modelPolicies = resolveModelPolicies(config.modelPolicies)82
for (const [index, policy] of modelPolicies.entries()) {83
if (policy.maxTokens === undefined && config.maxTokens === undefined84
&& policy.headroomTokens !== undefined) {85
policy.maxTokens = policy.headroomTokens86
}87
assertPositiveInteger(88
`BasicCompactionConfig: modelPolicies[${index}].maxTokens (explicit or from headroomTokens)`,89
policy.maxTokens ?? maxTokens,90
)91
validateRatioRetention(92
policy.thresholdRatio ?? thresholdRatio,93
resolveRetention(policy, retention),94
`BasicCompactionConfig: modelPolicies[${index}]`,95
)96
}98
return deepFreeze({99
thresholdRatio,100
headroomTokens,101
...retention,102
summarizationProvider: config.summarizationProvider ?? '',103
summarizationModel: config.summarizationModel ?? '',104
maxTokens,105
compactionRetries: config.compactionRetries ?? 1,106
maxOverflowRetries: config.maxOverflowRetries ?? 1,107
modelPolicies,108
auto: config.auto ?? true,109
})110
}112
/**113
* Merge the exact provider/model override over the validated default policy.114
* @param config - validated service defaults and override table.115
* @param target - exact durable provider/model route to match.116
* @returns detached immutable policy before model-capacity scaling.117
*/118
export function resolveTargetPolicy(119
config: ResolvedConfig,120
target: Pick<LlmCallConfig, 'provider' | 'model'>,121
): ResolvedTargetPolicy {122
const override = config.modelPolicies.find(policy => (123
policy.provider === target.provider && policy.model === target.model124
))125
const inheritedRetention: ResolvedRetention = config.retainTokens === undefined126
? { retainRatio: config.retainRatio }127
: { retainTokens: config.retainTokens }128
return deepFreeze({129
target: { provider: target.provider, model: target.model },130
thresholdRatio: override?.thresholdRatio ?? config.thresholdRatio,131
headroomTokens: override?.headroomTokens ?? config.headroomTokens,132
...resolveRetention(override ?? {}, inheritedRetention),133
summarizationProvider: override?.summarizationProvider ?? config.summarizationProvider,134
summarizationModel: override?.summarizationModel ?? config.summarizationModel,135
maxTokens: override?.maxTokens ?? config.maxTokens,136
compactionRetries: override?.compactionRetries ?? config.compactionRetries,137
maxOverflowRetries: override?.maxOverflowRetries ?? config.maxOverflowRetries,138
})139
}141
/**142
* Scale one routed policy into concrete token budgets for its model capacity.143
*144
* Pressure is capped by both the window fraction and the capacity remaining145
* after the routed output reservation plus compaction headroom. Retention scales146
* the message budget before headroom is deducted.147
*148
* @param policy - merged policy for the exact routed target.149
* @param contextWindow - positive adapter-owned capacity for that target.150
* @param reservedCompletionTokens - output tokens one routed request reserves.151
* @returns detached immutable pressure and retention budgets.152
*/153
export function resolveCompactSpec(154
policy: ResolvedTargetPolicy,155
contextWindow: number,156
reservedCompletionTokens: number,157
): ResolvedCompactSpec {158
const targetKey = `${policy.target.provider}/${policy.target.model}`159
if (!Number.isInteger(contextWindow) || contextWindow <= 0) {160
throw new TargetPressureConfigError(161
targetKey,162
`BasicCompactionConfig: contextWindow (${contextWindow}) must be a positive integer`,163
)164
}165
if (!Number.isInteger(reservedCompletionTokens) || reservedCompletionTokens < 0) {166
throw new TargetPressureConfigError(167
targetKey,168
`BasicCompactionConfig: reservedCompletionTokens (${reservedCompletionTokens}) `169
+ 'must be a non-negative integer',170
)171
}172
const messageBudgetTokens = contextWindow - reservedCompletionTokens173
if (messageBudgetTokens <= 0) {174
throw new TargetPressureConfigError(175
targetKey,176
`compaction-basic: ${targetKey} reserves ${reservedCompletionTokens} completion tokens `177
+ `of its ${contextWindow}-token context window, leaving no message budget; configure `178
+ "the adapter model's contextWindow above the effective request maxTokens",179
)180
}181
const pressureBudgetTokens = messageBudgetTokens - policy.headroomTokens182
if (pressureBudgetTokens <= 0) {183
throw new TargetPressureConfigError(184
targetKey,185
`compaction-basic: ${targetKey} reserves ${reservedCompletionTokens} completion tokens `186
+ `and ${policy.headroomTokens} headroom tokens of its ${contextWindow}-token context `187
+ 'window, leaving no pressure budget; reduce the effective request maxTokens or '188
+ 'compaction headroomTokens, or configure a larger adapter model contextWindow',189
)190
}191
const thresholdTokens = Math.floor(Math.min(192
contextWindow * policy.thresholdRatio,193
pressureBudgetTokens,194
))195
const retainTokens = policy.retainTokens === undefined196
? Math.floor(messageBudgetTokens * policy.retainRatio)197
: policy.retainTokens198
if (retainTokens >= thresholdTokens) {199
throw new TargetPressureConfigError(200
targetKey,201
`BasicCompactionConfig: ${policy.target.provider}/${policy.target.model} retainTokens `202
+ `(${retainTokens}) must be less than threshold tokens ${thresholdTokens}`,203
)204
}205
return deepFreeze({206
target: { ...policy.target },207
contextWindow,208
thresholdRatio: policy.thresholdRatio,209
thresholdTokens,210
retainTokens,211
summarizationProvider: policy.summarizationProvider,212
summarizationModel: policy.summarizationModel,213
maxTokens: policy.maxTokens,214
compactionRetries: policy.compactionRetries,215
maxOverflowRetries: policy.maxOverflowRetries,216
})217
}219
/** Choose an explicit retention form or inherit the already-resolved fallback. */220
function resolveRetention(221
config: CompactionPolicyConfig,222
fallback: ResolvedRetention,223
): ResolvedRetention {224
if (config.retainTokens !== undefined) return { retainTokens: config.retainTokens }225
if (config.retainRatio !== undefined) return { retainRatio: config.retainRatio }226
return fallback227
}229
/** Reject a capacity-independent retention conflict at plugin load. */230
function validateRatioRetention(231
thresholdRatio: number,232
retention: ResolvedRetention,233
name: string,234
): void {235
if (retention.retainRatio !== undefined && retention.retainRatio >= thresholdRatio) {236
throw new Error(237
`${name}: retainRatio (${retention.retainRatio}) must be less than `238
+ `the resolved thresholdRatio (${thresholdRatio})`,239
)240
}241
}243
/** Validate, detach, and reject duplicate exact-target policies. */244
function resolveModelPolicies(configured: unknown): ModelCompactPolicyConfig[] {245
if (configured === undefined) return []246
if (!Array.isArray(configured)) {247
throw new Error('BasicCompactionConfig: modelPolicies must be an array')248
}249
const seen = new Set<string>()250
return configured.map((source: unknown, index) => {251
const name = `BasicCompactionConfig: modelPolicies[${index}]`252
assertModelPolicy(source, name)253
const key = `${source.provider}\u0000${source.model}`254
if (seen.has(key)) {255
throw new Error(256
`BasicCompactionConfig: duplicate model policy for ${source.provider}/${source.model}`,257
)258
}259
seen.add(key)260
return { ...source }261
})262
}264
/** Validate one untrusted exact-target override and narrow its public type. */265
function assertModelPolicy(266
source: unknown,267
name: string,268
): asserts source is ModelCompactPolicyConfig {269
if (!isUnknownRecord(source)) throw new Error(`${name} must be an object`)270
validateKeys(source, MODEL_POLICY_KEYS, name)271
assertNonEmptyString(`${name}.provider`, source.provider)272
assertNonEmptyString(`${name}.model`, source.model)273
validatePolicy(source, name)274
}276
/** Validate the fields common to defaults and exact-target partial overrides. */277
function validatePolicy(278
config: CompactionPolicyConfig | Record<string, unknown>,279
name: string,280
): void {281
const thresholdRatio = config.thresholdRatio282
const headroomTokens = config.headroomTokens283
const retainRatio = config.retainRatio284
const retainTokens = config.retainTokens285
const maxTokens = config.maxTokens286
const compactionRetries = config.compactionRetries287
const maxOverflowRetries = config.maxOverflowRetries288
if (thresholdRatio !== undefined) assertRatio(`${name}.thresholdRatio`, thresholdRatio)289
if (headroomTokens !== undefined) assertNonNegativeInteger(`${name}.headroomTokens`, headroomTokens)290
if (retainRatio !== undefined) assertRatio(`${name}.retainRatio`, retainRatio)291
if (retainTokens !== undefined) assertNonNegativeInteger(`${name}.retainTokens`, retainTokens)292
if (retainRatio !== undefined && retainTokens !== undefined) {293
throw new Error(`${name}: retainRatio and retainTokens are mutually exclusive`)294
}295
if (maxTokens !== undefined) assertPositiveInteger(`${name}.maxTokens`, maxTokens)296
if (compactionRetries !== undefined) {297
assertNonNegativeInteger(`${name}.compactionRetries`, compactionRetries)298
}299
if (maxOverflowRetries !== undefined) {300
assertNonNegativeInteger(`${name}.maxOverflowRetries`, maxOverflowRetries)301
}303
validateSummarizationPair(config, name)304
}306
/** Require one scope to omit, clear, or replace the summarization target as a pair. */307
function validateSummarizationPair(308
config: CompactionPolicyConfig | Record<string, unknown>,309
name: string,310
): void {311
const provider = config.summarizationProvider312
const model = config.summarizationModel313
if (provider !== undefined && typeof provider !== 'string') {314
throw new Error(`${name}.summarizationProvider must be a string`)315
}316
if (model !== undefined && typeof model !== 'string') {317
throw new Error(`${name}.summarizationModel must be a string`)318
}319
if (provider === undefined && model === undefined) return320
if (provider === undefined || model === undefined321
|| (provider.length === 0) !== (model.length === 0)) {322
throw new Error(323
`${name}: summarizationProvider and summarizationModel must be set together `324
+ 'as an empty or non-empty pair',325
)326
}327
}329
/** Reject stale or misspelled keys before defaults can hide them. */330
function validateKeys(config: object, keys: ReadonlySet<string>, name: string): void {331
for (const key of Object.keys(config)) {332
if (!keys.has(key)) throw new Error(`${name}: unknown key "${key}"`)333
}334
}336
function isUnknownRecord(value: unknown): value is Record<string, unknown> {337
return typeof value === 'object' && value !== null && !Array.isArray(value)338
}340
function assertNonEmptyString(name: string, value: unknown): asserts value is string {341
if (typeof value !== 'string' || value.length === 0) {342
throw new Error(`${name} must be a non-empty string`)343
}344
}346
function assertPositiveInteger(name: string, value: unknown): asserts value is number {347
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {348
throw new Error(`${name} (${String(value)}) must be a positive integer`)349
}350
}352
function assertNonNegativeInteger(name: string, value: unknown): asserts value is number {353
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {354
throw new Error(`${name} (${String(value)}) must be a non-negative integer`)355
}356
}358
function assertRatio(name: string, value: unknown): asserts value is number {359
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > 1) {360
throw new Error(`${name} (${String(value)}) must be a number in (0, 1]`)361
}362
}