diff --git a/src/services/covers.js b/src/services/covers.js index 52cd265..8a85156 100644 --- a/src/services/covers.js +++ b/src/services/covers.js @@ -325,23 +325,14 @@ async function generateCoverViaImageGenerations({ prompt }) { } } - // Основной: Nyxos Plus + // Основной: RouterAI /responses (стабильный) try { - return await tryProvider(config.ai.imageBaseUrl, config.ai.imageApiKey); + return await generateCoverViaRouterAI({ prompt }); } catch (err) { const status = err.response?.status; if (!status || status >= 500) { - 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; - } + console.warn(`[Cover] RouterAI failed (${status||'timeout'}), trying Nyxos fallback...`); + return await tryProvider(config.ai.imageFallbackBaseUrl, config.ai.imageFallbackApiKey); } throw err; } diff --git a/src/services/postImages.js b/src/services/postImages.js index c6f4a86..d2465d9 100644 --- a/src/services/postImages.js +++ b/src/services/postImages.js @@ -85,41 +85,56 @@ ${style.image_prompt_instructions ? `\nChannel visual guidelines: ${style.image_ Composition: 16:9 wide format, balanced, suitable for social media. Strictly: no text, no letters, no logos, no faces of real people.`; - // Используем Nyxos /images/generations (первичный провайдер) - // с fallback на aiguoguo — тот же путь что для обложек статей - const model = config.ai.imageModel || 'gpt-image-2'; + // RouterAI /responses (primary) → Nyxos /images/generations (fallback) + const model = config.ai.routeraiImageModel || 'openai/gpt-5-image-mini'; - async function tryProvider(baseUrl, apiKey) { + async function tryRouterAI() { const started = Date.now(); try { - const res = await axios.post( - `${baseUrl}/images/generations`, - { model, prompt: prompt.slice(0, 4000), n: 1, size: '1024x1024', response_format: 'url' }, - { headers: { Authorization: `Bearer ${apiKey}` }, timeout: 45_000 } + const res = await axios.post(`${config.ai.routeraiBaseUrl}/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 ${config.ai.routeraiApiKey}` }, 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, meta: { channel_id: channel.id }, durationMs: Date.now()-started, succeeded: true }).catch(() => {}); + return Buffer.from(imgCall.result, 'base64'); + } catch (err) { + aiUsage.log({ provider: 'routerai', requestType: 'image_via_responses', model, imageCount: 1, meta: { channel_id: channel.id }, durationMs: Date.now()-started, succeeded: false, errorMessage: (err.response?.data?.error?.message || err.message || '').slice(0, 500) }).catch(() => {}); + throw err; + } + } + + async function tryNyxos() { + const m = config.ai.imageModel || 'gpt-image-2'; + const started = Date.now(); + try { + const res = await axios.post(`${config.ai.imageFallbackBaseUrl}/images/generations`, + { model: m, prompt: prompt.slice(0, 4000), n: 1, size: '1024x1024', response_format: 'url' }, + { headers: { Authorization: `Bearer ${config.ai.imageFallbackApiKey}` }, timeout: 90_000 } ); const item = res.data?.data?.[0]; if (!item) throw new Error('No image data'); - aiUsage.log({ provider: aiUsage.providerFromBaseUrl(baseUrl), requestType: 'image', model, imageCount: 1, meta: { channel_id: channel.id }, succeeded: true }).catch(() => {}); - if (item.url) { - const r = await axios.get(item.url, { responseType: 'arraybuffer', timeout: 60_000 }); - return Buffer.from(r.data); - } + aiUsage.log({ provider: 'nyxos', requestType: 'image', model: m, imageCount: 1, meta: { channel_id: channel.id }, succeeded: true }).catch(() => {}); + if (item.url) { const r = await axios.get(item.url, { responseType: 'arraybuffer', timeout: 60_000 }); return Buffer.from(r.data); } if (item.b64_json) return Buffer.from(item.b64_json, 'base64'); throw new Error('No url or b64_json'); } catch (err) { - aiUsage.log({ provider: aiUsage.providerFromBaseUrl(baseUrl), requestType: 'image', model, imageCount: 1, meta: { channel_id: channel.id }, succeeded: false, errorMessage: (err.response?.data?.error?.message || err.message || '').slice(0, 500) }).catch(() => {}); + aiUsage.log({ provider: 'nyxos', requestType: 'image', model: m, imageCount: 1, meta: { channel_id: channel.id }, succeeded: false, errorMessage: (err.response?.data?.error?.message || err.message || '').slice(0, 500) }).catch(() => {}); throw err; } } let bytes; try { - bytes = await tryProvider(config.ai.imageBaseUrl, config.ai.imageApiKey); + bytes = await tryRouterAI(); } catch (err) { const status = err.response?.status; if (!status || status >= 500) { - console.warn('[postImages] primary failed, trying fallback...'); - bytes = await tryProvider(config.ai.imageFallbackBaseUrl, config.ai.imageFallbackApiKey); + console.warn('[postImages] RouterAI failed, trying Nyxos fallback...'); + bytes = await tryNyxos(); } else { throw err; } }