1
/**2
* Fixed-density heuristic token pricing shared by the meter service and the3
* pure context-breakdown projection, so both surfaces price identical content4
* to identical numbers.5
*6
* @module @deepseek-ai/dsh-token-meter/estimate7
*/9
import type { ContentBlock, Message } from '@deepseek-ai/dsh-llm'10
import type { EpochHeader } from '@deepseek-ai/dsh-session'12
/** Fixed text-density estimate used until exact tokenization is needed. */13
const CHARS_PER_TOKEN = 415
/** Per-block structural overhead for JSON framing and type tags. */16
const BLOCK_OVERHEAD = 418
/** Role-field framing overhead added to every priced message. */19
export const ROLE_OVERHEAD = 421
/**22
* Structural JSON price of one block outside the typed pricing arms: the23
* fixed heuristic for merge-extended blocks and for image references, whose24
* request price is route-owned rather than fixed. Image offload marks do not25
* change this reference-only heuristic; route pricing owns their placeholders.26
* @param block - block to price without mutation.27
* @returns heuristic tokens for the block's JSON structure.28
*/29
export function estimateStructuralBlock(block: ContentBlock): number {30
if (block.type === 'image') {31
const { offloaded: _offloaded, ...reference } = block32
return BLOCK_OVERHEAD + Math.ceil(JSON.stringify(reference).length / CHARS_PER_TOKEN)33
}34
return BLOCK_OVERHEAD + Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN)35
}37
/**38
* Price content blocks recursively under the fixed density heuristic.39
* @param blocks - content blocks to price without mutation.40
* @returns heuristic tokens including per-block structural overhead.41
*/42
export function estimateContent(blocks: readonly ContentBlock[]): number {43
let tokens = 044
for (const block of blocks) {45
switch (block.type) {46
case 'text':47
case 'reasoning':48
tokens += Math.ceil(block.text.length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD49
break50
case 'tool-call':51
tokens += Math.ceil(block.name.length / CHARS_PER_TOKEN)52
+ Math.ceil(block.arguments.length / CHARS_PER_TOKEN)53
+ BLOCK_OVERHEAD54
break55
default:56
// ContentBlockMap is merge-extensible; unknown blocks (and image57
// references, whose request price is route-owned) retain a58
// conservative structural JSON price under the fixed heuristic.59
tokens += estimateStructuralBlock(block)60
}61
}62
return tokens63
}65
/**66
* Price the rendered system prompt: the `system/message` surface node's text.67
* Adapters serialize the prompt as a plain string — a system-role message or68
* the request's system field — not as a typed content block, so the price is69
* text density plus role framing with no per-block overhead.70
* @param message - system-role message to price without mutation.71
* @returns heuristic system-prompt tokens; 0 for empty content ("no system prompt").72
*/73
export function estimateSystemMessage(message: Message): number {74
if (message.content.length === 0) return 075
let characters = 076
for (const block of message.content) {77
characters += block.type === 'text' ? block.text.length : JSON.stringify(block).length78
}79
return Math.ceil(characters / CHARS_PER_TOKEN) + ROLE_OVERHEAD80
}82
/**83
* Heuristically price one model-visible message.84
* @param message - message to price without mutation.85
* @returns content and role-framing tokens under the fixed heuristic; a86
* system-role message prices as {@link estimateSystemMessage}.87
*/88
export function estimateMessage(message: Message): number {89
if (message.role === 'system') return estimateSystemMessage(message)90
return estimateContent(message.content) + ROLE_OVERHEAD91
}93
/**94
* Price the tool-schema part of a canonical request envelope — the envelope's95
* only priced field, since the system prompt is a surface node.96
* @param header - canonical envelope, or undefined before any request.97
* @returns heuristic tool-schema tokens; 0 when absent or empty.98
*/99
export function estimateToolsTokens(header: EpochHeader | undefined): number {100
if (header?.tools === undefined || header.tools.length === 0) return 0101
return Math.ceil(JSON.stringify(header.tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD102
}