fix: /v1/responses endpoint drops tool calls (function_call) in non-streaming mode

Also fixes streaming tool call event type and response status.

Non-streaming:
- ClaudeConverter.toOpenAIResponsesResponse now handles tool_use
  content blocks, emitting function_call output items
- OpenAIConverter.toOpenAIResponsesResponse now handles
  message.tool_calls, emitting function_call output items
- Both set status to 'requires_action' when tool calls are present

Streaming:
- OpenAIConverter uses response.function_call_arguments.delta
  instead of response.custom_tool_call_input.delta
- generateResponseCompleted sets status to 'requires_action'
  when tool calls are tracked in stream state

Fixes #471
This commit is contained in:
amarcin 2026-04-09 17:40:22 -07:00
parent 82a6ec2f43
commit e443d9d891
3 changed files with 52 additions and 14 deletions

View file

@ -1648,22 +1648,42 @@ export class ClaudeConverter extends BaseConverter {
* Claude响应 -> OpenAI Responses响应
*/
toOpenAIResponsesResponse(claudeResponse, model) {
const content = this.processClaudeResponseContent(claudeResponse.content);
const textContent = typeof content === 'string' ? content : JSON.stringify(content);
const output = [];
const messageContent = [];
let hasToolUse = false;
let output = [];
output.push({
// Process Claude content blocks, handling both text and tool_use
if (Array.isArray(claudeResponse.content)) {
for (const block of claudeResponse.content) {
if (block.type === 'text' && block.text) {
messageContent.push({
annotations: [],
logprobs: [],
text: block.text,
type: "output_text"
});
} else if (block.type === 'tool_use') {
hasToolUse = true;
output.push({
type: "function_call",
id: block.id || `fc_${uuidv4().replace(/-/g, '')}`,
call_id: block.id || `call_${uuidv4().replace(/-/g, '')}`,
name: block.name,
arguments: typeof block.input === 'string' ? block.input : JSON.stringify(block.input || {}),
status: "completed"
});
}
}
}
// Always include the message output item (even if content is empty)
output.unshift({
type: "message",
id: `msg_${uuidv4().replace(/-/g, '')}`,
summary: [],
role: "assistant",
status: "completed",
content: [{
annotations: [],
logprobs: [],
text: textContent,
type: "output_text"
}]
content: messageContent
});
return {
@ -1684,7 +1704,7 @@ export class ClaudeConverter extends BaseConverter {
reasoning: {},
safety_identifier: "user-" + uuidv4().replace(/-/g, ''),
service_tier: "default",
status: "completed",
status: hasToolUse ? "requires_action" : "completed",
store: false,
temperature: 1,
text: {

View file

@ -1642,11 +1642,29 @@ export class OpenAIConverter extends BaseConverter {
content: messageContent
});
// Handle tool calls (function_call output items)
if (message.tool_calls && message.tool_calls.length > 0) {
for (const tc of message.tool_calls) {
if (tc.type === 'function' && tc.function) {
output.push({
type: 'function_call',
id: tc.id || `fc_${Date.now()}`,
call_id: tc.id || `call_${Date.now()}`,
name: tc.function.name,
arguments: tc.function.arguments || '{}',
status: 'completed'
});
}
}
}
const hasToolCalls = message.tool_calls && message.tool_calls.length > 0;
return {
id: openaiResponse.id || `resp_${Date.now()}`,
object: 'response',
created_at: openaiResponse.created || Math.floor(Date.now() / 1000),
status: choice.finish_reason === 'stop' ? 'completed' : 'in_progress',
status: hasToolCalls ? 'requires_action' : (choice.finish_reason === 'stop' ? 'completed' : 'in_progress'),
model: model || openaiResponse.model || 'unknown',
output: output,
usage: openaiResponse.usage ? {
@ -1736,7 +1754,7 @@ export class OpenAIConverter extends BaseConverter {
item_id: toolCall.id || `call_${uuidv4().replace(/-/g, '')}`,
output_index: outputIndex,
sequence_number: 3,
type: "response.custom_tool_call_input.delta"
type: "response.function_call_arguments.delta"
});
}
}

View file

@ -304,7 +304,7 @@ function generateResponseCompleted(requestId, usage) {
},
safety_identifier: `user-${uuidv4().replace(/-/g, '')}`, // 随机值
service_tier: "default",
status: "completed",
status: (state.toolCalls && state.toolCalls.length > 0) ? "requires_action" : "completed",
store: false,
temperature: 1,
text: {