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
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;
377 }
378
379 // Find a choice with 0-index375 // Find a choice with 0-index
380 const choice = data.choices.find(choice => choice.index === 0);376 const choice = data.choices.find(choice => choice.index === 0);
381377
382 if (!choice) {378 if (choice) {
383 return;379 return choice.message.tool_calls;
380 }
384 }381 }
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 }
387 }397 }
388398
389 /**399 /**
@@ -407,7 +417,6 @@ 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)) {
411 if (!Array.isArray(toolCalls)) {420 if (!Array.isArray(toolCalls)) {
412 return result;421 return result;
413 }422 }
@@ -445,21 +454,6 @@ export class ToolManager {
445 };454 };
446 result.invocations.push(invocation);455 result.invocations.push(invocation);
447 }456 }
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
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,65 +147,93 @@ 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;
131 }153 }
132 });
133154
134 // Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling155 // 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 });
140 }160 }
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 eachother162 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') {
146 const imageEntry = message.content?.[1]?.image_url;166 const imageEntry = content?.image_url;
147 const imageData = imageEntry?.url;167 const imageData = imageEntry?.url;
148 const mimeType = imageData?.split(';')?.[0].split(':')?.[1];168 const mimeType = imageData?.split(';')?.[0].split(':')?.[1];
149 const base64Data = imageData?.split(',')?.[1];169 const base64Data = imageData?.split(',')?.[1];
150170
151 // Take care of name properties since claude messages don't support them171 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') {
152 if (message.name) {182 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}`;
157 }184 }
185
186 return content;
187 }
188
189 return content;
190 });
191 }
192
193 // Remove offending properties
158 delete message.name;194 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++;
159 }206 }
160207
161 if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role) {208 // Move the images
162 if (Array.isArray(message.content)) {209 if (j >= messages.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');
167 }216 }
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 }217 }
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 });
174 }226 }
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);
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() };