mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-05 00:47:48 +08:00
fix(usage): keep streamed call args in responses completion
The response.completed fallback rebuilt every call item with responsesCallInput(), which returns '{}' for a function_call lacking arguments. Since '{}' is truthy, ensureToolCall overwrote arguments already collected from streamed delta events. Guard the completed branch with responsesCallHasInput (matching the output_item.done branch) so empty/default inputs no longer clobber streamed args, and align its dedupe key with the streaming phase to avoid duplicate tool-call rendering when an item has no id. Drop the now-dead '工具调用' fallbacks since responsesCallName never returns empty.
This commit is contained in:
@@ -243,6 +243,45 @@ describe('Conversation stream compatibility', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps streamed function_call arguments when response.completed omits them', () => {
|
||||||
|
const requestBody = {
|
||||||
|
model: 'gpt-5.5',
|
||||||
|
stream: true,
|
||||||
|
input: 'What is the weather?',
|
||||||
|
}
|
||||||
|
const rawSse = [
|
||||||
|
'event: response.created',
|
||||||
|
`data: ${JSON.stringify({ type: 'response.created', response: { id: 'resp_fc_1', object: 'response', model: 'gpt-5.5', status: 'in_progress' } })}`,
|
||||||
|
'',
|
||||||
|
'event: response.output_item.added',
|
||||||
|
`data: ${JSON.stringify({ type: 'response.output_item.added', output_index: 0, item: { id: 'fc_1', type: 'function_call', status: 'in_progress', call_id: 'call_1', name: 'get_weather', arguments: '' } })}`,
|
||||||
|
'',
|
||||||
|
'event: response.function_call_arguments.delta',
|
||||||
|
`data: ${JSON.stringify({ type: 'response.function_call_arguments.delta', output_index: 0, item_id: 'fc_1', delta: '{"city":' })}`,
|
||||||
|
'',
|
||||||
|
'event: response.function_call_arguments.delta',
|
||||||
|
`data: ${JSON.stringify({ type: 'response.function_call_arguments.delta', output_index: 0, item_id: 'fc_1', delta: '"SF"}' })}`,
|
||||||
|
'',
|
||||||
|
// 最终项故意不带 arguments:解析器不应用 '{}' 冲掉已收集的增量参数
|
||||||
|
'event: response.completed',
|
||||||
|
`data: ${JSON.stringify({ type: 'response.completed', response: { id: 'resp_fc_1', object: 'response', model: 'gpt-5.5', status: 'completed', output: [{ id: 'fc_1', type: 'function_call', status: 'completed', call_id: 'call_1', name: 'get_weather' }] } })}`,
|
||||||
|
'',
|
||||||
|
'data: [DONE]',
|
||||||
|
'',
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
const parsed = parseResponse(rawSse, requestBody, 'openai:responses')
|
||||||
|
// 命中同一 key,不重复渲染
|
||||||
|
expect(parsed.messages).toHaveLength(1)
|
||||||
|
expect(parsed.messages[0]?.content).toHaveLength(1)
|
||||||
|
expect(parsed.messages[0]?.content[0]).toMatchObject({
|
||||||
|
type: 'tool_use',
|
||||||
|
toolName: 'get_weather',
|
||||||
|
toolId: 'call_1',
|
||||||
|
input: '{"city":"SF"}',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('renders HTML-entity encoded OpenAI tool arguments as formatted JSON', () => {
|
it('renders HTML-entity encoded OpenAI tool arguments as formatted JSON', () => {
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
model: 'gpt-5.4',
|
model: 'gpt-5.4',
|
||||||
|
|||||||
@@ -698,8 +698,9 @@ export class OpenAIParser implements ApiFormatParser {
|
|||||||
|
|
||||||
// 从 output 中提取文本和工具调用(备用方案)
|
// 从 output 中提取文本和工具调用(备用方案)
|
||||||
if (Array.isArray(response?.output)) {
|
if (Array.isArray(response?.output)) {
|
||||||
for (const rawItem of response.output as unknown[]) {
|
const output = response.output as unknown[]
|
||||||
const item = rawItem as RawObject
|
for (let index = 0; index < output.length; index++) {
|
||||||
|
const item = output[index] as RawObject
|
||||||
if (textParts.length === 0 && item?.type === 'message' && Array.isArray(item?.content)) {
|
if (textParts.length === 0 && item?.type === 'message' && Array.isArray(item?.content)) {
|
||||||
for (const rawContent of item.content as unknown[]) {
|
for (const rawContent of item.content as unknown[]) {
|
||||||
const content = rawContent as RawObject
|
const content = rawContent as RawObject
|
||||||
@@ -710,12 +711,14 @@ export class OpenAIParser implements ApiFormatParser {
|
|||||||
} else if (this.isResponsesCallItemType(item.type)) {
|
} else if (this.isResponsesCallItemType(item.type)) {
|
||||||
const itemId = typeof item.id === 'string' ? item.id : ''
|
const itemId = typeof item.id === 'string' ? item.id : ''
|
||||||
const toolId = this.responsesCallId(item)
|
const toolId = this.responsesCallId(item)
|
||||||
ensureToolCall(
|
// 与流式阶段使用同一套 key 命中同一条工具调用,避免重复渲染
|
||||||
itemId || toolId,
|
const key = itemId || toolId || outputIndexToToolKey.get(index) || String(index)
|
||||||
toolId,
|
// 仅在最终项确实带有输入时才覆盖,避免用 '{}' 等默认值
|
||||||
this.responsesCallName(item),
|
// 冲掉已通过增量事件收集到的参数
|
||||||
this.responsesCallInput(item)
|
const input = this.responsesCallHasInput(item)
|
||||||
)
|
? this.responsesCallInput(item)
|
||||||
|
: ''
|
||||||
|
ensureToolCall(key, toolId, this.responsesCallName(item), input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1007,7 +1010,7 @@ export class OpenAIParser implements ApiFormatParser {
|
|||||||
|
|
||||||
// Responses API call item -> 工具调用
|
// Responses API call item -> 工具调用
|
||||||
if (this.isResponsesCallItemType(itemType)) {
|
if (this.isResponsesCallItemType(itemType)) {
|
||||||
const toolName = this.responsesCallName(item) || '工具调用'
|
const toolName = this.responsesCallName(item)
|
||||||
const args = this.formatJson(this.responsesCallInput(item))
|
const args = this.formatJson(this.responsesCallInput(item))
|
||||||
return createMessageBlock('assistant', [
|
return createMessageBlock('assistant', [
|
||||||
createToolUseRenderBlock(toolName, args, this.responsesCallId(item)),
|
createToolUseRenderBlock(toolName, args, this.responsesCallId(item)),
|
||||||
@@ -1136,7 +1139,7 @@ export class OpenAIParser implements ApiFormatParser {
|
|||||||
} else if (this.isResponsesCallItemType(item.type)) {
|
} else if (this.isResponsesCallItemType(item.type)) {
|
||||||
blocks.push(createMessageBlock('assistant', [
|
blocks.push(createMessageBlock('assistant', [
|
||||||
createToolUseRenderBlock(
|
createToolUseRenderBlock(
|
||||||
this.responsesCallName(item) || '工具调用',
|
this.responsesCallName(item),
|
||||||
this.formatJson(this.responsesCallInput(item)),
|
this.formatJson(this.responsesCallInput(item)),
|
||||||
this.responsesCallId(item)
|
this.responsesCallId(item)
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user