Skip to content

Commit 9dbba29

Browse files
committed
dev: coerce semantics of tool calls/use in messages
If these APIs were standardized this would have been so much less painful, but alas, tool calls are implemented completely differently between Claude and OpenAI. Good job guys.
1 parent 484bb5c commit 9dbba29

File tree

2 files changed

+74
-1
lines changed

2 files changed

+74
-1
lines changed

src/backend/src/modules/puterai/OpenAICompletionService.js

+30
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,36 @@ class OpenAICompletionService extends BaseService {
294294
if ( o.type ) continue;
295295
o.type = 'image_url';
296296
}
297+
298+
// coerce tool calls
299+
for ( let i = content.length - 1 ; i >= 0 ; i-- ) {
300+
const content_block = content[i];
301+
302+
if ( content_block.type === 'tool_use' ) {
303+
if ( ! msg.hasOwnProperty('tool_calls') ) {
304+
msg.tool_calls = [];
305+
}
306+
msg.tool_calls.push({
307+
id: content_block.id,
308+
type: 'function',
309+
function: {
310+
name: content_block.name,
311+
arguments: JSON.stringify(content_block.input),
312+
}
313+
});
314+
content.splice(i, 1);
315+
}
316+
}
317+
318+
// coerce tool results
319+
// (we assume multiple tool results were already split into separate messages)
320+
for ( let i = content.length - 1 ; i >= 0 ; i-- ) {
321+
const content_block = content[i];
322+
if ( content_block.type !== 'tool_result' ) continue;
323+
msg.role = 'tool';
324+
msg.tool_call_id = content_block.tool_use_id;
325+
msg.content = content_block.content;
326+
}
297327
}
298328

299329
console.log('MODEL IN USE ------- ', model);

src/backend/src/modules/puterai/lib/Messages.js

+44-1
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,24 @@ module.exports = class Messages {
1919
}
2020
if ( ! message.content ) {
2121
if ( message.tool_calls ) {
22-
return message;
22+
message.content = [];
23+
for ( let i=0 ; i < message.tool_calls.length ; i++ ) {
24+
const tool_call = message.tool_calls[i];
25+
message.content.push({
26+
type: 'tool_use',
27+
id: tool_call.id,
28+
name: tool_call.function.name,
29+
input: tool_call.function.arguments,
30+
});
31+
}
32+
delete message.tool_calls;
2333
}
2434
throw new Error(`each message must have a 'content' property`);
2535
}
2636
if ( whatis(message.content) !== 'array' ) {
2737
message.content = [message.content];
2838
}
39+
// Coerce each content block into an object
2940
for ( let i=0 ; i < message.content.length ; i++ ) {
3041
if ( whatis(message.content[i]) === 'string' ) {
3142
message.content[i] = {
@@ -41,6 +52,16 @@ module.exports = class Messages {
4152
}
4253
}
4354

55+
// Remove "text" properties from content blocks with type=tool_result
56+
for ( let i=0 ; i < message.content.length ; i++ ) {
57+
if ( message.content[i].type !== 'tool_use' ) {
58+
continue;
59+
}
60+
if ( message.content[i].hasOwnProperty('text') ) {
61+
delete message.content[i].text;
62+
}
63+
}
64+
4465
console.log('???', message)
4566
return message;
4667
}
@@ -49,6 +70,28 @@ module.exports = class Messages {
4970
messages[i] = this.normalize_single_message(messages[i], params);
5071
}
5172

73+
// Split messages with tool_use content into separate messages
74+
// TODO: unit test this
75+
messages = [...messages];
76+
for ( let i=0 ; i < messages.length ; i++ ) {
77+
let message = messages[i];
78+
let separated_messages = [];
79+
for ( let j=0 ; j < message.content.length ; j++ ) {
80+
if ( message.content[j].type === 'tool_result' ) {
81+
separated_messages.push({
82+
role: message.role,
83+
content: [message.content[j]],
84+
});
85+
} else {
86+
separated_messages.push({
87+
role: message.role,
88+
content: [message.content[j]],
89+
});
90+
}
91+
}
92+
messages.splice(i, 1, ...separated_messages);
93+
}
94+
5295
// If multiple messages are from the same role, merge them
5396
let merged_messages = [];
5497
let current_role = null;

0 commit comments

Comments
 (0)