Spaces:
Running
Running
File size: 20,638 Bytes
ed8e0ee 9df5dcc ed8e0ee 713f2f6 9df5dcc 2cae477 984e8a0 713f2f6 9df5dcc 713f2f6 984e8a0 713f2f6 984e8a0 713f2f6 9df5dcc 713f2f6 984e8a0 ed8e0ee 2cae477 9df5dcc 984e8a0 2cae477 984e8a0 2cae477 713f2f6 e6d9a41 713f2f6 984e8a0 713f2f6 984e8a0 713f2f6 984e8a0 713f2f6 984e8a0 e6d9a41 9df5dcc 713f2f6 e6d9a41 9df5dcc e6d9a41 713f2f6 e6d9a41 713f2f6 984e8a0 713f2f6 984e8a0 e6d9a41 9df5dcc e6d9a41 713f2f6 9df5dcc 984e8a0 9df5dcc 713f2f6 984e8a0 713f2f6 ed8e0ee 984e8a0 2cae477 9df5dcc 2cae477 9df5dcc 2cae477 ed8e0ee 984e8a0 9df5dcc 984e8a0 9df5dcc 984e8a0 9df5dcc 984e8a0 9df5dcc 984e8a0 9df5dcc 713f2f6 e6d9a41 713f2f6 984e8a0 713f2f6 e6d9a41 713f2f6 9df5dcc 713f2f6 984e8a0 713f2f6 9df5dcc 984e8a0 713f2f6 984e8a0 713f2f6 984e8a0 713f2f6 984e8a0 713f2f6 e6d9a41 9df5dcc 984e8a0 e6d9a41 984e8a0 e6d9a41 9df5dcc e6d9a41 713f2f6 984e8a0 713f2f6 984e8a0 713f2f6 984e8a0 713f2f6 984e8a0 713f2f6 984e8a0 713f2f6 e6d9a41 713f2f6 984e8a0 713f2f6 984e8a0 ed8e0ee 984e8a0 713f2f6 ed8e0ee 984e8a0 9df5dcc ed8e0ee 984e8a0 ed8e0ee 984e8a0 9df5dcc ed8e0ee 984e8a0 ed8e0ee 984e8a0 ed8e0ee 984e8a0 ed8e0ee 984e8a0 713f2f6 9df5dcc ed8e0ee 984e8a0 ed8e0ee 984e8a0 ed8e0ee e6d9a41 984e8a0 9df5dcc 984e8a0 ed8e0ee 9df5dcc ed8e0ee 984e8a0 e6d9a41 984e8a0 ed8e0ee 984e8a0 e6d9a41 984e8a0 e6d9a41 984e8a0 9df5dcc e6d9a41 984e8a0 e6d9a41 ed8e0ee 984e8a0 713f2f6 984e8a0 713f2f6 984e8a0 e6d9a41 984e8a0 9df5dcc 713f2f6 984e8a0 713f2f6 984e8a0 713f2f6 9df5dcc 713f2f6 9df5dcc 713f2f6 ed8e0ee 713f2f6 984e8a0 ed8e0ee 713f2f6 ed8e0ee 984e8a0 713f2f6 984e8a0 713f2f6 984e8a0 713f2f6 984e8a0 713f2f6 9df5dcc 713f2f6 9df5dcc 984e8a0 713f2f6 ed8e0ee 984e8a0 9df5dcc ed8e0ee 713f2f6 9df5dcc 713f2f6 ed8e0ee 984e8a0 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 |
import express from 'express';
import { fal } from '@fal-ai/client';
// --- Key Management Setup ---
// Read comma-separated keys from the SINGLE environment variable FAL_KEY
const FAL_KEY_STRING = process.env.FAL_KEY;
// Read the custom API Key for proxy authentication
const API_KEY = process.env.API_KEY;
// --- (Initial checks for FAL_KEY_STRING, API_KEY, parsing falKeys remain the same) ---
if (!FAL_KEY_STRING) {
console.error("ERROR: FAL_KEY environment variable is not set.");
console.error("Ensure FAL_KEY contains a comma-separated list of your Fal AI keys.");
process.exit(1);
}
const falKeys = FAL_KEY_STRING.split(',')
.map(key => key.trim())
.filter(key => key.length > 0);
if (falKeys.length === 0) {
console.error("ERROR: No valid Fal keys found in the FAL_KEY environment variable after parsing.");
console.error("Ensure FAL_KEY is a comma-separated list, e.g., 'key1,key2,key3'.");
process.exit(1);
}
if (!API_KEY) {
console.error("ERROR: API_KEY environment variable is not set.");
process.exit(1);
}
// --- (End initial checks) ---
let currentKeyIndex = 0;
const invalidKeys = new Set();
console.log(`Loaded ${falKeys.length} Fal AI Key(s) from the FAL_KEY environment variable.`);
// --- (getNextValidKey function remains the same) ---
function getNextValidKey() {
if (invalidKeys.size >= falKeys.length) {
console.error("All Fal AI keys are marked as invalid.");
return null;
}
const initialIndex = currentKeyIndex;
let attempts = 0;
while (attempts < falKeys.length) {
const keyIndex = currentKeyIndex % falKeys.length;
const key = falKeys[keyIndex];
currentKeyIndex = (keyIndex + 1) % falKeys.length;
if (!invalidKeys.has(key)) {
console.log(`Using Fal Key index: ${keyIndex} (from FAL_KEY list)`);
return { key, index: keyIndex };
} else {
console.log(`Skipping invalid Fal Key index: ${keyIndex}`);
}
attempts++;
if (currentKeyIndex === initialIndex && attempts > 0) {
console.warn("Looped through all keys, potentially all are invalid.");
break;
}
}
console.error("Could not find a valid Fal AI key after checking all potentially available keys.");
return null;
}
// --- (isKeyRelatedError function remains the same) ---
function isKeyRelatedError(error) {
if (!error) return false;
const message = error.message?.toLowerCase() || '';
const status = error.status || error.statusCode;
if (status === 401 || status === 403 || status === 429) {
console.warn(`Detected potential key-related error (HTTP Status: ${status}).`);
return true;
}
const keyErrorPatterns = [
'invalid api key', 'authentication failed', 'permission denied',
'quota exceeded', 'forbidden', 'unauthorized', 'rate limit',
'credentials', 'api key missing', 'invalid credential',
'exhausted balance', 'user is locked' // Add specific messages if observed
];
if (keyErrorPatterns.some(pattern => message.includes(pattern))) {
console.warn(`Detected potential key-related error (message contains relevant pattern: "${message}")`);
return true;
}
// Also check the body.detail if status is 403, as seen in the logs
if (status === 403 && error.body?.detail) {
const detailMessage = error.body.detail.toLowerCase();
if (keyErrorPatterns.some(pattern => detailMessage.includes(pattern))) {
console.warn(`Detected potential key-related error (body.detail contains relevant pattern: "${detailMessage}")`);
return true;
}
}
return false;
}
// --- End Key Management Setup ---
const app = express();
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
const PORT = process.env.PORT || 3000;
// --- (apiKeyAuth middleware remains the same) ---
const apiKeyAuth = (req, res, next) => {
const authHeader = req.headers['authorization'];
if (!authHeader) {
console.warn('Unauthorized: No Authorization header provided');
return res.status(401).json({ error: 'Unauthorized: No API Key provided' });
}
const authParts = authHeader.split(' ');
if (authParts.length !== 2 || authParts[0].toLowerCase() !== 'bearer') {
console.warn('Unauthorized: Invalid Authorization header format. Expected "Bearer <key>".');
return res.status(401).json({ error: 'Unauthorized: Invalid Authorization header format' });
}
const providedKey = authParts[1];
if (providedKey !== API_KEY) {
console.warn('Unauthorized: Invalid API Key provided.');
return res.status(401).json({ error: 'Unauthorized: Invalid API Key' });
}
next();
};
app.use(['/v1/models', '/v1/chat/completions'], apiKeyAuth);
// --- (Global Limits, FAL_SUPPORTED_MODELS, getOwner remain the same) ---
const PROMPT_LIMIT = 4800;
const SYSTEM_PROMPT_LIMIT = 4800;
const FAL_SUPPORTED_MODELS = [ /* ... model list ... */ ];
const getOwner = (modelId) => { /* ... */ };
// --- (GET /v1/models endpoint remains the same) ---
app.get('/v1/models', (req, res) => { /* ... */ });
// --- (convertMessagesToFalPrompt function remains the same) ---
function convertMessagesToFalPrompt(messages) { /* ... */ }
/**
* MODIFIED: Makes a request to the Fal AI API, handling key rotation and retries on key-related errors.
* For stream requests, returns the stream AND the key info used.
* @param {object} falInput - The input object for the Fal AI API call.
* @param {boolean} [stream=false] - Whether to make a streaming request.
* @returns {Promise<object|{stream: AsyncIterable<object>, keyUsed: string, indexUsed: number}>}
* The result object for non-stream, or an object containing the stream and key info for stream.
* @throws {Error} If the request fails after trying all valid keys, or if a non-key-related error occurs during *initiation*.
*/
async function makeFalRequestWithRetry(falInput, stream = false) {
let attempts = 0;
const maxAttempts = falKeys.length;
const attemptedKeysInThisRequest = new Set();
while (attempts < maxAttempts) {
const keyInfo = getNextValidKey();
if (!keyInfo) {
console.error("makeFalRequestWithRetry: No valid Fal AI keys remaining.");
throw new Error("No valid Fal AI keys available (all marked as invalid).");
}
if (attemptedKeysInThisRequest.has(keyInfo.key)) {
console.warn(`Key at index ${keyInfo.index} was already attempted for this request. Skipping to find next different key.`);
continue;
}
attemptedKeysInThisRequest.add(keyInfo.key);
attempts++;
try {
console.log(`Attempt ${attempts}/${maxAttempts}: Trying Fal Key index ${keyInfo.index}...`);
console.warn(`Configuring GLOBAL fal client with key index ${keyInfo.index}. Review concurrency implications.`);
fal.config({ credentials: keyInfo.key });
if (stream) {
const falStream = await fal.stream("fal-ai/any-llm", { input: falInput });
console.log(`Successfully initiated stream with key index ${keyInfo.index}.`);
// **MODIFIED: Return stream AND key info**
return { stream: falStream, keyUsed: keyInfo.key, indexUsed: keyInfo.index };
} else {
// Non-stream logic remains the same
console.log(`Executing non-stream request with key index ${keyInfo.index}...`);
const result = await fal.subscribe("fal-ai/any-llm", { input: falInput, logs: true });
console.log(`Successfully received non-stream result with key index ${keyInfo.index}.`);
if (result && result.error) {
console.error(`Fal AI returned an error object within the non-stream result payload (Key Index ${keyInfo.index}):`, result.error);
if (isKeyRelatedError(result.error)) {
console.warn(`Marking Fal Key index ${keyInfo.index} as invalid due to error in response payload.`);
invalidKeys.add(keyInfo.key);
continue; // Try next key
} else {
throw new Error(`Fal AI error reported in result payload: ${JSON.stringify(result.error)}`);
}
}
return result; // Return only result for non-stream
}
} catch (error) {
// This catch block now primarily handles errors during *request initiation*
console.error(`Error caught during request initiation using Fal Key index ${keyInfo.index}:`, error.message || error);
if (isKeyRelatedError(error)) {
console.warn(`Marking Fal Key index ${keyInfo.index} as invalid due to caught initiation error.`);
invalidKeys.add(keyInfo.key);
// Continue loop to try the next key
} else {
console.error("Initiation error does not appear to be key-related. Failing request without further key retries.");
throw error; // Re-throw non-key-related initiation error
}
}
} // End while loop
// If loop finishes, all keys failed during initiation
throw new Error(`Request initiation failed after trying ${attempts} unique Fal key(s). All failed with key-related errors or were already marked invalid.`);
}
// POST /v1/chat/completions endpoint - Handles chat requests
app.post('/v1/chat/completions', async (req, res) => {
const { model, messages, stream = false, reasoning = false, ...restOpenAIParams } = req.body;
console.log(`--> POST /v1/chat/completions | Model: ${model} | Stream: ${stream}`);
// --- (Input validation for model, messages remains the same) ---
if (!model || !messages || !Array.isArray(messages) || messages.length === 0) {
console.error("Invalid request: Missing 'model' or 'messages' array is empty/invalid.");
return res.status(400).json({ error: 'Bad Request: `model` and a non-empty `messages` array are required.' });
}
let keyUsedForRequest = null; // Variable to store the key used for this request, if successful initiation
let indexUsedForRequest = null;
try {
const { prompt, system_prompt } = convertMessagesToFalPrompt(messages);
const falInput = { /* ... falInput setup ... */ };
falInput.model = model;
falInput.prompt = prompt;
if (system_prompt && system_prompt.length > 0) {
falInput.system_prompt = system_prompt;
}
falInput.reasoning = !!reasoning;
console.log("Attempting Fal request with key rotation/retry logic...");
console.log(`Prepared Input Lengths - System Prompt: ${system_prompt?.length || 0}, Prompt: ${prompt?.length || 0}`);
if (stream) {
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
/* ... other headers ... */
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('Access-Control-Allow-Origin', '*');
res.flushHeaders();
let previousOutput = '';
let streamResult; // To hold the object { stream, keyUsed, indexUsed }
try {
// **MODIFIED: Get stream and key info**
streamResult = await makeFalRequestWithRetry(falInput, true);
const falStream = streamResult.stream;
keyUsedForRequest = streamResult.keyUsed; // Store the key used for this stream
indexUsedForRequest = streamResult.indexUsed;
// Process the stream events asynchronously
for await (const event of falStream) {
// --- (Stream event processing logic remains the same) ---
const currentOutput = (event && typeof event.output === 'string') ? event.output : '';
const isPartial = (event && typeof event.partial === 'boolean') ? event.partial : true;
const errorInfo = (event && event.error) ? event.error : null;
if (errorInfo) {
// Log error from within the stream, but might continue processing
console.error("Error received *within* fal stream event payload:", errorInfo);
const errorChunk = { /* ... error chunk details ... */ };
if (!res.writableEnded) { res.write(`data: ${JSON.stringify(errorChunk)}\n\n`); }
else { console.warn("Stream ended before writing event error."); }
// Decide whether to break or continue based on error severity if needed
}
let deltaContent = '';
if (currentOutput.startsWith(previousOutput)) {
deltaContent = currentOutput.substring(previousOutput.length);
} else if (currentOutput.length > 0) {
console.warn("Fal stream output mismatch/reset. Sending full current output as delta.");
deltaContent = currentOutput;
previousOutput = '';
}
previousOutput = currentOutput;
if (deltaContent || !isPartial) {
const openAIChunk = { /* ... chunk details ... */ };
openAIChunk.id = `chatcmpl-${Date.now()}`;
openAIChunk.object = "chat.completion.chunk";
openAIChunk.created = Math.floor(Date.now() / 1000);
openAIChunk.model = model;
openAIChunk.choices = [{ index: 0, delta: { content: deltaContent }, finish_reason: isPartial === false ? "stop" : null }];
if (!res.writableEnded) { res.write(`data: ${JSON.stringify(openAIChunk)}\n\n`); }
else { console.warn("Stream ended before writing data chunk."); }
}
// --- (End stream event processing) ---
} // End for-await loop
// Send [DONE] marker
if (!res.writableEnded) {
res.write(`data: [DONE]\n\n`);
res.end();
console.log("<-- Stream finished successfully and [DONE] sent.");
} else {
console.log("<-- Stream processing finished, but connection was already ended before [DONE].");
}
} catch (streamError) {
// **MODIFIED CATCH BLOCK for stream processing errors**
// This catches errors from makeFalRequestWithRetry (initiation failure)
// OR errors thrown during the 'for await...of falStream' loop.
console.error('Error during stream request processing:', streamError.message || streamError);
// **NEW: Check if the error is key-related and invalidate the key if needed**
// We only do this if keyUsedForRequest has been set (meaning initiation succeeded)
// And if the error occurred *during* the stream processing, not during initiation
// (initiation errors are handled inside makeFalRequestWithRetry)
// The check `keyUsedForRequest !== null` helps distinguish.
if (keyUsedForRequest && isKeyRelatedError(streamError)) {
console.warn(`Marking Fal Key index ${indexUsedForRequest} as invalid due to error during stream processing.`);
invalidKeys.add(keyUsedForRequest);
}
// else: The error was either not key-related, or occurred during initiation (already handled)
// --- (Error reporting logic to client remains the same) ---
try {
if (!res.headersSent) {
// Error likely during initiation (caught from makeFalRequestWithRetry)
const errorMessage = (streamError instanceof Error) ? streamError.message : JSON.stringify(streamError);
res.status(502).json({ error: 'Failed to initiate Fal stream', details: errorMessage });
console.log("<-- Stream initiation failed response sent (502).");
} else if (!res.writableEnded) {
// Error during stream processing after headers sent
const errorDetails = (streamError instanceof Error) ? streamError.message : JSON.stringify(streamError);
res.write(`data: ${JSON.stringify({ error: { message: "Stream processing error after initiation", type: "proxy_error", details: errorDetails } })}\n\n`);
res.write(`data: [DONE]\n\n`);
res.end();
console.log("<-- Stream error sent within stream, stream ended.");
} else {
console.log("<-- Stream error occurred, but connection was already ended.");
}
} catch (finalError) {
console.error('Error sending stream error message to client:', finalError);
if (!res.writableEnded) { res.end(); }
}
// --- (End error reporting) ---
}
} else {
// --- Non-Stream Logic (remains the same, uses makeFalRequestWithRetry directly) ---
try {
const result = await makeFalRequestWithRetry(falInput, false);
const openAIResponse = { /* ... construct response ... */ };
openAIResponse.id = `chatcmpl-${result.requestId || Date.now()}`;
openAIResponse.object = "chat.completion";
openAIResponse.created = Math.floor(Date.now() / 1000);
openAIResponse.model = model;
openAIResponse.choices = [{ index: 0, message: { role: "assistant", content: result.output || "" }, finish_reason: "stop" }];
openAIResponse.usage = { prompt_tokens: null, completion_tokens: null, total_tokens: null };
openAIResponse.system_fingerprint = null;
if (result.reasoning) { openAIResponse.fal_reasoning = result.reasoning; }
res.json(openAIResponse);
console.log("<-- Non-stream response sent successfully.");
} catch (error) {
console.error('Error during non-stream request processing:', error.message || error);
if (!res.headersSent) {
const errorMessage = (error instanceof Error) ? error.message : JSON.stringify(error);
const finalMessage = errorMessage.includes("No valid Fal AI keys available") || errorMessage.includes("Request failed after trying")
? `Fal request failed: ${errorMessage}`
: `Internal Server Error processing Fal request: ${errorMessage}`;
res.status(502).json({ error: 'Fal Request Failed', details: finalMessage });
console.log("<-- Non-stream error response sent (502).");
} else {
console.error("Headers already sent for non-stream error response? This is unexpected.");
if (!res.writableEnded) { res.end(); }
}
}
}
} catch (error) {
// --- (Outer catch block for setup errors remains the same) ---
console.error('Unhandled error before initiating Fal request (likely setup or input conversion):', error.message || error);
if (!res.headersSent) {
const errorMessage = (error instanceof Error) ? error.message : JSON.stringify(error);
res.status(500).json({ error: 'Internal Server Error in Proxy Setup', details: errorMessage });
console.log("<-- Proxy setup error response sent (500).");
} else {
console.error("Headers already sent when catching setup error. Ending response.");
if (!res.writableEnded) { res.end(); }
}
}
});
// --- (Server listen and root path handler remain the same) ---
app.listen(PORT, () => { /* ... startup messages ... */ });
app.get('/', (req, res) => { /* ... root message ... */ }); |