Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input
# Conflicts: # apps/cli/README.i18n.yaml # docs/core-data-structures/core.i18n.yaml # docs/core-data-structures/llm-streaming.i18n.yaml # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # packages/client/connection/src/client/fixture.ts # packages/client/connection/src/client/index.ts # packages/client/runtime/README.i18n.yaml # packages/client/runtime/README.md # packages/client/runtime/README.zh.md # packages/client/ui-conversation/README.i18n.yaml # packages/client/ui-conversation/src/client/input/hub.ts # packages/client/ui-conversation/src/client/service.ts # packages/client/ui-conversation/src/client/skeleton/InputBar.tsx # packages/compact/compact-basic/src/summarizer.ts # packages/compact/compact-basic/tests/compact-basic.spec.ts # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/api/rpc.schema.ts # packages/host/apiproxy/src/api/rpc.ts # packages/host/apiproxy/src/api/sessions.ts # packages/host/apiproxy/tests/rpc-schemas.spec.ts # packages/llm/llm-deepseek/tests/serialize.spec.ts # packages/llm/llm-pi-ai/tests/adapter.spec.ts # packages/llm/llm-pi-ai/tests/convert.spec.ts # packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts # packages/llm/token-meter/tests/token-meter.spec.ts # packages/ui/tui/README.i18n.yaml
This commit is contained in:
@@ -349,8 +349,8 @@ export class TokenMeterService extends Service {
|
||||
}
|
||||
assembler.push(sourceEvent.data.chunk)
|
||||
}
|
||||
const providerMessage = assembler.message()
|
||||
return providerMessage.content.length === 0 ? 0 : this.estimateMessage(providerMessage)
|
||||
const providerContent = assembler.blocks()
|
||||
return providerContent.length === 0 ? 0 : this._estimateContent(providerContent) + ROLE_OVERHEAD
|
||||
}
|
||||
|
||||
/** Price content blocks recursively under the fixed density heuristic. */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, expectTypeOf, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { AttachmentId } from '@deepseek-ai/dsh-attachment'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { createUserMessage, CallId, createMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContentBlock, Message, TokenUsage } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { Session, SessionId, canonicalHeader } from '@deepseek-ai/dsh-session'
|
||||
import type { EpochHeader, SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
@@ -13,7 +13,13 @@ function header(model: string, extras: Omit<EpochHeader, 'config'> = {}): EpochH
|
||||
}
|
||||
|
||||
function textMessage(text: string, role: Message['role'] = 'user'): Message {
|
||||
return { role, content: [{ type: 'text', text }] }
|
||||
return createMessage({
|
||||
role,
|
||||
content: [{ type: 'text', text }],
|
||||
source: role === 'assistant'
|
||||
? { kind: 'model', provider: 'mock', model: 'mock' }
|
||||
: { kind: 'user' },
|
||||
})
|
||||
}
|
||||
|
||||
function appendHeader(session: Session, value: EpochHeader): void {
|
||||
@@ -66,13 +72,19 @@ function appendSuccessfulCall(
|
||||
? { surfaceOp: 'append' as const }
|
||||
: { surfaceOp: 'append' as const, sourceEventSeqs: provenance === 'empty' ? [] : sources }
|
||||
session.append('assistant/message', {
|
||||
provenance: {
|
||||
provider: value.config.provider,
|
||||
model: value.config.model,
|
||||
},
|
||||
turn,
|
||||
step,
|
||||
content: durableText.length === 0 ? [] : [{ type: 'text', text: durableText }],
|
||||
message: createMessage({
|
||||
role: 'assistant',
|
||||
content: durableText.length === 0 ? [] : [{ type: 'text', text: durableText }],
|
||||
source: {
|
||||
kind: 'model',
|
||||
...{
|
||||
provider: value.config.provider,
|
||||
model: value.config.model,
|
||||
},
|
||||
},
|
||||
}),
|
||||
...options.usage === undefined ? {} : { usage: options.usage },
|
||||
}, intent)
|
||||
session.append('step/end', { turn, step })
|
||||
@@ -136,7 +148,10 @@ describe('TokenMeterService pricing', () => {
|
||||
},
|
||||
{ type: 'future-block', payload: 'abcd' } as unknown as ContentBlock,
|
||||
]
|
||||
const estimated = service.estimateMessage({ role: 'assistant', content: blocks })
|
||||
const estimated = service.estimateMessage(createMessage({
|
||||
role: 'assistant', content: blocks,
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
}))
|
||||
expect(estimated).toBe(813)
|
||||
expect(service.estimateMessage(textMessage('abcd'))).toBe(9)
|
||||
})
|
||||
@@ -165,10 +180,10 @@ describe('TokenMeterService pricing', () => {
|
||||
it('keeps an earlier unified snapshot detached from later replay', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('detached'))
|
||||
session.append('user/message', {
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'first' }],
|
||||
source: { kind: 'user' },
|
||||
}, { surfaceOp: 'append' })
|
||||
}), { surfaceOp: 'append' })
|
||||
const snapshot = service.measure(session)
|
||||
const snapshotCopy = structuredClone(snapshot)
|
||||
expect(Object.isFrozen(snapshot.nodes)).toBe(true)
|
||||
@@ -181,10 +196,10 @@ describe('TokenMeterService pricing', () => {
|
||||
;(snapshot.nodes[0] as { seq: number; tokens: number }).tokens = 1
|
||||
}).toThrow(TypeError)
|
||||
|
||||
session.append('user/message', {
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'second' }],
|
||||
source: { kind: 'user' },
|
||||
}, { surfaceOp: 'append' })
|
||||
}), { surfaceOp: 'append' })
|
||||
const advanced = service.measure(session)
|
||||
expect(advanced.logRevision).toBe(2)
|
||||
expect(advanced.nodes).toHaveLength(2)
|
||||
@@ -197,10 +212,10 @@ describe('TokenMeterService pricing', () => {
|
||||
it('prices header, tools, and surface when no reusable usage exists', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('heuristic'))
|
||||
session.append('user/message', {
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'question' }],
|
||||
source: { kind: 'user' },
|
||||
}, { surfaceOp: 'append' })
|
||||
}), { surfaceOp: 'append' })
|
||||
appendHeader(session, header('deepseek-v4-flash', {
|
||||
system: 'system',
|
||||
tools: [{ name: 'read', description: 'read', parameters: { type: 'object' } }],
|
||||
@@ -215,10 +230,10 @@ describe('TokenMeterService pricing', () => {
|
||||
it('keeps request-header overrides out of the returned surface', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('override-surface'))
|
||||
session.append('user/message', {
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'question' }],
|
||||
source: { kind: 'user' },
|
||||
}, { surfaceOp: 'append' })
|
||||
}), { surfaceOp: 'append' })
|
||||
|
||||
const logged = service.measure(session)
|
||||
const overridden = service.measure(session, header('another-model', {
|
||||
@@ -243,10 +258,10 @@ describe('replay anchors and surface folds', () => {
|
||||
it('uses disjoint provider usage and signed durable-output rewrites', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('usage'))
|
||||
session.append('user/message', {
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'before' }],
|
||||
source: { kind: 'user' },
|
||||
}, { surfaceOp: 'append' })
|
||||
}), { surfaceOp: 'append' })
|
||||
appendSuccessfulCall(session, header('deepseek-v4-flash'), {
|
||||
providerText: 'short',
|
||||
durableText: 'a much longer rewritten durable assistant answer',
|
||||
@@ -274,10 +289,10 @@ describe('replay anchors and surface folds', () => {
|
||||
const anchored = service.measure(session)
|
||||
expect(anchored.baseline.kind).toBe('estimated')
|
||||
const assistant = anchored.nodes[0]!.seq
|
||||
session.append('user/message', {
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'short' }],
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
}, {
|
||||
}), {
|
||||
surfaceOp: { op: 'replace', start: assistant, end: assistant },
|
||||
sourceEventSeqs: [assistant],
|
||||
})
|
||||
@@ -301,10 +316,10 @@ describe('replay anchors and surface folds', () => {
|
||||
const anchored = service.measure(session)
|
||||
expect(anchored.baseline.kind).toBe('estimated')
|
||||
expect(anchored.surfaceDeltaTokens).toBe(0)
|
||||
session.append('user/message', {
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'later' }],
|
||||
source: { kind: 'user' },
|
||||
}, { surfaceOp: 'append' })
|
||||
}), { surfaceOp: 'append' })
|
||||
const advanced = service.measure(session)
|
||||
expect(advanced.surfaceDeltaTokens).toBeGreaterThan(0)
|
||||
})
|
||||
@@ -389,10 +404,10 @@ describe('replay anchors and surface folds', () => {
|
||||
usage: USAGE,
|
||||
providerText: 'long provider answer '.repeat(100),
|
||||
})
|
||||
original.append('user/message', {
|
||||
original.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'new tail' }],
|
||||
source: { kind: 'user' },
|
||||
}, { surfaceOp: 'append' })
|
||||
}), { surfaceOp: 'append' })
|
||||
const seeded = new Session(SessionId('surface-seeded'), original.events)
|
||||
const before = service.measure(seeded)
|
||||
expect(before.nodes).toHaveLength(2)
|
||||
@@ -400,10 +415,10 @@ describe('replay anchors and surface folds', () => {
|
||||
expectSurfaceTotal(before)
|
||||
|
||||
const first = seeded.surface.nodes[0]!
|
||||
seeded.append('user/message', {
|
||||
seeded.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'replacement' }],
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
}, { surfaceOp: { op: 'replace', start: first, end: first }, sourceEventSeqs: [first] })
|
||||
}), { surfaceOp: { op: 'replace', start: first, end: first }, sourceEventSeqs: [first] })
|
||||
const after = service.measure(seeded)
|
||||
expect(after.nodes).toHaveLength(2)
|
||||
expect(after.nodes[0]!.seq).toBe(seeded.events.length - 1)
|
||||
@@ -442,10 +457,16 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
const session = new Session(SessionId('bad-step'))
|
||||
appendHeader(session, header('deepseek-v4-flash'))
|
||||
session.append('assistant/message', {
|
||||
provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
|
||||
turn: 1,
|
||||
step: 1,
|
||||
content: [{ type: 'text', text: 'bad' }],
|
||||
message: createMessage({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'bad' }],
|
||||
source: {
|
||||
kind: 'model',
|
||||
...{ provider: 'mock', model: 'deepseek-v4-flash' },
|
||||
},
|
||||
}),
|
||||
}, { surfaceOp: 'append', sourceEventSeqs: [] })
|
||||
expectRepeatedFailure(meter(), session, /no matching step\/start/)
|
||||
})
|
||||
@@ -465,10 +486,16 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
appendHeader(late, header('deepseek-v4-flash'))
|
||||
late.append('step/end', { turn: 1, step: 1 })
|
||||
late.append('assistant/message', {
|
||||
provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
|
||||
turn: 1,
|
||||
step: 1,
|
||||
content: [],
|
||||
message: createMessage({
|
||||
role: 'assistant',
|
||||
content: [],
|
||||
source: {
|
||||
kind: 'model',
|
||||
...{ provider: 'mock', model: 'deepseek-v4-flash' },
|
||||
},
|
||||
}),
|
||||
}, { surfaceOp: 'append', sourceEventSeqs: [] })
|
||||
expectRepeatedFailure(
|
||||
meter(),
|
||||
@@ -495,10 +522,10 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
{
|
||||
name: 'non-chunk',
|
||||
appendSource(session) {
|
||||
return [session.append('user/message', {
|
||||
return [session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'x' }],
|
||||
source: { kind: 'user' },
|
||||
}, { surfaceOp: 'append' }).seq]
|
||||
}), { surfaceOp: 'append' }).seq]
|
||||
},
|
||||
pattern: /is not assistant\/chunk/,
|
||||
},
|
||||
@@ -520,10 +547,16 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
appendHeader(session, header('deepseek-v4-flash'))
|
||||
const sourceEventSeqs = testCase.appendSource(session)
|
||||
session.append('assistant/message', {
|
||||
provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
|
||||
turn: 1,
|
||||
step: 1,
|
||||
content: [{ type: 'text', text: 'bad' }],
|
||||
message: createMessage({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'bad' }],
|
||||
source: {
|
||||
kind: 'model',
|
||||
...{ provider: 'mock', model: 'deepseek-v4-flash' },
|
||||
},
|
||||
}),
|
||||
usage: { inputTokens: 1, outputTokens: 1 },
|
||||
}, { surfaceOp: 'append', sourceEventSeqs })
|
||||
expect(() => meter().measure(session)).toThrow(testCase.pattern)
|
||||
@@ -544,10 +577,16 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
seq: duplicate.seq,
|
||||
time: 0,
|
||||
data: {
|
||||
provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
|
||||
turn: 1,
|
||||
step: 1,
|
||||
content: [],
|
||||
message: createMessage({
|
||||
role: 'assistant',
|
||||
content: [],
|
||||
source: {
|
||||
kind: 'model',
|
||||
...{ provider: 'mock', model: 'deepseek-v4-flash' },
|
||||
},
|
||||
}),
|
||||
usage: { inputTokens: 1, outputTokens: 0 },
|
||||
},
|
||||
surfaceOp: 'append',
|
||||
@@ -563,10 +602,16 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
seq: future.seq,
|
||||
time: 0,
|
||||
data: {
|
||||
provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
|
||||
turn: 1,
|
||||
step: 1,
|
||||
content: [],
|
||||
message: createMessage({
|
||||
role: 'assistant',
|
||||
content: [],
|
||||
source: {
|
||||
kind: 'model',
|
||||
...{ provider: 'mock', model: 'deepseek-v4-flash' },
|
||||
},
|
||||
}),
|
||||
usage: { inputTokens: 1, outputTokens: 0 },
|
||||
},
|
||||
surfaceOp: 'append',
|
||||
@@ -577,17 +622,23 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
|
||||
it('does not partially apply a malformed assistant replacement', () => {
|
||||
const session = new Session(SessionId('transactional-replace'))
|
||||
session.append('user/message', {
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'head' }],
|
||||
source: { kind: 'user' },
|
||||
}, { surfaceOp: 'append' })
|
||||
}), { surfaceOp: 'append' })
|
||||
appendHeader(session, header('deepseek-v4-flash'))
|
||||
const head = session.events[0]!.seq
|
||||
session.append('assistant/message', {
|
||||
provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
|
||||
turn: 1,
|
||||
step: 1,
|
||||
content: [{ type: 'text', text: 'replacement' }],
|
||||
message: createMessage({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'replacement' }],
|
||||
source: {
|
||||
kind: 'model',
|
||||
...{ provider: 'mock', model: 'deepseek-v4-flash' },
|
||||
},
|
||||
}),
|
||||
}, { surfaceOp: { op: 'replace', start: head, end: head }, sourceEventSeqs: [head] })
|
||||
expectRepeatedFailure(
|
||||
meter(),
|
||||
@@ -598,18 +649,18 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
|
||||
it('rejects corrupt replacement ranges without advancing the replay cursor', () => {
|
||||
const session = new Session(SessionId('bad-replace'))
|
||||
const head = session.append('user/message', {
|
||||
const head = session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'head' }],
|
||||
source: { kind: 'user' },
|
||||
}, { surfaceOp: 'append' }).seq
|
||||
}), { surfaceOp: 'append' }).seq
|
||||
appendUnchecked(session, {
|
||||
type: 'user/message',
|
||||
seq: session.seq,
|
||||
time: 0,
|
||||
data: {
|
||||
data: createUserMessage({
|
||||
content: [{ type: 'text', text: 'bad' }],
|
||||
source: { kind: 'user' },
|
||||
},
|
||||
}),
|
||||
surfaceOp: { op: 'replace', start: 99, end: 99 },
|
||||
sourceEventSeqs: [head],
|
||||
})
|
||||
@@ -633,10 +684,10 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
|
||||
}] })
|
||||
activeMeter.measure(session)
|
||||
session.append('user/message', {
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'one' }],
|
||||
source: { kind: 'user' },
|
||||
}, { surfaceOp: 'append' })
|
||||
}), { surfaceOp: 'append' })
|
||||
expect(revisions).toEqual([2])
|
||||
expect(activeMeter.measure(session).logRevision).toBe(2)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user