diff --git a/docs/core-data-structures/token-meter.md b/docs/core-data-structures/token-meter.md index a6e70f56f6..e082ea08a9 100644 --- a/docs/core-data-structures/token-meter.md +++ b/docs/core-data-structures/token-meter.md @@ -23,7 +23,7 @@ interface TokenMeasurement { } ``` -`baseline.kind === 'usage'` means the latest successful provider call has the same canonical request envelope. `estimated` means the service priced the complete envelope and surface with its fixed heuristic. A later successful request replaces the earlier anchor; signed `surfaceDeltaTokens` preserves growth and shrinkage relative to a matching anchor. `totalTokens` remains request-and-response pressure, while `surfaceTokens` is the surface-only heuristic total and equals the sum of the node prices. +`baseline.kind === 'usage'` means the latest successful provider call has the same canonical request envelope and its total is no lower than that call's full heuristic anchor. `estimated` means no reusable conservative usage anchor exists, so the service priced the complete envelope and surface with its fixed heuristic. A later successful request replaces the earlier anchor; signed `surfaceDeltaTokens` preserves growth and shrinkage relative to a matching anchor. `totalTokens` remains request-and-response pressure, while `surfaceTokens` is the surface-only heuristic total and equals the sum of the node prices. ## `TokenSurfaceNode` diff --git a/packages/llm/token-meter/README.md b/packages/llm/token-meter/README.md index 4b2a03833e..d879112897 100644 --- a/packages/llm/token-meter/README.md +++ b/packages/llm/token-meter/README.md @@ -19,7 +19,7 @@ The estimator intentionally uses one fixed heuristic: four characters per token `measure()` synchronizes once and returns one detached, deeply immutable snapshot. `totalTokens` is request-and-response pressure, while `surfaceTokens` is the surface-only heuristic total and equals the sum of `nodes[].tokens`. A `requestHeader` override affects pressure fields only; the surface fields still describe the current session. Every call clones the positional nodes, so measurement is O(surface). -The fold tracks request headers and deltas, step boundaries, surface appends and replacements, successful assistant messages, provider usage, and assistant-chunk provenance. Provider usage is reused only when the latest successful call's canonical request envelope matches the measured envelope; a later success replaces the earlier anchor. Otherwise the complete current envelope and surface are estimated. Surface changes remain signed relative to a matching anchor, including negative deltas after shrinking replacements. +The fold tracks request headers and deltas, step boundaries, surface appends and replacements, successful assistant messages, provider usage, and assistant-chunk provenance. Provider usage is reused only when the latest successful call's canonical request envelope matches the measured envelope and its total is no lower than that call's full heuristic anchor; a later success replaces the earlier anchor. Otherwise the complete current envelope and surface are estimated. Surface changes remain signed relative to a matching anchor, including negative deltas after shrinking replacements. Usage accounting sums disjoint input, cache-read, cache-write, and output buckets; reasoning is not added again. Every successful call records an assistant anchor, including content-less calls. An explicit empty provenance list means a known empty provider stream, while absent legacy provenance conservatively treats the durable assistant output as provider output. diff --git a/packages/llm/token-meter/src/index.ts b/packages/llm/token-meter/src/index.ts index 3c446f4778..0a3bf4180a 100644 --- a/packages/llm/token-meter/src/index.ts +++ b/packages/llm/token-meter/src/index.ts @@ -128,8 +128,9 @@ export class TokenMeterService extends Service { * Measure current request pressure and surface through the durable tail. * * Provider usage is reused only when the latest successful call's canonical - * request envelope matches `requestHeader`; otherwise the complete envelope - * and surface are heuristically repriced. + * request envelope matches `requestHeader` and its total is no lower than + * that call's full heuristic anchor; otherwise the complete envelope and + * surface are heuristically repriced. * * `requestHeader` affects request pressure only; surface fields always * describe the current session surface. Every call clones those positional @@ -266,14 +267,17 @@ export class TokenMeterService extends Service { event, eventTokens, ) + const anchorSurfaceTokens = stepStart.surfaceTokens + providerAssistantTokens + const providerTokens = usageTokens(event.data.usage) + const estimatedAnchorTokens = this._estimateHeader(nextHeader) + anchorSurfaceTokens nextAnchor = { header: nextHeader, - surfaceTokens: stepStart.surfaceTokens + providerAssistantTokens, - baseline: { - kind: 'usage', - tokens: usageTokens(event.data.usage), - usage: event.data.usage, - }, + surfaceTokens: anchorSurfaceTokens, + // Signed heuristic deltas remain conservative only from an anchor + // that is at least as large as the matching full heuristic price. + baseline: providerTokens >= estimatedAnchorTokens + ? { kind: 'usage', tokens: providerTokens, usage: event.data.usage } + : { kind: 'estimated', tokens: estimatedAnchorTokens }, } } else { const anchorSurfaceTokens = stepStart.surfaceTokens + eventTokens diff --git a/packages/llm/token-meter/tests/token-meter.spec.ts b/packages/llm/token-meter/tests/token-meter.spec.ts index 18f78afad2..f5d6afc4b3 100644 --- a/packages/llm/token-meter/tests/token-meter.spec.ts +++ b/packages/llm/token-meter/tests/token-meter.spec.ts @@ -252,6 +252,36 @@ describe('replay anchors and surface folds', () => { }).toThrow(TypeError) }) + it('selects a heuristic anchor when provider usage would undercut its scale', () => { + const service = meter() + const session = new Session(SessionId('low-usage-anchor')) + const system = 'system context' + const requestHeader = header('deepseek-v4-flash', { system }) + appendSuccessfulCall(session, requestHeader, { + providerText: 'abcd'.repeat(512), + usage: { inputTokens: 20, outputTokens: 7 }, + }) + + const anchored = service.measure(session) + expect(anchored.baseline.kind).toBe('estimated') + const assistant = anchored.nodes[0]!.seq + session.append('user/message', { + content: [{ type: 'text', text: 'short' }], + source: { kind: 'plugin', plugin: 'test' }, + }, { + surfaceOp: { op: 'replace', start: assistant, end: assistant }, + sourceEventSeqs: [assistant], + }) + + const shrunken = service.measure(session) + expect(27 + shrunken.surfaceDeltaTokens).toBeLessThan(0) + expect(shrunken.totalTokens).toBeGreaterThan(0) + expect(shrunken.totalTokens).toBe(service.measure( + session, + header('different-model', { system }), + ).totalTokens) + }) + it('uses an estimated anchor when provider usage is absent', () => { const service = meter() const session = new Session(SessionId('missing-usage'))