Represent tool results as tool-role messages and migrate released wrappers without losing outer own JSON fields, call identity, error state, or writer field order. Refuse nested or wrapper-only information that has no lossless current representation before successor publication. Validate current event relationships directly instead of projecting V4 data back into V3. Keep lifecycle, PTC, title/command, compaction, system-field and retired-syntax checks on catalog restoration and ordinary JSONL opens. The Worker JSON equality helper must inspect own keys so it cannot admit PTC arguments that Node rejects. Keep the source role change, required native admission and consumer fixture updates in one complete review layer. Explain shared-type changes in the persistence record and the native reader rationale in its existing note. Use checked branded fixture identities and discriminant narrowing instead of unknown double casts; fixture values and assertions remain intact. This recomposes the original tool-role work by _Kerman with the independently validated migration and admission repairs by Tianyi Cui. Frozen generations and snapshot input version selection are retained. Co-authored-by: Tianyi Cui <[email protected]>
103 lines
4.1 KiB
TypeScript
103 lines
4.1 KiB
TypeScript
/**
|
|
* Fixed-density heuristic token pricing shared by the meter service and the
|
|
* pure context-breakdown projection, so both surfaces price identical content
|
|
* to identical numbers.
|
|
*
|
|
* @module @deepseek-ai/dsh-token-meter/estimate
|
|
*/
|
|
|
|
import type { ContentBlock, Message } from '@deepseek-ai/dsh-llm'
|
|
import type { EpochHeader } from '@deepseek-ai/dsh-session'
|
|
|
|
/** Fixed text-density estimate used until exact tokenization is needed. */
|
|
const CHARS_PER_TOKEN = 4
|
|
|
|
/** Per-block structural overhead for JSON framing and type tags. */
|
|
const BLOCK_OVERHEAD = 4
|
|
|
|
/** Role-field framing overhead added to every priced message. */
|
|
export const ROLE_OVERHEAD = 4
|
|
|
|
/**
|
|
* Structural JSON price of one block outside the typed pricing arms: the
|
|
* fixed heuristic for merge-extended blocks and for image references, whose
|
|
* request price is route-owned rather than fixed. Image offload marks do not
|
|
* change this reference-only heuristic; route pricing owns their placeholders.
|
|
* @param block - block to price without mutation.
|
|
* @returns heuristic tokens for the block's JSON structure.
|
|
*/
|
|
export function estimateStructuralBlock(block: ContentBlock): number {
|
|
if (block.type === 'image') {
|
|
const { offloaded: _offloaded, ...reference } = block
|
|
return BLOCK_OVERHEAD + Math.ceil(JSON.stringify(reference).length / CHARS_PER_TOKEN)
|
|
}
|
|
return BLOCK_OVERHEAD + Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN)
|
|
}
|
|
|
|
/**
|
|
* Price content blocks recursively under the fixed density heuristic.
|
|
* @param blocks - content blocks to price without mutation.
|
|
* @returns heuristic tokens including per-block structural overhead.
|
|
*/
|
|
export function estimateContent(blocks: readonly ContentBlock[]): number {
|
|
let tokens = 0
|
|
for (const block of blocks) {
|
|
switch (block.type) {
|
|
case 'text':
|
|
case 'reasoning':
|
|
tokens += Math.ceil(block.text.length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD
|
|
break
|
|
case 'tool-call':
|
|
tokens += Math.ceil(block.name.length / CHARS_PER_TOKEN)
|
|
+ Math.ceil(block.arguments.length / CHARS_PER_TOKEN)
|
|
+ BLOCK_OVERHEAD
|
|
break
|
|
default:
|
|
// ContentBlockMap is merge-extensible; unknown blocks (and image
|
|
// references, whose request price is route-owned) retain a
|
|
// conservative structural JSON price under the fixed heuristic.
|
|
tokens += estimateStructuralBlock(block)
|
|
}
|
|
}
|
|
return tokens
|
|
}
|
|
|
|
/**
|
|
* Price the rendered system prompt: the `system/message` surface node's text.
|
|
* Adapters serialize the prompt as a plain string — a system-role message or
|
|
* the request's system field — not as a typed content block, so the price is
|
|
* text density plus role framing with no per-block overhead.
|
|
* @param message - system-role message to price without mutation.
|
|
* @returns heuristic system-prompt tokens; 0 for empty content ("no system prompt").
|
|
*/
|
|
export function estimateSystemMessage(message: Message): number {
|
|
if (message.content.length === 0) return 0
|
|
let characters = 0
|
|
for (const block of message.content) {
|
|
characters += block.type === 'text' ? block.text.length : JSON.stringify(block).length
|
|
}
|
|
return Math.ceil(characters / CHARS_PER_TOKEN) + ROLE_OVERHEAD
|
|
}
|
|
|
|
/**
|
|
* Heuristically price one model-visible message.
|
|
* @param message - message to price without mutation.
|
|
* @returns content and role-framing tokens under the fixed heuristic; a
|
|
* system-role message prices as {@link estimateSystemMessage}.
|
|
*/
|
|
export function estimateMessage(message: Message): number {
|
|
if (message.role === 'system') return estimateSystemMessage(message)
|
|
return estimateContent(message.content) + ROLE_OVERHEAD
|
|
}
|
|
|
|
/**
|
|
* Price the tool-schema part of a canonical request envelope — the envelope's
|
|
* only priced field, since the system prompt is a surface node.
|
|
* @param header - canonical envelope, or undefined before any request.
|
|
* @returns heuristic tool-schema tokens; 0 when absent or empty.
|
|
*/
|
|
export function estimateToolsTokens(header: EpochHeader | undefined): number {
|
|
if (header?.tools === undefined || header.tools.length === 0) return 0
|
|
return Math.ceil(JSON.stringify(header.tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD
|
|
}
|