From 2a61cc08c240eb690dcc490f49cdd5a419902e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=20=28Claude=29?= Date: Thu, 11 Jun 2026 13:13:31 +0300 Subject: [PATCH] feat: RouterAI as 3rd image fallback via /responses + image_generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app_settings: ROUTERAI_BASE_URL, ROUTERAI_API_KEY, ROUTERAI_IMAGE_MODEL - config/index.js: routeraiBaseUrl, routeraiApiKey, routeraiImageModel - covers.js: generateCoverViaRouterAI() через /responses endpoint Цепочка: aiguoguo → Nyxos → RouterAI → local SVG --- src/config/index.js | 3 +++ src/services/covers.js | 42 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/config/index.js b/src/config/index.js index 9491fb2..f1c230d 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -70,6 +70,9 @@ async function reloadAi() { config.ai.imageApiKey = pick('AI_IMAGE_API_KEY', 'AI_IMAGE_API_KEY', config.ai.apiKey); config.ai.imageFallbackBaseUrl = (s['AI_IMAGE_FALLBACK_BASE_URL'] && s['AI_IMAGE_FALLBACK_BASE_URL'].trim()) || 'https://api.aiguoguo199.com/v1'; config.ai.imageFallbackApiKey = (s['AI_IMAGE_FALLBACK_API_KEY'] && s['AI_IMAGE_FALLBACK_API_KEY'].trim()) || config.ai.imageApiKey; + config.ai.routeraiBaseUrl = (s['ROUTERAI_BASE_URL'] || 'https://routerai.ru/api/v1').trim(); + config.ai.routeraiApiKey = (s['ROUTERAI_API_KEY'] || '').trim() || null; + config.ai.routeraiImageModel = (s['ROUTERAI_IMAGE_MODEL'] || 'openai/gpt-5-image-mini').trim(); config.ai.imageModel = pick('AI_IMAGE_MODEL', 'AI_MODEL_IMAGE', 'gpt-image-2'); config.ai.imageModelViaResponses = pick('AI_IMAGE_MODEL_VIA_RESPONSES', 'AI_MODEL_IMAGE_VIA_RESPONSES', 'gpt-5.5'); config.ai.models.post = pick('AI_TEXT_MODEL_POST', 'AI_MODEL_POST', 'claude-haiku-4-5-20251001'); diff --git a/src/services/covers.js b/src/services/covers.js index 512f054..52cd265 100644 --- a/src/services/covers.js +++ b/src/services/covers.js @@ -270,6 +270,35 @@ async function generateCoverViaImagesEndpoint({ prompt }) { * Основной путь — /images/generations (aiguoguo199.com или любой OpenAI-совместимый). * Более стабильный чем /responses, поддерживает gpt-image-1-mini/gpt-image-2. */ +/** + * RouterAI — стабильный провайдер через /responses + image_generation tool. + * Используется как третий fallback когда aiguoguo и Nyxos недоступны. + */ +async function generateCoverViaRouterAI({ prompt }) { + const base = config.ai.routeraiBaseUrl; + const key = config.ai.routeraiApiKey; + const model = config.ai.routeraiImageModel || 'openai/gpt-5-image-mini'; + const started = Date.now(); + try { + const res = await axios.post(`${base}/responses`, { + model, + input: `Use the image_generation tool to create this illustration. Only call the tool, no text.\n\n${prompt.slice(0, 3000)}`, + tools: [{ type: 'image_generation' }], + tool_choice: { type: 'image_generation' }, + }, { + headers: { Authorization: `Bearer ${key}` }, + timeout: 120_000, + }); + const imgCall = (res.data?.output || []).find(o => o.type === 'image_generation_call'); + if (!imgCall?.result) throw new Error('No image in RouterAI response'); + aiUsage.log({ provider: 'routerai', requestType: 'image_via_responses', model, imageCount: 1, durationMs: Date.now()-started, succeeded: true }).catch(() => {}); + return { bytes: Buffer.from(imgCall.result, 'base64'), format: imgCall.output_format || 'png' }; + } catch (err) { + aiUsage.log({ provider: 'routerai', requestType: 'image_via_responses', model, imageCount: 1, durationMs: Date.now()-started, succeeded: false, errorMessage: (err.response?.data?.error?.message || err.message || '').slice(0,500) }).catch(() => {}); + throw err; + } +} + async function generateCoverViaImageGenerations({ prompt }) { const model = config.ai.imageModel || 'gpt-image-2'; @@ -302,8 +331,17 @@ async function generateCoverViaImageGenerations({ prompt }) { } catch (err) { const status = err.response?.status; if (!status || status >= 500) { - console.warn(`[Cover] primary failed (${status||'timeout'}), trying fallback aiguoguo...`); - return await tryProvider(config.ai.imageFallbackBaseUrl, config.ai.imageFallbackApiKey); + console.warn(`[Cover] primary failed (${status||'timeout'}), trying Nyxos fallback...`); + try { + return await tryProvider(config.ai.imageFallbackBaseUrl, config.ai.imageFallbackApiKey); + } catch (err2) { + const status2 = err2.response?.status; + if ((!status2 || status2 >= 500) && config.ai.routeraiApiKey) { + console.warn(`[Cover] Nyxos failed (${status2||'timeout'}), trying RouterAI /responses...`); + return await generateCoverViaRouterAI({ prompt }); + } + throw err2; + } } throw err; }