token-meter.spec.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. import { describe, expect, expectTypeOf, it } from 'vitest'
  2. import { Context } from 'cordis'
  3. import { CallId } from '@deepseek-ai/dsh-llm'
  4. import type { ContentBlock, Message, TokenUsage } from '@deepseek-ai/dsh-llm'
  5. import SessionStore, { Session, SessionId, canonicalHeader } from '@deepseek-ai/dsh-session'
  6. import type { EpochHeader, SessionEvent } from '@deepseek-ai/dsh-session'
  7. import TokenMeterService from '@deepseek-ai/dsh-token-meter'
  8. import type { TokenMeasurement, TokenMeterConfig } from '@deepseek-ai/dsh-token-meter'
  9. function header(model: string, extras: Omit<EpochHeader, 'config'> = {}): EpochHeader {
  10. return canonicalHeader({ config: { provider: 'mock', model }, ...extras })
  11. }
  12. function textMessage(text: string, role: Message['role'] = 'user'): Message {
  13. return { role, content: [{ type: 'text', text }] }
  14. }
  15. function appendHeader(session: Session, value: EpochHeader): void {
  16. session.append('request/header', { header: value, reason: 'initial' })
  17. }
  18. /** Inject malformed persisted history after the live append boundary for defensive replay tests. */
  19. function appendUnchecked(session: Session, event: SessionEvent): void {
  20. const log = (session as unknown as { log: SessionEvent[] }).log
  21. log.push(event)
  22. }
  23. interface SuccessfulCallOptions {
  24. turn?: number
  25. step?: number
  26. providerText?: string
  27. durableText?: string
  28. usage?: TokenUsage
  29. provenance?: 'exact' | 'empty' | 'absent'
  30. }
  31. function appendSuccessfulCall(
  32. session: Session,
  33. value: EpochHeader,
  34. options: SuccessfulCallOptions = {},
  35. ): void {
  36. const turn = options.turn ?? 1
  37. const step = options.step ?? 1
  38. const providerText = options.providerText ?? 'provider answer'
  39. const durableText = options.durableText ?? providerText
  40. const provenance = options.provenance ?? 'exact'
  41. session.append('step/start', { turn, step })
  42. appendHeader(session, value)
  43. const sources: number[] = []
  44. if (provenance === 'exact') {
  45. const chunks = [
  46. { type: 'block-start' as const, index: 0, blockType: 'text' as const },
  47. { type: 'text-delta' as const, index: 0, text: providerText },
  48. { type: 'block-end' as const, index: 0, block: { type: 'text' as const, text: providerText } },
  49. ...options.usage === undefined ? [] : [{ type: 'usage' as const, usage: options.usage }],
  50. { type: 'finish' as const, reason: { kind: 'stop' as const } },
  51. ]
  52. for (const chunk of chunks) {
  53. sources.push(session.append('assistant/chunk', { turn, step, chunk }).seq)
  54. }
  55. }
  56. const intent = provenance === 'absent'
  57. ? { surfaceOp: 'append' as const }
  58. : { surfaceOp: 'append' as const, sourceEventSeqs: provenance === 'empty' ? [] : sources }
  59. session.append('assistant/message', {
  60. provenance: {
  61. provider: value.config.provider,
  62. model: value.config.model,
  63. },
  64. turn,
  65. step,
  66. content: durableText.length === 0 ? [] : [{ type: 'text', text: durableText }],
  67. ...options.usage === undefined ? {} : { usage: options.usage },
  68. }, intent)
  69. session.append('step/end', { turn, step })
  70. }
  71. function meter(config: TokenMeterConfig = {}): TokenMeterService {
  72. return new TokenMeterService(new Context(), config)
  73. }
  74. function expectSurfaceTotal(measurement: TokenMeasurement): void {
  75. expect(measurement.nodes.reduce((total, node) => total + node.tokens, 0))
  76. .toBe(measurement.surfaceTokens)
  77. }
  78. describe('TokenMeterService configuration and registration', () => {
  79. it('exposes an empty public configuration type', () => {
  80. expectTypeOf<{}>().toExtend<TokenMeterConfig>()
  81. expectTypeOf<{ contextWindow: number }>().not.toExtend<TokenMeterConfig>()
  82. })
  83. it.each(['models', 'contextWindow', 'contextWidow'])(
  84. 'rejects stale or unknown top-level config key %s',
  85. (key) => {
  86. expect(() => meter({ [key]: {} } as unknown as TokenMeterConfig))
  87. .toThrow(`TokenMeterConfig: unknown key "${key}"`)
  88. },
  89. )
  90. it('registers and unregisters ctx.tokenMeter with its plugin fiber', async () => {
  91. const ctx = new Context()
  92. await ctx.plugin(SessionStore)
  93. const fiber = await ctx.plugin(TokenMeterService)
  94. expect(ctx.get('tokenMeter')).toBeInstanceOf(TokenMeterService)
  95. await fiber.dispose()
  96. expect(ctx.get('tokenMeter')).toBeUndefined()
  97. })
  98. })
  99. describe('TokenMeterService pricing', () => {
  100. it('prices every built-in content shape and merge-extended blocks with one fixed heuristic', () => {
  101. const service = meter()
  102. const blocks: ContentBlock[] = [
  103. { type: 'text', text: 'abcd' },
  104. { type: 'reasoning', text: 'ab' },
  105. { type: 'tool-call', id: CallId('c'), name: 'read', arguments: '{"x":1}' },
  106. {
  107. type: 'tool-result',
  108. toolCallId: CallId('c'),
  109. content: [{ type: 'text', text: 'xy' }],
  110. isError: false,
  111. },
  112. { type: 'future-block', payload: 'abcd' } as unknown as ContentBlock,
  113. ]
  114. const estimated = service.estimateMessage({ role: 'assistant', content: blocks })
  115. expect(estimated).toBeGreaterThan(30)
  116. expect(service.estimateMessage(textMessage('abcd'))).toBe(9)
  117. })
  118. it('returns a detached deeply immutable empty measurement', () => {
  119. const service = meter()
  120. const session = new Session(SessionId('empty'))
  121. const result = service.measure(session)
  122. expect(result).toEqual({
  123. logRevision: 0,
  124. baseline: { kind: 'none', tokens: 0 },
  125. surfaceDeltaTokens: 0,
  126. totalTokens: 0,
  127. surfaceTokens: 0,
  128. nodes: [],
  129. })
  130. expect(Object.isFrozen(result)).toBe(true)
  131. expect(Object.isFrozen(result.baseline)).toBe(true)
  132. expect(Object.isFrozen(result.nodes)).toBe(true)
  133. expectSurfaceTotal(result)
  134. expect(() => {
  135. ;(result as { totalTokens: number }).totalTokens = 1
  136. }).toThrow(TypeError)
  137. })
  138. it('keeps an earlier unified snapshot detached from later replay', () => {
  139. const service = meter()
  140. const session = new Session(SessionId('detached'))
  141. session.append('user/message', {
  142. content: [{ type: 'text', text: 'first' }],
  143. source: { kind: 'user' },
  144. }, { surfaceOp: 'append' })
  145. const snapshot = service.measure(session)
  146. const snapshotCopy = structuredClone(snapshot)
  147. expect(Object.isFrozen(snapshot.nodes)).toBe(true)
  148. expect(Object.isFrozen(snapshot.nodes[0])).toBe(true)
  149. expectSurfaceTotal(snapshot)
  150. expect(() => {
  151. ;(snapshot.nodes as Array<{ seq: number; tokens: number }>).push({ seq: 99, tokens: 1 })
  152. }).toThrow(TypeError)
  153. expect(() => {
  154. ;(snapshot.nodes[0] as { seq: number; tokens: number }).tokens = 1
  155. }).toThrow(TypeError)
  156. session.append('user/message', {
  157. content: [{ type: 'text', text: 'second' }],
  158. source: { kind: 'user' },
  159. }, { surfaceOp: 'append' })
  160. const advanced = service.measure(session)
  161. expect(advanced.logRevision).toBe(2)
  162. expect(advanced.nodes).toHaveLength(2)
  163. expectSurfaceTotal(advanced)
  164. expect(snapshot).toEqual(snapshotCopy)
  165. expect(snapshot.logRevision).toBe(1)
  166. expect(snapshot.nodes).toHaveLength(1)
  167. })
  168. it('prices header, prefix, tools, and surface when no reusable usage exists', () => {
  169. const service = meter()
  170. const session = new Session(SessionId('heuristic'))
  171. session.append('user/message', {
  172. content: [{ type: 'text', text: 'question' }],
  173. source: { kind: 'user' },
  174. }, { surfaceOp: 'append' })
  175. appendHeader(session, header('deepseek-v4-flash', {
  176. system: 'system',
  177. messagePrefix: [textMessage('prefix')],
  178. tools: [{ name: 'read', description: 'read', parameters: { type: 'object' } }],
  179. }))
  180. const result = service.measure(session)
  181. expect(result.baseline.kind).toBe('estimated')
  182. expect(result.totalTokens).toBeGreaterThan(result.surfaceTokens)
  183. expect(result.logRevision).toBe(session.events.length)
  184. expectSurfaceTotal(result)
  185. })
  186. it('keeps request-header overrides out of the returned surface', () => {
  187. const service = meter()
  188. const session = new Session(SessionId('override-surface'))
  189. session.append('user/message', {
  190. content: [{ type: 'text', text: 'question' }],
  191. source: { kind: 'user' },
  192. }, { surfaceOp: 'append' })
  193. const logged = service.measure(session)
  194. const overridden = service.measure(session, header('another-model', {
  195. system: 'large override '.repeat(100),
  196. }))
  197. expect(overridden.totalTokens).toBeGreaterThan(logged.totalTokens)
  198. expect(overridden.surfaceTokens).toBe(logged.surfaceTokens)
  199. expect(overridden.nodes).toEqual(logged.nodes)
  200. expectSurfaceTotal(overridden)
  201. })
  202. })
  203. describe('replay anchors and surface folds', () => {
  204. const USAGE: TokenUsage = {
  205. inputTokens: 20,
  206. cacheReadTokens: 3,
  207. cacheWriteTokens: 4,
  208. outputTokens: 7,
  209. reasoningTokens: 6,
  210. }
  211. it('uses disjoint provider usage and signed durable-output rewrites', () => {
  212. const service = meter()
  213. const session = new Session(SessionId('usage'))
  214. session.append('user/message', {
  215. content: [{ type: 'text', text: 'before' }],
  216. source: { kind: 'user' },
  217. }, { surfaceOp: 'append' })
  218. appendSuccessfulCall(session, header('deepseek-v4-flash'), {
  219. providerText: 'short',
  220. durableText: 'a much longer rewritten durable assistant answer',
  221. usage: USAGE,
  222. })
  223. const result = service.measure(session)
  224. expect(result.baseline).toMatchObject({ kind: 'usage', tokens: 34, usage: USAGE })
  225. expect(result.surfaceDeltaTokens).toBeGreaterThan(0)
  226. expect(result.totalTokens).toBe(34 + result.surfaceDeltaTokens)
  227. expect(() => {
  228. ;((result.baseline as { usage: { inputTokens: number } }).usage.inputTokens) = 1
  229. }).toThrow(TypeError)
  230. })
  231. it('selects a heuristic anchor when provider usage would undercut its scale', () => {
  232. const service = meter()
  233. const session = new Session(SessionId('low-usage-anchor'))
  234. const system = 'system context'
  235. const requestHeader = header('deepseek-v4-flash', { system })
  236. appendSuccessfulCall(session, requestHeader, {
  237. providerText: 'abcd'.repeat(512),
  238. usage: { inputTokens: 20, outputTokens: 7 },
  239. })
  240. const anchored = service.measure(session)
  241. expect(anchored.baseline.kind).toBe('estimated')
  242. const assistant = anchored.nodes[0]!.seq
  243. session.append('user/message', {
  244. content: [{ type: 'text', text: 'short' }],
  245. source: { kind: 'plugin', plugin: 'test' },
  246. }, {
  247. surfaceOp: { op: 'replace', start: assistant, end: assistant },
  248. sourceEventSeqs: [assistant],
  249. })
  250. const shrunken = service.measure(session)
  251. expect(27 + shrunken.surfaceDeltaTokens).toBeLessThan(0)
  252. expect(shrunken.totalTokens).toBeGreaterThan(0)
  253. expect(shrunken.totalTokens).toBe(service.measure(
  254. session,
  255. header('different-model', { system }),
  256. ).totalTokens)
  257. })
  258. it('uses an estimated anchor when provider usage is absent', () => {
  259. const service = meter()
  260. const session = new Session(SessionId('missing-usage'))
  261. appendSuccessfulCall(session, header('deepseek-v4-flash', { system: 's' }), {
  262. providerText: 'provider',
  263. durableText: 'rewritten',
  264. })
  265. const anchored = service.measure(session)
  266. expect(anchored.baseline.kind).toBe('estimated')
  267. expect(anchored.surfaceDeltaTokens).toBe(0)
  268. session.append('user/message', {
  269. content: [{ type: 'text', text: 'later' }],
  270. source: { kind: 'user' },
  271. }, { surfaceOp: 'append' })
  272. const advanced = service.measure(session)
  273. expect(advanced.surfaceDeltaTokens).toBeGreaterThan(0)
  274. })
  275. it('distinguishes explicit empty provenance from absent legacy provenance', () => {
  276. const explicit = new Session(SessionId('explicit-empty'))
  277. const legacy = new Session(SessionId('legacy-absent'))
  278. appendSuccessfulCall(explicit, header('deepseek-v4-flash'), {
  279. durableText: 'listener injected text',
  280. providerText: '',
  281. usage: USAGE,
  282. provenance: 'empty',
  283. })
  284. appendSuccessfulCall(legacy, header('deepseek-v4-flash'), {
  285. durableText: 'listener injected text',
  286. providerText: '',
  287. usage: USAGE,
  288. provenance: 'absent',
  289. })
  290. const service = meter()
  291. expect(service.measure(explicit).surfaceDeltaTokens).toBeGreaterThan(0)
  292. expect(service.measure(legacy).surfaceDeltaTokens).toBe(0)
  293. })
  294. it('keeps only the latest successful request anchor across model switches', () => {
  295. const service = meter()
  296. const session = new Session(SessionId('switch'))
  297. const alphaHeader = header('alpha', { system: 'same envelope' })
  298. appendSuccessfulCall(session, alphaHeader, { usage: USAGE, providerText: 'alpha' })
  299. expect(service.measure(session).baseline).toMatchObject({ kind: 'usage', tokens: 34 })
  300. appendSuccessfulCall(session, header('beta'), {
  301. turn: 1,
  302. step: 2,
  303. usage: { inputTokens: 100, outputTokens: 50 },
  304. providerText: 'beta response',
  305. })
  306. expect(service.measure(session).baseline).toMatchObject({ kind: 'usage', tokens: 150 })
  307. appendHeader(session, alphaHeader)
  308. const switchedBack = service.measure(session)
  309. expect(switchedBack.baseline.kind).toBe('estimated')
  310. expect(switchedBack.surfaceDeltaTokens).toBe(0)
  311. })
  312. it('invalidates usage for any canonical envelope change or explicit override', () => {
  313. const service = meter()
  314. const session = new Session(SessionId('envelope'))
  315. const anchoredHeader = header('deepseek-v4-flash', { system: 'one' })
  316. appendSuccessfulCall(session, anchoredHeader, { usage: USAGE })
  317. expect(service.measure(session, { ...anchoredHeader, tools: [] }).baseline.kind).toBe('usage')
  318. expect(service.measure(session, header('deepseek-v4-flash', { system: 'two' })).baseline.kind)
  319. .toBe('estimated')
  320. expect(service.measure(session, header('deepseek-v4-pro', { system: 'one' })).baseline.kind)
  321. .toBe('estimated')
  322. expect(service.measure(session, {
  323. ...anchoredHeader,
  324. config: { ...anchoredHeader.config, temperature: 0.2 },
  325. }).baseline.kind).toBe('estimated')
  326. expect(service.measure(session, {
  327. ...anchoredHeader,
  328. messagePrefix: [textMessage('prefix')],
  329. }).baseline.kind).toBe('estimated')
  330. expect(service.measure(session, {
  331. ...anchoredHeader,
  332. tools: [{ name: 'read', description: 'read', parameters: { type: 'object' } }],
  333. }).baseline.kind).toBe('estimated')
  334. })
  335. it('folds the latest full header snapshot into the effective envelope', () => {
  336. const session = new Session(SessionId('header-snapshot'))
  337. appendHeader(session, header('deepseek-v4-flash'))
  338. session.append('request/header', {
  339. header: header('deepseek-v4-pro'),
  340. reason: 'change',
  341. })
  342. const result = meter().measure(session)
  343. expect(result.baseline.kind).toBe('estimated')
  344. expect(result.logRevision).toBe(2)
  345. })
  346. it('replays seeded append and replace operations with signed deltas', () => {
  347. const service = meter()
  348. const original = new Session(SessionId('surface-original'))
  349. appendSuccessfulCall(original, header('deepseek-v4-flash'), {
  350. usage: USAGE,
  351. providerText: 'long provider answer '.repeat(100),
  352. })
  353. original.append('user/message', {
  354. content: [{ type: 'text', text: 'new tail' }],
  355. source: { kind: 'user' },
  356. }, { surfaceOp: 'append' })
  357. const seeded = new Session(SessionId('surface-seeded'), original.events)
  358. const before = service.measure(seeded)
  359. expect(before.nodes).toHaveLength(2)
  360. expect(before.surfaceDeltaTokens).toBeGreaterThan(0)
  361. expectSurfaceTotal(before)
  362. const first = seeded.surface.nodes[0]!
  363. seeded.append('user/message', {
  364. content: [{ type: 'text', text: 'replacement' }],
  365. source: { kind: 'plugin', plugin: 'test' },
  366. }, { surfaceOp: { op: 'replace', start: first, end: first }, sourceEventSeqs: [first] })
  367. const after = service.measure(seeded)
  368. expect(after.nodes).toHaveLength(2)
  369. expect(after.nodes[0]!.seq).toBe(seeded.events.length - 1)
  370. expect(after.logRevision).toBe(seeded.events.length)
  371. expect(Object.isFrozen(after.nodes)).toBe(true)
  372. expect(Object.isFrozen(after.nodes[0])).toBe(true)
  373. expect(after.surfaceDeltaTokens).toBeLessThan(0)
  374. expectSurfaceTotal(after)
  375. expect(before.nodes).toHaveLength(2)
  376. expect(before.logRevision).toBe(original.events.length)
  377. expect(before.surfaceDeltaTokens).toBeGreaterThan(0)
  378. })
  379. it('prices an empty assistant surface anchor as zero', () => {
  380. const session = new Session(SessionId('empty-assistant'))
  381. appendSuccessfulCall(session, header('deepseek-v4-flash'), {
  382. providerText: '',
  383. durableText: '',
  384. provenance: 'empty',
  385. })
  386. const measurement = meter().measure(session)
  387. const assistant = session.events.find(event => event.type === 'assistant/message')!
  388. expect(measurement.nodes).toEqual([{ seq: assistant.seq, tokens: 0 }])
  389. expect(measurement.surfaceTokens).toBe(0)
  390. expectSurfaceTotal(measurement)
  391. })
  392. })
  393. describe('malformed replay and listener lifecycle', () => {
  394. function expectRepeatedFailure(service: TokenMeterService, session: Session, pattern: RegExp): void {
  395. expect(() => service.measure(session)).toThrow(pattern)
  396. expect(() => service.measure(session)).toThrow(pattern)
  397. }
  398. it('rejects an assistant without its step boundary transactionally', () => {
  399. const session = new Session(SessionId('bad-step'))
  400. appendHeader(session, header('deepseek-v4-flash'))
  401. session.append('assistant/message', {
  402. provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
  403. turn: 1,
  404. step: 1,
  405. content: [{ type: 'text', text: 'bad' }],
  406. }, { surfaceOp: 'append', sourceEventSeqs: [] })
  407. expectRepeatedFailure(meter(), session, /no matching step\/start/)
  408. })
  409. it('clears completed step boundaries and rejects overlapping or late step events', () => {
  410. const overlapping = new Session(SessionId('overlapping-step'))
  411. overlapping.append('step/start', { turn: 1, step: 1 })
  412. overlapping.append('step/start', { turn: 1, step: 2 })
  413. expectRepeatedFailure(
  414. meter(),
  415. overlapping,
  416. /arrived before turn 1\/step 1 ended/,
  417. )
  418. const late = new Session(SessionId('late-assistant'))
  419. late.append('step/start', { turn: 1, step: 1 })
  420. appendHeader(late, header('deepseek-v4-flash'))
  421. late.append('step/end', { turn: 1, step: 1 })
  422. late.append('assistant/message', {
  423. provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
  424. turn: 1,
  425. step: 1,
  426. content: [],
  427. }, { surfaceOp: 'append', sourceEventSeqs: [] })
  428. expectRepeatedFailure(
  429. meter(),
  430. late,
  431. /no matching step\/start/,
  432. )
  433. const mismatchedEnd = new Session(SessionId('mismatched-end'))
  434. mismatchedEnd.append('step/start', { turn: 1, step: 1 })
  435. mismatchedEnd.append('step/end', { turn: 1, step: 2 })
  436. expectRepeatedFailure(
  437. meter(),
  438. mismatchedEnd,
  439. /step\/end .* no matching step\/start/,
  440. )
  441. })
  442. it('rejects invalid assistant provenance', () => {
  443. const cases: Array<{
  444. name: string
  445. appendSource(session: Session): number[]
  446. pattern: RegExp
  447. }> = [
  448. {
  449. name: 'non-chunk',
  450. appendSource(session) {
  451. return [session.append('user/message', {
  452. content: [{ type: 'text', text: 'x' }],
  453. source: { kind: 'user' },
  454. }, { surfaceOp: 'append' }).seq]
  455. },
  456. pattern: /is not assistant\/chunk/,
  457. },
  458. {
  459. name: 'wrong-step',
  460. appendSource(session) {
  461. return [session.append('assistant/chunk', {
  462. turn: 1,
  463. step: 2,
  464. chunk: { type: 'finish', reason: { kind: 'stop' } },
  465. }).seq]
  466. },
  467. pattern: /belongs to another step/,
  468. },
  469. ]
  470. for (const testCase of cases) {
  471. const session = new Session(SessionId(`bad-source-${testCase.name}`))
  472. session.append('step/start', { turn: 1, step: 1 })
  473. appendHeader(session, header('deepseek-v4-flash'))
  474. const sourceEventSeqs = testCase.appendSource(session)
  475. session.append('assistant/message', {
  476. provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
  477. turn: 1,
  478. step: 1,
  479. content: [{ type: 'text', text: 'bad' }],
  480. usage: { inputTokens: 1, outputTokens: 1 },
  481. }, { surfaceOp: 'append', sourceEventSeqs })
  482. expect(() => meter().measure(session)).toThrow(testCase.pattern)
  483. }
  484. })
  485. it('rejects repeated and non-earlier assistant provenance', () => {
  486. const duplicate = new Session(SessionId('duplicate-source'))
  487. duplicate.append('step/start', { turn: 1, step: 1 })
  488. appendHeader(duplicate, header('deepseek-v4-flash'))
  489. const source = duplicate.append('assistant/chunk', {
  490. turn: 1,
  491. step: 1,
  492. chunk: { type: 'finish', reason: { kind: 'stop' } },
  493. }).seq
  494. appendUnchecked(duplicate, {
  495. type: 'assistant/message',
  496. seq: duplicate.seq,
  497. time: 0,
  498. data: {
  499. provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
  500. turn: 1,
  501. step: 1,
  502. content: [],
  503. usage: { inputTokens: 1, outputTokens: 0 },
  504. },
  505. surfaceOp: 'append',
  506. sourceEventSeqs: [source, source],
  507. })
  508. expect(() => meter().measure(duplicate)).toThrow(/repeats source seq/)
  509. const future = new Session(SessionId('future-source'))
  510. future.append('step/start', { turn: 1, step: 1 })
  511. appendHeader(future, header('deepseek-v4-flash'))
  512. appendUnchecked(future, {
  513. type: 'assistant/message',
  514. seq: future.seq,
  515. time: 0,
  516. data: {
  517. provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
  518. turn: 1,
  519. step: 1,
  520. content: [],
  521. usage: { inputTokens: 1, outputTokens: 0 },
  522. },
  523. surfaceOp: 'append',
  524. sourceEventSeqs: [99],
  525. })
  526. expect(() => meter().measure(future)).toThrow(/is not earlier/)
  527. })
  528. it('does not partially apply a malformed assistant replacement', () => {
  529. const session = new Session(SessionId('transactional-replace'))
  530. session.append('user/message', {
  531. content: [{ type: 'text', text: 'head' }],
  532. source: { kind: 'user' },
  533. }, { surfaceOp: 'append' })
  534. appendHeader(session, header('deepseek-v4-flash'))
  535. const head = session.events[0]!.seq
  536. session.append('assistant/message', {
  537. provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
  538. turn: 1,
  539. step: 1,
  540. content: [{ type: 'text', text: 'replacement' }],
  541. }, { surfaceOp: { op: 'replace', start: head, end: head }, sourceEventSeqs: [head] })
  542. expectRepeatedFailure(
  543. meter(),
  544. session,
  545. /no matching step\/start/,
  546. )
  547. })
  548. it('rejects corrupt replacement ranges without advancing the replay cursor', () => {
  549. const session = new Session(SessionId('bad-replace'))
  550. const head = session.append('user/message', {
  551. content: [{ type: 'text', text: 'head' }],
  552. source: { kind: 'user' },
  553. }, { surfaceOp: 'append' }).seq
  554. appendUnchecked(session, {
  555. type: 'user/message',
  556. seq: session.seq,
  557. time: 0,
  558. data: {
  559. content: [{ type: 'text', text: 'bad' }],
  560. source: { kind: 'user' },
  561. },
  562. surfaceOp: { op: 'replace', start: 99, end: 99 },
  563. sourceEventSeqs: [head],
  564. })
  565. expectRepeatedFailure(meter(), session, /invalid current range/)
  566. })
  567. it('handles earlier-reader catch-up, eager observation, and service reload', async () => {
  568. const ctx = new Context()
  569. await ctx.plugin(SessionStore)
  570. let activeMeter: TokenMeterService | undefined
  571. const revisions: number[] = []
  572. ctx.on('session/event', (session) => {
  573. if (activeMeter !== undefined) revisions.push(activeMeter.measure(session).logRevision)
  574. })
  575. const firstFiber = await ctx.plugin(TokenMeterService)
  576. activeMeter = ctx.tokenMeter
  577. const session = ctx.sessions.create(SessionId('listener-order'), { seed: [{
  578. type: 'turn/start',
  579. seq: 0,
  580. time: 1,
  581. data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
  582. }] })
  583. activeMeter.measure(session)
  584. session.append('user/message', {
  585. content: [{ type: 'text', text: 'one' }],
  586. source: { kind: 'user' },
  587. }, { surfaceOp: 'append' })
  588. expect(revisions).toEqual([2])
  589. expect(activeMeter.measure(session).logRevision).toBe(2)
  590. await firstFiber.dispose()
  591. const secondFiber = await ctx.plugin(TokenMeterService)
  592. activeMeter = ctx.tokenMeter
  593. expect(activeMeter.measure(session).logRevision).toBe(2)
  594. await secondFiber.dispose()
  595. })
  596. })