返回源码地图

packages/compaction/compaction-basic/src/config.ts

main snapshot · da00f7f5358f · 正文引用章节 09 / 09;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/**
2 * Load-time validation and routed-model policy resolution for compaction-basic.
3 *
4 * @module @deepseek-ai/dsh-compaction-basic/config
5 */
6
7import type { LlmCallConfig } from '@deepseek-ai/dsh-llm'
8import { deepFreeze } from '@deepseek-ai/dsh-util-values'
9import type {
10 BasicCompactionConfig,
11 CompactionPolicyConfig,
12 ModelCompactPolicyConfig,
13 ResolvedCompactSpec,
14 ResolvedConfig,
15 ResolvedRetention,
16 ResolvedTargetPolicy,
17} from './types.ts'
18
19/** Default request-pressure fraction for every routed model. */
20const DEFAULT_THRESHOLD_RATIO = 0.8
21
22/** Default verbatim-tail fraction for every routed model. */
23const DEFAULT_RETAIN_RATIO = 0.16
24
25/** Fields shared by top-level defaults and exact-target overrides. */
26const POLICY_CONFIG_KEYS = [
27 'thresholdRatio',
28 'headroomTokens',
29 'retainRatio',
30 'retainTokens',
31 'summarizationProvider',
32 'summarizationModel',
33 'maxTokens',
34 'compactionRetries',
35 'maxOverflowRetries',
36] as const
37
38/** Complete public top-level configuration key set. */
39const BASIC_COMPACT_CONFIG_KEYS: ReadonlySet<string> = new Set([
40 ...POLICY_CONFIG_KEYS,
41 'modelPolicies',
42 'auto',
43])
44
45/** Complete exact-target override key set. */
46const MODEL_POLICY_KEYS: ReadonlySet<string> = new Set([
47 'provider',
48 'model',
49 ...POLICY_CONFIG_KEYS,
50])
51
52/** Target-specific pressure configuration failure eligible for warning suppression. */
53export 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}
62
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 */
68export 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 }
74
75 const headroomTokens = config.headroomTokens ?? 65_536
76 const maxTokens = config.maxTokens ?? headroomTokens
77 assertPositiveInteger('BasicCompactionConfig.maxTokens (explicit or from headroomTokens)', maxTokens)
78 const thresholdRatio = config.thresholdRatio ?? DEFAULT_THRESHOLD_RATIO
79 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 === undefined
84 && policy.headroomTokens !== undefined) {
85 policy.maxTokens = policy.headroomTokens
86 }
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 }
97
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}
111
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 */
118export 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.model
124 ))
125 const inheritedRetention: ResolvedRetention = config.retainTokens === undefined
126 ? { 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}
140
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 remaining
145 * after the routed output reservation plus compaction headroom. Retention scales
146 * 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 */
153export 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 - reservedCompletionTokens
173 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.headroomTokens
182 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 === undefined
196 ? Math.floor(messageBudgetTokens * policy.retainRatio)
197 : policy.retainTokens
198 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}
218
219/** Choose an explicit retention form or inherit the already-resolved fallback. */
220function 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 fallback
227}
228
229/** Reject a capacity-independent retention conflict at plugin load. */
230function 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}
242
243/** Validate, detach, and reject duplicate exact-target policies. */
244function 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}
263
264/** Validate one untrusted exact-target override and narrow its public type. */
265function 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}
275
276/** Validate the fields common to defaults and exact-target partial overrides. */
277function validatePolicy(
278 config: CompactionPolicyConfig | Record<string, unknown>,
279 name: string,
280): void {
281 const thresholdRatio = config.thresholdRatio
282 const headroomTokens = config.headroomTokens
283 const retainRatio = config.retainRatio
284 const retainTokens = config.retainTokens
285 const maxTokens = config.maxTokens
286 const compactionRetries = config.compactionRetries
287 const maxOverflowRetries = config.maxOverflowRetries
288 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 }
302
303 validateSummarizationPair(config, name)
304}
305
306/** Require one scope to omit, clear, or replace the summarization target as a pair. */
307function validateSummarizationPair(
308 config: CompactionPolicyConfig | Record<string, unknown>,
309 name: string,
310): void {
311 const provider = config.summarizationProvider
312 const model = config.summarizationModel
313 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) return
320 if (provider === undefined || model === undefined
321 || (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}
328
329/** Reject stale or misspelled keys before defaults can hide them. */
330function 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}
335
336function isUnknownRecord(value: unknown): value is Record<string, unknown> {
337 return typeof value === 'object' && value !== null && !Array.isArray(value)
338}
339
340function 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}
345
346function 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}
351
352function 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}
357
358function 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}