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
+4 -2
View File
@@ -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');
+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');
}
/**