Claude: new prompt converter + non-streaming tools
| @@ -339,10 +339,9 @@ export class ToolManager { | |||
| 339 | 339 | ||
| 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 | } |
| 373 | 372 | ||
| 374 | // Parsed tool calls from non-streaming data | 373 | // 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 | } |
| 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 | chat_completion_sources.GROQ, | 417 | chat_completion_sources.GROQ, |
| 408 | ]; | 418 | ]; |
| 409 | 419 | ||
| 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 | } |
| 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 | return result; | 458 | return result; |
| 465 | } | 459 | } |
| @@ -491,6 +485,9 @@ export class ToolManager { | |||
| 491 | * @param {ToolInvocation[]} invocations Successful tool invocations | 485 | * @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, |
| @@ -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 messages | 128 | // 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 | } |
| @@ -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 | }); |
| 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 | // Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling | 219 | // 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 | }); |
| 191 | 238 | ||
| 192 | return { messages: mergedMessages, systemPrompt: systemPrompt.trim() }; | 239 | return { messages: mergedMessages, systemPrompt: systemPrompt.trim() }; |