Claude: new prompt converter + non-streaming tools

c3c10a629e000f3d7479f9a50fbb6d28316acaa0

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

3 files changed, +141 -99Ignore whitespace
public/scripts/tool-calling.js+54 -57
@@ -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;
375+ // Find a choice with 0-index
376+ const choice = data.choices.find(choice => choice.index === 0);
377+
378+ if (choice) {
379+ return choice.message.tool_calls;
380+ }
377381 }
378382
379- // Find a choice with 0-index
383+ if (Array.isArray(data?.content)) {
380- const choice = data.choices.find(choice => choice.index === 0);
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+ });
381394
382- if (!choice) {
395+ return content;
383- return;
384396 }
385-
386- return choice.message.tool_calls;
387397 }
388398
389399 /**
@@ -407,59 +417,43 @@ export class ToolManager {
407417 chat_completion_sources.GROQ,
408418 ];
409419
410420 if (oaiCompatibleSources!Array.includesisArray(oai_settings.chat_completion_sourcetoolCalls)) {
411- if (!Array.isArray(toolCalls)) {
421+ return result;
412- return result;
413- }
414-
415- for (const toolCall of toolCalls) {
416- if (typeof toolCall.function !== 'object') {
417- continue;
418- }
419-
420- console.log('Function tool call:', toolCall);
421- const id = toolCall.id;
422- const parameters = toolCall.function.arguments;
423- const name = toolCall.function.name;
424- const displayName = ToolManager.getDisplayName(name);
425- result.hadToolCalls = true;
426-
427- const message = ToolManager.formatToolCallMessage(name, parameters);
428- const toast = message && toastr.info(message, 'Tool Calling', { timeOut: 0 });
429- const toolResult = await ToolManager.invokeFunctionTool(name, parameters);
430- toastr.clear(toast);
431- console.log('Function tool result:', result);
432-
433- // Save a successful invocation
434- if (toolResult instanceof Error) {
435- result.errors.push(toolResult);
436- continue;
437- }
438-
439- const invocation = {
440- id,
441- displayName,
442- name,
443- parameters,
444- result: toolResult,
445- };
446- result.invocations.push(invocation);
447- }
448422 }
449423
450- /*
424+ for (const toolCall of toolCalls) {
451- if ([chat_completion_sources.CLAUDE].includes(oai_settings.chat_completion_source)) {
425+ if (typeof toolCall.function !== 'object') {
452- if (!Array.isArray(data?.content)) {
426+ continue;
453- return;
454427 }
455428
456- for (const content of data.content) {
429+ console.log('Function tool call:', toolCall);
457- if (content.type === 'tool_use') {
430+ const id = toolCall.id;
458- const args = { name: content.name, arguments: JSON.stringify(content.input) };
431+ const parameters = toolCall.function.arguments;
459- }
432+ const name = toolCall.function.name;
433+ const displayName = ToolManager.getDisplayName(name);
434+ result.hadToolCalls = true;
435+
436+ const message = ToolManager.formatToolCallMessage(name, parameters);
437+ const toast = message && toastr.info(message, 'Tool Calling', { timeOut: 0 });
438+ const toolResult = await ToolManager.invokeFunctionTool(name, parameters);
439+ toastr.clear(toast);
440+ console.log('Function tool result:', result);
441+
442+ // Save a successful invocation
443+ if (toolResult instanceof Error) {
444+ result.errors.push(toolResult);
445+ continue;
460446 }
447+
448+ const invocation = {
449+ id,
450+ displayName,
451+ name,
452+ parameters,
453+ result: toolResult,
454+ };
455+ result.invocations.push(invocation);
461456 }
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,13 +147,80 @@ 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;
153+ }
154+
155+ // Convert everything to an array of it would be easier to work with
156+ if (typeof message.content === 'string') {
157+ // Take care of name properties since claude messages don't support them
158+ if (message.name) {
159+ message.content = `${message.name}: ${message.content}`;
160+ }
161+
162+ message.content = [{ type: 'text', text: message.content }];
163+ } else if (Array.isArray(message.content)) {
164+ message.content = message.content.map((content) => {
165+ if (content.type === 'image_url') {
166+ const imageEntry = content?.image_url;
167+ const imageData = imageEntry?.url;
168+ const mimeType = imageData?.split(';')?.[0].split(':')?.[1];
169+ const base64Data = imageData?.split(',')?.[1];
170+
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') {
182+ if (message.name) {
183+ content.text = `${message.name}: ${content.text}`;
184+ }
185+
186+ return content;
187+ }
188+
189+ return content;
190+ });
131191 }
192+
193+ // Remove offending properties
194+ delete message.name;
195+ delete message.tool_calls;
196+ delete message.tool_call_id;
132197 });
133198
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++;
206+ }
207+
208+ // Move the images
209+ if (j >= messages.length) {
210+ // If there is no user message after the assistant message, add a new one
211+ messages.splice(i + 1, 0, { role: 'user', content: [] });
212+ }
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');
216+ }
217+ }
218+
134219 // Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling
135220 if (prefillString) {
136221 messages.push({
137222 role: 'assistant',
223+ // Dangling whitespace are not allowed for prefilling
138224 content: prefillString.trimEnd(),
139225 });
140226 }
@@ -143,50 +229,11 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
143229 // Also handle multi-modality, holy slop.
144230 let mergedMessages = [];
145231 messages.forEach((message) => {
146- const imageEntry = message.content?.[1]?.image_url;
147- const imageData = imageEntry?.url;
148- const mimeType = imageData?.split(';')?.[0].split(':')?.[1];
149- const base64Data = imageData?.split(',')?.[1];
150-
151- // Take care of name properties since claude messages don't support them
152- if (message.name) {
153- if (Array.isArray(message.content)) {
154- message.content[0].text = `${message.name}: ${message.content[0].text}`;
155- } else {
156- message.content = `${message.name}: ${message.content}`;
157- }
158- delete message.name;
159- }
160-
161232 if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role) {
162- if (Array.isArray(message.content)) {
233+ mergedMessages[mergedMessages.length - 1].content.push(...message.content);
163- if (Array.isArray(mergedMessages[mergedMessages.length - 1].content)) {
164- mergedMessages[mergedMessages.length - 1].content[0].text += '\n\n' + message.content[0].text;
165- } else {
166- mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content[0].text;
167- }
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;
173- }
174- }
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() };