mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-09 04:02:35 +00:00
feat(llm): 在 compaction 中按路由为图片请求压力计价
Closes #2848. - dsh-llm 新增 LlmAdapter.imageRequestPricing 同步钩子与 LlmImageRequestPricing/LlmImageRequestPrice 词汇,ctx.llm 按路由解析 - llm-deepseek 用官方公布的 v4 视觉计算器逐句移植(14px patch、3:1 降采样、384 上限、最坏对齐 pad)实现该钩子,复现请求投影的最旧优先 offload 与像素预算缩放;纯几何 requestImageDimensions 上移到 dsh-attachment - token-meter 表层 fold 存储与路由无关的节点事实,measure() 按生效 envelope 的路由为图片出现处定价;锚点存快照并按同一路由重定价;TokenSurfaceNode 同时携带路由价 tokens 与固定启发式 heuristicTokens - compaction 触发、保留与选段读取同一套路由价,记录的 shadowedTokenCount 保持启发式以维持 O(1) 投影 fold 一致 - llm-replay 支持按模型的 imageRequestTokens 声明;新增 keyless 的 image-compaction ACP 快照场景端到端验证装配应用
This commit is contained in:
@@ -1878,3 +1878,119 @@ describe('automatic listener and loader composition', () => {
|
||||
expect(await recover(ctx, agent(session, MODEL), overflow())).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('route-priced image pressure', () => {
|
||||
const IMAGE_VISUAL_TOKENS = 300
|
||||
const IMAGE_HANDLE_TEXT = 'request preview'
|
||||
|
||||
class PricedContextAdapter extends ContextAdapter {
|
||||
override imageRequestPricing(): { priceImages: (images: readonly unknown[]) => Array<{ visualTokens: number; text: string }> } {
|
||||
return {
|
||||
priceImages: images => images.map(() => ({
|
||||
visualTokens: IMAGE_VISUAL_TOKENS,
|
||||
text: IMAGE_HANDLE_TEXT,
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pricedContext(contextWindow = 1_000): Context {
|
||||
const ctx = new Context()
|
||||
void new LlmRuntime(ctx)
|
||||
void new TokenMeter(ctx)
|
||||
ctx.llm.registerAdapter([MODEL], new PricedContextAdapter(contextWindow))
|
||||
return ctx
|
||||
}
|
||||
|
||||
/** Closed short-text turns whose user messages each carry one image. */
|
||||
function imageConversation(turns = 4): Session {
|
||||
const session = Session.create(SessionId(`image-dense-${turns}`))
|
||||
for (let turn = 1; turn <= turns; turn += 1) {
|
||||
session.append('turn/start', { turn })
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [
|
||||
{ type: 'text', text: `image turn ${turn}` },
|
||||
{
|
||||
type: 'image',
|
||||
attachment: {
|
||||
attachmentId: AttachmentId(`sha256:${String(turn).repeat(8)}`),
|
||||
mediaType: 'image/png',
|
||||
bytes: 2048,
|
||||
width: 800,
|
||||
height: 800,
|
||||
name: `shot-${turn}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
source: { kind: 'user' },
|
||||
}), { surfaceOp: 'append' })
|
||||
session.append('step/start', { turn, step: 1 })
|
||||
if (turn === 1) {
|
||||
session.append('request/header', {
|
||||
header: { config: { provider: MODEL, model: MODEL } },
|
||||
reason: 'initial',
|
||||
})
|
||||
}
|
||||
session.append('assistant/message', {
|
||||
turn,
|
||||
step: 1,
|
||||
message: createMessage({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: `ok ${turn}` }],
|
||||
source: {
|
||||
kind: 'model',
|
||||
...{ provider: MODEL, model: MODEL },
|
||||
},
|
||||
}),
|
||||
}, { surfaceOp: 'append' })
|
||||
session.append('step/end', { turn, step: 1 })
|
||||
session.append('turn/end', { turn, reason: { kind: 'completed' } })
|
||||
}
|
||||
session.append('turn/start', { turn: turns + 1 })
|
||||
return session
|
||||
}
|
||||
|
||||
it('selects an image-dense range only when the routed price counts visual tokens', () => {
|
||||
const session = imageConversation()
|
||||
const routed = pricedContext().tokenMeter.measure(session)
|
||||
const neutral = createContext().tokenMeter.measure(session)
|
||||
|
||||
expect(routed.surfaceTokens).toBeGreaterThan(neutral.surfaceTokens + 4 * IMAGE_VISUAL_TOKENS - 200)
|
||||
expect(routed.nodes.map(node => node.seq)).toEqual(neutral.nodes.map(node => node.seq))
|
||||
expect(routed.nodes.map(node => node.heuristicTokens)).toEqual(neutral.nodes.map(node => node.tokens))
|
||||
|
||||
// The same verbatim tail budget retains almost everything under the
|
||||
// neutral heuristic but forces a cut once visual tokens are counted.
|
||||
expect(selectCompactableRange(session, neutral, 350)).toBeNull()
|
||||
const range = selectCompactableRange(session, routed, 350)
|
||||
expect(range).not.toBeNull()
|
||||
})
|
||||
|
||||
it('triggers pressure compaction from routed visual tokens and logs heuristic shadow prices', async () => {
|
||||
const ctx = pricedContext(1_000)
|
||||
const session = imageConversation()
|
||||
const before = ctx.tokenMeter.measure(session)
|
||||
const compact = new TestCompactionEngine(ctx, {
|
||||
auto: false,
|
||||
thresholdRatio: 0.8,
|
||||
retainTokens: 350,
|
||||
})
|
||||
|
||||
// The same history stays below the 800-token threshold without pricing.
|
||||
const neutralResult = await compactIfNeeded(service({
|
||||
auto: false,
|
||||
thresholdRatio: 0.8,
|
||||
retainTokens: 350,
|
||||
}), session)
|
||||
expect(neutralResult).toBeNull()
|
||||
|
||||
const result = await compact.compactIfNeeded(agent(session), 'pressure', SIGNAL)
|
||||
expect(result).not.toBeNull()
|
||||
const summaryEvent = session.events.find(event => event.type === 'compaction/summary')
|
||||
expect(summaryEvent).toBeDefined()
|
||||
const shadowedHeuristic = before.nodes
|
||||
.filter(node => result?.shadowedSeqs.includes(node.seq))
|
||||
.reduce((total, node) => total + node.heuristicTokens, 0)
|
||||
expect(summaryEvent?.data.shadowedTokenCount).toBe(shadowedHeuristic)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user