|
|
|
@ -207,6 +207,56 @@ function firstString(value: unknown): string | undefined { |
|
|
|
return undefined; |
|
|
|
} |
|
|
|
|
|
|
|
const NEWAPIWG_TRANSIENT_RETRY_DELAYS_MS = [500, 1500]; |
|
|
|
|
|
|
|
function isNewApiWGTransientError(message: string): boolean { |
|
|
|
return /system\s+disk\s+overloaded/i.test(message); |
|
|
|
} |
|
|
|
|
|
|
|
export function normalizeNewApiWGError(message: string, modelName = "NewApiWG"): string { |
|
|
|
const normalized = message.trim(); |
|
|
|
if (isNewApiWGTransientError(normalized)) { |
|
|
|
return `${modelName}: provider is temporarily overloaded. Please retry in a moment or switch models.`; |
|
|
|
} |
|
|
|
return normalized; |
|
|
|
} |
|
|
|
|
|
|
|
async function wait(ms: number): Promise<void> { |
|
|
|
await new Promise((resolve) => setTimeout(resolve, ms)); |
|
|
|
} |
|
|
|
|
|
|
|
async function fetchNewApiWGJsonWithRetry( |
|
|
|
url: string, |
|
|
|
init: RequestInit, |
|
|
|
modelName: string, |
|
|
|
fallbackError: string |
|
|
|
): Promise<{ response: Response; data: unknown }> { |
|
|
|
for (let attempt = 0; attempt <= NEWAPIWG_TRANSIENT_RETRY_DELAYS_MS.length; attempt++) { |
|
|
|
const response = await fetch(url, init); |
|
|
|
const data = await response.json().catch(() => ({})); |
|
|
|
|
|
|
|
if (response.ok) { |
|
|
|
return { response, data }; |
|
|
|
} |
|
|
|
|
|
|
|
const message = firstString(data) || fallbackError; |
|
|
|
const canRetry = |
|
|
|
attempt < NEWAPIWG_TRANSIENT_RETRY_DELAYS_MS.length && |
|
|
|
isNewApiWGTransientError(message); |
|
|
|
|
|
|
|
if (!canRetry) { |
|
|
|
return { response, data }; |
|
|
|
} |
|
|
|
|
|
|
|
console.warn( |
|
|
|
`[NewApiWG] ${modelName} returned transient overload; retrying attempt ${attempt + 2}` |
|
|
|
); |
|
|
|
await wait(NEWAPIWG_TRANSIENT_RETRY_DELAYS_MS[attempt]); |
|
|
|
} |
|
|
|
|
|
|
|
throw new Error("unreachable"); |
|
|
|
} |
|
|
|
|
|
|
|
async function urlToDataUrl(url: string): Promise<string> { |
|
|
|
const response = await fetch(url); |
|
|
|
if (!response.ok) throw new Error(`Failed to fetch generated media: ${response.status}`); |
|
|
|
@ -419,7 +469,7 @@ async function generateImageViaNewApiWGChat( |
|
|
|
] |
|
|
|
: prompt; |
|
|
|
|
|
|
|
const response = await fetch(`${baseUrl}/chat/completions`, { |
|
|
|
const { response, data } = await fetchNewApiWGJsonWithRetry(`${baseUrl}/chat/completions`, { |
|
|
|
method: "POST", |
|
|
|
headers: { |
|
|
|
"Content-Type": "application/json", |
|
|
|
@ -432,17 +482,18 @@ async function generateImageViaNewApiWGChat( |
|
|
|
...dynamicInputsWithoutPrompt(input), |
|
|
|
...(input.parameters || {}), |
|
|
|
}), |
|
|
|
}); |
|
|
|
}, input.model.name, "NewApiWG chat image API error"); |
|
|
|
|
|
|
|
const data = await response.json().catch(() => ({})); |
|
|
|
if (!response.ok) { |
|
|
|
const message = firstString(data) || `NewApiWG chat image API error: ${response.status}`; |
|
|
|
return { success: false, error: message }; |
|
|
|
return { success: false, error: normalizeNewApiWGError(message, input.model.name) }; |
|
|
|
} |
|
|
|
|
|
|
|
const image = extractNewApiWGImage(data); |
|
|
|
if (!image) { |
|
|
|
const text = data?.choices?.[0]?.message?.content; |
|
|
|
const text = data && typeof data === "object" |
|
|
|
? firstString((data as Record<string, unknown>).choices) |
|
|
|
: undefined; |
|
|
|
return { |
|
|
|
success: false, |
|
|
|
error: typeof text === "string" && text.length > 0 |
|
|
|
@ -529,7 +580,7 @@ async function generateImageViaNewApiWGGemini( |
|
|
|
: "2K"; |
|
|
|
const modelName = toGeminiNativeModelName(input.model.id); |
|
|
|
|
|
|
|
const response = await fetch(`${normalizeNewApiWGGeminiBaseUrl(baseUrl)}/models/${encodeURIComponent(modelName)}:generateContent`, { |
|
|
|
const { response, data } = await fetchNewApiWGJsonWithRetry(`${normalizeNewApiWGGeminiBaseUrl(baseUrl)}/models/${encodeURIComponent(modelName)}:generateContent`, { |
|
|
|
method: "POST", |
|
|
|
headers: { |
|
|
|
"Content-Type": "application/json", |
|
|
|
@ -546,12 +597,11 @@ async function generateImageViaNewApiWGGemini( |
|
|
|
}, |
|
|
|
...dynamicInputsWithoutPrompt(input), |
|
|
|
}), |
|
|
|
}); |
|
|
|
}, input.model.name, "NewApiWG Gemini image API error"); |
|
|
|
|
|
|
|
const data = await response.json().catch(() => ({})); |
|
|
|
if (!response.ok) { |
|
|
|
const message = firstString(data) || `NewApiWG Gemini image API error: ${response.status}`; |
|
|
|
return { success: false, error: message }; |
|
|
|
return { success: false, error: normalizeNewApiWGError(message, input.model.name) }; |
|
|
|
} |
|
|
|
|
|
|
|
const image = extractGeminiNativeImage(data); |
|
|
|
@ -611,7 +661,7 @@ export async function generateWithNewApiWG( |
|
|
|
const endpoint = `${normalizedBaseUrl}/${isVideo ? "video" : "images"}/generations`; |
|
|
|
const imageSize = typeof input.parameters?.size === "string" ? input.parameters.size : "1024x1024"; |
|
|
|
|
|
|
|
const response = await fetch(endpoint, { |
|
|
|
const { response, data } = await fetchNewApiWGJsonWithRetry(endpoint, { |
|
|
|
method: "POST", |
|
|
|
headers: { |
|
|
|
"Content-Type": "application/json", |
|
|
|
@ -626,15 +676,13 @@ export async function generateWithNewApiWG( |
|
|
|
...(!isVideo ? { size: imageSize, response_format: "b64_json" } : {}), |
|
|
|
...(input.parameters || {}), |
|
|
|
}), |
|
|
|
}); |
|
|
|
}, input.model.name, `NewApiWG ${isVideo ? "video" : "image"} API error`); |
|
|
|
|
|
|
|
if (!response.ok) { |
|
|
|
const error = await response.json().catch(() => ({})); |
|
|
|
const message = firstString(error) || `NewApiWG image API error: ${response.status}`; |
|
|
|
return { success: false, error: message }; |
|
|
|
const message = firstString(data) || `NewApiWG image API error: ${response.status}`; |
|
|
|
return { success: false, error: normalizeNewApiWGError(message, input.model.name) }; |
|
|
|
} |
|
|
|
|
|
|
|
const data = await response.json(); |
|
|
|
if (isVideo) { |
|
|
|
const immediateUrl = getNewApiWGVideoUrl(data); |
|
|
|
if (immediateUrl) { |
|
|
|
@ -647,17 +695,23 @@ export async function generateWithNewApiWG( |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const first = data.data?.[0] ?? data.images?.[0] ?? data.videos?.[0] ?? data.output?.[0] ?? data; |
|
|
|
const b64 = first?.b64_json || first?.base64 || first?.image_base64; |
|
|
|
const url = first?.url || first?.image_url || first?.video_url || firstString(first); |
|
|
|
const dataRecord = data && typeof data === "object" ? data as Record<string, unknown> : {}; |
|
|
|
const dataItems = Array.isArray(dataRecord.data) ? dataRecord.data : []; |
|
|
|
const imageItems = Array.isArray(dataRecord.images) ? dataRecord.images : []; |
|
|
|
const videoItems = Array.isArray(dataRecord.videos) ? dataRecord.videos : []; |
|
|
|
const outputItems = Array.isArray(dataRecord.output) ? dataRecord.output : []; |
|
|
|
const first = dataItems[0] ?? imageItems[0] ?? videoItems[0] ?? outputItems[0] ?? data; |
|
|
|
const firstRecord = first && typeof first === "object" ? first as Record<string, unknown> : {}; |
|
|
|
const b64 = firstRecord.b64_json || firstRecord.base64 || firstRecord.image_base64; |
|
|
|
const url = firstRecord.url || firstRecord.image_url || firstRecord.video_url || firstString(first); |
|
|
|
|
|
|
|
if (b64) { |
|
|
|
if (typeof b64 === "string" && b64.length > 0) { |
|
|
|
const mime = isVideo ? "video/mp4" : "image/png"; |
|
|
|
const prefix = b64.startsWith("data:") ? "" : `data:${mime};base64,`; |
|
|
|
return { success: true, outputs: [{ type: isVideo ? "video" : "image", data: `${prefix}${b64}` }] }; |
|
|
|
} |
|
|
|
|
|
|
|
if (url) { |
|
|
|
if (typeof url === "string" && url.length > 0) { |
|
|
|
const dataUrl = url.startsWith("data:") ? url : await urlToDataUrl(url); |
|
|
|
return { success: true, outputs: [{ type: isVideo ? "video" : "image", data: dataUrl, url }] }; |
|
|
|
} |
|
|
|
|