mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
Enhance image and embedding provider support
- Added new image models for GPT 5.2, 5.3, and 5.4, including capabilities for text-to-image and editing. - Updated embedding handling to include optional dimensions in requests. - Introduced support for custom embedding providers, allowing dynamic fetching and validation of custom nodes. - Improved image generation handling with Codex integration, including progress tracking and error handling. - Enhanced UI components to support adding custom embeddings and displaying their status.
This commit is contained in:
@@ -10,7 +10,6 @@ import { getModelInfo } from "../services/model.js";
|
||||
import { handleImageGenerationCore } from "open-sse/handlers/imageGenerationCore.js";
|
||||
import { errorResponse, unavailableResponse } from "open-sse/utils/error.js";
|
||||
import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js";
|
||||
import * as log from "../utils/logger.js";
|
||||
import { updateProviderCredentials, checkAndRefreshToken } from "../services/tokenRefresh.js";
|
||||
|
||||
// Providers that don't require credentials (noAuth)
|
||||
@@ -25,66 +24,35 @@ export async function handleImageGeneration(request) {
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
log.warn("IMAGE", "Invalid JSON body");
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body");
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const preferredConnectionId = request.headers.get("x-connection-id") || null;
|
||||
const wantsStream = (request.headers.get("accept") || "").includes("text/event-stream");
|
||||
const modelStr = body.model;
|
||||
|
||||
log.request("POST", `${url.pathname} | ${modelStr}`);
|
||||
|
||||
const apiKey = extractApiKey(request);
|
||||
if (apiKey) {
|
||||
log.debug("AUTH", `API Key: ${log.maskKey(apiKey)}`);
|
||||
} else {
|
||||
log.debug("AUTH", "No API key provided (local mode)");
|
||||
}
|
||||
|
||||
const settings = await getSettings();
|
||||
if (settings.requireApiKey) {
|
||||
if (!apiKey) {
|
||||
log.warn("AUTH", "Missing API key (requireApiKey=true)");
|
||||
return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key");
|
||||
}
|
||||
if (!apiKey) return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key");
|
||||
const valid = await isValidApiKey(apiKey);
|
||||
if (!valid) {
|
||||
log.warn("AUTH", "Invalid API key (requireApiKey=true)");
|
||||
return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key");
|
||||
}
|
||||
if (!valid) return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key");
|
||||
}
|
||||
|
||||
if (!modelStr) {
|
||||
log.warn("IMAGE", "Missing model");
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing model");
|
||||
}
|
||||
|
||||
if (!body.prompt) {
|
||||
log.warn("IMAGE", "Missing prompt");
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing required field: prompt");
|
||||
}
|
||||
if (!modelStr) return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing model");
|
||||
if (!body.prompt) return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing required field: prompt");
|
||||
|
||||
const modelInfo = await getModelInfo(modelStr);
|
||||
if (!modelInfo.provider) {
|
||||
log.warn("IMAGE", "Invalid model format", { model: modelStr });
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid model format");
|
||||
}
|
||||
if (!modelInfo.provider) return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid model format");
|
||||
|
||||
const { provider, model } = modelInfo;
|
||||
|
||||
if (modelStr !== `${provider}/${model}`) {
|
||||
log.info("ROUTING", `${modelStr} → ${provider}/${model}`);
|
||||
} else {
|
||||
log.info("ROUTING", `Provider: ${provider}, Model: ${model}`);
|
||||
}
|
||||
|
||||
// noAuth providers — no credential needed
|
||||
if (NO_AUTH_PROVIDERS.has(provider)) {
|
||||
const result = await handleImageGenerationCore({
|
||||
body,
|
||||
modelInfo: { provider, model },
|
||||
credentials: null,
|
||||
log,
|
||||
});
|
||||
if (result.success) return result.response;
|
||||
return errorResponse(result.status || HTTP_STATUS.BAD_GATEWAY, result.error || "Image generation failed");
|
||||
@@ -96,32 +64,27 @@ export async function handleImageGeneration(request) {
|
||||
let lastStatus = null;
|
||||
|
||||
while (true) {
|
||||
const credentials = await getProviderCredentials(provider, excludeConnectionIds, model);
|
||||
const credentials = await getProviderCredentials(provider, excludeConnectionIds, model, { preferredConnectionId });
|
||||
|
||||
if (!credentials || credentials.allRateLimited) {
|
||||
if (credentials?.allRateLimited) {
|
||||
const errorMsg = lastError || credentials.lastError || "Unavailable";
|
||||
const status = lastStatus || Number(credentials.lastErrorCode) || HTTP_STATUS.SERVICE_UNAVAILABLE;
|
||||
log.warn("IMAGE", `[${provider}/${model}] ${errorMsg} (${credentials.retryAfterHuman})`);
|
||||
return unavailableResponse(status, `[${provider}/${model}] ${errorMsg}`, credentials.retryAfter, credentials.retryAfterHuman);
|
||||
}
|
||||
if (excludeConnectionIds.size === 0) {
|
||||
log.error("AUTH", `No credentials for provider: ${provider}`);
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, `No credentials for provider: ${provider}`);
|
||||
}
|
||||
log.warn("IMAGE", "No more accounts available", { provider });
|
||||
return errorResponse(lastStatus || HTTP_STATUS.SERVICE_UNAVAILABLE, lastError || "All accounts unavailable");
|
||||
}
|
||||
|
||||
log.info("AUTH", `\x1b[32mUsing ${provider} account: ${credentials.connectionName}\x1b[0m`);
|
||||
|
||||
const refreshedCredentials = await checkAndRefreshToken(provider, credentials);
|
||||
|
||||
const result = await handleImageGenerationCore({
|
||||
body,
|
||||
modelInfo: { provider, model },
|
||||
credentials: refreshedCredentials,
|
||||
log,
|
||||
streamToClient: wantsStream,
|
||||
onCredentialsRefreshed: async (newCreds) => {
|
||||
await updateProviderCredentials(credentials.connectionId, {
|
||||
accessToken: newCreds.accessToken,
|
||||
@@ -140,7 +103,6 @@ export async function handleImageGeneration(request) {
|
||||
const { shouldFallback } = await markAccountUnavailable(credentials.connectionId, result.status, result.error, provider, model);
|
||||
|
||||
if (shouldFallback) {
|
||||
log.warn("AUTH", `Account ${credentials.connectionName} unavailable (${result.status}), trying fallback`);
|
||||
excludeConnectionIds.add(credentials.connectionId);
|
||||
lastError = result.error;
|
||||
lastStatus = result.status;
|
||||
|
||||
@@ -15,11 +15,12 @@ let selectionMutex = Promise.resolve();
|
||||
* @param {Set<string>|string|null} excludeConnectionIds - Connection ID(s) to exclude (for retry with next account)
|
||||
* @param {string|null} model - Model name for per-model rate limit filtering
|
||||
*/
|
||||
export async function getProviderCredentials(provider, excludeConnectionIds = null, model = null) {
|
||||
export async function getProviderCredentials(provider, excludeConnectionIds = null, model = null, options = {}) {
|
||||
// Normalize to Set for consistent handling
|
||||
const excludeSet = excludeConnectionIds instanceof Set
|
||||
? excludeConnectionIds
|
||||
: (excludeConnectionIds ? new Set([excludeConnectionIds]) : new Set());
|
||||
const preferredConnectionId = options?.preferredConnectionId || null;
|
||||
// Acquire mutex to prevent race conditions
|
||||
const currentMutex = selectionMutex;
|
||||
let resolveMutex;
|
||||
@@ -87,7 +88,16 @@ export async function getProviderCredentials(provider, excludeConnectionIds = nu
|
||||
const strategy = providerOverride.fallbackStrategy || settings.fallbackStrategy || "fill-first";
|
||||
|
||||
let connection;
|
||||
if (strategy === "round-robin") {
|
||||
// Pin to preferred connection if specified and available
|
||||
if (preferredConnectionId) {
|
||||
connection = availableConnections.find((c) => c.id === preferredConnectionId);
|
||||
if (connection) {
|
||||
log.info("AUTH", `${provider} | pinned to ${connection.id?.slice(0, 8)} (${connection.name || connection.email || "unnamed"})`);
|
||||
}
|
||||
}
|
||||
if (connection) {
|
||||
// skip strategy
|
||||
} else if (strategy === "round-robin") {
|
||||
const stickyLimit = providerOverride.stickyRoundRobinLimit || settings.stickyRoundRobinLimit || 3;
|
||||
|
||||
// Sort by lastUsed (most recent first) to find current candidate
|
||||
|
||||
@@ -33,6 +33,13 @@ export async function getModelInfo(modelStr) {
|
||||
if (matchedAnthropic) {
|
||||
return { provider: matchedAnthropic.id, model: parsed.model };
|
||||
}
|
||||
|
||||
// Check Custom Embedding nodes
|
||||
const embeddingNodes = await getProviderNodes({ type: "custom-embedding" });
|
||||
const matchedEmbedding = embeddingNodes.find((node) => node.prefix === parsed.providerAlias);
|
||||
if (matchedEmbedding) {
|
||||
return { provider: matchedEmbedding.id, model: parsed.model };
|
||||
}
|
||||
}
|
||||
return {
|
||||
provider: parsed.provider,
|
||||
|
||||
Reference in New Issue
Block a user