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:
Yichen Jiang
2026-07-29 10:18:53 +08:00
763 changed files with 19809 additions and 7096 deletions
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { CallId, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import { AttachmentId } from '@deepseek-ai/dsh-attachment'
import { createUserMessage, CallId, ReasoningEffortId, createMessage } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-llm'
import { serializeMessages, serializeRequest } from '../src/serialize.ts'
@@ -11,27 +11,34 @@ function request(overrides: Partial<GenerateOptions> = {}): GenerateOptions {
describe('serializeMessages', () => {
it('maps user text to string content', () => {
const wire = serializeMessages([
{ role: 'user', content: [{ type: 'text', text: 'hello ' }, { type: 'text', text: 'world' }] },
createUserMessage({
content: [{ type: 'text', text: 'hello ' }, { type: 'text', text: 'world' }],
source: { kind: 'plugin', plugin: 'test' },
}),
])
expect(wire).toEqual([{ role: 'user', content: 'hello world' }])
})
it('maps system-role messages in history', () => {
const wire = serializeMessages([
{ role: 'system', content: [{ type: 'text', text: 'be brief' }] },
createMessage({
role: 'system', content: [{ type: 'text', text: 'be brief' }],
source: { kind: 'plugin', plugin: 'test' },
}),
])
expect(wire).toEqual([{ role: 'system', content: 'be brief' }])
})
it('maps plain assistant text without reasoning_content', () => {
const wire = serializeMessages([
{
createMessage({
role: 'assistant',
content: [
{ type: 'reasoning', text: 'thinking…' },
{ type: 'text', text: 'answer' },
],
},
source: { kind: 'plugin', plugin: 'test' },
}),
])
// Tool-call-free turn: reasoning is dropped (ignored by the API anyway).
expect(wire).toEqual([{ role: 'assistant', content: 'answer' }])
@@ -39,13 +46,14 @@ describe('serializeMessages', () => {
it('passes reasoning_content back on tool-call turns (official passback rule)', () => {
const wire = serializeMessages([
{
createMessage({
role: 'assistant',
content: [
{ type: 'reasoning', text: 'I should check the weather.' },
{ type: 'tool-call', id: CallId('call-1'), name: 'get_weather', arguments: '{"city":"Paris"}' },
],
},
source: { kind: 'plugin', plugin: 'test' },
}),
])
expect(wire).toEqual([{
role: 'assistant',
@@ -59,13 +67,14 @@ describe('serializeMessages', () => {
it('serializes parallel tool calls in order', () => {
const wire = serializeMessages([
{
createMessage({
role: 'assistant',
content: [
{ type: 'tool-call', id: CallId('a'), name: 'one', arguments: '{}' },
{ type: 'tool-call', id: CallId('b'), name: 'two', arguments: '{}' },
],
},
source: { kind: 'plugin', plugin: 'test' },
}),
])
const assistant = wire[0] as { tool_calls: { id: string }[] }
expect(assistant.tool_calls.map(call => call.id)).toEqual(['a', 'b'])
@@ -73,37 +82,37 @@ describe('serializeMessages', () => {
it('turns tool results into role:tool messages', () => {
const wire = serializeMessages([
{
role: 'user',
createUserMessage({
content: [{
type: 'tool-result',
toolCallId: CallId('call-1'),
content: [{ type: 'text', text: 'Sunny 22C' }],
}],
},
source: { kind: 'plugin', plugin: 'test' },
}),
])
expect(wire).toEqual([{ role: 'tool', tool_call_id: 'call-1', content: 'Sunny 22C' }])
})
it('sends a sentinel for empty tool-result content', () => {
const wire = serializeMessages([
{
role: 'user',
createUserMessage({
content: [{ type: 'tool-result', toolCallId: CallId('call-1'), content: [] }],
},
source: { kind: 'plugin', plugin: 'test' },
}),
])
expect(wire).toEqual([{ role: 'tool', tool_call_id: 'call-1', content: '(no output)' }])
})
it('splits mixed user text + tool results into separate wire messages', () => {
const wire = serializeMessages([
{
role: 'user',
createUserMessage({
content: [
{ type: 'text', text: 'context note' },
{ type: 'tool-result', toolCallId: CallId('call-1'), content: [{ type: 'text', text: 'ok' }] },
],
},
source: { kind: 'plugin', plugin: 'test' },
}),
])
expect(wire).toEqual([
{ role: 'user', content: 'context note' },
@@ -113,20 +122,19 @@ describe('serializeMessages', () => {
it('skips plugin-added block types (merge-extensible ContentBlockMap)', () => {
const wire = serializeMessages([
{
role: 'user',
createUserMessage({
content: [
{ type: 'chart', data: 'x' } as unknown as ContentBlock,
{ type: 'text', text: 'see chart' },
],
},
source: { kind: 'plugin', plugin: 'test' },
}),
])
expect(wire).toEqual([{ role: 'user', content: 'see chart' }])
})
it('rejects image blocks instead of silently flattening them away', () => {
expect(() => serializeMessages([{
role: 'user',
expect(() => serializeMessages([createUserMessage({
content: [{
type: 'image',
attachment: {
@@ -134,17 +142,24 @@ describe('serializeMessages', () => {
mediaType: 'image/png', bytes: 68, width: 1, height: 1,
},
}],
}])).toThrow(expect.objectContaining({ code: 'UNSUPPORTED_CONTENT' }))
source: { kind: 'plugin', plugin: 'test' },
})])).toThrow(expect.objectContaining({ code: 'UNSUPPORTED_CONTENT' }))
})
it('emits an empty user message rather than dropping block-less messages', () => {
const wire = serializeMessages([{ role: 'user', content: [] }])
const wire = serializeMessages([createUserMessage({
content: [],
source: { kind: 'plugin', plugin: 'test' },
})])
expect(wire).toEqual([{ role: 'user', content: '' }])
})
})
describe('serializeRequest', () => {
const history: Message[] = [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }]
const history: Message[] = [createUserMessage({
content: [{ type: 'text', text: 'hi' }],
source: { kind: 'plugin', plugin: 'test' },
})]
it('always streams with usage and maps the basics', () => {
const wire = serializeRequest(request({ messages: history }))
@@ -260,7 +275,10 @@ describe('review fixes: assistant content shapes', () => {
// Aborted/empty assistant turns: no text, no calls → "". The earlier
// null shape was live-falsified: the API 400s a null-content assistant
// message without tool_calls ("content or tool_calls must be set").
const wire = serializeMessages([{ role: 'assistant', content: [] }])
const wire = serializeMessages([createMessage({
role: 'assistant', content: [],
source: { kind: 'plugin', plugin: 'test' },
})])
expect(wire).toEqual([{ role: 'assistant', content: '' }])
})
@@ -269,15 +287,19 @@ describe('review fixes: assistant content shapes', () => {
// greeting did, live). The passback rule keeps reasoning_content off
// plain turns, and content must still be SET — a null here poisoned the
// session log and bricked every later turn of that session.
const wire = serializeMessages([{ role: 'assistant', content: [{ type: 'reasoning', text: '你好!有什么我可以帮你的吗?' }] }])
const wire = serializeMessages([createMessage({
role: 'assistant', content: [{ type: 'reasoning', text: '你好!有什么我可以帮你的吗?' }],
source: { kind: 'plugin', plugin: 'test' },
})])
expect(wire).toEqual([{ role: 'assistant', content: '' }])
})
it('serializes tool-call turns with empty string content, not null', () => {
const wire = serializeMessages([{
const wire = serializeMessages([createMessage({
role: 'assistant',
content: [{ type: 'tool-call', id: CallId('c'), name: 'f', arguments: '{}' }],
}])
source: { kind: 'plugin', plugin: 'test' },
})])
expect(wire[0]).toMatchObject({ content: '' })
})
})