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
340 const supportedSources = [340 const supportedSources = [
341 chat_completion_sources.OPENAI,341 chat_completion_sources.OPENAI,
342 //chat_completion_sources.COHERE,
343 chat_completion_sources.CUSTOM,342 chat_completion_sources.CUSTOM,
344 chat_completion_sources.MISTRALAI,343 chat_completion_sources.MISTRALAI,
345 //chat_completion_sources.CLAUDE,344 chat_completion_sources.CLAUDE,
346 chat_completion_sources.OPENROUTER,345 chat_completion_sources.OPENROUTER,
347 chat_completion_sources.GROQ,346 chat_completion_sources.GROQ,
348 ];347 ];
@@ -372,18 +371,29 @@ export class ToolManager {
372 }371 }
373372
374 // Parsed tool calls from non-streaming data373 // Parsed tool calls from non-streaming data
375 if (!Array.isArray(data?.choices)) {374 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 }
377 }381 }
378382
379 // Find a choice with 0-index383 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;
384 }396 }
385
386 return choice.message.tool_calls;
387 }397 }
388398
389 /**399 /**
@@ -407,59 +417,43 @@ export class ToolManager {
407 chat_completion_sources.GROQ,417 chat_completion_sources.GROQ,
408 ];418 ];
409419
410 if (oaiCompatibleSources.includes(oai_settings.chat_completion_source)) {420 if (!Array.isArray(toolCalls)) {
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 }
448 }422 }
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;
454 }427 }
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;
460 }446 }
447
448 const invocation = {
449 id,
450 displayName,
451 name,
452 parameters,
453 result: toolResult,
454 };
455 result.invocations.push(invocation);
461 }456 }
462 */
463457
464 return result;458 return result;
465 }459 }
@@ -491,6 +485,9 @@ export class ToolManager {
491 * @param {ToolInvocation[]} invocations Successful tool invocations485 * @param {ToolInvocation[]} invocations Successful tool invocations
492 */486 */
493 static saveFunctionToolInvocations(invocations) {487 static saveFunctionToolInvocations(invocations) {
488 if (!Array.isArray(invocations) || invocations.length === 0) {
489 return;
490 }
494 const message = {491 const message = {
495 name: systemUserName,492 name: systemUserName,
496 force_avatar: system_avatar,493 force_avatar: system_avatar,
src/endpoints/backends/chat-completions.js+0 -2
@@ -124,7 +124,6 @@ async function sendClaudeRequest(request, response) {
124 } else {124 } else {
125 delete requestBody.system;125 delete requestBody.system;
126 }126 }
127 /*
128 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {127 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
129 // Claude doesn't do prefills on function calls, and doesn't allow empty messages128 // Claude doesn't do prefills on function calls, and doesn't allow empty messages
130 if (convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') {129 if (convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') {
@@ -137,7 +136,6 @@ async function sendClaudeRequest(request, response) {
137 .map(tool => tool.function)136 .map(tool => tool.function)
138 .map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));137 .map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));
139 }138 }
140 */
141 if (enableSystemPromptCache) {139 if (enableSystemPromptCache) {
142 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';140 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
143 }141 }
src/prompt-converters.js+87 -40
@@ -118,8 +118,27 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
118 });118 });
119 }119 }
120 }120 }
121
121 // Now replace all further messages that have the role 'system' with the role 'user'. (or all if we're not using one)122 // Now replace all further messages that have the role 'system' with the role 'user'. (or all if we're not using one)
122 messages.forEach((message) => {123 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
123 if (message.role === 'system') {142 if (message.role === 'system') {
124 if (userName && message.name === 'example_user') {143 if (userName && message.name === 'example_user') {
125 message.content = `${userName}: ${message.content}`;144 message.content = `${userName}: ${message.content}`;
@@ -128,13 +147,80 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
128 message.content = `${charName}: ${message.content}`;147 message.content = `${charName}: ${message.content}`;
129 }148 }
130 message.role = 'user';149 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 });
131 }191 }
192
193 // Remove offending properties
194 delete message.name;
195 delete message.tool_calls;
196 delete message.tool_call_id;
132 });197 });
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
134 // Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling219 // Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling
135 if (prefillString) {220 if (prefillString) {
136 messages.push({221 messages.push({
137 role: 'assistant',222 role: 'assistant',
223 // Dangling whitespace are not allowed for prefilling
138 content: prefillString.trimEnd(),224 content: prefillString.trimEnd(),
139 });225 });
140 }226 }
@@ -143,50 +229,11 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
143 // Also handle multi-modality, holy slop.229 // Also handle multi-modality, holy slop.
144 let mergedMessages = [];230 let mergedMessages = [];
145 messages.forEach((message) => {231 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
161 if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role) {232 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 }
175 } else {234 } else {
176 mergedMessages.push(message);235 mergedMessages.push(message);
177 }236 }
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 }
190 });237 });
191238
192 return { messages: mergedMessages, systemPrompt: systemPrompt.trim() };239 return { messages: mergedMessages, systemPrompt: systemPrompt.trim() };