diff --git a/src/config/index.js b/src/config/index.js index ff142ac..9491fb2 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -66,9 +66,11 @@ async function reloadAi() { config.ai.baseUrl = pick('AI_TEXT_BASE_URL', 'AI_BASE_URL', 'https://aiprimetech.io/v1'); config.ai.apiKey = pick('AI_TEXT_API_KEY', 'AI_API_KEY', null); - config.ai.imageBaseUrl= pick('AI_IMAGE_BASE_URL', 'AI_IMAGE_BASE_URL', 'https://api.aiguoguo199.com/v1'); + config.ai.imageBaseUrl= pick('AI_IMAGE_BASE_URL', 'AI_IMAGE_BASE_URL', 'https://plus.nyxos.workers.dev/v1'); config.ai.imageApiKey = pick('AI_IMAGE_API_KEY', 'AI_IMAGE_API_KEY', config.ai.apiKey); - config.ai.imageModel = pick('AI_IMAGE_MODEL', 'AI_MODEL_IMAGE', 'gpt-image-1-mini'); + 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.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'); config.ai.models.article = pick('AI_TEXT_MODEL_ARTICLE', 'AI_MODEL_ARTICLE', 'claude-sonnet-4-6'); diff --git a/src/services/covers.js b/src/services/covers.js index ec982dc..621457c 100644 --- a/src/services/covers.js +++ b/src/services/covers.js @@ -266,56 +266,42 @@ async function generateCoverViaImagesEndpoint({ prompt }) { * Более стабильный чем /responses, поддерживает gpt-image-1-mini/gpt-image-2. */ async function generateCoverViaImageGenerations({ prompt }) { - const model = config.ai.imageModel || 'gpt-image-1-mini'; - const baseUrl = config.ai.imageBaseUrl || config.ai.baseUrl; - const started = Date.now(); - let res; - try { - res = await axios.post( - `${baseUrl}/images/generations`, - { - model, - prompt: prompt.slice(0, 4000), - n: 1, - size: '1024x1024', - response_format: 'url', // рекомендовано провайдером: url быстрее чем b64 (~5MB) - }, - { - headers: { Authorization: `Bearer ${config.ai.imageApiKey}` }, - timeout: 120_000, + const model = config.ai.imageModel || 'gpt-image-2'; + + async function tryProvider(baseUrl, apiKey) { + 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: 120_000 } + ); + const item = res.data?.data?.[0]; + if (!item) throw new Error('No image data in response'); + aiUsage.log({ provider: aiUsage.providerFromBaseUrl(baseUrl), requestType: 'image', model, imageCount: 1, durationMs: Date.now()-started, succeeded: true }).catch(() => {}); + if (item.url) { + const r = await axios.get(item.url, { responseType: 'arraybuffer', timeout: 60_000 }); + return { bytes: Buffer.from(r.data), format: 'png' }; } - ); + if (item.b64_json) return { bytes: Buffer.from(item.b64_json, 'base64'), format: 'png' }; + throw new Error('No url or b64_json in response'); + } catch (err) { + aiUsage.log({ provider: aiUsage.providerFromBaseUrl(baseUrl), requestType: 'image', model, imageCount: 1, durationMs: Date.now()-started, succeeded: false, errorMessage: (err.response?.data?.error?.message || err.message || '').slice(0, 500) }).catch(() => {}); + throw err; + } + } + + // Основной: Nyxos Plus + try { + return await tryProvider(config.ai.imageBaseUrl, config.ai.imageApiKey); } catch (err) { - aiUsage.log({ - provider: aiUsage.providerFromBaseUrl(baseUrl), - requestType: 'image', model, imageCount: 1, - durationMs: Date.now() - started, succeeded: false, - errorMessage: (err.response?.data?.error?.message || err.message || '').slice(0, 500), - }).catch(() => {}); + 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); + } throw err; } - const item = res.data?.data?.[0]; - if (!item) { - aiUsage.log({ - provider: aiUsage.providerFromBaseUrl(baseUrl), - requestType: 'image', model, imageCount: 1, - durationMs: Date.now() - started, succeeded: false, - errorMessage: 'No image data in response', - }).catch(() => {}); - throw new Error('No image data in response'); - } - aiUsage.log({ - provider: aiUsage.providerFromBaseUrl(baseUrl), - requestType: 'image', model, imageCount: 1, - durationMs: Date.now() - started, succeeded: true, - }).catch(() => {}); - // Приоритет: url (быстро) → b64_json (fallback для старых моделей) - if (item.url) { - const r = await axios.get(item.url, { responseType: 'arraybuffer', timeout: 60_000 }); - return { bytes: Buffer.from(r.data), format: 'png' }; - } - if (item.b64_json) return { bytes: Buffer.from(item.b64_json, 'base64'), format: 'png' }; - throw new Error('No url or b64_json in response'); } /**