mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 08:27:46 +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', () => {
|
||||
const requestBody = {
|
||||
model: 'gpt-5.4',
|
||||
|
||||
@@ -698,8 +698,9 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
|
||||
// 从 output 中提取文本和工具调用(备用方案)
|
||||
if (Array.isArray(response?.output)) {
|
||||
for (const rawItem of response.output as unknown[]) {
|
||||
const item = rawItem as RawObject
|
||||
const output = response.output as unknown[]
|
||||
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)) {
|
||||
for (const rawContent of item.content as unknown[]) {
|
||||
const content = rawContent as RawObject
|
||||
@@ -710,12 +711,14 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
} else if (this.isResponsesCallItemType(item.type)) {
|
||||
const itemId = typeof item.id === 'string' ? item.id : ''
|
||||
const toolId = this.responsesCallId(item)
|
||||
ensureToolCall(
|
||||
itemId || toolId,
|
||||
toolId,
|
||||
this.responsesCallName(item),
|
||||
this.responsesCallInput(item)
|
||||
)
|
||||
// 与流式阶段使用同一套 key 命中同一条工具调用,避免重复渲染
|
||||
const key = itemId || toolId || outputIndexToToolKey.get(index) || String(index)
|
||||
// 仅在最终项确实带有输入时才覆盖,避免用 '{}' 等默认值
|
||||
// 冲掉已通过增量事件收集到的参数
|
||||
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 -> 工具调用
|
||||
if (this.isResponsesCallItemType(itemType)) {
|
||||
const toolName = this.responsesCallName(item) || '工具调用'
|
||||
const toolName = this.responsesCallName(item)
|
||||
const args = this.formatJson(this.responsesCallInput(item))
|
||||
return createMessageBlock('assistant', [
|
||||
createToolUseRenderBlock(toolName, args, this.responsesCallId(item)),
|
||||
@@ -1136,7 +1139,7 @@ export class OpenAIParser implements ApiFormatParser {
|
||||
} else if (this.isResponsesCallItemType(item.type)) {
|
||||
blocks.push(createMessageBlock('assistant', [
|
||||
createToolUseRenderBlock(
|
||||
this.responsesCallName(item) || '工具调用',
|
||||
this.responsesCallName(item),
|
||||
this.formatJson(this.responsesCallInput(item)),
|
||||
this.responsesCallId(item)
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user