Gemini: Add tool calling
| @@ -1962,7 +1962,7 @@ | ||
| 1962 | 1962 | </span> |
| 1963 | 1963 | </div> |
| 1964 | 1964 | </div> |
| 1965 | 1965 | <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek,makersuite"> |
| 1966 | 1966 | <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand"> |
| 1967 | 1967 | <input id="openai_function_calling" type="checkbox" /> |
| 1968 | 1968 | <span data-i18n="Enable function calling">Enable function calling</span> |
| @@ -137,9 +137,14 @@ async function* parseStreamData(json) { | ||
| 137 | 137 | else if (Array.isArray(json.candidates)) { |
| 138 | 138 | for (let i = 0; i < json.candidates.length; i++) { |
| 139 | 139 | const isNotPrimary = json.candidates?.[0]?.index > 0; |
| 140 | + const hasToolCalls = json?.candidates?.[0]?.content?.parts?.some(p => p?.functionCall); | |
| 140 | 141 | if (isNotPrimary || json.candidates.length === 0) { |
| 141 | 142 | return null; |
| 142 | 143 | } |
| 144 | + if (hasToolCalls) { | |
| 145 | + yield { data: json, chunk: '' }; | |
| 146 | + return; | |
| 147 | + } | |
| 143 | 148 | if (typeof json.candidates[0].content === 'object' && Array.isArray(json.candidates[i].content.parts)) { |
| 144 | 149 | for (let j = 0; j < json.candidates[i].content.parts.length; j++) { |
| 145 | 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 | 584 | chat_completion_sources.GROQ, |
| 565 | 585 | chat_completion_sources.COHERE, |
| 566 | 586 | chat_completion_sources.DEEPSEEK, |
| 587 | + chat_completion_sources.MAKERSUITE, | |
| 567 | 588 | ]; |
| 568 | 589 | return supportedSources.includes(oai_settings.chat_completion_source); |
| 569 | 590 | } |
| @@ -585,8 +606,11 @@ export class ToolManager { | ||
| 585 | 606 | * @returns {any[]} Tool calls from the response data |
| 586 | 607 | */ |
| 587 | 608 | static #getToolCallsFromData(data) { |
| 609 | + const getRandomId = () => Math.random().toString(36).substring(2); | |
| 588 | 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 | 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 | 615 | // Parsed tool calls from streaming data |
| 592 | 616 | if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) { |
| @@ -594,6 +618,10 @@ export class ToolManager { | ||
| 594 | 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 | 625 | if (typeof data[0]?.[0]?.tool_calls === 'object') { |
| 598 | 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 | 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 | 637 | // Parsed tool calls from non-streaming data |
| 605 | 638 | if (Array.isArray(data?.choices)) { |
| 606 | 639 | // Find a choice with 0-index |
| @@ -385,6 +385,19 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 385 | 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 | 401 | let body = { |
| 389 | 402 | contents: prompt.contents, |
| 390 | 403 | safetySettings: safetySettings, |
| @@ -454,10 +467,11 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 454 | 467 | } |
| 455 | 468 | |
| 456 | 469 | const responseContent = candidates[0].content ?? candidates[0].output; |
| 470 | + const functionCall = (candidates?.[0]?.content?.parts ?? []).some(part => part.functionCall); | |
| 457 | 471 | console.warn('Google AI Studio response:', responseContent); |
| 458 | 472 | |
| 459 | 473 | const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n'); |
| 460 | 474 | if (!responseText && !functionCall) { |
| 461 | 475 | let message = 'Google AI Studio Candidate text empty'; |
| 462 | 476 | console.warn(message, generateResponseJson); |
| 463 | 477 | return response.send({ error: { message } }); |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | import crypto from 'node:crypto'; |
| 2 | 2 | import { getConfigValue, tryParse } from './util.js'; |
| 3 | 3 | |
| 4 | 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 | 413 | const system_instruction = { parts: { text: sys_prompt.trim() } }; |
| 414 | + const toolNameMap = {}; | |
| 414 | 415 | |
| 415 | 416 | const contents = []; |
| 416 | 417 | messages.forEach((message, index) => { |
| 417 | 418 | // fix the roles |
| 418 | 419 | if (message.role === 'system' || message.role === 'tool') { |
| 419 | 420 | message.role = 'user'; |
| 420 | 421 | } else if (message.role === 'assistant') { |
| 421 | 422 | message.role = 'model'; |
| @@ -423,7 +424,21 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) { | ||
| 423 | 424 | |
| 424 | 425 | // Convert the content to an array of parts |
| 425 | 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 | 444 | // similar story as claude |
| @@ -455,6 +470,25 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) { | ||
| 455 | 470 | message.content.forEach((part) => { |
| 456 | 471 | if (part.type === 'text') { |
| 457 | 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 | 492 | } else if (part.type === 'image_url' && isMultimodal) { |
| 459 | 493 | const mimeType = part.image_url.url.split(';')[0].split(':')[1]; |
| 460 | 494 | const base64Data = part.image_url.url.split(',')[1]; |
| @@ -473,7 +507,7 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) { | ||
| 473 | 507 | if (part.text) { |
| 474 | 508 | contents[contents.length - 1].parts[0].text += '\n\n' + part.text; |
| 475 | 509 | } |
| 476 | 510 | if (part.inlineData || part.functionCall) { |
| 477 | 511 | contents[contents.length - 1].parts.push(part); |
| 478 | 512 | } |
| 479 | 513 | }); |