Gemini inline images (#3681) * Gemini images for non-streaming * Parse images on stream * Add toggle for image request * Add extraction params to extractImageFromData * Add explicit break and return * Add more JSdoc to processImageAttachment * Add file name prefix * Add object argument for saveReply * Add defaults to saveReply params * Use type for saveReply result * Change type check in saveReply backward compat
Signed| @@ -1998,6 +1998,23 @@ | ||
| 1998 | 1998 | </div> |
| 1999 | 1999 | </div> |
| 2000 | 2000 | <div class="range-block" data-source="makersuite"> |
| 2001 | + <label for="openai_request_images" class="checkbox_label widthFreeExpand"> | |
| 2002 | + <input id="openai_request_images" type="checkbox" /> | |
| 2003 | + <span> | |
| 2004 | + <span data-i18n="Request inline images">Request inline images</span> | |
| 2005 | + <i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Flash Experimental"></i> | |
| 2006 | + </span> | |
| 2007 | + </label> | |
| 2008 | + <div class="toggle-description justifyLeft marginBot5"> | |
| 2009 | + <span data-i18n="Allows the model to return image attachments."> | |
| 2010 | + Allows the model to return image attachments. | |
| 2011 | + </span> | |
| 2012 | + <em data-source="makersuite" data-i18n="Request inline images_desc_2"> | |
| 2013 | + Incompatible with the following features: function calling, web search, system prompt. | |
| 2014 | + </em> | |
| 2015 | + </div> | |
| 2016 | + </div> | |
| 2017 | + <div class="range-block" data-source="makersuite"> | |
| 2001 | 2018 | <label for="use_makersuite_sysprompt" class="checkbox_label widthFreeExpand"> |
| 2002 | 2019 | <input id="use_makersuite_sysprompt" type="checkbox" /> |
| 2003 | 2020 | <span> |
| @@ -171,6 +171,7 @@ import { | ||
| 171 | 171 | isElementInViewport, |
| 172 | 172 | copyText, |
| 173 | 173 | escapeHtml, |
| 174 | + saveBase64AsFile, | |
| 174 | 175 | } from './scripts/utils.js'; |
| 175 | 176 | import { debounce_timeout } from './scripts/constants.js'; |
| 176 | 177 | |
| @@ -3203,6 +3204,8 @@ class StreamingProcessor { | ||
| 3203 | 3204 | this.reasoningHandler = new ReasoningHandler(timeStarted); |
| 3204 | 3205 | /** @type {PromptReasoning} */ |
| 3205 | 3206 | this.promptReasoning = promptReasoning; |
| 3207 | + /** @type {string} */ | |
| 3208 | + this.image = ''; | |
| 3206 | 3209 | } |
| 3207 | 3210 | |
| 3208 | 3211 | /** |
| @@ -3250,7 +3253,7 @@ class StreamingProcessor { | ||
| 3250 | 3253 | this.sendTextarea.value = ''; |
| 3251 | 3254 | this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true })); |
| 3252 | 3255 | } else { |
| 3253 | 3256 | await saveReply({ type: this.type, text,getMessage: truetext, '',fromStreaming: [],true ''}); |
| 3254 | 3257 | messageId = chat.length - 1; |
| 3255 | 3258 | await this.#checkDomElements(messageId, continueOnReasoning); |
| 3256 | 3259 | this.markUIGenStarted(); |
| @@ -3372,6 +3375,11 @@ class StreamingProcessor { | ||
| 3372 | 3375 | chat[messageId].swipe_info.push(...swipeInfoArray); |
| 3373 | 3376 | } |
| 3374 | 3377 | |
| 3378 | + if (this.image) { | |
| 3379 | + await processImageAttachment(chat[messageId], { imageUrl: this.image, parsedImage: null }); | |
| 3380 | + appendMediaToMessage(chat[messageId], $(this.messageDom)); | |
| 3381 | + } | |
| 3382 | + | |
| 3375 | 3383 | if (this.type !== 'impersonate') { |
| 3376 | 3384 | await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type); |
| 3377 | 3385 | await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId, this.type); |
| @@ -3468,6 +3476,7 @@ class StreamingProcessor { | ||
| 3468 | 3476 | } |
| 3469 | 3477 | // Get the updated reasoning string into the handler |
| 3470 | 3478 | this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning); |
| 3479 | + this.image = state?.image ?? ''; | |
| 3471 | 3480 | await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text); |
| 3472 | 3481 | await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text)); |
| 3473 | 3482 | } |
| @@ -4866,6 +4875,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4866 | 4875 | let getMessage = extractMessageFromData(data); |
| 4867 | 4876 | let title = extractTitleFromData(data); |
| 4868 | 4877 | let reasoning = extractReasoningFromData(data); |
| 4878 | + let imageUrl = extractImageFromData(data); | |
| 4869 | 4879 | kobold_horde_model = title; |
| 4870 | 4880 | |
| 4871 | 4881 | const swipes = extractMultiSwipes(data, type); |
| @@ -4898,10 +4908,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4898 | 4908 | else { |
| 4899 | 4909 | // Without streaming we'll be having a full message on continuation. Treat it as a last chunk. |
| 4900 | 4910 | if (originalType !== 'continue') { |
| 4901 | 4911 | ({ type, getMessage } = await saveReply({ type, getMessage, false, title, swipes, reasoning, imageUrl })); |
| 4902 | 4912 | } |
| 4903 | 4913 | else { |
| 4904 | 4914 | ({ type, getMessage } = await saveReply({ type: 'appendFinal', getMessage, false, title, swipes, reasoning, imageUrl })); |
| 4905 | 4915 | } |
| 4906 | 4916 | |
| 4907 | 4917 | // This relies on `saveReply` having been called to add the message to the chat, so it must be last. |
| @@ -5726,6 +5736,32 @@ function extractTitleFromData(data) { | ||
| 5726 | 5736 | } |
| 5727 | 5737 | |
| 5728 | 5738 | /** |
| 5739 | + * Extracts the image from the response data. | |
| 5740 | + * @param {object} data Response data | |
| 5741 | + * @param {object} [options] Extraction options | |
| 5742 | + * @param {string} [options.mainApi] Main API to use | |
| 5743 | + * @param {string} [options.chatCompletionSource] Chat completion source | |
| 5744 | + * @returns {string} Extracted image | |
| 5745 | + */ | |
| 5746 | +function extractImageFromData(data, { mainApi = null, chatCompletionSource = null } = {}) { | |
| 5747 | + switch (mainApi ?? main_api) { | |
| 5748 | + case 'openai': { | |
| 5749 | + switch (chatCompletionSource ?? oai_settings.chat_completion_source) { | |
| 5750 | + case chat_completion_sources.MAKERSUITE: { | |
| 5751 | + const inlineData = data?.responseContent?.parts?.find(x => x.inlineData)?.inlineData; | |
| 5752 | + if (inlineData) { | |
| 5753 | + return `data:${inlineData.mimeType};base64,${inlineData.data}`; | |
| 5754 | + } | |
| 5755 | + } break; | |
| 5756 | + | |
| 5757 | + } | |
| 5758 | + } break; | |
| 5759 | + } | |
| 5760 | + | |
| 5761 | + return undefined; | |
| 5762 | +} | |
| 5763 | + | |
| 5764 | +/** | |
| 5729 | 5765 | * parseAndSaveLogprobs receives the full data response for a non-streaming |
| 5730 | 5766 | * generation, parses logprobs for all tokens in the message, and saves them |
| 5731 | 5767 | * to the currently active message. |
| @@ -5974,7 +6010,59 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc | ||
| 5974 | 6010 | return getMessage; |
| 5975 | 6011 | } |
| 5976 | 6012 | |
| 5977 | -export async function saveReply(type, getMessage, fromStreaming, title, swipes, reasoning) { | |
| 6013 | +/** | |
| 6014 | + * Adds an image to the message. | |
| 6015 | + * @param {object} message Message object | |
| 6016 | + * @param {object} sources Image sources | |
| 6017 | + * @param {ParsedImage} [sources.parsedImage] Parsed image | |
| 6018 | + * @param {string} [sources.imageUrl] Image URL | |
| 6019 | + * | |
| 6020 | + * @returns {Promise<void>} | |
| 6021 | + */ | |
| 6022 | +async function processImageAttachment(message, { parsedImage, imageUrl }) { | |
| 6023 | + if (parsedImage?.image) { | |
| 6024 | + saveImageToMessage(parsedImage, message); | |
| 6025 | + return; | |
| 6026 | + } | |
| 6027 | + | |
| 6028 | + if (!imageUrl) { | |
| 6029 | + return; | |
| 6030 | + } | |
| 6031 | + | |
| 6032 | + let url = imageUrl; | |
| 6033 | + if (isDataURL(url)) { | |
| 6034 | + const fileName = `inline_image_${Date.now().toString()}`; | |
| 6035 | + const [mime, base64] = /^data:(.*?);base64,(.*)$/.exec(imageUrl).slice(1); | |
| 6036 | + url = await saveBase64AsFile(base64, message.name, fileName, mime.split('/')[1]); | |
| 6037 | + } | |
| 6038 | + saveImageToMessage({ image: url, inline: true }, message); | |
| 6039 | +} | |
| 6040 | + | |
| 6041 | +/** | |
| 6042 | + * Saves a resulting message to the chat. | |
| 6043 | + * @param {SaveReplyParams} params | |
| 6044 | + * @returns {Promise<SaveReplyResult>} Promise when the message is saved | |
| 6045 | + * | |
| 6046 | + * @typedef {object} SaveReplyParams | |
| 6047 | + * @property {string} type Type of generation | |
| 6048 | + * @property {string} getMessage Generated message | |
| 6049 | + * @property {boolean} [fromStreaming] If the message is from streaming | |
| 6050 | + * @property {string} [title] Message tooltip | |
| 6051 | + * @property {string[]} [swipes] Extra swipes | |
| 6052 | + * @property {string} [reasoning] Message reasoning | |
| 6053 | + * @property {string} [imageUrl] Link to an image | |
| 6054 | + * | |
| 6055 | + * @typedef {object} SaveReplyResult | |
| 6056 | + * @property {string} type Type of generation | |
| 6057 | + * @property {string} getMessage Generated message | |
| 6058 | + */ | |
| 6059 | +export async function saveReply({ type, getMessage, fromStreaming = false, title = '', swipes = [], reasoning = '', imageUrl = '' }) { | |
| 6060 | + // Backward compatibility | |
| 6061 | + if (arguments.length > 1 && typeof arguments[0] !== 'object') { | |
| 6062 | + console.trace('saveReply called with positional arguments. Please use an object instead.'); | |
| 6063 | + [type, getMessage, fromStreaming, title, swipes, reasoning, imageUrl] = arguments; | |
| 6064 | + } | |
| 6065 | + | |
| 5978 | 6066 | if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined || |
| 5979 | 6067 | chat[chat.length - 1]['is_user'])) { |
| 5980 | 6068 | type = 'normal'; |
| @@ -5995,8 +6083,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, | ||
| 5995 | 6083 | |
| 5996 | 6084 | let oldMessage = ''; |
| 5997 | 6085 | const generationFinished = new Date(); |
| 5998 | 6086 | const imgparsedImage = extractImageFromMessage(getMessage); |
| 5999 | 6087 | getMessage = imgparsedImage.getMessage; |
| 6000 | 6088 | if (type === 'swipe') { |
| 6001 | 6089 | oldMessage = chat[chat.length - 1]['mes']; |
| 6002 | 6090 | chat[chat.length - 1]['swipes'].length++; |
| @@ -6010,6 +6098,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, | ||
| 6010 | 6098 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); |
| 6011 | 6099 | chat[chat.length - 1]['extra']['reasoning'] = reasoning; |
| 6012 | 6100 | chat[chat.length - 1]['extra']['reasoning_duration'] = null; |
| 6101 | + await processImageAttachment(chat[chat.length - 1], { parsedImage, imageUrl }); | |
| 6013 | 6102 | if (power_user.message_token_count_enabled) { |
| 6014 | 6103 | const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; |
| 6015 | 6104 | chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); |
| @@ -6033,6 +6122,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, | ||
| 6033 | 6122 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); |
| 6034 | 6123 | chat[chat.length - 1]['extra']['reasoning'] = reasoning; |
| 6035 | 6124 | chat[chat.length - 1]['extra']['reasoning_duration'] = null; |
| 6125 | + await processImageAttachment(chat[chat.length - 1], { parsedImage, imageUrl }); | |
| 6036 | 6126 | if (power_user.message_token_count_enabled) { |
| 6037 | 6127 | const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; |
| 6038 | 6128 | chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); |
| @@ -6052,6 +6142,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, | ||
| 6052 | 6142 | chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); |
| 6053 | 6143 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); |
| 6054 | 6144 | chat[chat.length - 1]['extra']['reasoning'] += reasoning; |
| 6145 | + await processImageAttachment(chat[chat.length - 1], { parsedImage, imageUrl }); | |
| 6055 | 6146 | // We don't know if the reasoning duration extended, so we don't update it here on purpose. |
| 6056 | 6147 | if (power_user.message_token_count_enabled) { |
| 6057 | 6148 | const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; |
| @@ -6097,7 +6188,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, | ||
| 6097 | 6188 | chat[chat.length - 1]['extra']['gen_id'] = group_generation_id; |
| 6098 | 6189 | } |
| 6099 | 6190 | |
| 6100 | 6191 | saveImageToMessage(img,await processImageAttachment(chat[chat.length - 1], { parsedImage, imageUrl: imageUrl }); |
| 6101 | 6192 | const chat_id = (chat.length - 1); |
| 6102 | 6193 | |
| 6103 | 6194 | !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type); |
| @@ -6203,6 +6294,12 @@ export function syncMesToSwipe(messageId = null) { | ||
| 6203 | 6294 | return true; |
| 6204 | 6295 | } |
| 6205 | 6296 | |
| 6297 | +/** | |
| 6298 | + * Saves the image to the message object. | |
| 6299 | + * @param {ParsedImage} img Image object | |
| 6300 | + * @param {object} mes Chat message object | |
| 6301 | + * @typedef {{ image?: string, title?: string, inline?: boolean }} ParsedImage | |
| 6302 | + */ | |
| 6206 | 6303 | function saveImageToMessage(img, mes) { |
| 6207 | 6304 | if (mes && img.image) { |
| 6208 | 6305 | if (!mes.extra || typeof mes.extra !== 'object') { |
| @@ -6210,6 +6307,7 @@ function saveImageToMessage(img, mes) { | ||
| 6210 | 6307 | } |
| 6211 | 6308 | mes.extra.image = img.image; |
| 6212 | 6309 | mes.extra.title = img.title; |
| 6310 | + mes.extra.inline_image = img.inline; | |
| 6213 | 6311 | } |
| 6214 | 6312 | } |
| 6215 | 6313 | |
| @@ -6252,7 +6350,7 @@ function extractImageFromMessage(getMessage) { | ||
| 6252 | 6350 | const image = results ? results[1] : ''; |
| 6253 | 6351 | const title = results ? results[2] : ''; |
| 6254 | 6352 | getMessage = getMessage.replace(regex, ''); |
| 6255 | 6353 | return { getMessage, image, title, inline: true }; |
| 6256 | 6354 | } |
| 6257 | 6355 | |
| 6258 | 6356 | /** |
| @@ -305,6 +305,7 @@ export const settingsToUpdate = { | ||
| 305 | 305 | seed: ['#seed_openai', 'seed', false], |
| 306 | 306 | n: ['#n_openai', 'n', false], |
| 307 | 307 | bypass_status_check: ['#openai_bypass_status_check', 'bypass_status_check', true], |
| 308 | + request_images: ['#openai_request_images', 'request_images', true], | |
| 308 | 309 | }; |
| 309 | 310 | |
| 310 | 311 | const default_settings = { |
| @@ -383,6 +384,7 @@ const default_settings = { | ||
| 383 | 384 | show_thoughts: true, |
| 384 | 385 | reasoning_effort: 'medium', |
| 385 | 386 | enable_web_search: false, |
| 387 | + request_images: false, | |
| 386 | 388 | seed: -1, |
| 387 | 389 | n: 1, |
| 388 | 390 | }; |
| @@ -463,6 +465,7 @@ const oai_settings = { | ||
| 463 | 465 | show_thoughts: true, |
| 464 | 466 | reasoning_effort: 'medium', |
| 465 | 467 | enable_web_search: false, |
| 468 | + request_images: false, | |
| 466 | 469 | seed: -1, |
| 467 | 470 | n: 1, |
| 468 | 471 | }; |
| @@ -2014,6 +2017,7 @@ async function sendOpenAIRequest(type, messages, signal) { | ||
| 2014 | 2017 | 'include_reasoning': Boolean(oai_settings.show_thoughts), |
| 2015 | 2018 | 'reasoning_effort': String(oai_settings.reasoning_effort), |
| 2016 | 2019 | 'enable_web_search': Boolean(oai_settings.enable_web_search), |
| 2020 | + 'request_images': Boolean(oai_settings.request_images), | |
| 2017 | 2021 | }; |
| 2018 | 2022 | |
| 2019 | 2023 | if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) { |
| @@ -2200,7 +2204,7 @@ async function sendOpenAIRequest(type, messages, signal) { | ||
| 2200 | 2204 | let text = ''; |
| 2201 | 2205 | const swipes = []; |
| 2202 | 2206 | const toolCalls = []; |
| 2203 | 2207 | const state = { reasoning: '', image: '' }; |
| 2204 | 2208 | while (true) { |
| 2205 | 2209 | const { done, value } = await reader.read(); |
| 2206 | 2210 | if (done) return; |
| @@ -2258,6 +2262,10 @@ function getStreamingReply(data, state) { | ||
| 2258 | 2262 | } |
| 2259 | 2263 | return data?.delta?.text || ''; |
| 2260 | 2264 | } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) { |
| 2265 | + const inlineData = data?.candidates?.[0]?.content?.parts?.find(x => x.inlineData)?.inlineData; | |
| 2266 | + if (inlineData) { | |
| 2267 | + state.image = `data:${inlineData.mimeType};base64,${inlineData.data}`; | |
| 2268 | + } | |
| 2261 | 2269 | if (oai_settings.show_thoughts) { |
| 2262 | 2270 | state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || ''); |
| 2263 | 2271 | } |
| @@ -3242,6 +3250,7 @@ function loadOpenAISettings(data, settings) { | ||
| 3242 | 3250 | oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts; |
| 3243 | 3251 | oai_settings.reasoning_effort = settings.reasoning_effort ?? default_settings.reasoning_effort; |
| 3244 | 3252 | oai_settings.enable_web_search = settings.enable_web_search ?? default_settings.enable_web_search; |
| 3253 | + oai_settings.request_images = settings.request_images ?? default_settings.request_images; | |
| 3245 | 3254 | oai_settings.seed = settings.seed ?? default_settings.seed; |
| 3246 | 3255 | oai_settings.n = settings.n ?? default_settings.n; |
| 3247 | 3256 | |
| @@ -3370,6 +3379,7 @@ function loadOpenAISettings(data, settings) { | ||
| 3370 | 3379 | $('#n_openai').val(oai_settings.n); |
| 3371 | 3380 | $('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts); |
| 3372 | 3381 | $('#openai_enable_web_search').prop('checked', oai_settings.enable_web_search); |
| 3382 | + $('#openai_request_images').prop('checked', oai_settings.request_images); | |
| 3373 | 3383 | |
| 3374 | 3384 | $('#openai_reasoning_effort').val(oai_settings.reasoning_effort); |
| 3375 | 3385 | $(`#openai_reasoning_effort option[value="${oai_settings.reasoning_effort}"]`).prop('selected', true); |
| @@ -3641,6 +3651,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { | ||
| 3641 | 3651 | show_thoughts: settings.show_thoughts, |
| 3642 | 3652 | reasoning_effort: settings.reasoning_effort, |
| 3643 | 3653 | enable_web_search: settings.enable_web_search, |
| 3654 | + request_images: settings.request_images, | |
| 3644 | 3655 | seed: settings.seed, |
| 3645 | 3656 | n: settings.n, |
| 3646 | 3657 | }; |
| @@ -5603,6 +5614,11 @@ export function initOpenAI() { | ||
| 5603 | 5614 | saveSettingsDebounced(); |
| 5604 | 5615 | }); |
| 5605 | 5616 | |
| 5617 | + $('#openai_request_images').on('input', function () { | |
| 5618 | + oai_settings.request_images = !!$(this).prop('checked'); | |
| 5619 | + saveSettingsDebounced(); | |
| 5620 | + }); | |
| 5621 | + | |
| 5606 | 5622 | if (!CSS.supports('field-sizing', 'content')) { |
| 5607 | 5623 | $(document).on('input', '#openai_settings .autoSetHeight', function () { |
| 5608 | 5624 | resetScrollHeight($(this)); |
| @@ -138,10 +138,11 @@ async function* parseStreamData(json) { | ||
| 138 | 138 | for (let i = 0; i < json.candidates.length; i++) { |
| 139 | 139 | const isNotPrimary = json.candidates?.[0]?.index > 0; |
| 140 | 140 | const hasToolCalls = json?.candidates?.[0]?.content?.parts?.some(p => p?.functionCall); |
| 141 | + const hasInlineData = json?.candidates?.[0]?.content?.parts?.some(p => p?.inlineData); | |
| 141 | 142 | if (isNotPrimary || json.candidates.length === 0) { |
| 142 | 143 | return null; |
| 143 | 144 | } |
| 144 | 145 | if (hasToolCalls || hasInlineData) { |
| 145 | 146 | yield { data: json, chunk: '' }; |
| 146 | 147 | return; |
| 147 | 148 | } |
| @@ -338,6 +338,7 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 338 | 338 | const model = String(request.body.model); |
| 339 | 339 | const stream = Boolean(request.body.stream); |
| 340 | 340 | const enableWebSearch = Boolean(request.body.enable_web_search); |
| 341 | + const requestImages = Boolean(request.body.request_images); | |
| 341 | 342 | const isThinking = model.includes('thinking'); |
| 342 | 343 | |
| 343 | 344 | const generationConfig = { |
| @@ -356,7 +357,12 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 356 | 357 | delete generationConfig.stopSequences; |
| 357 | 358 | } |
| 358 | 359 | |
| 359 | - const should_use_system_prompt = ( | |
| 360 | + const useMultiModal = requestImages && (model.includes('gemini-2.0-flash-exp')); | |
| 361 | + if (useMultiModal) { | |
| 362 | + generationConfig.responseModalities = ['text', 'image']; | |
| 363 | + } | |
| 364 | + | |
| 365 | + const useSystemPrompt = !useMultiModal && ( | |
| 360 | 366 | model.includes('gemini-2.0-pro') || |
| 361 | 367 | model.includes('gemini-2.0-flash') || |
| 362 | 368 | model.includes('gemini-2.0-flash-thinking-exp') || |
| @@ -366,7 +372,7 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 366 | 372 | ) && request.body.use_makersuite_sysprompt; |
| 367 | 373 | |
| 368 | 374 | const tools = []; |
| 369 | 375 | const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_promptuseSystemPrompt, getPromptNames(request)); |
| 370 | 376 | let safetySettings = GEMINI_SAFETY; |
| 371 | 377 | |
| 372 | 378 | // These old models do not support setting the threshold to OFF at all. |
| @@ -379,14 +385,14 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 379 | 385 | } |
| 380 | 386 | // Most of the other models allow for setting the threshold of filters, except for HARM_CATEGORY_CIVIC_INTEGRITY, to OFF. |
| 381 | 387 | |
| 382 | 388 | if (enableWebSearch && !useMultiModal) { |
| 383 | 389 | const searchTool = model.includes('1.5') || model.includes('1.0') |
| 384 | 390 | ? ({ google_search_retrieval: {} }) |
| 385 | 391 | : ({ google_search: {} }); |
| 386 | 392 | tools.push(searchTool); |
| 387 | 393 | } |
| 388 | 394 | |
| 389 | 395 | if (Array.isArray(request.body.tools) && request.body.tools.length > 0 && !useMultiModal) { |
| 390 | 396 | const functionDeclarations = []; |
| 391 | 397 | for (const tool of request.body.tools) { |
| 392 | 398 | if (tool.type === 'function') { |
| @@ -405,7 +411,7 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 405 | 411 | generationConfig: generationConfig, |
| 406 | 412 | }; |
| 407 | 413 | |
| 408 | 414 | if (should_use_system_promptuseSystemPrompt) { |
| 409 | 415 | body.systemInstruction = prompt.system_instruction; |
| 410 | 416 | } |
| 411 | 417 | |
| @@ -469,10 +475,11 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 469 | 475 | |
| 470 | 476 | const responseContent = candidates[0].content ?? candidates[0].output; |
| 471 | 477 | const functionCall = (candidates?.[0]?.content?.parts ?? []).some(part => part.functionCall); |
| 478 | + const inlineData = (candidates?.[0]?.content?.parts ?? []).some(part => part.inlineData); | |
| 472 | 479 | console.warn('Google AI Studio response:', responseContent); |
| 473 | 480 | |
| 474 | 481 | const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n'); |
| 475 | 482 | if (!responseText && !functionCall && !inlineData) { |
| 476 | 483 | let message = 'Google AI Studio Candidate text empty'; |
| 477 | 484 | console.warn(message, generateResponseJson); |
| 478 | 485 | return response.send({ error: { message } }); |