feat: Nyxos Plus as primary image provider, aiguoguo as fallback

- app_settings: AI_IMAGE_BASE_URL → https://plus.nyxos.workers.dev/v1
- app_settings: AI_IMAGE_FALLBACK_BASE_URL/API_KEY → aiguoguo (резерв)
- config/index.js: загружает imageFallbackBaseUrl + imageFallbackApiKey
- covers.js: generateCoverViaImageGenerations пробует Nyxos, при 5xx/timeout
  автоматически переключается на aiguoguo
This commit is contained in:
Ник (Claude)
2026-06-10 09:55:32 +03:00
parent bcb6583883
commit d1e6e2ef4a
2 changed files with 36 additions and 48 deletions
+32 -46
View File
@@ -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');
}
/**