fix(token-meter): reject stale config (round 2)

This commit is contained in:
Hypatia May
2026-07-16 13:23:20 +08:00
parent 5c243e8a8d
commit 19a56ec542
9 changed files with 110 additions and 34 deletions

View File

@@ -8,7 +8,7 @@ Replay-aware token measurement through the singleton `ctx.tokenMeter` service. I
|---|---:|---|
| `contextWindow` | `128000` | Positive integer service-wide context capacity. |
The estimator intentionally uses one fixed heuristic: four characters per token plus structural overhead for roles, blocks, and request-envelope fields. `contextWindow` is the only deployment setting. Direct construction validates it; Loader mounts first apply the package's Schemastery shape validation.
The estimator intentionally uses one fixed heuristic: four characters per token plus structural overhead for roles, blocks, and request-envelope fields. `contextWindow` is the only deployment setting. Direct construction validates it; Loader mounts first apply the package's Schemastery shape validation. Unrecognized top-level keys are rejected.
## Measurement contract

View File

@@ -23,6 +23,9 @@ export type * from './types.ts'
/** Default service-wide provider context capacity. */
const DEFAULT_CONTEXT_WINDOW = 128_000
/** Complete public configuration key set. */
const TOKEN_METER_CONFIG_KEYS: ReadonlySet<string> = new Set(['contextWindow'])
/** Fixed text-density estimate used until exact tokenization is needed. */
const CHARS_PER_TOKEN = 4
@@ -69,8 +72,20 @@ function optionalHeaderEquals(
return headerEquals(left, right)
}
/** Reject stale or misspelled keys before defaults can hide them. */
function validateConfigKeys(config: TokenMeterConfig): void {
for (const key of Object.keys(config)) {
if (!TOKEN_METER_CONFIG_KEYS.has(key)) {
throw new Error(
`TokenMeterConfig: unknown key "${key}" (allowed: contextWindow)`,
)
}
}
}
/** Resolve and validate the one service-wide context capacity. */
function resolveContextWindow(config: TokenMeterConfig): number {
validateConfigKeys(config)
const contextWindow = config.contextWindow === undefined
? DEFAULT_CONTEXT_WINDOW
: config.contextWindow

View File

@@ -81,6 +81,11 @@ describe('TokenMeterService configuration and registration', () => {
expect(meter({ contextWindow: 32_000 }).contextWindow).toBe(32_000)
})
it.each(['models', 'contextWidow'])('rejects unknown top-level config key %s', (key) => {
expect(() => meter({ [key]: {} }))
.toThrow(`TokenMeterConfig: unknown key "${key}"`)
})
it.each([
{ contextWindow: 0 },
{ contextWindow: -1 },