Audio inlining for OpenAI and Custom-compatible (#4964) * Audio inlining for OpenAI and Custom-compatible * Add context sizes * chatgpt-image-latest * Add quality control for gpt-image
Signed| @@ -2024,7 +2024,7 @@ | |||
| 2024 | <i class="icon-supported fa-solid fa-film" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i> | 2024 | <i class="icon-supported fa-solid fa-film" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i> |
| 2025 | <i class="icon-unsupported fa-solid fa-film" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i> | 2025 | <i class="icon-unsupported fa-solid fa-film" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i> |
| 2026 | </div> | 2026 | </div> |
| 2027 | <div id="openai_audio_inlining_supported" data-source="makersuite,vertexai,openrouter"> | 2027 | <div id="openai_audio_inlining_supported" data-source="makersuite,vertexai,openrouter,openai,custom"> |
| 2028 | <i class="icon-supported fa-solid fa-music" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i> | 2028 | <i class="icon-supported fa-solid fa-music" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i> |
| 2029 | <i class="icon-unsupported fa-solid fa-music" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i> | 2029 | <i class="icon-unsupported fa-solid fa-music" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i> |
| 2030 | </div> | 2030 | </div> |
| @@ -320,6 +320,7 @@ const defaultSettings = { | |||
| 320 | // OpenAI settings | 320 | // OpenAI settings |
| 321 | openai_style: 'vivid', | 321 | openai_style: 'vivid', |
| 322 | openai_quality: 'standard', | 322 | openai_quality: 'standard', |
| 323 | openai_quality_gpt: 'auto', | ||
| 323 | openai_duration: '8', | 324 | openai_duration: '8', |
| 324 | 325 | ||
| 325 | style: 'Default', | 326 | style: 'Default', |
| @@ -528,6 +529,7 @@ async function loadSettings() { | |||
| 528 | $('#sd_interactive_mode').prop('checked', extension_settings.sd.interactive_mode); | 529 | $('#sd_interactive_mode').prop('checked', extension_settings.sd.interactive_mode); |
| 529 | $('#sd_openai_style').val(extension_settings.sd.openai_style); | 530 | $('#sd_openai_style').val(extension_settings.sd.openai_style); |
| 530 | $('#sd_openai_quality').val(extension_settings.sd.openai_quality); | 531 | $('#sd_openai_quality').val(extension_settings.sd.openai_quality); |
| 532 | $('#sd_openai_quality_gpt').val(extension_settings.sd.openai_quality_gpt); | ||
| 531 | $('#sd_openai_duration').val(extension_settings.sd.openai_duration); | 533 | $('#sd_openai_duration').val(extension_settings.sd.openai_duration); |
| 532 | $('#sd_comfy_type').val(extension_settings.sd.comfy_type); | 534 | $('#sd_comfy_type').val(extension_settings.sd.comfy_type); |
| 533 | $('#sd_comfy_url').val(extension_settings.sd.comfy_url); | 535 | $('#sd_comfy_url').val(extension_settings.sd.comfy_url); |
| @@ -1850,7 +1852,7 @@ function switchModelSpecificControls(modelId) { | |||
| 1850 | 1852 | ||
| 1851 | modelControls.each(function () { | 1853 | modelControls.each(function () { |
| 1852 | const models = String($(this).attr('data-sd-model') || '').split(',').map(m => m.trim()); | 1854 | const models = String($(this).attr('data-sd-model') || '').split(',').map(m => m.trim()); |
| 1853 | $(this).toggle(models.includes(modelId)); | 1855 | $(this).toggle(models.some(m => modelId.includes(m))); |
| 1854 | }); | 1856 | }); |
| 1855 | } | 1857 | } |
| 1856 | 1858 | ||
| @@ -2169,6 +2171,7 @@ async function loadOpenAiModels() { | |||
| 2169 | { value: 'gpt-image-1.5', text: 'gpt-image-1.5' }, | 2171 | { value: 'gpt-image-1.5', text: 'gpt-image-1.5' }, |
| 2170 | { value: 'gpt-image-1-mini', text: 'gpt-image-1-mini' }, | 2172 | { value: 'gpt-image-1-mini', text: 'gpt-image-1-mini' }, |
| 2171 | { value: 'gpt-image-1', text: 'gpt-image-1' }, | 2173 | { value: 'gpt-image-1', text: 'gpt-image-1' }, |
| 2174 | { value: 'chatgpt-image-latest', text: 'chatgpt-image-latest' }, | ||
| 2172 | { value: 'dall-e-3', text: 'dall-e-3' }, | 2175 | { value: 'dall-e-3', text: 'dall-e-3' }, |
| 2173 | { value: 'dall-e-2', text: 'dall-e-2' }, | 2176 | { value: 'dall-e-2', text: 'dall-e-2' }, |
| 2174 | { value: 'sora-2', text: 'sora-2' }, | 2177 | { value: 'sora-2', text: 'sora-2' }, |
| @@ -3697,7 +3700,7 @@ async function generateOpenAiImage(prompt, signal) { | |||
| 3697 | 3700 | ||
| 3698 | const isDalle2 = /dall-e-2/.test(extension_settings.sd.model); | 3701 | const isDalle2 = /dall-e-2/.test(extension_settings.sd.model); |
| 3699 | const isDalle3 = /dall-e-3/.test(extension_settings.sd.model); | 3702 | const isDalle3 = /dall-e-3/.test(extension_settings.sd.model); |
| 3700 | const isGptImg = /gpt-image-1/.test(extension_settings.sd.model); | 3703 | const isGptImg = /gpt-image-(1|latest)/.test(extension_settings.sd.model); |
| 3701 | const isSora2 = /sora-2/.test(extension_settings.sd.model); | 3704 | const isSora2 = /sora-2/.test(extension_settings.sd.model); |
| 3702 | 3705 | ||
| 3703 | if (isDalle2 && prompt.length > dalle2PromptLimit) { | 3706 | if (isDalle2 && prompt.length > dalle2PromptLimit) { |
| @@ -3770,7 +3773,7 @@ async function generateOpenAiImage(prompt, signal) { | |||
| 3770 | model: extension_settings.sd.model, | 3773 | model: extension_settings.sd.model, |
| 3771 | size: `${width}x${height}`, | 3774 | size: `${width}x${height}`, |
| 3772 | n: 1, | 3775 | n: 1, |
| 3773 | quality: isDalle3 ? extension_settings.sd.openai_quality : undefined, | 3776 | quality: isDalle3 ? extension_settings.sd.openai_quality : (isGptImg ? extension_settings.sd.openai_quality_gpt : undefined), |
| 3774 | style: isDalle3 ? extension_settings.sd.openai_style : undefined, | 3777 | style: isDalle3 ? extension_settings.sd.openai_style : undefined, |
| 3775 | response_format: isDalle2 || isDalle3 ? 'b64_json' : undefined, | 3778 | response_format: isDalle2 || isDalle3 ? 'b64_json' : undefined, |
| 3776 | moderation: isGptImg ? 'low' : undefined, | 3779 | moderation: isGptImg ? 'low' : undefined, |
| @@ -5309,6 +5312,10 @@ jQuery(async () => { | |||
| 5309 | extension_settings.sd.electronhub_quality = String($(this).val()); | 5312 | extension_settings.sd.electronhub_quality = String($(this).val()); |
| 5310 | saveSettingsDebounced(); | 5313 | saveSettingsDebounced(); |
| 5311 | }); | 5314 | }); |
| 5315 | $('#sd_openai_quality_gpt').on('input', function () { | ||
| 5316 | extension_settings.sd.openai_quality_gpt = String($(this).val()); | ||
| 5317 | saveSettingsDebounced(); | ||
| 5318 | }); | ||
| 5312 | 5319 | ||
| 5313 | if (!CSS.supports('field-sizing', 'content')) { | 5320 | if (!CSS.supports('field-sizing', 'content')) { |
| 5314 | $('.sd_settings .inline-drawer-toggle').on('click', function () { | 5321 | $('.sd_settings .inline-drawer-toggle').on('click', function () { |
| @@ -178,7 +178,16 @@ | |||
| 178 | <option value="natural">Natural</option> | 178 | <option value="natural">Natural</option> |
| 179 | </select> | 179 | </select> |
| 180 | </div> | 180 | </div> |
| 181 | <div data-sd-model="dall-e-3,cogview-4-250304" class="flex1"> | 181 | <div data-sd-model="gpt-image" class="flex1"> |
| 182 | <label for="sd_openai_quality_gpt" data-i18n="Image Quality">Image Quality</label> | ||
| 183 | <select id="sd_openai_quality_gpt"> | ||
| 184 | <option value="auto" data-i18n="Auto">Auto</option> | ||
| 185 | <option value="low" data-i18n="Low">Low</option> | ||
| 186 | <option value="medium" data-i18n="Medium">Medium</option> | ||
| 187 | <option value="high" data-i18n="High">High</option> | ||
| 188 | </select> | ||
| 189 | </div> | ||
| 190 | <div data-sd-model="dall-e-3,cogview-4" class="flex1"> | ||
| 182 | <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label> | 191 | <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label> |
| 183 | <select id="sd_openai_quality"> | 192 | <select id="sd_openai_quality"> |
| 184 | <option value="standard" data-i18n="Standard">Standard</option> | 193 | <option value="standard" data-i18n="Standard">Standard</option> |
| @@ -4689,6 +4689,9 @@ function getMaxContextOpenAI(value) { | |||
| 4689 | else if (value.includes('gpt-4.1')) { | 4689 | else if (value.includes('gpt-4.1')) { |
| 4690 | return max_1mil; | 4690 | return max_1mil; |
| 4691 | } | 4691 | } |
| 4692 | else if (value.includes('gpt-audio')) { | ||
| 4693 | return max_128k; | ||
| 4694 | } | ||
| 4692 | else if (value.startsWith('o1')) { | 4695 | else if (value.startsWith('o1')) { |
| 4693 | return max_128k; | 4696 | return max_128k; |
| 4694 | } | 4697 | } |
| @@ -4707,6 +4710,9 @@ function getMaxContextOpenAI(value) { | |||
| 4707 | else if (['gpt-4-32k', 'gpt-4-32k-0314', 'gpt-4-32k-0613'].includes(value)) { | 4710 | else if (['gpt-4-32k', 'gpt-4-32k-0314', 'gpt-4-32k-0613'].includes(value)) { |
| 4708 | return max_32k; | 4711 | return max_32k; |
| 4709 | } | 4712 | } |
| 4713 | else if (value.includes('gpt-realtime')) { | ||
| 4714 | return max_32k; | ||
| 4715 | } | ||
| 4710 | else if (['gpt-3.5-turbo-16k', 'gpt-3.5-turbo-16k-0613'].includes(value)) { | 4716 | else if (['gpt-3.5-turbo-16k', 'gpt-3.5-turbo-16k-0613'].includes(value)) { |
| 4711 | return max_16k; | 4717 | return max_16k; |
| 4712 | } | 4718 | } |
| @@ -5946,21 +5952,30 @@ export function isAudioInliningSupported() { | |||
| 5946 | return false; | 5952 | return false; |
| 5947 | } | 5953 | } |
| 5948 | 5954 | ||
| 5949 | // Only Gemini models support audio for now | ||
| 5950 | const audioSupportedModels = [ | 5955 | const audioSupportedModels = [ |
| 5951 | 'gemini-2.0', | 5956 | 'gemini-2.0', |
| 5952 | 'gemini-2.5', | 5957 | 'gemini-2.5', |
| 5953 | 'gemini-3', | 5958 | 'gemini-3', |
| 5954 | 'gemini-exp-1206', | 5959 | 'gemini-exp-1206', |
| 5960 | 'gpt-4o-audio', | ||
| 5961 | 'gpt-4o-realtime', | ||
| 5962 | 'gpt-4o-mini-audio', | ||
| 5963 | 'gpt-4o-mini-realtime', | ||
| 5964 | 'gpt-audio', | ||
| 5965 | 'gpt-realtime', | ||
| 5955 | ]; | 5966 | ]; |
| 5956 | 5967 | ||
| 5957 | switch (oai_settings.chat_completion_source) { | 5968 | switch (oai_settings.chat_completion_source) { |
| 5969 | case chat_completion_sources.OPENAI: | ||
| 5970 | return audioSupportedModels.some(model => oai_settings.openai_model.includes(model)); | ||
| 5958 | case chat_completion_sources.MAKERSUITE: | 5971 | case chat_completion_sources.MAKERSUITE: |
| 5959 | return audioSupportedModels.some(model => oai_settings.google_model.includes(model)); | 5972 | return audioSupportedModels.some(model => oai_settings.google_model.includes(model)); |
| 5960 | case chat_completion_sources.VERTEXAI: | 5973 | case chat_completion_sources.VERTEXAI: |
| 5961 | return audioSupportedModels.some(model => oai_settings.vertexai_model.includes(model)); | 5974 | return audioSupportedModels.some(model => oai_settings.vertexai_model.includes(model)); |
| 5962 | case chat_completion_sources.OPENROUTER: | 5975 | case chat_completion_sources.OPENROUTER: |
| 5963 | return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.openrouter_model)?.architecture?.input_modalities?.includes('audio')); | 5976 | return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.openrouter_model)?.architecture?.input_modalities?.includes('audio')); |
| 5977 | case chat_completion_sources.CUSTOM: | ||
| 5978 | return true; | ||
| 5964 | default: | 5979 | default: |
| 5965 | return false; | 5980 | return false; |
| 5966 | } | 5981 | } |
| @@ -2063,6 +2063,8 @@ router.post('/generate', async function (request, response) { | |||
| 2063 | if (getConfigValue('openai.randomizeUserId', false, 'boolean')) { | 2063 | if (getConfigValue('openai.randomizeUserId', false, 'boolean')) { |
| 2064 | bodyParams['user'] = uuidv4(); | 2064 | bodyParams['user'] = uuidv4(); |
| 2065 | } | 2065 | } |
| 2066 | |||
| 2067 | embedOpenRouterMedia(request.body.messages, { audio: true, video: false }); | ||
| 2066 | } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) { | 2068 | } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) { |
| 2067 | apiUrl = 'https://openrouter.ai/api/v1'; | 2069 | apiUrl = 'https://openrouter.ai/api/v1'; |
| 2068 | apiKey = readSecret(request.user.directories, SECRET_KEYS.OPENROUTER); | 2070 | apiKey = readSecret(request.user.directories, SECRET_KEYS.OPENROUTER); |
| @@ -2122,7 +2124,7 @@ router.post('/generate', async function (request, response) { | |||
| 2122 | const enableGeminiSystemPromptCache = getConfigValue('gemini.enableSystemPromptCache', false, 'boolean'); | 2124 | const enableGeminiSystemPromptCache = getConfigValue('gemini.enableSystemPromptCache', false, 'boolean'); |
| 2123 | 2125 | ||
| 2124 | if (Array.isArray(request.body.messages)) { | 2126 | if (Array.isArray(request.body.messages)) { |
| 2125 | embedOpenRouterMedia(request.body.messages); | 2127 | embedOpenRouterMedia(request.body.messages, { audio: true, video: true }); |
| 2126 | addOpenRouterSignatures(request.body.messages, request.body.model); | 2128 | addOpenRouterSignatures(request.body.messages, request.body.model); |
| 2127 | 2129 | ||
| 2128 | if (isClaude) { | 2130 | if (isClaude) { |
| @@ -2160,6 +2162,7 @@ router.post('/generate', async function (request, response) { | |||
| 2160 | 2162 | ||
| 2161 | mergeObjectWithYaml(bodyParams, request.body.custom_include_body); | 2163 | mergeObjectWithYaml(bodyParams, request.body.custom_include_body); |
| 2162 | mergeObjectWithYaml(headers, request.body.custom_include_headers); | 2164 | mergeObjectWithYaml(headers, request.body.custom_include_headers); |
| 2165 | embedOpenRouterMedia(request.body.messages, { audio: true, video: false }); | ||
| 2163 | } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.PERPLEXITY) { | 2166 | } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.PERPLEXITY) { |
| 2164 | apiUrl = API_PERPLEXITY; | 2167 | apiUrl = API_PERPLEXITY; |
| 2165 | apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY); | 2168 | apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY); |
| @@ -1296,10 +1296,14 @@ export function calculateGoogleBudgetTokens(maxTokens, reasoningEffort, model) { | |||
| 1296 | } | 1296 | } |
| 1297 | 1297 | ||
| 1298 | /** | 1298 | /** |
| 1299 | * Embed media content in OpenRouter messages. | 1299 | * Embed media content in OpenRouter messages (OpenAI-compatible). |
| 1300 | * @param {object[]} messages Array of messages | 1300 | * @param {object[]} messages Array of messages |
| 1301 | * @param {object} options Options for embedding | ||
| 1302 | * @param {boolean} [options.audio] Enable audio embedding (default: true) | ||
| 1303 | * @param {boolean} [options.video] Enable video embedding (default: true) | ||
| 1304 | * @returns {void} | ||
| 1301 | */ | 1305 | */ |
| 1302 | export function embedOpenRouterMedia(messages) { | 1306 | export function embedOpenRouterMedia(messages, { audio = true, video = true } = { audio: true, video: true }) { |
| 1303 | if (!Array.isArray(messages)) { | 1307 | if (!Array.isArray(messages)) { |
| 1304 | return; | 1308 | return; |
| 1305 | } | 1309 | } |
| @@ -1310,11 +1314,11 @@ export function embedOpenRouterMedia(messages) { | |||
| 1310 | } | 1314 | } |
| 1311 | 1315 | ||
| 1312 | for (const contentPart of message.content) { | 1316 | for (const contentPart of message.content) { |
| 1313 | if (contentPart?.type === 'video_url' && contentPart.video_url?.url?.startsWith('data:')) { | 1317 | if (video && contentPart?.type === 'video_url' && contentPart.video_url?.url?.startsWith('data:')) { |
| 1314 | contentPart.type = 'input_video'; | 1318 | contentPart.type = 'input_video'; |
| 1315 | } | 1319 | } |
| 1316 | 1320 | ||
| 1317 | if (contentPart?.type === 'audio_url' && contentPart.audio_url?.url?.startsWith('data:')) { | 1321 | if (audio && contentPart?.type === 'audio_url' && contentPart.audio_url?.url?.startsWith('data:')) { |
| 1318 | const formatMap = { | 1322 | const formatMap = { |
| 1319 | 'audio/mpeg': 'mp3', | 1323 | 'audio/mpeg': 'mp3', |
| 1320 | 'audio/wav': 'wav', | 1324 | 'audio/wav': 'wav', |