Auto-add OpenRouter captioning models

6393eda81ee991290ac05521ed59dbf2fa8825eb

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

3 files changed, +89 -22Showing whitespace changes
public/scripts/extensions/caption/index.js+54 -22
@@ -1,6 +1,6 @@
11import { ensureImageFormatSupported, getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js';
22import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';
33import { appendMediaToMessage, callPopup, eventSource, event_types, getRequestHeaders, main_api, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';
44import { getMessageTimeStamp } from '../../RossAscends-mods.js';
55import { SECRET_KEYS, secret_state } from '../../secrets.js';
66import { getMultimodalCaption } from '../shared.js';
@@ -431,14 +431,9 @@ jQuery(async function () {
431431 $('#form_sheld').append(imgForm);
432432 $('#img_file').on('change', (e) => onSelectImage(e.originalEvent, '', false));
433433 }
434434 async function switchMultimodalBlocks() {
435+ await addOpenRouterModels();
435436 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- });
442437 $('#caption_multimodal_block').toggle(isMultimodal);
443438 $('#caption_prompt_block').toggle(isMultimodal);
444439 $('#caption_multimodal_api').val(extension_settings.caption.multimodal_api);
@@ -448,30 +443,48 @@ jQuery(async function () {
448443 const types = type.split(',');
449444 $(this).toggle(types.includes(extension_settings.caption.multimodal_api));
450445 });
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- });
463446 }
464447 async function addSettings() {
465448 const html = await renderExtensionTemplateAsync('caption', 'settings', { TEMPLATE_DEFAULT, PROMPT_DEFAULT });
466449 $('#caption_container').append(html);
467450 }
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
469482 await addSettings();
470483 addPictureSendForm();
471484 addSendPictureButton();
472485 setImageIcon();
473486 migrateSettings();
474487 await switchMultimodalBlocks();
475488
476489 $('#caption_refine_mode').prop('checked', !!(extension_settings.caption.refine_mode));
477490 $('#caption_allow_reverse_proxy').prop('checked', !!(extension_settings.caption.allow_reverse_proxy));
@@ -506,6 +519,24 @@ jQuery(async function () {
506519 extension_settings.caption.auto_mode = !!$('#caption_auto_mode').prop('checked');
507520 saveSettingsDebounced();
508521 });
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
510541 const onMessageEvent = async (index) => {
511542 if (!extension_settings.caption.auto_mode) {
@@ -538,7 +569,8 @@ jQuery(async function () {
538569 }
539570 });
540571
541572 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'caption',
573+ name: 'caption',
542574 callback: captionCommandCallback,
543575 returns: 'caption',
544576 namedArgumentList: [
server.js+3 -0
@@ -610,6 +610,9 @@ app.use('/api/search', require('./src/endpoints/search').router);
610610// Ooba/OpenAI text completions
611611app.use('/api/backends/text-completions', require('./src/endpoints/backends/text-completions').router);
612612
613+// OpenRouter
614+app.use('/api/openrouter', require('./src/endpoints/openrouter').router);
615+
613616// KoboldAI
614617app.use('/api/backends/kobold', require('./src/endpoints/backends/kobold').router);
615618
src/endpoints/openrouter.js+32 -0
@@ -0,0 +1,32 @@
1+const express = require('express');
2+const { jsonParser } = require('../express-common');
3+
4+const router = express.Router();
5+const API_OPENROUTER = 'https://openrouter.ai/api/v1';
6+
7+router.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+
32+module.exports = { router };