Claude: new prompt converter + non-streaming tools
| @@ -339,10 +339,9 @@ export class ToolManager { | ||
| 339 | 339 | |
| 340 | 340 | const supportedSources = [ |
| 341 | 341 | chat_completion_sources.OPENAI, |
| 342 | - //chat_completion_sources.COHERE, | |
| 343 | 342 | chat_completion_sources.CUSTOM, |
| 344 | 343 | chat_completion_sources.MISTRALAI, |
| 345 | 344 | //chat_completion_sources.CLAUDE, |
| 346 | 345 | chat_completion_sources.OPENROUTER, |
| 347 | 346 | chat_completion_sources.GROQ, |
| 348 | 347 | ]; |
| @@ -372,18 +371,29 @@ export class ToolManager { | ||
| 372 | 371 | } |
| 373 | 372 | |
| 374 | 373 | // Parsed tool calls from non-streaming data |
| 375 | 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 | } |
| 378 | 382 | |
| 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 | + }); | |
| 381 | 394 | |
| 382 | - if (!choice) { | |
| 395 | + return content; | |
| 383 | - return; | |
| 384 | 396 | } |
| 385 | - | |
| 386 | - return choice.message.tool_calls; | |
| 387 | 397 | } |
| 388 | 398 | |
| 389 | 399 | /** |
| @@ -407,59 +417,43 @@ export class ToolManager { | ||
| 407 | 417 | chat_completion_sources.GROQ, |
| 408 | 418 | ]; |
| 409 | 419 | |
| 410 | 420 | 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 | - } | |
| 448 | 422 | } |
| 449 | 423 | |
| 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 | } |
| 455 | 428 | |
| 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 | - */ | |
| 463 | 457 | |
| 464 | 458 | return result; |
| 465 | 459 | } |
| @@ -491,6 +485,9 @@ export class ToolManager { | ||
| 491 | 485 | * @param {ToolInvocation[]} invocations Successful tool invocations |
| 492 | 486 | */ |
| 493 | 487 | static saveFunctionToolInvocations(invocations) { |
| 488 | + if (!Array.isArray(invocations) || invocations.length === 0) { | |
| 489 | + return; | |
| 490 | + } | |
| 494 | 491 | const message = { |
| 495 | 492 | name: systemUserName, |
| 496 | 493 | force_avatar: system_avatar, |
| @@ -124,7 +124,6 @@ async function sendClaudeRequest(request, response) { | ||
| 124 | 124 | } else { |
| 125 | 125 | delete requestBody.system; |
| 126 | 126 | } |
| 127 | - /* | |
| 128 | 127 | if (Array.isArray(request.body.tools) && request.body.tools.length > 0) { |
| 129 | 128 | // Claude doesn't do prefills on function calls, and doesn't allow empty messages |
| 130 | 129 | if (convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') { |
| @@ -137,7 +136,6 @@ async function sendClaudeRequest(request, response) { | ||
| 137 | 136 | .map(tool => tool.function) |
| 138 | 137 | .map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters })); |
| 139 | 138 | } |
| 140 | - */ | |
| 141 | 139 | if (enableSystemPromptCache) { |
| 142 | 140 | additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31'; |
| 143 | 141 | } |
| @@ -118,8 +118,27 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi | ||
| 118 | 118 | }); |
| 119 | 119 | } |
| 120 | 120 | } |
| 121 | + | |
| 121 | 122 | // Now replace all further messages that have the role 'system' with the role 'user'. (or all if we're not using one) |
| 122 | 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 | 142 | if (message.role === 'system') { |
| 124 | 143 | if (userName && message.name === 'example_user') { |
| 125 | 144 | message.content = `${userName}: ${message.content}`; |
| @@ -128,13 +147,80 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi | ||
| 128 | 147 | message.content = `${charName}: ${message.content}`; |
| 129 | 148 | } |
| 130 | 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 | }); |
| 133 | 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++; | |
| 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 | 219 | // Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling |
| 135 | 220 | if (prefillString) { |
| 136 | 221 | messages.push({ |
| 137 | 222 | role: 'assistant', |
| 223 | + // Dangling whitespace are not allowed for prefilling | |
| 138 | 224 | content: prefillString.trimEnd(), |
| 139 | 225 | }); |
| 140 | 226 | } |
| @@ -143,50 +229,11 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi | ||
| 143 | 229 | // Also handle multi-modality, holy slop. |
| 144 | 230 | let mergedMessages = []; |
| 145 | 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 | 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 | 234 | } else { |
| 176 | 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 | }); |
| 191 | 238 | |
| 192 | 239 | return { messages: mergedMessages, systemPrompt: systemPrompt.trim() }; |