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:
+4
-2
@@ -66,9 +66,11 @@ async function reloadAi() {
|
|||||||
|
|
||||||
config.ai.baseUrl = pick('AI_TEXT_BASE_URL', 'AI_BASE_URL', 'https://aiprimetech.io/v1');
|
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.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.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.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.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');
|
config.ai.models.article = pick('AI_TEXT_MODEL_ARTICLE', 'AI_MODEL_ARTICLE', 'claude-sonnet-4-6');
|
||||||
|
|||||||
+32
-46
@@ -266,56 +266,42 @@ async function generateCoverViaImagesEndpoint({ prompt }) {
|
|||||||
* Более стабильный чем /responses, поддерживает gpt-image-1-mini/gpt-image-2.
|
* Более стабильный чем /responses, поддерживает gpt-image-1-mini/gpt-image-2.
|
||||||
*/
|
*/
|
||||||
async function generateCoverViaImageGenerations({ prompt }) {
|
async function generateCoverViaImageGenerations({ prompt }) {
|
||||||
const model = config.ai.imageModel || 'gpt-image-1-mini';
|
const model = config.ai.imageModel || 'gpt-image-2';
|
||||||
const baseUrl = config.ai.imageBaseUrl || config.ai.baseUrl;
|
|
||||||
const started = Date.now();
|
async function tryProvider(baseUrl, apiKey) {
|
||||||
let res;
|
const started = Date.now();
|
||||||
try {
|
try {
|
||||||
res = await axios.post(
|
const res = await axios.post(
|
||||||
`${baseUrl}/images/generations`,
|
`${baseUrl}/images/generations`,
|
||||||
{
|
{ model, prompt: prompt.slice(0, 4000), n: 1, size: '1024x1024', response_format: 'url' },
|
||||||
model,
|
{ headers: { Authorization: `Bearer ${apiKey}` }, timeout: 120_000 }
|
||||||
prompt: prompt.slice(0, 4000),
|
);
|
||||||
n: 1,
|
const item = res.data?.data?.[0];
|
||||||
size: '1024x1024',
|
if (!item) throw new Error('No image data in response');
|
||||||
response_format: 'url', // рекомендовано провайдером: url быстрее чем b64 (~5MB)
|
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 });
|
||||||
headers: { Authorization: `Bearer ${config.ai.imageApiKey}` },
|
return { bytes: Buffer.from(r.data), format: 'png' };
|
||||||
timeout: 120_000,
|
|
||||||
}
|
}
|
||||||
);
|
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) {
|
} catch (err) {
|
||||||
aiUsage.log({
|
const status = err.response?.status;
|
||||||
provider: aiUsage.providerFromBaseUrl(baseUrl),
|
if (!status || status >= 500) {
|
||||||
requestType: 'image', model, imageCount: 1,
|
console.warn(`[Cover] primary failed (${status||'timeout'}), trying fallback aiguoguo...`);
|
||||||
durationMs: Date.now() - started, succeeded: false,
|
return await tryProvider(config.ai.imageFallbackBaseUrl, config.ai.imageFallbackApiKey);
|
||||||
errorMessage: (err.response?.data?.error?.message || err.message || '').slice(0, 500),
|
}
|
||||||
}).catch(() => {});
|
|
||||||
throw err;
|
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user