Gemini: Add tool calling
| @@ -1962,7 +1962,7 @@ | |||
| 1962 | </span> | 1962 | </span> |
| 1963 | </div> | 1963 | </div> |
| 1964 | </div> | 1964 | </div> |
| 1965 | <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek"> | 1965 | <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek,makersuite"> |
| 1966 | <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand"> | 1966 | <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand"> |
| 1967 | <input id="openai_function_calling" type="checkbox" /> | 1967 | <input id="openai_function_calling" type="checkbox" /> |
| 1968 | <span data-i18n="Enable function calling">Enable function calling</span> | 1968 | <span data-i18n="Enable function calling">Enable function calling</span> |
| @@ -137,9 +137,14 @@ async function* parseStreamData(json) { | |||
| 137 | else if (Array.isArray(json.candidates)) { | 137 | else if (Array.isArray(json.candidates)) { |
| 138 | for (let i = 0; i < json.candidates.length; i++) { | 138 | for (let i = 0; i < json.candidates.length; i++) { |
| 139 | const isNotPrimary = json.candidates?.[0]?.index > 0; | 139 | const isNotPrimary = json.candidates?.[0]?.index > 0; |
| 140 | const hasToolCalls = json?.candidates?.[0]?.content?.parts?.some(p => p?.functionCall); | ||
| 140 | if (isNotPrimary || json.candidates.length === 0) { | 141 | if (isNotPrimary || json.candidates.length === 0) { |
| 141 | return null; | 142 | return null; |
| 142 | } | 143 | } |
| 144 | if (hasToolCalls) { | ||
| 145 | yield { data: json, chunk: '' }; | ||
| 146 | return; | ||
| 147 | } | ||
| 143 | if (typeof json.candidates[0].content === 'object' && Array.isArray(json.candidates[i].content.parts)) { | 148 | if (typeof json.candidates[0].content === 'object' && Array.isArray(json.candidates[i].content.parts)) { |
| 144 | for (let j = 0; j < json.candidates[i].content.parts.length; j++) { | 149 | for (let j = 0; j < json.candidates[i].content.parts.length; j++) { |
| 145 | if (typeof json.candidates[i].content.parts[j].text === 'string') { | 150 | if (typeof json.candidates[i].content.parts[j].text === 'string') { |
| @@ -506,6 +506,26 @@ export class ToolManager { | |||
| 506 | } | 506 | } |
| 507 | } | 507 | } |
| 508 | } | 508 | } |
| 509 | if (Array.isArray(parsed?.candidates)) { | ||
| 510 | for (let choiceIndex = 0; choiceIndex < parsed.candidates.length; choiceIndex++) { | ||
| 511 | const candidate = parsed.candidates[choiceIndex]; | ||
| 512 | if (Array.isArray(candidate?.content?.parts)) { | ||
| 513 | for (let toolCallIndex = 0; toolCallIndex < candidate.content.parts.length; toolCallIndex++) { | ||
| 514 | const part = candidate.content.parts[toolCallIndex]; | ||
| 515 | if (part.functionCall) { | ||
| 516 | if (!Array.isArray(toolCalls[choiceIndex])) { | ||
| 517 | toolCalls[choiceIndex] = []; | ||
| 518 | } | ||
| 519 | if (toolCalls[choiceIndex][toolCallIndex] === undefined) { | ||
| 520 | toolCalls[choiceIndex][toolCallIndex] = {}; | ||
| 521 | } | ||
| 522 | const targetToolCall = toolCalls[choiceIndex][toolCallIndex]; | ||
| 523 | ToolManager.#applyToolCallDelta(targetToolCall, part.functionCall); | ||
| 524 | } | ||
| 525 | } | ||
| 526 | } | ||
| 527 | } | ||
| 528 | } | ||
| 509 | } | 529 | } |
| 510 | 530 | ||
| 511 | /** | 531 | /** |
| @@ -564,6 +584,7 @@ export class ToolManager { | |||
| 564 | chat_completion_sources.GROQ, | 584 | chat_completion_sources.GROQ, |
| 565 | chat_completion_sources.COHERE, | 585 | chat_completion_sources.COHERE, |
| 566 | chat_completion_sources.DEEPSEEK, | 586 | chat_completion_sources.DEEPSEEK, |
| 587 | chat_completion_sources.MAKERSUITE, | ||
| 567 | ]; | 588 | ]; |
| 568 | return supportedSources.includes(oai_settings.chat_completion_source); | 589 | return supportedSources.includes(oai_settings.chat_completion_source); |
| 569 | } | 590 | } |
| @@ -585,8 +606,11 @@ export class ToolManager { | |||
| 585 | * @returns {any[]} Tool calls from the response data | 606 | * @returns {any[]} Tool calls from the response data |
| 586 | */ | 607 | */ |
| 587 | static #getToolCallsFromData(data) { | 608 | static #getToolCallsFromData(data) { |
| 609 | const getRandomId = () => Math.random().toString(36).substring(2); | ||
| 588 | const isClaudeToolCall = c => Array.isArray(c) ? c.filter(x => x).every(isClaudeToolCall) : c?.input && c?.name && c?.id; | 610 | const isClaudeToolCall = c => Array.isArray(c) ? c.filter(x => x).every(isClaudeToolCall) : c?.input && c?.name && c?.id; |
| 611 | const isGoogleToolCall = c => Array.isArray(c) ? c.filter(x => x).every(isGoogleToolCall) : c?.name && c?.args; | ||
| 589 | const convertClaudeToolCall = c => ({ id: c.id, function: { name: c.name, arguments: c.input } }); | 612 | const convertClaudeToolCall = c => ({ id: c.id, function: { name: c.name, arguments: c.input } }); |
| 613 | const convertGoogleToolCall = (c) => ({ id: getRandomId(), function: { name: c.name, arguments: c.args } }); | ||
| 590 | 614 | ||
| 591 | // Parsed tool calls from streaming data | 615 | // Parsed tool calls from streaming data |
| 592 | if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) { | 616 | if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) { |
| @@ -594,6 +618,10 @@ export class ToolManager { | |||
| 594 | return data[0].filter(x => x).map(convertClaudeToolCall); | 618 | return data[0].filter(x => x).map(convertClaudeToolCall); |
| 595 | } | 619 | } |
| 596 | 620 | ||
| 621 | if (isGoogleToolCall(data[0])) { | ||
| 622 | return data[0].filter(x => x).map(convertGoogleToolCall); | ||
| 623 | } | ||
| 624 | |||
| 597 | if (typeof data[0]?.[0]?.tool_calls === 'object') { | 625 | if (typeof data[0]?.[0]?.tool_calls === 'object') { |
| 598 | return Array.isArray(data[0]?.[0]?.tool_calls) ? data[0][0].tool_calls : [data[0][0].tool_calls]; | 626 | return Array.isArray(data[0]?.[0]?.tool_calls) ? data[0][0].tool_calls : [data[0][0].tool_calls]; |
| 599 | } | 627 | } |
| @@ -601,6 +629,11 @@ export class ToolManager { | |||
| 601 | return data[0]; | 629 | return data[0]; |
| 602 | } | 630 | } |
| 603 | 631 | ||
| 632 | // Google AI Studio tool calls | ||
| 633 | if (Array.isArray(data?.responseContent?.parts)) { | ||
| 634 | return data.responseContent.parts.filter(p => p.functionCall).map(p => convertGoogleToolCall(p.functionCall)); | ||
| 635 | } | ||
| 636 | |||
| 604 | // Parsed tool calls from non-streaming data | 637 | // Parsed tool calls from non-streaming data |
| 605 | if (Array.isArray(data?.choices)) { | 638 | if (Array.isArray(data?.choices)) { |
| 606 | // Find a choice with 0-index | 639 | // Find a choice with 0-index |
| @@ -385,6 +385,19 @@ async function sendMakerSuiteRequest(request, response) { | |||
| 385 | tools.push(searchTool); | 385 | tools.push(searchTool); |
| 386 | } | 386 | } |
| 387 | 387 | ||
| 388 | if (Array.isArray(request.body.tools) && request.body.tools.length > 0) { | ||
| 389 | const functionDeclarations = []; | ||
| 390 | for (const tool of request.body.tools) { | ||
| 391 | if (tool.type === 'function') { | ||
| 392 | if (tool.function.parameters?.$schema) { | ||
| 393 | delete tool.function.parameters.$schema; | ||
| 394 | } | ||
| 395 | functionDeclarations.push(tool.function); | ||
| 396 | } | ||
| 397 | } | ||
| 398 | tools.push({ function_declarations: functionDeclarations }); | ||
| 399 | } | ||
| 400 | |||
| 388 | let body = { | 401 | let body = { |
| 389 | contents: prompt.contents, | 402 | contents: prompt.contents, |
| 390 | safetySettings: safetySettings, | 403 | safetySettings: safetySettings, |
| @@ -454,10 +467,11 @@ async function sendMakerSuiteRequest(request, response) { | |||
| 454 | } | 467 | } |
| 455 | 468 | ||
| 456 | const responseContent = candidates[0].content ?? candidates[0].output; | 469 | const responseContent = candidates[0].content ?? candidates[0].output; |
| 470 | const functionCall = (candidates?.[0]?.content?.parts ?? []).some(part => part.functionCall); | ||
| 457 | console.warn('Google AI Studio response:', responseContent); | 471 | console.warn('Google AI Studio response:', responseContent); |
| 458 | 472 | ||
| 459 | const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n'); | 473 | const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n'); |
| 460 | if (!responseText) { | 474 | if (!responseText && !functionCall) { |
| 461 | let message = 'Google AI Studio Candidate text empty'; | 475 | let message = 'Google AI Studio Candidate text empty'; |
| 462 | console.warn(message, generateResponseJson); | 476 | console.warn(message, generateResponseJson); |
| 463 | return response.send({ error: { message } }); | 477 | return response.send({ error: { message } }); |
| @@ -1,5 +1,5 @@ | |||
| 1 | import crypto from 'node:crypto'; | 1 | import crypto from 'node:crypto'; |
| 2 | import { getConfigValue } from './util.js'; | 2 | import { getConfigValue, tryParse } from './util.js'; |
| 3 | 3 | ||
| 4 | const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.'); | 4 | const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.'); |
| 5 | 5 | ||
| @@ -411,11 +411,12 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) { | |||
| 411 | } | 411 | } |
| 412 | 412 | ||
| 413 | const system_instruction = { parts: { text: sys_prompt.trim() } }; | 413 | const system_instruction = { parts: { text: sys_prompt.trim() } }; |
| 414 | const toolNameMap = {}; | ||
| 414 | 415 | ||
| 415 | const contents = []; | 416 | const contents = []; |
| 416 | messages.forEach((message, index) => { | 417 | messages.forEach((message, index) => { |
| 417 | // fix the roles | 418 | // fix the roles |
| 418 | if (message.role === 'system') { | 419 | if (message.role === 'system' || message.role === 'tool') { |
| 419 | message.role = 'user'; | 420 | message.role = 'user'; |
| 420 | } else if (message.role === 'assistant') { | 421 | } else if (message.role === 'assistant') { |
| 421 | message.role = 'model'; | 422 | message.role = 'model'; |
| @@ -423,7 +424,21 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) { | |||
| 423 | 424 | ||
| 424 | // Convert the content to an array of parts | 425 | // Convert the content to an array of parts |
| 425 | if (!Array.isArray(message.content)) { | 426 | if (!Array.isArray(message.content)) { |
| 426 | message.content = [{ type: 'text', text: String(message.content ?? '') }]; | 427 | const content = (() => { |
| 428 | const hasToolCalls = Array.isArray(message.tool_calls) && message.tool_calls.length > 0; | ||
| 429 | const hasToolCallId = typeof message.tool_call_id === 'string' && message.tool_call_id.length > 0; | ||
| 430 | |||
| 431 | if (hasToolCalls) { | ||
| 432 | return { type: 'tool_calls', tool_calls: message.tool_calls }; | ||
| 433 | } | ||
| 434 | |||
| 435 | if (hasToolCallId) { | ||
| 436 | return { type: 'tool_call_id', tool_call_id: message.tool_call_id, content: String(message.content ?? '') }; | ||
| 437 | } | ||
| 438 | |||
| 439 | return { type: 'text', text: String(message.content ?? '') }; | ||
| 440 | })(); | ||
| 441 | message.content = [content]; | ||
| 427 | } | 442 | } |
| 428 | 443 | ||
| 429 | // similar story as claude | 444 | // similar story as claude |
| @@ -455,6 +470,25 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) { | |||
| 455 | message.content.forEach((part) => { | 470 | message.content.forEach((part) => { |
| 456 | if (part.type === 'text') { | 471 | if (part.type === 'text') { |
| 457 | parts.push({ text: part.text }); | 472 | parts.push({ text: part.text }); |
| 473 | } else if (part.type === 'tool_call_id') { | ||
| 474 | const name = toolNameMap[part.tool_call_id] ?? 'unknown'; | ||
| 475 | parts.push({ | ||
| 476 | functionResponse: { | ||
| 477 | name: name, | ||
| 478 | response: { name: name, content: part.content }, | ||
| 479 | }, | ||
| 480 | }); | ||
| 481 | } else if (part.type === 'tool_calls') { | ||
| 482 | part.tool_calls.forEach((toolCall) => { | ||
| 483 | parts.push({ | ||
| 484 | functionCall: { | ||
| 485 | name: toolCall.function.name, | ||
| 486 | args: tryParse(toolCall.function.arguments) ?? toolCall.function.arguments, | ||
| 487 | }, | ||
| 488 | }); | ||
| 489 | |||
| 490 | toolNameMap[toolCall.id] = toolCall.function.name; | ||
| 491 | }); | ||
| 458 | } else if (part.type === 'image_url' && isMultimodal) { | 492 | } else if (part.type === 'image_url' && isMultimodal) { |
| 459 | const mimeType = part.image_url.url.split(';')[0].split(':')[1]; | 493 | const mimeType = part.image_url.url.split(';')[0].split(':')[1]; |
| 460 | const base64Data = part.image_url.url.split(',')[1]; | 494 | const base64Data = part.image_url.url.split(',')[1]; |
| @@ -473,7 +507,7 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) { | |||
| 473 | if (part.text) { | 507 | if (part.text) { |
| 474 | contents[contents.length - 1].parts[0].text += '\n\n' + part.text; | 508 | contents[contents.length - 1].parts[0].text += '\n\n' + part.text; |
| 475 | } | 509 | } |
| 476 | if (part.inlineData) { | 510 | if (part.inlineData || part.functionCall) { |
| 477 | contents[contents.length - 1].parts.push(part); | 511 | contents[contents.length - 1].parts.push(part); |
| 478 | } | 512 | } |
| 479 | }); | 513 | }); |