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

0017358f8b6de330b88e0b23e4731bcb3249285a

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
5 files changed, +155 -16Ignore whitespace
public/index.html+17 -0
@@ -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>
public/script.js+106 -8
@@ -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';
175import { debounce_timeout } from './scripts/constants.js';176import { debounce_timeout } from './scripts/constants.js';
176177
@@ -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 }
32073210
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 }
33743377
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 handler3477 // 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;
48704880
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 }
49064916
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}
57275737
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 */
5746function 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-streaming5765 * parseAndSaveLogprobs receives the full data response for a non-streaming
5730 * generation, parses logprobs for all tokens in the message, and saves them5766 * 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}
59766012
5977export 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 */
6022async 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 */
6059export 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,
59956083
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 }
60996190
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);
61026193
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}
62056296
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 */
6206function saveImageToMessage(img, mes) {6303function 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}
62156313
@@ -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}
62576355
6258/**6356/**
public/scripts/openai.js+17 -1
@@ -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};
309310
310const default_settings = {311const 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 };
20182022
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;
32473256
@@ -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);
33733383
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 });
56055616
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));
public/scripts/sse-stream.js+2 -1
@@ -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 }
src/endpoints/backends/chat-completions.js+13 -6
@@ -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');
342343
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 }
358359
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;
367373
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;
371377
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.
381387
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 }
388394
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 };
407413
408 if (should_use_system_prompt) {414 if (useSystemPrompt) {
409 body.systemInstruction = prompt.system_instruction;415 body.systemInstruction = prompt.system_instruction;
410 }416 }
411417
@@ -469,10 +475,11 @@ async function sendMakerSuiteRequest(request, response) {
469475
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);
473480
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 } });