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 | </div> | 1998 | </div> |
| 1999 | </div> | 1999 | </div> |
| 2000 | <div class="range-block" data-source="makersuite"> | 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 | <label for="use_makersuite_sysprompt" class="checkbox_label widthFreeExpand"> | 2018 | <label for="use_makersuite_sysprompt" class="checkbox_label widthFreeExpand"> |
| 2002 | <input id="use_makersuite_sysprompt" type="checkbox" /> | 2019 | <input id="use_makersuite_sysprompt" type="checkbox" /> |
| 2003 | <span> | 2020 | <span> |
| @@ -171,6 +171,7 @@ import { | |||
| 171 | isElementInViewport, | 171 | isElementInViewport, |
| 172 | copyText, | 172 | copyText, |
| 173 | escapeHtml, | 173 | escapeHtml, |
| 174 | saveBase64AsFile, | ||
| 174 | } from './scripts/utils.js'; | 175 | } from './scripts/utils.js'; |
| 175 | import { debounce_timeout } from './scripts/constants.js'; | 176 | import { debounce_timeout } from './scripts/constants.js'; |
| 176 | 177 | ||
| @@ -3203,6 +3204,8 @@ class StreamingProcessor { | |||
| 3203 | this.reasoningHandler = new ReasoningHandler(timeStarted); | 3204 | this.reasoningHandler = new ReasoningHandler(timeStarted); |
| 3204 | /** @type {PromptReasoning} */ | 3205 | /** @type {PromptReasoning} */ |
| 3205 | this.promptReasoning = promptReasoning; | 3206 | this.promptReasoning = promptReasoning; |
| 3207 | /** @type {string} */ | ||
| 3208 | this.image = ''; | ||
| 3206 | } | 3209 | } |
| 3207 | 3210 | ||
| 3208 | /** | 3211 | /** |
| @@ -3250,7 +3253,7 @@ class StreamingProcessor { | |||
| 3250 | this.sendTextarea.value = ''; | 3253 | this.sendTextarea.value = ''; |
| 3251 | this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true })); | 3254 | this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true })); |
| 3252 | } else { | 3255 | } else { |
| 3253 | await saveReply(this.type, text, true, '', [], ''); | 3256 | await saveReply({ type: this.type, getMessage: text, fromStreaming: true }); |
| 3254 | messageId = chat.length - 1; | 3257 | messageId = chat.length - 1; |
| 3255 | await this.#checkDomElements(messageId, continueOnReasoning); | 3258 | await this.#checkDomElements(messageId, continueOnReasoning); |
| 3256 | this.markUIGenStarted(); | 3259 | this.markUIGenStarted(); |
| @@ -3372,6 +3375,11 @@ class StreamingProcessor { | |||
| 3372 | chat[messageId].swipe_info.push(...swipeInfoArray); | 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 | if (this.type !== 'impersonate') { | 3383 | if (this.type !== 'impersonate') { |
| 3376 | await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type); | 3384 | await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type); |
| 3377 | await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId, this.type); | 3385 | await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId, this.type); |
| @@ -3468,6 +3476,7 @@ class StreamingProcessor { | |||
| 3468 | } | 3476 | } |
| 3469 | // Get the updated reasoning string into the handler | 3477 | // Get the updated reasoning string into the handler |
| 3470 | this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning); | 3478 | this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning); |
| 3479 | this.image = state?.image ?? ''; | ||
| 3471 | await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text); | 3480 | await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text); |
| 3472 | await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text)); | 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 | let getMessage = extractMessageFromData(data); | 4875 | let getMessage = extractMessageFromData(data); |
| 4867 | let title = extractTitleFromData(data); | 4876 | let title = extractTitleFromData(data); |
| 4868 | let reasoning = extractReasoningFromData(data); | 4877 | let reasoning = extractReasoningFromData(data); |
| 4878 | let imageUrl = extractImageFromData(data); | ||
| 4869 | kobold_horde_model = title; | 4879 | kobold_horde_model = title; |
| 4870 | 4880 | ||
| 4871 | const swipes = extractMultiSwipes(data, type); | 4881 | const swipes = extractMultiSwipes(data, type); |
| @@ -4898,10 +4908,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | |||
| 4898 | else { | 4908 | else { |
| 4899 | // Without streaming we'll be having a full message on continuation. Treat it as a last chunk. | 4909 | // Without streaming we'll be having a full message on continuation. Treat it as a last chunk. |
| 4900 | if (originalType !== 'continue') { | 4910 | if (originalType !== 'continue') { |
| 4901 | ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes, reasoning)); | 4911 | ({ type, getMessage } = await saveReply({ type, getMessage, title, swipes, reasoning, imageUrl })); |
| 4902 | } | 4912 | } |
| 4903 | else { | 4913 | else { |
| 4904 | ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes, reasoning)); | 4914 | ({ type, getMessage } = await saveReply({ type: 'appendFinal', getMessage, title, swipes, reasoning, imageUrl })); |
| 4905 | } | 4915 | } |
| 4906 | 4916 | ||
| 4907 | // This relies on `saveReply` having been called to add the message to the chat, so it must be last. | 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 | * parseAndSaveLogprobs receives the full data response for a non-streaming | 5765 | * parseAndSaveLogprobs receives the full data response for a non-streaming |
| 5730 | * generation, parses logprobs for all tokens in the message, and saves them | 5766 | * generation, parses logprobs for all tokens in the message, and saves them |
| 5731 | * to the currently active message. | 5767 | * to the currently active message. |
| @@ -5974,7 +6010,59 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc | |||
| 5974 | return getMessage; | 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 | if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined || | 6066 | if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined || |
| 5979 | chat[chat.length - 1]['is_user'])) { | 6067 | chat[chat.length - 1]['is_user'])) { |
| 5980 | type = 'normal'; | 6068 | type = 'normal'; |
| @@ -5995,8 +6083,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, | |||
| 5995 | 6083 | ||
| 5996 | let oldMessage = ''; | 6084 | let oldMessage = ''; |
| 5997 | const generationFinished = new Date(); | 6085 | const generationFinished = new Date(); |
| 5998 | const img = extractImageFromMessage(getMessage); | 6086 | const parsedImage = extractImageFromMessage(getMessage); |
| 5999 | getMessage = img.getMessage; | 6087 | getMessage = parsedImage.getMessage; |
| 6000 | if (type === 'swipe') { | 6088 | if (type === 'swipe') { |
| 6001 | oldMessage = chat[chat.length - 1]['mes']; | 6089 | oldMessage = chat[chat.length - 1]['mes']; |
| 6002 | chat[chat.length - 1]['swipes'].length++; | 6090 | chat[chat.length - 1]['swipes'].length++; |
| @@ -6010,6 +6098,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, | |||
| 6010 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); | 6098 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); |
| 6011 | chat[chat.length - 1]['extra']['reasoning'] = reasoning; | 6099 | chat[chat.length - 1]['extra']['reasoning'] = reasoning; |
| 6012 | chat[chat.length - 1]['extra']['reasoning_duration'] = null; | 6100 | chat[chat.length - 1]['extra']['reasoning_duration'] = null; |
| 6101 | await processImageAttachment(chat[chat.length - 1], { parsedImage, imageUrl }); | ||
| 6013 | if (power_user.message_token_count_enabled) { | 6102 | if (power_user.message_token_count_enabled) { |
| 6014 | const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; | 6103 | const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; |
| 6015 | chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); | 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 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); | 6122 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); |
| 6034 | chat[chat.length - 1]['extra']['reasoning'] = reasoning; | 6123 | chat[chat.length - 1]['extra']['reasoning'] = reasoning; |
| 6035 | chat[chat.length - 1]['extra']['reasoning_duration'] = null; | 6124 | chat[chat.length - 1]['extra']['reasoning_duration'] = null; |
| 6125 | await processImageAttachment(chat[chat.length - 1], { parsedImage, imageUrl }); | ||
| 6036 | if (power_user.message_token_count_enabled) { | 6126 | if (power_user.message_token_count_enabled) { |
| 6037 | const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; | 6127 | const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; |
| 6038 | chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); | 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 | chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); | 6142 | chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); |
| 6053 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); | 6143 | chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); |
| 6054 | chat[chat.length - 1]['extra']['reasoning'] += reasoning; | 6144 | chat[chat.length - 1]['extra']['reasoning'] += reasoning; |
| 6145 | await processImageAttachment(chat[chat.length - 1], { parsedImage, imageUrl }); | ||
| 6055 | // We don't know if the reasoning duration extended, so we don't update it here on purpose. | 6146 | // We don't know if the reasoning duration extended, so we don't update it here on purpose. |
| 6056 | if (power_user.message_token_count_enabled) { | 6147 | if (power_user.message_token_count_enabled) { |
| 6057 | const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; | 6148 | const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; |
| @@ -6097,7 +6188,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, | |||
| 6097 | chat[chat.length - 1]['extra']['gen_id'] = group_generation_id; | 6188 | chat[chat.length - 1]['extra']['gen_id'] = group_generation_id; |
| 6098 | } | 6189 | } |
| 6099 | 6190 | ||
| 6100 | saveImageToMessage(img, chat[chat.length - 1]); | 6191 | await processImageAttachment(chat[chat.length - 1], { parsedImage, imageUrl: imageUrl }); |
| 6101 | const chat_id = (chat.length - 1); | 6192 | const chat_id = (chat.length - 1); |
| 6102 | 6193 | ||
| 6103 | !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type); | 6194 | !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type); |
| @@ -6203,6 +6294,12 @@ export function syncMesToSwipe(messageId = null) { | |||
| 6203 | return true; | 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 | function saveImageToMessage(img, mes) { | 6303 | function saveImageToMessage(img, mes) { |
| 6207 | if (mes && img.image) { | 6304 | if (mes && img.image) { |
| 6208 | if (!mes.extra || typeof mes.extra !== 'object') { | 6305 | if (!mes.extra || typeof mes.extra !== 'object') { |
| @@ -6210,6 +6307,7 @@ function saveImageToMessage(img, mes) { | |||
| 6210 | } | 6307 | } |
| 6211 | mes.extra.image = img.image; | 6308 | mes.extra.image = img.image; |
| 6212 | mes.extra.title = img.title; | 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 | const image = results ? results[1] : ''; | 6350 | const image = results ? results[1] : ''; |
| 6253 | const title = results ? results[2] : ''; | 6351 | const title = results ? results[2] : ''; |
| 6254 | getMessage = getMessage.replace(regex, ''); | 6352 | getMessage = getMessage.replace(regex, ''); |
| 6255 | return { getMessage, image, title }; | 6353 | return { getMessage, image, title, inline: true }; |
| 6256 | } | 6354 | } |
| 6257 | 6355 | ||
| 6258 | /** | 6356 | /** |
| @@ -305,6 +305,7 @@ export const settingsToUpdate = { | |||
| 305 | seed: ['#seed_openai', 'seed', false], | 305 | seed: ['#seed_openai', 'seed', false], |
| 306 | n: ['#n_openai', 'n', false], | 306 | n: ['#n_openai', 'n', false], |
| 307 | bypass_status_check: ['#openai_bypass_status_check', 'bypass_status_check', true], | 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 | const default_settings = { | 311 | const default_settings = { |
| @@ -383,6 +384,7 @@ const default_settings = { | |||
| 383 | show_thoughts: true, | 384 | show_thoughts: true, |
| 384 | reasoning_effort: 'medium', | 385 | reasoning_effort: 'medium', |
| 385 | enable_web_search: false, | 386 | enable_web_search: false, |
| 387 | request_images: false, | ||
| 386 | seed: -1, | 388 | seed: -1, |
| 387 | n: 1, | 389 | n: 1, |
| 388 | }; | 390 | }; |
| @@ -463,6 +465,7 @@ const oai_settings = { | |||
| 463 | show_thoughts: true, | 465 | show_thoughts: true, |
| 464 | reasoning_effort: 'medium', | 466 | reasoning_effort: 'medium', |
| 465 | enable_web_search: false, | 467 | enable_web_search: false, |
| 468 | request_images: false, | ||
| 466 | seed: -1, | 469 | seed: -1, |
| 467 | n: 1, | 470 | n: 1, |
| 468 | }; | 471 | }; |
| @@ -2014,6 +2017,7 @@ async function sendOpenAIRequest(type, messages, signal) { | |||
| 2014 | 'include_reasoning': Boolean(oai_settings.show_thoughts), | 2017 | 'include_reasoning': Boolean(oai_settings.show_thoughts), |
| 2015 | 'reasoning_effort': String(oai_settings.reasoning_effort), | 2018 | 'reasoning_effort': String(oai_settings.reasoning_effort), |
| 2016 | 'enable_web_search': Boolean(oai_settings.enable_web_search), | 2019 | 'enable_web_search': Boolean(oai_settings.enable_web_search), |
| 2020 | 'request_images': Boolean(oai_settings.request_images), | ||
| 2017 | }; | 2021 | }; |
| 2018 | 2022 | ||
| 2019 | if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) { | 2023 | if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) { |
| @@ -2200,7 +2204,7 @@ async function sendOpenAIRequest(type, messages, signal) { | |||
| 2200 | let text = ''; | 2204 | let text = ''; |
| 2201 | const swipes = []; | 2205 | const swipes = []; |
| 2202 | const toolCalls = []; | 2206 | const toolCalls = []; |
| 2203 | const state = { reasoning: '' }; | 2207 | const state = { reasoning: '', image: '' }; |
| 2204 | while (true) { | 2208 | while (true) { |
| 2205 | const { done, value } = await reader.read(); | 2209 | const { done, value } = await reader.read(); |
| 2206 | if (done) return; | 2210 | if (done) return; |
| @@ -2258,6 +2262,10 @@ function getStreamingReply(data, state) { | |||
| 2258 | } | 2262 | } |
| 2259 | return data?.delta?.text || ''; | 2263 | return data?.delta?.text || ''; |
| 2260 | } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) { | 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 | if (oai_settings.show_thoughts) { | 2269 | if (oai_settings.show_thoughts) { |
| 2262 | state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || ''); | 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 | oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts; | 3250 | oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts; |
| 3243 | oai_settings.reasoning_effort = settings.reasoning_effort ?? default_settings.reasoning_effort; | 3251 | oai_settings.reasoning_effort = settings.reasoning_effort ?? default_settings.reasoning_effort; |
| 3244 | oai_settings.enable_web_search = settings.enable_web_search ?? default_settings.enable_web_search; | 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 | oai_settings.seed = settings.seed ?? default_settings.seed; | 3254 | oai_settings.seed = settings.seed ?? default_settings.seed; |
| 3246 | oai_settings.n = settings.n ?? default_settings.n; | 3255 | oai_settings.n = settings.n ?? default_settings.n; |
| 3247 | 3256 | ||
| @@ -3370,6 +3379,7 @@ function loadOpenAISettings(data, settings) { | |||
| 3370 | $('#n_openai').val(oai_settings.n); | 3379 | $('#n_openai').val(oai_settings.n); |
| 3371 | $('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts); | 3380 | $('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts); |
| 3372 | $('#openai_enable_web_search').prop('checked', oai_settings.enable_web_search); | 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 | $('#openai_reasoning_effort').val(oai_settings.reasoning_effort); | 3384 | $('#openai_reasoning_effort').val(oai_settings.reasoning_effort); |
| 3375 | $(`#openai_reasoning_effort option[value="${oai_settings.reasoning_effort}"]`).prop('selected', true); | 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 | show_thoughts: settings.show_thoughts, | 3651 | show_thoughts: settings.show_thoughts, |
| 3642 | reasoning_effort: settings.reasoning_effort, | 3652 | reasoning_effort: settings.reasoning_effort, |
| 3643 | enable_web_search: settings.enable_web_search, | 3653 | enable_web_search: settings.enable_web_search, |
| 3654 | request_images: settings.request_images, | ||
| 3644 | seed: settings.seed, | 3655 | seed: settings.seed, |
| 3645 | n: settings.n, | 3656 | n: settings.n, |
| 3646 | }; | 3657 | }; |
| @@ -5603,6 +5614,11 @@ export function initOpenAI() { | |||
| 5603 | saveSettingsDebounced(); | 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 | if (!CSS.supports('field-sizing', 'content')) { | 5622 | if (!CSS.supports('field-sizing', 'content')) { |
| 5607 | $(document).on('input', '#openai_settings .autoSetHeight', function () { | 5623 | $(document).on('input', '#openai_settings .autoSetHeight', function () { |
| 5608 | resetScrollHeight($(this)); | 5624 | resetScrollHeight($(this)); |
| @@ -138,10 +138,11 @@ async function* parseStreamData(json) { | |||
| 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 | const hasToolCalls = json?.candidates?.[0]?.content?.parts?.some(p => p?.functionCall); |
| 141 | const hasInlineData = json?.candidates?.[0]?.content?.parts?.some(p => p?.inlineData); | ||
| 141 | if (isNotPrimary || json.candidates.length === 0) { | 142 | if (isNotPrimary || json.candidates.length === 0) { |
| 142 | return null; | 143 | return null; |
| 143 | } | 144 | } |
| 144 | if (hasToolCalls) { | 145 | if (hasToolCalls || hasInlineData) { |
| 145 | yield { data: json, chunk: '' }; | 146 | yield { data: json, chunk: '' }; |
| 146 | return; | 147 | return; |
| 147 | } | 148 | } |
| @@ -338,6 +338,7 @@ async function sendMakerSuiteRequest(request, response) { | |||
| 338 | const model = String(request.body.model); | 338 | const model = String(request.body.model); |
| 339 | const stream = Boolean(request.body.stream); | 339 | const stream = Boolean(request.body.stream); |
| 340 | const enableWebSearch = Boolean(request.body.enable_web_search); | 340 | const enableWebSearch = Boolean(request.body.enable_web_search); |
| 341 | const requestImages = Boolean(request.body.request_images); | ||
| 341 | const isThinking = model.includes('thinking'); | 342 | const isThinking = model.includes('thinking'); |
| 342 | 343 | ||
| 343 | const generationConfig = { | 344 | const generationConfig = { |
| @@ -356,7 +357,12 @@ async function sendMakerSuiteRequest(request, response) { | |||
| 356 | delete generationConfig.stopSequences; | 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 | model.includes('gemini-2.0-pro') || | 366 | model.includes('gemini-2.0-pro') || |
| 361 | model.includes('gemini-2.0-flash') || | 367 | model.includes('gemini-2.0-flash') || |
| 362 | model.includes('gemini-2.0-flash-thinking-exp') || | 368 | model.includes('gemini-2.0-flash-thinking-exp') || |
| @@ -366,7 +372,7 @@ async function sendMakerSuiteRequest(request, response) { | |||
| 366 | ) && request.body.use_makersuite_sysprompt; | 372 | ) && request.body.use_makersuite_sysprompt; |
| 367 | 373 | ||
| 368 | const tools = []; | 374 | const tools = []; |
| 369 | const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, getPromptNames(request)); | 375 | const prompt = convertGooglePrompt(request.body.messages, model, useSystemPrompt, getPromptNames(request)); |
| 370 | let safetySettings = GEMINI_SAFETY; | 376 | let safetySettings = GEMINI_SAFETY; |
| 371 | 377 | ||
| 372 | // These old models do not support setting the threshold to OFF at all. | 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 | // Most of the other models allow for setting the threshold of filters, except for HARM_CATEGORY_CIVIC_INTEGRITY, to OFF. | 386 | // Most of the other models allow for setting the threshold of filters, except for HARM_CATEGORY_CIVIC_INTEGRITY, to OFF. |
| 381 | 387 | ||
| 382 | if (enableWebSearch) { | 388 | if (enableWebSearch && !useMultiModal) { |
| 383 | const searchTool = model.includes('1.5') || model.includes('1.0') | 389 | const searchTool = model.includes('1.5') || model.includes('1.0') |
| 384 | ? ({ google_search_retrieval: {} }) | 390 | ? ({ google_search_retrieval: {} }) |
| 385 | : ({ google_search: {} }); | 391 | : ({ google_search: {} }); |
| 386 | tools.push(searchTool); | 392 | tools.push(searchTool); |
| 387 | } | 393 | } |
| 388 | 394 | ||
| 389 | if (Array.isArray(request.body.tools) && request.body.tools.length > 0) { | 395 | if (Array.isArray(request.body.tools) && request.body.tools.length > 0 && !useMultiModal) { |
| 390 | const functionDeclarations = []; | 396 | const functionDeclarations = []; |
| 391 | for (const tool of request.body.tools) { | 397 | for (const tool of request.body.tools) { |
| 392 | if (tool.type === 'function') { | 398 | if (tool.type === 'function') { |
| @@ -405,7 +411,7 @@ async function sendMakerSuiteRequest(request, response) { | |||
| 405 | generationConfig: generationConfig, | 411 | generationConfig: generationConfig, |
| 406 | }; | 412 | }; |
| 407 | 413 | ||
| 408 | if (should_use_system_prompt) { | 414 | if (useSystemPrompt) { |
| 409 | body.systemInstruction = prompt.system_instruction; | 415 | body.systemInstruction = prompt.system_instruction; |
| 410 | } | 416 | } |
| 411 | 417 | ||
| @@ -469,10 +475,11 @@ async function sendMakerSuiteRequest(request, response) { | |||
| 469 | 475 | ||
| 470 | const responseContent = candidates[0].content ?? candidates[0].output; | 476 | const responseContent = candidates[0].content ?? candidates[0].output; |
| 471 | const functionCall = (candidates?.[0]?.content?.parts ?? []).some(part => part.functionCall); | 477 | const functionCall = (candidates?.[0]?.content?.parts ?? []).some(part => part.functionCall); |
| 478 | const inlineData = (candidates?.[0]?.content?.parts ?? []).some(part => part.inlineData); | ||
| 472 | console.warn('Google AI Studio response:', responseContent); | 479 | console.warn('Google AI Studio response:', responseContent); |
| 473 | 480 | ||
| 474 | const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n'); | 481 | const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n'); |
| 475 | if (!responseText && !functionCall) { | 482 | if (!responseText && !functionCall && !inlineData) { |
| 476 | let message = 'Google AI Studio Candidate text empty'; | 483 | let message = 'Google AI Studio Candidate text empty'; |
| 477 | console.warn(message, generateResponseJson); | 484 | console.warn(message, generateResponseJson); |
| 478 | return response.send({ error: { message } }); | 485 | return response.send({ error: { message } }); |