Merge remote-tracking branch 'origin/master' into worktree/pr504-retarget-latest-master
# Conflicts: # docs/event-producer-consumer.md # examples/tui-agent/README.md # examples/tui-agent/composition.md # examples/tui-agent/cordis.yml # examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.expected.txt # examples/tui-agent/tests/tui-keyless-smoke.e2e.ts # packages/ui/tui/README.md # vitest.config.ts
This commit is contained in:
@@ -40,10 +40,15 @@ function serializeAssistant(message: Message): WireMessage {
|
||||
|
||||
return {
|
||||
role: 'assistant',
|
||||
// Tool-call turns send "" rather than null: the live API answers both,
|
||||
// but the official samples replay message.content verbatim (which is ""
|
||||
// for pure tool-call responses) and some gateways reject null outright.
|
||||
content: text.length > 0 ? text : toolCalls.length > 0 ? '' : null,
|
||||
// Text-less turns send "" — NEVER null. Pure tool-call turns: the
|
||||
// official samples replay message.content verbatim (which is "") and
|
||||
// some gateways reject null outright. Reasoning-ONLY turns (the model
|
||||
// can answer entirely in the reasoning channel, e.g. a v4-flash
|
||||
// greeting): the live API rejects null-content/no-tool_calls assistant
|
||||
// messages with a 400 ("content or tool_calls must be set"), and since
|
||||
// the message sits durably in the session log, a null here bricks every
|
||||
// later turn of that session.
|
||||
content: text,
|
||||
// Official passback rule (guides/thinking_mode.mdx): reasoning_content
|
||||
// must return on tool-call turns; it is ignored on plain turns, so we
|
||||
// drop it there to save tokens.
|
||||
|
||||
@@ -187,12 +187,22 @@ describe('serializeRequest', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('assistant empty and tool-call content shapes', () => {
|
||||
it('serializes a content-less, tool-call-less assistant message as null content', () => {
|
||||
// Aborted/empty assistant turns: no text, no calls → null (the wire
|
||||
// accepts it; "" is reserved for tool-call turns per the samples).
|
||||
describe('review fixes: assistant content shapes', () => {
|
||||
it('serializes a content-less, tool-call-less assistant message as "" content, never null', () => {
|
||||
// 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: [] }])
|
||||
expect(wire).toEqual([{ role: 'assistant', content: null }])
|
||||
expect(wire).toEqual([{ role: 'assistant', content: '' }])
|
||||
})
|
||||
|
||||
it('serializes a reasoning-ONLY assistant message as "" content with the reasoning dropped', () => {
|
||||
// The model can answer entirely in the reasoning channel (a v4-flash
|
||||
// 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: '你好!有什么我可以帮你的吗?' }] }])
|
||||
expect(wire).toEqual([{ role: 'assistant', content: '' }])
|
||||
})
|
||||
|
||||
it('serializes tool-call turns with empty string content, not null', () => {
|
||||
|
||||
Reference in New Issue
Block a user