Claude: new prompt converter + non-streaming tools

c3c10a629e000f3d7479f9a50fbb6d28316acaa0

Cohee <18619528+Cohee1207@users.noreply.github.com>

3 files changed, +109 -67Showing whitespace changes
public/scripts/tool-calling.js+22 -25
@@ -339,10 +339,9 @@ export class ToolManager {
339339
340340 const supportedSources = [
341341 chat_completion_sources.OPENAI,
342- //chat_completion_sources.COHERE,
343342 chat_completion_sources.CUSTOM,
344343 chat_completion_sources.MISTRALAI,
345344 //chat_completion_sources.CLAUDE,
346345 chat_completion_sources.OPENROUTER,
347346 chat_completion_sources.GROQ,
348347 ];
@@ -372,18 +371,29 @@ export class ToolManager {
372371 }
373372
374373 // Parsed tool calls from non-streaming data
375374 if (!Array.isArray(data?.choices)) {
376- return;
377- }
378-
379375 // Find a choice with 0-index
380376 const choice = data.choices.find(choice => choice.index === 0);
381377
382378 if (!choice) {
383- return;
379+ return choice.message.tool_calls;
380+ }
384381 }
385382
386- return choice.message.tool_calls;
383+ if (Array.isArray(data?.content)) {
384+ // Claude tool calls to OpenAI tool calls
385+ const content = data.content.filter(c => c.type === 'tool_use').map(c => {
386+ return {
387+ id: c.id,
388+ function: {
389+ name: c.name,
390+ arguments: c.input,
391+ },
392+ };
393+ });
394+
395+ return content;
396+ }
387397 }
388398
389399 /**
@@ -407,7 +417,6 @@ export class ToolManager {
407417 chat_completion_sources.GROQ,
408418 ];
409419
410- if (oaiCompatibleSources.includes(oai_settings.chat_completion_source)) {
411420 if (!Array.isArray(toolCalls)) {
412421 return result;
413422 }
@@ -445,21 +454,6 @@ export class ToolManager {
445454 };
446455 result.invocations.push(invocation);
447456 }
448- }
449-
450- /*
451- if ([chat_completion_sources.CLAUDE].includes(oai_settings.chat_completion_source)) {
452- if (!Array.isArray(data?.content)) {
453- return;
454- }
455-
456- for (const content of data.content) {
457- if (content.type === 'tool_use') {
458- const args = { name: content.name, arguments: JSON.stringify(content.input) };
459- }
460- }
461- }
462- */
463457
464458 return result;
465459 }
@@ -491,6 +485,9 @@ export class ToolManager {
491485 * @param {ToolInvocation[]} invocations Successful tool invocations
492486 */
493487 static saveFunctionToolInvocations(invocations) {
488+ if (!Array.isArray(invocations) || invocations.length === 0) {
489+ return;
490+ }
494491 const message = {
495492 name: systemUserName,
496493 force_avatar: system_avatar,
src/endpoints/backends/chat-completions.js+0 -2
@@ -124,7 +124,6 @@ async function sendClaudeRequest(request, response) {
124124 } else {
125125 delete requestBody.system;
126126 }
127- /*
128127 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
129128 // Claude doesn't do prefills on function calls, and doesn't allow empty messages
130129 if (convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') {
@@ -137,7 +136,6 @@ async function sendClaudeRequest(request, response) {
137136 .map(tool => tool.function)
138137 .map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));
139138 }
140- */
141139 if (enableSystemPromptCache) {
142140 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
143141 }
src/prompt-converters.js+87 -40
@@ -118,8 +118,27 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
118118 });
119119 }
120120 }
121+
121122 // Now replace all further messages that have the role 'system' with the role 'user'. (or all if we're not using one)
122123 messages.forEach((message) => {
124+ if (message.role === 'assistant' && message.tool_calls) {
125+ message.content = message.tool_calls.map((tc) => ({
126+ type: 'tool_use',
127+ id: tc.id,
128+ name: tc.function.name,
129+ input: tc.function.arguments,
130+ }));
131+ }
132+
133+ if (message.role === 'tool') {
134+ message.role = 'user';
135+ message.content = [{
136+ type: 'tool_result',
137+ tool_use_id: message.tool_call_id,
138+ content: message.content,
139+ }];
140+ }
141+
123142 if (message.role === 'system') {
124143 if (userName && message.name === 'example_user') {
125144 message.content = `${userName}: ${message.content}`;
@@ -128,65 +147,93 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
128147 message.content = `${charName}: ${message.content}`;
129148 }
130149 message.role = 'user';
150+
151+ // Delete name here so it doesn't get added later
152+ delete message.name;
131153 }
132- });
133154
134- // Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling
155+ // Convert everything to an array of it would be easier to work with
135- if (prefillString) {
156+ if (typeof message.content === 'string') {
136- messages.push({
157+ // Take care of name properties since claude messages don't support them
137- role: 'assistant',
158+ if (message.name) {
138- content: prefillString.trimEnd(),
159+ message.content = `${message.name}: ${message.content}`;
139- });
140160 }
141161
142- // Since the messaging endpoint only supports user assistant roles in turns, we have to merge messages with the same role if they follow eachother
162+ message.content = [{ type: 'text', text: message.content }];
143- // Also handle multi-modality, holy slop.
163+ } else if (Array.isArray(message.content)) {
144- let mergedMessages = [];
164+ message.content = message.content.map((content) => {
145- messages.forEach((message) => {
165+ if (content.type === 'image_url') {
146166 const imageEntry = message.content?.[1]?.image_url;
147167 const imageData = imageEntry?.url;
148168 const mimeType = imageData?.split(';')?.[0].split(':')?.[1];
149169 const base64Data = imageData?.split(',')?.[1];
150170
151- // Take care of name properties since claude messages don't support them
171+ return {
172+ type: 'image',
173+ source: {
174+ type: 'base64',
175+ media_type: mimeType,
176+ data: base64Data,
177+ },
178+ };
179+ }
180+
181+ if (content.type === 'text') {
152182 if (message.name) {
153- if (Array.isArray(message.content)) {
183+ content.text = `${message.name}: ${content.text}`;
154- message.content[0].text = `${message.name}: ${message.content[0].text}`;
155- } else {
156- message.content = `${message.name}: ${message.content}`;
157184 }
185+
186+ return content;
187+ }
188+
189+ return content;
190+ });
191+ }
192+
193+ // Remove offending properties
158194 delete message.name;
195+ delete message.tool_calls;
196+ delete message.tool_call_id;
197+ });
198+
199+ // Images in assistant messages should be moved to the next user message
200+ for (let i = 0; i < messages.length; i++) {
201+ if (messages[i].role === 'assistant' && messages[i].content.some(c => c.type === 'image')) {
202+ // Find the next user message
203+ let j = i + 1;
204+ while (j < messages.length && messages[j].role !== 'user') {
205+ j++;
159206 }
160207
161- if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role) {
208+ // Move the images
162209 if (Array.isArray(messagej >= messages.content)length) {
163- if (Array.isArray(mergedMessages[mergedMessages.length - 1].content)) {
210+ // If there is no user message after the assistant message, add a new one
164- mergedMessages[mergedMessages.length - 1].content[0].text += '\n\n' + message.content[0].text;
211+ messages.splice(i + 1, 0, { role: 'user', content: [] });
165- } else {
212+ }
166- mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content[0].text;
213+
214+ messages[j].content.push(...messages[i].content.filter(c => c.type === 'image'));
215+ messages[i].content = messages[i].content.filter(c => c.type !== 'image');
167216 }
168- } else {
169- if (Array.isArray(mergedMessages[mergedMessages.length - 1].content)) {
170- mergedMessages[mergedMessages.length - 1].content[0].text += '\n\n' + message.content;
171- } else {
172- mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content;
173217 }
218+
219+ // Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling
220+ if (prefillString) {
221+ messages.push({
222+ role: 'assistant',
223+ // Dangling whitespace are not allowed for prefilling
224+ content: prefillString.trimEnd(),
225+ });
174226 }
227+
228+ // Since the messaging endpoint only supports user assistant roles in turns, we have to merge messages with the same role if they follow eachother
229+ // Also handle multi-modality, holy slop.
230+ let mergedMessages = [];
231+ messages.forEach((message) => {
232+ if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role) {
233+ mergedMessages[mergedMessages.length - 1].content.push(...message.content);
175234 } else {
176235 mergedMessages.push(message);
177236 }
178- if (imageData) {
179- mergedMessages[mergedMessages.length - 1].content = [
180- { type: 'text', text: mergedMessages[mergedMessages.length - 1].content[0]?.text || mergedMessages[mergedMessages.length - 1].content },
181- {
182- type: 'image', source: {
183- type: 'base64',
184- media_type: mimeType,
185- data: base64Data,
186- },
187- },
188- ];
189- }
190237 });
191238
192239 return { messages: mergedMessages, systemPrompt: systemPrompt.trim() };