Auto-add OpenRouter captioning models

6393eda81ee991290ac05521ed59dbf2fa8825eb

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

3 files changed, +90 -23Ignore whitespace
public/scripts/extensions/caption/index.js+55 -23
@@ -1,6 +1,6 @@
1import { ensureImageFormatSupported, getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js';1import { ensureImageFormatSupported, getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js';
2import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';2import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';
3import { appendMediaToMessage, callPopup, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';3import { appendMediaToMessage, callPopup, eventSource, event_types, getRequestHeaders, main_api, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';
4import { getMessageTimeStamp } from '../../RossAscends-mods.js';4import { getMessageTimeStamp } from '../../RossAscends-mods.js';
5import { SECRET_KEYS, secret_state } from '../../secrets.js';5import { SECRET_KEYS, secret_state } from '../../secrets.js';
6import { getMultimodalCaption } from '../shared.js';6import { getMultimodalCaption } from '../shared.js';
@@ -431,14 +431,9 @@ jQuery(async function () {
431 $('#form_sheld').append(imgForm);431 $('#form_sheld').append(imgForm);
432 $('#img_file').on('change', (e) => onSelectImage(e.originalEvent, '', false));432 $('#img_file').on('change', (e) => onSelectImage(e.originalEvent, '', false));
433 }433 }
434 function switchMultimodalBlocks() {434 async function switchMultimodalBlocks() {
435 await addOpenRouterModels();
435 const isMultimodal = extension_settings.caption.source === 'multimodal';436 const isMultimodal = extension_settings.caption.source === 'multimodal';
436 $('#caption_ollama_pull').on('click', (e) => {
437 const presetModel = extension_settings.caption.multimodal_model !== 'ollama_current' ? extension_settings.caption.multimodal_model : '';
438 e.preventDefault();
439 $('#ollama_download_model').trigger('click');
440 $('#dialogue_popup_input').val(presetModel);
441 });
442 $('#caption_multimodal_block').toggle(isMultimodal);437 $('#caption_multimodal_block').toggle(isMultimodal);
443 $('#caption_prompt_block').toggle(isMultimodal);438 $('#caption_prompt_block').toggle(isMultimodal);
444 $('#caption_multimodal_api').val(extension_settings.caption.multimodal_api);439 $('#caption_multimodal_api').val(extension_settings.caption.multimodal_api);
@@ -448,30 +443,48 @@ jQuery(async function () {
448 const types = type.split(',');443 const types = type.split(',');
449 $(this).toggle(types.includes(extension_settings.caption.multimodal_api));444 $(this).toggle(types.includes(extension_settings.caption.multimodal_api));
450 });445 });
451 $('#caption_multimodal_api').on('change', () => {
452 const api = String($('#caption_multimodal_api').val());
453 const model = String($(`#caption_multimodal_model option[data-type="${api}"]`).first().val());
454 extension_settings.caption.multimodal_api = api;
455 extension_settings.caption.multimodal_model = model;
456 saveSettingsDebounced();
457 switchMultimodalBlocks();
458 });
459 $('#caption_multimodal_model').on('change', () => {
460 extension_settings.caption.multimodal_model = String($('#caption_multimodal_model').val());
461 saveSettingsDebounced();
462 });
463 }446 }
464 async function addSettings() {447 async function addSettings() {
465 const html = await renderExtensionTemplateAsync('caption', 'settings', { TEMPLATE_DEFAULT, PROMPT_DEFAULT });448 const html = await renderExtensionTemplateAsync('caption', 'settings', { TEMPLATE_DEFAULT, PROMPT_DEFAULT });
466 $('#caption_container').append(html);449 $('#caption_container').append(html);
467 }450 }
451 async function addOpenRouterModels() {
452 const dropdown = document.getElementById('caption_multimodal_model');
453 if (!(dropdown instanceof HTMLSelectElement)) {
454 return;
455 }
456 if (extension_settings.caption.source !== 'multimodal' || extension_settings.caption.multimodal_api !== 'openrouter') {
457 return;
458 }
459 const options = Array.from(dropdown.options);
460 const response = await fetch('/api/openrouter/models/multimodal', {
461 method: 'POST',
462 headers: getRequestHeaders(),
463 });
464 if (!response.ok) {
465 return;
466 }
467 const modelIds = await response.json();
468 if (Array.isArray(modelIds) && modelIds.length > 0) {
469 modelIds.forEach((modelId) => {
470 if (!modelId || typeof modelId !== 'string' || options.some(o => o.value === modelId)) {
471 return;
472 }
473 const option = document.createElement('option');
474 option.value = modelId;
475 option.textContent = modelId;
476 option.dataset.type = 'openrouter';
477 dropdown.add(option);
478 });
479 }
480 }
468481
469 await addSettings();482 await addSettings();
470 addPictureSendForm();483 addPictureSendForm();
471 addSendPictureButton();484 addSendPictureButton();
472 setImageIcon();485 setImageIcon();
473 migrateSettings();486 migrateSettings();
474 switchMultimodalBlocks();487 await switchMultimodalBlocks();
475488
476 $('#caption_refine_mode').prop('checked', !!(extension_settings.caption.refine_mode));489 $('#caption_refine_mode').prop('checked', !!(extension_settings.caption.refine_mode));
477 $('#caption_allow_reverse_proxy').prop('checked', !!(extension_settings.caption.allow_reverse_proxy));490 $('#caption_allow_reverse_proxy').prop('checked', !!(extension_settings.caption.allow_reverse_proxy));
@@ -506,6 +519,24 @@ jQuery(async function () {
506 extension_settings.caption.auto_mode = !!$('#caption_auto_mode').prop('checked');519 extension_settings.caption.auto_mode = !!$('#caption_auto_mode').prop('checked');
507 saveSettingsDebounced();520 saveSettingsDebounced();
508 });521 });
522 $('#caption_ollama_pull').on('click', (e) => {
523 const presetModel = extension_settings.caption.multimodal_model !== 'ollama_current' ? extension_settings.caption.multimodal_model : '';
524 e.preventDefault();
525 $('#ollama_download_model').trigger('click');
526 $('#dialogue_popup_input').val(presetModel);
527 });
528 $('#caption_multimodal_api').on('change', () => {
529 const api = String($('#caption_multimodal_api').val());
530 const model = String($(`#caption_multimodal_model option[data-type="${api}"]`).first().val());
531 extension_settings.caption.multimodal_api = api;
532 extension_settings.caption.multimodal_model = model;
533 saveSettingsDebounced();
534 switchMultimodalBlocks();
535 });
536 $('#caption_multimodal_model').on('change', () => {
537 extension_settings.caption.multimodal_model = String($('#caption_multimodal_model').val());
538 saveSettingsDebounced();
539 });
509540
510 const onMessageEvent = async (index) => {541 const onMessageEvent = async (index) => {
511 if (!extension_settings.caption.auto_mode) {542 if (!extension_settings.caption.auto_mode) {
@@ -531,14 +562,15 @@ jQuery(async function () {
531 await captionExistingMessage(data);562 await captionExistingMessage(data);
532 appendMediaToMessage(data, messageBlock, false);563 appendMediaToMessage(data, messageBlock, false);
533 await saveChatConditional();564 await saveChatConditional();
534 } catch(e) {565 } catch (e) {
535 console.error('Message image recaption failed', e);566 console.error('Message image recaption failed', e);
536 } finally {567 } finally {
537 messageImg.removeClass(animationClass);568 messageImg.removeClass(animationClass);
538 }569 }
539 });570 });
540571
541 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'caption',572 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
573 name: 'caption',
542 callback: captionCommandCallback,574 callback: captionCommandCallback,
543 returns: 'caption',575 returns: 'caption',
544 namedArgumentList: [576 namedArgumentList: [
server.js+3 -0
@@ -610,6 +610,9 @@ app.use('/api/search', require('./src/endpoints/search').router);
610// Ooba/OpenAI text completions610// Ooba/OpenAI text completions
611app.use('/api/backends/text-completions', require('./src/endpoints/backends/text-completions').router);611app.use('/api/backends/text-completions', require('./src/endpoints/backends/text-completions').router);
612612
613// OpenRouter
614app.use('/api/openrouter', require('./src/endpoints/openrouter').router);
615
613// KoboldAI616// KoboldAI
614app.use('/api/backends/kobold', require('./src/endpoints/backends/kobold').router);617app.use('/api/backends/kobold', require('./src/endpoints/backends/kobold').router);
615618
src/endpoints/openrouter.js+32 -0
@@ -0,0 +1,32 @@
1const express = require('express');
2const { jsonParser } = require('../express-common');
3
4const router = express.Router();
5const API_OPENROUTER = 'https://openrouter.ai/api/v1';
6
7router.post('/models/multimodal', jsonParser, async (_req, res) => {
8 try {
9 // The endpoint is available without authentication
10 const response = await fetch(`${API_OPENROUTER}/models`, {
11 method: 'GET',
12 headers: {
13 'Accept': 'application/json',
14 },
15 });
16
17 if (!response.ok) {
18 return res.json([]);
19 }
20
21 const data = await response.json();
22 const models = data?.data || [];
23 const multimodalModels = models.filter(m => m?.architecture?.modality === 'text+image->text').map(m => m.id);
24
25 return res.json(multimodalModels);
26 } catch (error) {
27 console.error(error);
28 return res.sendStatus(500);
29 }
30});
31
32module.exports = { router };