chat_completions tool call fixes (#2434)

* chat_completions tool call fixes

* update chat completions test
This commit is contained in:
Neal Shah authored and GitHub committed 2026-02-06 15:10:27 -08:00
1 parent 06fe07e384
commit 28cedec9de
7 files changed
+509 -23

No files matched your search

@@ -132,6 +132,8 @@ module.exports = eggspress('/openai/v1/chat/completions', {
let buffer = '';
let usage = null;
let toolCallIndex = 0;
let sawToolCalls = false;
const sendChunk = (delta, finishReason = null, extra = {}) => {
const payload = {
@@ -170,9 +172,11 @@ module.exports = eggspress('/openai/v1/chat/completions', {
sendChunk({ content: event.text });
}
if ( event.type === 'tool_use' ) {
sawToolCalls = true;
sendChunk({
tool_calls: [
{
index: toolCallIndex++,
id: event.id,
type: 'function',
function: {
@@ -181,7 +185,7 @@ module.exports = eggspress('/openai/v1/chat/completions', {
},
},
],
}, 'tool_calls');
});
}
if ( event.type === 'usage' ) {
usage = event.usage;
@@ -190,7 +194,8 @@ module.exports = eggspress('/openai/v1/chat/completions', {
});
streamValue.on('end', () => {
sendChunk({}, 'stop', usage ? { usage: buildUsage(usage) } : {});
const finishReason = sawToolCalls ? 'tool_calls' : 'stop';
sendChunk({}, finishReason, usage ? { usage: buildUsage(usage) } : {});
res.write('data: [DONE]\n\n');
res.end();
});
@@ -54,9 +54,13 @@ export const normalize_tools_object = (tools) => {
fn.parameters ||
fn.input_schema;
normal_fn.parameters = parameters ?? {
type: 'object',
};
if ( !parameters || typeof parameters !== 'object' ) {
parameters = { type: 'object' };
} else if ( ! parameters.type ) {
parameters.type = 'object';
}
normal_fn.parameters = parameters;
if ( parameters.properties ) {
parameters = normalize_json_schema(parameters);
+40 -14
View File
@@ -44,6 +44,22 @@ export const normalize_single_message = (message, params = {}) => {
throw new Error('each message must have a \'content\' property');
}
}
// Normalize OpenAI-style tool results into internal tool_result blocks
if ( message.role === 'tool' ) {
const tool_use_id = message.tool_call_id || message.tool_use_id || message.id;
const tool_content = message.content;
message.tool_use_id = tool_use_id;
message.content = [
{
type: 'tool_result',
tool_use_id,
content: typeof tool_content === 'string'
? tool_content
: JSON.stringify(tool_content ?? {}),
},
];
}
if ( ! Array.isArray(message.content) ) {
message.content = [message.content];
}
@@ -90,33 +106,43 @@ export const normalize_messages = (messages, params = {}) => {
messages[i] = normalize_single_message(messages[i], params);
}
// Split messages with tool_use content into separate messages
// Split messages with multiple content blocks into separate messages.
// Keep assistant tool_use blocks together to preserve OpenAI tool-call ordering.
// TODO: unit test this
messages = [...messages];
for ( let i = 0 ; i < messages.length ; i++ ) {
let message = messages[i];
let separated_messages = [];
const has_tool_use = message.role === 'assistant' &&
message.content?.some(c => c?.type === 'tool_use');
if ( has_tool_use ) {
separated_messages.push(message);
messages.splice(i, 1, ...separated_messages);
continue;
}
for ( let j = 0 ; j < message.content.length ; j++ ) {
if ( message.content[j].type === 'tool_result' ) {
separated_messages.push({
...message,
content: [message.content[j]],
});
} else {
separated_messages.push({
...message,
content: [message.content[j]],
});
}
separated_messages.push({
...message,
content: [message.content[j]],
});
}
messages.splice(i, 1, ...separated_messages);
}
// If multiple messages are from the same role, merge them
// but avoid merging tool_use/tool_result messages, since order matters
const hasToolContent = (message) => {
if ( !message || !Array.isArray(message.content) ) return false;
return message.content.some((part) =>
part && (part.type === 'tool_use' || part.type === 'tool_result'));
};
let merged_messages = [];
let current_role = null;
for ( let i = 0 ; i < messages.length ; i++ ) {
if ( current_role === messages[i].role ) {
const can_merge = current_role === messages[i].role &&
!hasToolContent(messages[i]) &&
!hasToolContent(merged_messages[merged_messages.length - 1]);
if ( can_merge ) {
merged_messages[merged_messages.length - 1].content.push(...messages[i].content);
} else {
merged_messages.push(messages[i]);
@@ -180,4 +206,4 @@ export const extract_text = (messages) => {
return '';
}
}).join(' ');
};
};