feat(client): price a file attachment by the handle the model receives

A file block never reaches a model as bytes: dsh projects every one to
`fileHandleText` at request assembly, and the token meter prices that handle in
place of the block's structural JSON price. An attachment therefore costs a
fixed ~60 tokens whatever its size, so the card now shows that price beside the
size — computed from the three facts a log carries (name, byte size, digest
prefix) and priced with the meter's own text arm, now shared with the fold's
message price instead of restated there.

The handle also names the stored copy's read path, which only the live
environment resolves and no log holds, so the row prices the pathless arm and
reads as a lower bound (`≥`) rather than an `≈`. The fold keeps the meter's
fixed structural price for the same block, exactly as the harness does: one
figure answers the request, the other the surface.
This commit is contained in:
bowenliang123 committed 2026-10-13 23:15:39 +08:00
1 parent da43fe48c7
commit dd05bbda4e
9 files changed
+129 -14

No files matched your search

+23 -6
View File
@@ -10,6 +10,7 @@
import { useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from 'react'
import { attachmentExtension } from '../../shared/attachment'
import type { AttachmentRef } from '../../shared/attachment'
import { estimateFileTokens } from '../../shared/fileTokens'
import { estimateImageTokens } from '../../shared/imageTokens'
import { fmtBytes } from '../format'
import type { AttachmentOpener, ImageLoader, ImageRefLike } from '../services'
@@ -45,6 +46,13 @@ function nameOf(ref: AttachmentRef, t: ViewKit['t']): string {
return ref.name ?? t(ref.kind === 'image' ? 'attach.image' : 'attach.file')
}
/** One labelled figure of a card. */
interface Fact {
label: string
value: string
tip?: string
}
/** The type line of an attachment (the harness file card's own rule): the upper-cased extension,
* else the attachment's kind. */
function typeOf(ref: AttachmentRef, t: ViewKit['t']): string {
@@ -81,15 +89,22 @@ function useImageSrc(attachment: AttachmentRef, load: ImageLoader | undefined):
return state
}
/** An attachment's facts as label/value rows: an image's dimensions and token estimate, a file's
* size — the same figures the chat and the harness's own file card show. */
function factsOf(ref: AttachmentRef, t: ViewKit['t'], fmt: ViewKit['fmt']): Array<{ label: string; value: string; tip?: string }> {
/** An attachment's facts as label/value rows: an image's dimensions and its vision price, a file's
* size and the floor its handle costs — the same figures the chat and the harness's own file card show. */
function factsOf(ref: AttachmentRef, t: ViewKit['t'], fmt: ViewKit['fmt']): Fact[] {
const dimsOf = (w: number | undefined, h: number | undefined): string | null =>
w !== undefined && h !== undefined ? `${w}×${h}` : null
if (ref.kind !== 'image') {
return ref.bytes === undefined ? [] : [{ label: t('attach.size'), value: fmtBytes(ref.bytes) }]
const rows: Fact[] = []
if (ref.bytes !== undefined) rows.push({ label: t('attach.size'), value: fmtBytes(ref.bytes) })
// A lower bound: the handle also names the stored copy's read path, which no log carries.
const tokens = estimateFileTokens(ref)
if (tokens !== null) {
rows.push({ label: t('attach.token'), value: `≥${fmt(tokens)}`, tip: t('attach.fileTokensTip') })
}
return rows
}
const rows: Array<{ label: string; value: string; tip?: string }> = []
const rows: Fact[] = []
// No byte size is stored for the raw raster — dims only.
const rawDims = ref.originalDimensions !== undefined
? dimsOf(ref.originalDimensions.width, ref.originalDimensions.height)
@@ -105,7 +120,9 @@ function factsOf(ref: AttachmentRef, t: ViewKit['t'], fmt: ViewKit['fmt']): Arra
const tokens = ref.width !== undefined && ref.height !== undefined
? estimateImageTokens(ref.width, ref.height)
: null
if (tokens !== null) rows.push({ label: t('attach.token'), value: `≈${fmt(tokens)}`, tip: t('attach.tokensTip') })
if (tokens !== null) {
rows.push({ label: t('attach.token'), value: `≈${fmt(tokens)}`, tip: t('attach.imageTokensTip') })
}
return rows
}
+2 -1
View File
@@ -447,7 +447,8 @@ export const DICT_EN = {
'attach.raw': 'Raw',
'attach.sent': 'Sent',
'attach.token': 'Token',
'attach.tokensTip': 'Estimated token cost via DeepSeek\'s official image-size conversion (384-token cap per image)',
'attach.imageTokensTip': 'Estimated token cost via DeepSeek\'s official image-size conversion (384-token cap per image)',
'attach.fileTokensTip': 'A lower bound: the model receives a handle naming this file, its size, its digest, and where the stored copy can be read — never its contents — so the cost barely moves with the file\'s size',
'ov.title': 'Context Insights',
'ov.unavailable': 'The session list is unavailable',
'ov.range.label': 'Range',
+2 -1
View File
@@ -446,7 +446,8 @@ export const DICT_ZH = {
'attach.raw': '原图',
'attach.sent': '发送',
'attach.token': 'Token',
'attach.tokensTip': '按 DeepSeek 官方图片尺寸换算(单图上限 384 tokens)估算的 token 消耗',
'attach.imageTokensTip': '按 DeepSeek 官方图片尺寸换算(单图上限 384 tokens)估算的 token 消耗',
'attach.fileTokensTip': '这是一个下限:模型收到的是一个句柄(文件名、大小、摘要前缀,以及副本的读取路径),不会收到文件内容,所以开销几乎不随文件大小变化',
'ov.title': '上下文洞察',
'ov.unavailable': '会话列表暂不可用',
'ov.range.label': '统计范围',
+2 -1
View File
@@ -6,6 +6,7 @@
* (shared/imageTokens.ts), falling back to the meter's JSON price.
*/
import { estimateTextTokens } from '../shared/estimate'
import { estimateImageTokens } from '../shared/imageTokens'
const CHARS_PER_TOKEN = 4
@@ -42,7 +43,7 @@ function estimateBlocks(blocks: unknown): number {
switch (block.type) {
case 'text':
case 'reasoning':
tokens += Math.ceil((block.text || '').length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD
tokens += estimateTextTokens(block.text || '')
break
case 'tool-call':
tokens += Math.ceil((block.name || '').length / CHARS_PER_TOKEN)
+8 -1
View File
@@ -1,14 +1,21 @@
/** Token heuristics shared by the host fold and the client boundary, mirroring the harness
* token-meter's fixed density (4 chars ≈ 1 token, +4 role framing) on both sides. */
* token-meter's fixed density (4 chars ≈ 1 token, +4 per content block, +4 role framing) on both
* sides. */
const CHARS_PER_TOKEN = 4
const ROLE_OVERHEAD = 4
const BLOCK_OVERHEAD = 4
export function estimateSystemTokens(text: unknown): number {
if (typeof text !== 'string' || text.length === 0) return 0
return Math.ceil(text.length / CHARS_PER_TOKEN) + ROLE_OVERHEAD
}
/** Price one text block exactly like the harness meter's text arm. */
export function estimateTextTokens(text: string): number {
return Math.ceil(text.length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD
}
/** Price a `system/message` payload exactly like the harness's `estimateSystemMessage`: text
* density over every block plus role framing, no per-block overhead. */
export function estimateSystemContent(blocks: unknown): number {
+27
View File
@@ -0,0 +1,27 @@
/**
* Per-file token estimate for a durable file attachment — a file's BYTES never reach a model.
*
* dsh projects every `file` block to `dsh-llm`'s `fileHandleText` at request assembly and the token
* meter prices that text in place of the block's structural JSON price, so a file costs its handle
* whatever its size. The handle names the file, its byte size, and its digest prefix — the facts a
* log carries. It also names the stored copy's read path, which is execution-world state no log
* holds (`llm.fileRequestText` resolves it through the live attachment store and fs mapping), so
* this prices the pathless arm the harness emits when the copy is unreadable: the FLOOR the
* path-bearing handle grows from, which is why the card shows it as a lower bound.
*/
import { estimateTextTokens } from './estimate'
const DIGEST_PREFIX_LENGTH = 'sha256:'.length
const DIGEST_SHOWN = 8
/** One file's handle price, or null when the log proves neither the name nor the byte size the
* handle's identity is built from. */
export function estimateFileTokens(ref: { attachmentId: string; name?: string; bytes?: number }): number | null {
if (ref.name === undefined || ref.bytes === undefined) return null
const digest = ref.attachmentId.slice(DIGEST_PREFIX_LENGTH, DIGEST_PREFIX_LENGTH + DIGEST_SHOWN)
const identity = `File ${JSON.stringify(ref.name)} (${ref.bytes} bytes, sha256:${digest})`
const handle = `[${identity} was uploaded, but the current execution environment cannot access a`
+ ' readable path. Report that limitation if its contents are needed; do not claim to have read it.]'
return estimateTextTokens(handle)
}