Calmlo commited on
Commit
c9e0fd6
·
verified ·
1 Parent(s): 984e8a0

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +374 -85
server.js CHANGED
@@ -1,4 +1,5 @@
1
  import express from 'express';
 
2
  import { fal } from '@fal-ai/client';
3
 
4
  // --- Key Management Setup ---
@@ -7,132 +8,367 @@ const FAL_KEY_STRING = process.env.FAL_KEY;
7
  // Read the custom API Key for proxy authentication
8
  const API_KEY = process.env.API_KEY;
9
 
10
- // --- (Initial checks for FAL_KEY_STRING, API_KEY, parsing falKeys remain the same) ---
11
  if (!FAL_KEY_STRING) {
12
  console.error("ERROR: FAL_KEY environment variable is not set.");
13
  console.error("Ensure FAL_KEY contains a comma-separated list of your Fal AI keys.");
14
- process.exit(1);
15
  }
 
 
16
  const falKeys = FAL_KEY_STRING.split(',')
17
- .map(key => key.trim())
18
- .filter(key => key.length > 0);
 
19
  if (falKeys.length === 0) {
20
  console.error("ERROR: No valid Fal keys found in the FAL_KEY environment variable after parsing.");
21
  console.error("Ensure FAL_KEY is a comma-separated list, e.g., 'key1,key2,key3'.");
22
- process.exit(1);
23
  }
 
24
  if (!API_KEY) {
25
  console.error("ERROR: API_KEY environment variable is not set.");
26
- process.exit(1);
27
  }
28
- // --- (End initial checks) ---
29
 
30
 
31
  let currentKeyIndex = 0;
 
32
  const invalidKeys = new Set();
 
33
  console.log(`Loaded ${falKeys.length} Fal AI Key(s) from the FAL_KEY environment variable.`);
34
 
35
- // --- (getNextValidKey function remains the same) ---
 
 
 
 
36
  function getNextValidKey() {
 
37
  if (invalidKeys.size >= falKeys.length) {
38
  console.error("All Fal AI keys are marked as invalid.");
39
- return null;
40
  }
 
41
  const initialIndex = currentKeyIndex;
42
- let attempts = 0;
43
  while (attempts < falKeys.length) {
44
  const keyIndex = currentKeyIndex % falKeys.length;
45
  const key = falKeys[keyIndex];
 
 
46
  currentKeyIndex = (keyIndex + 1) % falKeys.length;
 
 
47
  if (!invalidKeys.has(key)) {
 
48
  console.log(`Using Fal Key index: ${keyIndex} (from FAL_KEY list)`);
49
- return { key, index: keyIndex };
50
  } else {
51
  console.log(`Skipping invalid Fal Key index: ${keyIndex}`);
52
  }
 
53
  attempts++;
 
54
  if (currentKeyIndex === initialIndex && attempts > 0) {
55
  console.warn("Looped through all keys, potentially all are invalid.");
56
  break;
57
  }
58
  }
 
 
59
  console.error("Could not find a valid Fal AI key after checking all potentially available keys.");
60
  return null;
61
  }
62
 
63
- // --- (isKeyRelatedError function remains the same) ---
 
 
 
 
 
64
  function isKeyRelatedError(error) {
65
- if (!error) return false;
 
66
  const message = error.message?.toLowerCase() || '';
 
67
  const status = error.status || error.statusCode;
 
 
68
  if (status === 401 || status === 403 || status === 429) {
69
  console.warn(`Detected potential key-related error (HTTP Status: ${status}).`);
70
  return true;
71
  }
 
 
72
  const keyErrorPatterns = [
73
  'invalid api key', 'authentication failed', 'permission denied',
74
  'quota exceeded', 'forbidden', 'unauthorized', 'rate limit',
75
  'credentials', 'api key missing', 'invalid credential',
76
- 'exhausted balance', 'user is locked' // Add specific messages if observed
77
  ];
78
  if (keyErrorPatterns.some(pattern => message.includes(pattern))) {
79
  console.warn(`Detected potential key-related error (message contains relevant pattern: "${message}")`);
80
  return true;
81
  }
82
- // Also check the body.detail if status is 403, as seen in the logs
 
83
  if (status === 403 && error.body?.detail) {
84
- const detailMessage = error.body.detail.toLowerCase();
85
  if (keyErrorPatterns.some(pattern => detailMessage.includes(pattern))) {
86
  console.warn(`Detected potential key-related error (body.detail contains relevant pattern: "${detailMessage}")`);
87
  return true;
88
  }
89
  }
 
 
 
 
90
  return false;
91
  }
92
  // --- End Key Management Setup ---
93
 
94
  const app = express();
 
95
  app.use(express.json({ limit: '50mb' }));
96
  app.use(express.urlencoded({ extended: true, limit: '50mb' }));
 
97
  const PORT = process.env.PORT || 3000;
98
 
99
- // --- (apiKeyAuth middleware remains the same) ---
100
  const apiKeyAuth = (req, res, next) => {
101
  const authHeader = req.headers['authorization'];
 
102
  if (!authHeader) {
103
  console.warn('Unauthorized: No Authorization header provided');
104
  return res.status(401).json({ error: 'Unauthorized: No API Key provided' });
105
  }
 
 
106
  const authParts = authHeader.split(' ');
107
  if (authParts.length !== 2 || authParts[0].toLowerCase() !== 'bearer') {
108
  console.warn('Unauthorized: Invalid Authorization header format. Expected "Bearer <key>".');
109
  return res.status(401).json({ error: 'Unauthorized: Invalid Authorization header format' });
110
  }
 
111
  const providedKey = authParts[1];
112
  if (providedKey !== API_KEY) {
113
  console.warn('Unauthorized: Invalid API Key provided.');
114
  return res.status(401).json({ error: 'Unauthorized: Invalid API Key' });
115
  }
 
 
116
  next();
117
  };
 
 
118
  app.use(['/v1/models', '/v1/chat/completions'], apiKeyAuth);
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
- // --- (Global Limits, FAL_SUPPORTED_MODELS, getOwner remain the same) ---
122
- const PROMPT_LIMIT = 4800;
123
- const SYSTEM_PROMPT_LIMIT = 4800;
124
- const FAL_SUPPORTED_MODELS = [ /* ... model list ... */ ];
125
- const getOwner = (modelId) => { /* ... */ };
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
- // --- (GET /v1/models endpoint remains the same) ---
128
- app.get('/v1/models', (req, res) => { /* ... */ });
129
 
130
- // --- (convertMessagesToFalPrompt function remains the same) ---
131
- function convertMessagesToFalPrompt(messages) { /* ... */ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
 
134
  /**
135
- * MODIFIED: Makes a request to the Fal AI API, handling key rotation and retries on key-related errors.
136
  * For stream requests, returns the stream AND the key info used.
137
  * @param {object} falInput - The input object for the Fal AI API call.
138
  * @param {boolean} [stream=false] - Whether to make a streaming request.
@@ -166,10 +402,10 @@ async function makeFalRequestWithRetry(falInput, stream = false) {
166
  if (stream) {
167
  const falStream = await fal.stream("fal-ai/any-llm", { input: falInput });
168
  console.log(`Successfully initiated stream with key index ${keyInfo.index}.`);
169
- // **MODIFIED: Return stream AND key info**
170
  return { stream: falStream, keyUsed: keyInfo.key, indexUsed: keyInfo.index };
171
  } else {
172
- // Non-stream logic remains the same
173
  console.log(`Executing non-stream request with key index ${keyInfo.index}...`);
174
  const result = await fal.subscribe("fal-ai/any-llm", { input: falInput, logs: true });
175
  console.log(`Successfully received non-stream result with key index ${keyInfo.index}.`);
@@ -186,7 +422,7 @@ async function makeFalRequestWithRetry(falInput, stream = false) {
186
  return result; // Return only result for non-stream
187
  }
188
  } catch (error) {
189
- // This catch block now primarily handles errors during *request initiation*
190
  console.error(`Error caught during request initiation using Fal Key index ${keyInfo.index}:`, error.message || error);
191
  if (isKeyRelatedError(error)) {
192
  console.warn(`Marking Fal Key index ${keyInfo.index} as invalid due to caught initiation error.`);
@@ -209,41 +445,52 @@ app.post('/v1/chat/completions', async (req, res) => {
209
  const { model, messages, stream = false, reasoning = false, ...restOpenAIParams } = req.body;
210
  console.log(`--> POST /v1/chat/completions | Model: ${model} | Stream: ${stream}`);
211
 
212
- // --- (Input validation for model, messages remains the same) ---
213
  if (!model || !messages || !Array.isArray(messages) || messages.length === 0) {
214
  console.error("Invalid request: Missing 'model' or 'messages' array is empty/invalid.");
215
  return res.status(400).json({ error: 'Bad Request: `model` and a non-empty `messages` array are required.' });
216
  }
 
 
 
 
217
 
218
- let keyUsedForRequest = null; // Variable to store the key used for this request, if successful initiation
219
  let indexUsedForRequest = null;
220
 
221
  try {
 
 
222
  const { prompt, system_prompt } = convertMessagesToFalPrompt(messages);
223
- const falInput = { /* ... falInput setup ... */ };
224
- falInput.model = model;
225
- falInput.prompt = prompt;
 
 
 
226
  if (system_prompt && system_prompt.length > 0) {
227
  falInput.system_prompt = system_prompt;
228
  }
229
- falInput.reasoning = !!reasoning;
230
 
231
  console.log("Attempting Fal request with key rotation/retry logic...");
232
  console.log(`Prepared Input Lengths - System Prompt: ${system_prompt?.length || 0}, Prompt: ${prompt?.length || 0}`);
233
 
 
234
  if (stream) {
 
235
  res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
236
- /* ... other headers ... */
237
  res.setHeader('Cache-Control', 'no-cache');
238
  res.setHeader('Connection', 'keep-alive');
239
- res.setHeader('Access-Control-Allow-Origin', '*');
240
- res.flushHeaders();
241
 
242
- let previousOutput = '';
243
  let streamResult; // To hold the object { stream, keyUsed, indexUsed }
244
 
245
  try {
246
- // **MODIFIED: Get stream and key info**
 
247
  streamResult = await makeFalRequestWithRetry(falInput, true);
248
  const falStream = streamResult.stream;
249
  keyUsedForRequest = streamResult.keyUsed; // Store the key used for this stream
@@ -251,19 +498,24 @@ app.post('/v1/chat/completions', async (req, res) => {
251
 
252
  // Process the stream events asynchronously
253
  for await (const event of falStream) {
254
- // --- (Stream event processing logic remains the same) ---
255
  const currentOutput = (event && typeof event.output === 'string') ? event.output : '';
256
  const isPartial = (event && typeof event.partial === 'boolean') ? event.partial : true;
257
  const errorInfo = (event && event.error) ? event.error : null;
258
 
 
259
  if (errorInfo) {
260
- // Log error from within the stream, but might continue processing
261
  console.error("Error received *within* fal stream event payload:", errorInfo);
262
- const errorChunk = { /* ... error chunk details ... */ };
 
 
 
263
  if (!res.writableEnded) { res.write(`data: ${JSON.stringify(errorChunk)}\n\n`); }
264
  else { console.warn("Stream ended before writing event error."); }
265
- // Decide whether to break or continue based on error severity if needed
266
  }
 
 
267
  let deltaContent = '';
268
  if (currentOutput.startsWith(previousOutput)) {
269
  deltaContent = currentOutput.substring(previousOutput.length);
@@ -273,46 +525,48 @@ app.post('/v1/chat/completions', async (req, res) => {
273
  previousOutput = '';
274
  }
275
  previousOutput = currentOutput;
 
 
276
  if (deltaContent || !isPartial) {
277
- const openAIChunk = { /* ... chunk details ... */ };
278
- openAIChunk.id = `chatcmpl-${Date.now()}`;
279
- openAIChunk.object = "chat.completion.chunk";
280
- openAIChunk.created = Math.floor(Date.now() / 1000);
281
- openAIChunk.model = model;
282
- openAIChunk.choices = [{ index: 0, delta: { content: deltaContent }, finish_reason: isPartial === false ? "stop" : null }];
 
 
 
 
 
283
  if (!res.writableEnded) { res.write(`data: ${JSON.stringify(openAIChunk)}\n\n`); }
284
  else { console.warn("Stream ended before writing data chunk."); }
285
  }
286
- // --- (End stream event processing) ---
287
  } // End for-await loop
288
 
289
- // Send [DONE] marker
290
  if (!res.writableEnded) {
291
  res.write(`data: [DONE]\n\n`);
292
- res.end();
293
  console.log("<-- Stream finished successfully and [DONE] sent.");
294
  } else {
295
- console.log("<-- Stream processing finished, but connection was already ended before [DONE].");
296
  }
297
 
298
  } catch (streamError) {
299
- // **MODIFIED CATCH BLOCK for stream processing errors**
300
- // This catches errors from makeFalRequestWithRetry (initiation failure)
301
- // OR errors thrown during the 'for await...of falStream' loop.
302
- console.error('Error during stream request processing:', streamError.message || streamError);
303
-
304
- // **NEW: Check if the error is key-related and invalidate the key if needed**
305
- // We only do this if keyUsedForRequest has been set (meaning initiation succeeded)
306
- // And if the error occurred *during* the stream processing, not during initiation
307
- // (initiation errors are handled inside makeFalRequestWithRetry)
308
- // The check `keyUsedForRequest !== null` helps distinguish.
309
  if (keyUsedForRequest && isKeyRelatedError(streamError)) {
310
  console.warn(`Marking Fal Key index ${indexUsedForRequest} as invalid due to error during stream processing.`);
311
  invalidKeys.add(keyUsedForRequest);
312
  }
313
- // else: The error was either not key-related, or occurred during initiation (already handled)
314
 
315
- // --- (Error reporting logic to client remains the same) ---
316
  try {
317
  if (!res.headersSent) {
318
  // Error likely during initiation (caught from makeFalRequestWithRetry)
@@ -323,7 +577,7 @@ app.post('/v1/chat/completions', async (req, res) => {
323
  // Error during stream processing after headers sent
324
  const errorDetails = (streamError instanceof Error) ? streamError.message : JSON.stringify(streamError);
325
  res.write(`data: ${JSON.stringify({ error: { message: "Stream processing error after initiation", type: "proxy_error", details: errorDetails } })}\n\n`);
326
- res.write(`data: [DONE]\n\n`);
327
  res.end();
328
  console.log("<-- Stream error sent within stream, stream ended.");
329
  } else {
@@ -333,27 +587,40 @@ app.post('/v1/chat/completions', async (req, res) => {
333
  console.error('Error sending stream error message to client:', finalError);
334
  if (!res.writableEnded) { res.end(); }
335
  }
336
- // --- (End error reporting) ---
337
  }
338
 
339
  } else {
340
- // --- Non-Stream Logic (remains the same, uses makeFalRequestWithRetry directly) ---
341
  try {
 
342
  const result = await makeFalRequestWithRetry(falInput, false);
343
- const openAIResponse = { /* ... construct response ... */ };
344
- openAIResponse.id = `chatcmpl-${result.requestId || Date.now()}`;
345
- openAIResponse.object = "chat.completion";
346
- openAIResponse.created = Math.floor(Date.now() / 1000);
347
- openAIResponse.model = model;
348
- openAIResponse.choices = [{ index: 0, message: { role: "assistant", content: result.output || "" }, finish_reason: "stop" }];
349
- openAIResponse.usage = { prompt_tokens: null, completion_tokens: null, total_tokens: null };
350
- openAIResponse.system_fingerprint = null;
351
- if (result.reasoning) { openAIResponse.fal_reasoning = result.reasoning; }
 
 
 
 
 
 
 
 
 
 
352
 
353
  res.json(openAIResponse);
354
  console.log("<-- Non-stream response sent successfully.");
 
355
  } catch (error) {
356
- console.error('Error during non-stream request processing:', error.message || error);
 
357
  if (!res.headersSent) {
358
  const errorMessage = (error instanceof Error) ? error.message : JSON.stringify(error);
359
  const finalMessage = errorMessage.includes("No valid Fal AI keys available") || errorMessage.includes("Request failed after trying")
@@ -366,13 +633,15 @@ app.post('/v1/chat/completions', async (req, res) => {
366
  if (!res.writableEnded) { res.end(); }
367
  }
368
  }
369
- }
370
 
371
  } catch (error) {
372
- // --- (Outer catch block for setup errors remains the same) ---
 
373
  console.error('Unhandled error before initiating Fal request (likely setup or input conversion):', error.message || error);
374
  if (!res.headersSent) {
375
  const errorMessage = (error instanceof Error) ? error.message : JSON.stringify(error);
 
376
  res.status(500).json({ error: 'Internal Server Error in Proxy Setup', details: errorMessage });
377
  console.log("<-- Proxy setup error response sent (500).");
378
  } else {
@@ -382,6 +651,26 @@ app.post('/v1/chat/completions', async (req, res) => {
382
  }
383
  });
384
 
385
- // --- (Server listen and root path handler remain the same) ---
386
- app.listen(PORT, () => { /* ... startup messages ... */ });
387
- app.get('/', (req, res) => { /* ... root message ... */ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import express from 'express';
2
+ // Import the 'fal' object directly for configuration within the retry loop
3
  import { fal } from '@fal-ai/client';
4
 
5
  // --- Key Management Setup ---
 
8
  // Read the custom API Key for proxy authentication
9
  const API_KEY = process.env.API_KEY;
10
 
11
+ // --- Initial Environment Variable Checks ---
12
  if (!FAL_KEY_STRING) {
13
  console.error("ERROR: FAL_KEY environment variable is not set.");
14
  console.error("Ensure FAL_KEY contains a comma-separated list of your Fal AI keys.");
15
+ process.exit(1); // Exit if no Fal keys are provided
16
  }
17
+
18
+ // Parse the comma-separated keys from FAL_KEY_STRING
19
  const falKeys = FAL_KEY_STRING.split(',')
20
+ .map(key => key.trim()) // Remove leading/trailing whitespace
21
+ .filter(key => key.length > 0); // Remove any empty strings resulting from extra commas
22
+
23
  if (falKeys.length === 0) {
24
  console.error("ERROR: No valid Fal keys found in the FAL_KEY environment variable after parsing.");
25
  console.error("Ensure FAL_KEY is a comma-separated list, e.g., 'key1,key2,key3'.");
26
+ process.exit(1); // Exit if parsing results in zero valid keys
27
  }
28
+
29
  if (!API_KEY) {
30
  console.error("ERROR: API_KEY environment variable is not set.");
31
+ process.exit(1); // Exit if the proxy auth key is missing
32
  }
33
+ // --- End Initial Checks ---
34
 
35
 
36
  let currentKeyIndex = 0;
37
+ // Keep track of keys that failed persistently during runtime
38
  const invalidKeys = new Set();
39
+
40
  console.log(`Loaded ${falKeys.length} Fal AI Key(s) from the FAL_KEY environment variable.`);
41
 
42
+ /**
43
+ * Gets the next available valid Fal AI key in a round-robin fashion.
44
+ * Skips keys that have been marked as invalid.
45
+ * @returns {object|null} An object containing the key and its original index { key, index }, or null if no valid keys remain.
46
+ */
47
  function getNextValidKey() {
48
+ // Check if all keys have been marked as invalid
49
  if (invalidKeys.size >= falKeys.length) {
50
  console.error("All Fal AI keys are marked as invalid.");
51
+ return null; // No valid keys left
52
  }
53
+
54
  const initialIndex = currentKeyIndex;
55
+ let attempts = 0; // Prevent infinite loops in edge cases
56
  while (attempts < falKeys.length) {
57
  const keyIndex = currentKeyIndex % falKeys.length;
58
  const key = falKeys[keyIndex];
59
+
60
+ // Move to the next index for the *next* call, regardless of validity
61
  currentKeyIndex = (keyIndex + 1) % falKeys.length;
62
+
63
+ // Check if the current key is NOT in the invalid set
64
  if (!invalidKeys.has(key)) {
65
+ // Found a valid key
66
  console.log(`Using Fal Key index: ${keyIndex} (from FAL_KEY list)`);
67
+ return { key, index: keyIndex }; // Return the key and its original index
68
  } else {
69
  console.log(`Skipping invalid Fal Key index: ${keyIndex}`);
70
  }
71
+
72
  attempts++;
73
+ // Safety check: If we've looped back to the start after trying, break.
74
  if (currentKeyIndex === initialIndex && attempts > 0) {
75
  console.warn("Looped through all keys, potentially all are invalid.");
76
  break;
77
  }
78
  }
79
+
80
+ // If we exit the loop, it means no valid key was found
81
  console.error("Could not find a valid Fal AI key after checking all potentially available keys.");
82
  return null;
83
  }
84
 
85
+ /**
86
+ * Checks if an error object likely indicates an issue with the Fal AI API key.
87
+ * This is heuristic-based and may need refinement based on observed Fal errors.
88
+ * @param {Error|object|null} error - The error object caught.
89
+ * @returns {boolean} True if the error seems key-related, false otherwise.
90
+ */
91
  function isKeyRelatedError(error) {
92
+ if (!error) return false; // Handle null/undefined errors
93
+
94
  const message = error.message?.toLowerCase() || '';
95
+ // Check common HTTP status properties
96
  const status = error.status || error.statusCode;
97
+
98
+ // Check for specific HTTP status codes (401: Unauthorized, 403: Forbidden, 429: Too Many Requests/Quota)
99
  if (status === 401 || status === 403 || status === 429) {
100
  console.warn(`Detected potential key-related error (HTTP Status: ${status}).`);
101
  return true;
102
  }
103
+
104
+ // Check for common error message patterns (case-insensitive)
105
  const keyErrorPatterns = [
106
  'invalid api key', 'authentication failed', 'permission denied',
107
  'quota exceeded', 'forbidden', 'unauthorized', 'rate limit',
108
  'credentials', 'api key missing', 'invalid credential',
109
+ 'exhausted balance', 'user is locked' // Added based on observed logs
110
  ];
111
  if (keyErrorPatterns.some(pattern => message.includes(pattern))) {
112
  console.warn(`Detected potential key-related error (message contains relevant pattern: "${message}")`);
113
  return true;
114
  }
115
+
116
+ // Check the body.detail if status is 403 (as seen in Fal errors)
117
  if (status === 403 && error.body?.detail) {
118
+ const detailMessage = String(error.body.detail).toLowerCase();
119
  if (keyErrorPatterns.some(pattern => detailMessage.includes(pattern))) {
120
  console.warn(`Detected potential key-related error (body.detail contains relevant pattern: "${detailMessage}")`);
121
  return true;
122
  }
123
  }
124
+
125
+ // Add more specific checks based on observed Fal AI errors if needed
126
+ // e.g., if (error.code === 'FAL_AUTH_FAILURE') return true;
127
+
128
  return false;
129
  }
130
  // --- End Key Management Setup ---
131
 
132
  const app = express();
133
+ // Increase payload size limits if needed
134
  app.use(express.json({ limit: '50mb' }));
135
  app.use(express.urlencoded({ extended: true, limit: '50mb' }));
136
+
137
  const PORT = process.env.PORT || 3000;
138
 
139
+ // API Key Authentication Middleware
140
  const apiKeyAuth = (req, res, next) => {
141
  const authHeader = req.headers['authorization'];
142
+
143
  if (!authHeader) {
144
  console.warn('Unauthorized: No Authorization header provided');
145
  return res.status(401).json({ error: 'Unauthorized: No API Key provided' });
146
  }
147
+
148
+ // Expecting "Bearer YOUR_API_KEY"
149
  const authParts = authHeader.split(' ');
150
  if (authParts.length !== 2 || authParts[0].toLowerCase() !== 'bearer') {
151
  console.warn('Unauthorized: Invalid Authorization header format. Expected "Bearer <key>".');
152
  return res.status(401).json({ error: 'Unauthorized: Invalid Authorization header format' });
153
  }
154
+
155
  const providedKey = authParts[1];
156
  if (providedKey !== API_KEY) {
157
  console.warn('Unauthorized: Invalid API Key provided.');
158
  return res.status(401).json({ error: 'Unauthorized: Invalid API Key' });
159
  }
160
+
161
+ // Key is valid, proceed to the next middleware or route handler
162
  next();
163
  };
164
+
165
+ // Apply API Key Authentication to relevant endpoints
166
  app.use(['/v1/models', '/v1/chat/completions'], apiKeyAuth);
167
 
168
+ // === Global Limits Definition ===
169
+ const PROMPT_LIMIT = 4800; // Max length for the main 'prompt' field
170
+ const SYSTEM_PROMPT_LIMIT = 4800; // Max length for the 'system_prompt' field
171
+ // === End Limits Definition ===
172
+
173
+ // Define the list of models supported by fal-ai/any-llm (Update as needed)
174
+ const FAL_SUPPORTED_MODELS = [
175
+ "anthropic/claude-3.7-sonnet",
176
+ "anthropic/claude-3.5-sonnet",
177
+ "anthropic/claude-3-5-haiku",
178
+ "anthropic/claude-3-haiku",
179
+ "google/gemini-pro-1.5",
180
+ "google/gemini-flash-1.5",
181
+ "google/gemini-flash-1.5-8b",
182
+ "google/gemini-2.0-flash-001",
183
+ "meta-llama/llama-3.2-1b-instruct",
184
+ "meta-llama/llama-3.2-3b-instruct",
185
+ "meta-llama/llama-3.1-8b-instruct",
186
+ "meta-llama/llama-3.1-70b-instruct",
187
+ "openai/gpt-4o-mini",
188
+ "openai/gpt-4o",
189
+ "deepseek/deepseek-r1",
190
+ "meta-llama/llama-4-maverick",
191
+ "meta-llama/llama-4-scout"
192
+ // Add or remove models here
193
+ ];
194
+
195
+ // Helper function to extract the owner/organization from a model ID string
196
+ const getOwner = (modelId) => {
197
+ if (modelId && typeof modelId === 'string' && modelId.includes('/')) {
198
+ return modelId.split('/')[0];
199
+ }
200
+ // Default owner if format is unexpected or missing
201
+ return 'fal-ai';
202
+ }
203
 
204
+ // GET /v1/models endpoint - Returns the list of supported models
205
+ app.get('/v1/models', (req, res) => {
206
+ console.log("Received request for GET /v1/models");
207
+ try {
208
+ const modelsData = FAL_SUPPORTED_MODELS.map(modelId => ({
209
+ id: modelId,
210
+ object: "model",
211
+ created: Math.floor(Date.now() / 1000), // Use current timestamp
212
+ owned_by: getOwner(modelId)
213
+ }));
214
+ res.json({ object: "list", data: modelsData });
215
+ console.log("Successfully returned model list.");
216
+ } catch (error) {
217
+ console.error("Error processing GET /v1/models:", error);
218
+ res.status(500).json({ error: "Failed to retrieve model list." });
219
+ }
220
+ });
221
 
 
 
222
 
223
+ /**
224
+ * Converts OpenAI-style messages array to Fal AI's prompt and system_prompt format.
225
+ * Implements System prompt top-priority, separator, and recency-based history filling.
226
+ * Includes robustness checks for input validation and internal errors.
227
+ * @param {Array<object>} messages - Array of message objects ({ role: string, content: string|null }).
228
+ * @returns {object} An object containing { system_prompt: string, prompt: string }.
229
+ * @throws {Error} If input is fundamentally invalid (e.g., not an array) or an unexpected internal processing error occurs.
230
+ */
231
+ function convertMessagesToFalPrompt(messages) {
232
+ // --- Optional Debug Log: Uncomment to see the exact input causing issues ---
233
+ // console.log(">>> Entering convertMessagesToFalPrompt with messages:", JSON.stringify(messages, null, 2));
234
+
235
+ // --- Input Validation ---
236
+ if (!Array.isArray(messages)) {
237
+ console.error("!!! ERROR in convertMessagesToFalPrompt: Input 'messages' is not an array.");
238
+ // Throw an error here because this is a fundamental type mismatch from the caller.
239
+ throw new Error("Invalid input: 'messages' must be an array.");
240
+ }
241
+ if (messages.length === 0) {
242
+ console.warn("Warning in convertMessagesToFalPrompt: Input 'messages' array is empty.");
243
+ // Return empty strings if no messages, this is valid input.
244
+ return { system_prompt: "", prompt: "" };
245
+ }
246
+ // --- End Input Validation ---
247
+
248
+ // --- Main Processing Logic ---
249
+ try { // *** Wrap core logic in try...catch to handle internal errors ***
250
+ let fixed_system_prompt_content = "";
251
+ const conversation_message_blocks = [];
252
+ // console.log(`Original messages count: ${messages.length}`);
253
+
254
+ // 1. Separate System messages, format User/Assistant messages
255
+ for (const message of messages) {
256
+ // ** Validate individual message structure **
257
+ if (!message || typeof message !== 'object' || typeof message.role !== 'string') {
258
+ console.warn(`--> Skipping invalid message object in convertMessagesToFalPrompt: ${JSON.stringify(message)}`);
259
+ continue; // Skip this malformed message, proceed with others
260
+ }
261
+
262
+ // ** Safely handle content (null/undefined/non-string become empty string) **
263
+ let content = (message.content === null || message.content === undefined) ? "" : String(message.content);
264
+
265
+ switch (message.role) {
266
+ case 'system':
267
+ fixed_system_prompt_content += `System: ${content}\n\n`;
268
+ break;
269
+ case 'user':
270
+ conversation_message_blocks.push(`Human: ${content}\n\n`);
271
+ break;
272
+ case 'assistant':
273
+ conversation_message_blocks.push(`Assistant: ${content}\n\n`);
274
+ break;
275
+ default:
276
+ console.warn(`--> Unsupported role encountered in convertMessagesToFalPrompt: '${message.role}'. Skipping message.`);
277
+ continue; // Skip messages with unsupported roles
278
+ }
279
+ }
280
+
281
+ // 2. Truncate combined system messages if they exceed the limit
282
+ if (fixed_system_prompt_content.length > SYSTEM_PROMPT_LIMIT) {
283
+ const originalLength = fixed_system_prompt_content.length;
284
+ fixed_system_prompt_content = fixed_system_prompt_content.substring(0, SYSTEM_PROMPT_LIMIT);
285
+ console.warn(`Combined system messages truncated from ${originalLength} to ${SYSTEM_PROMPT_LIMIT} characters.`);
286
+ }
287
+ fixed_system_prompt_content = fixed_system_prompt_content.trim();
288
+
289
+ // 3. Calculate remaining space in system_prompt for history
290
+ let space_occupied_by_fixed_system = 0;
291
+ if (fixed_system_prompt_content.length > 0) {
292
+ space_occupied_by_fixed_system = fixed_system_prompt_content.length + 4; // Approx for spacing
293
+ }
294
+ const remaining_system_limit = Math.max(0, SYSTEM_PROMPT_LIMIT - space_occupied_by_fixed_system);
295
+
296
+ // 4. Fill history backwards (recency): Prioritize 'prompt', then 'system_prompt' overflow
297
+ const prompt_history_blocks = [];
298
+ const system_prompt_history_blocks = [];
299
+ let current_prompt_length = 0;
300
+ let current_system_history_length = 0;
301
+ let promptFull = (PROMPT_LIMIT <= 0);
302
+ let systemHistoryFull = (remaining_system_limit <= 0);
303
+
304
+ for (let i = conversation_message_blocks.length - 1; i >= 0; i--) {
305
+ const message_block = conversation_message_blocks[i];
306
+ const block_length = (typeof message_block === 'string') ? message_block.length : 0; // Ensure it's a string
307
+
308
+ if (block_length === 0) continue;
309
+ if (promptFull && systemHistoryFull) break;
310
+
311
+ // Try fitting into the main 'prompt' first
312
+ if (!promptFull) {
313
+ if (current_prompt_length + block_length <= PROMPT_LIMIT) {
314
+ prompt_history_blocks.unshift(message_block);
315
+ current_prompt_length += block_length;
316
+ continue;
317
+ } else {
318
+ promptFull = true;
319
+ }
320
+ }
321
+
322
+ // If prompt is full, try fitting into the 'system_prompt' remaining space
323
+ if (!systemHistoryFull) {
324
+ if (current_system_history_length + block_length <= remaining_system_limit) {
325
+ system_prompt_history_blocks.unshift(message_block);
326
+ current_system_history_length += block_length;
327
+ continue;
328
+ } else {
329
+ systemHistoryFull = true;
330
+ }
331
+ }
332
+ }
333
+
334
+ // 5. Combine the final prompt and system_prompt parts
335
+ const system_prompt_history_content = system_prompt_history_blocks.join('').trim();
336
+ const final_prompt = prompt_history_blocks.join('').trim();
337
+ const SEPARATOR = "\n\n------- Earlier Conversation History -------\n\n";
338
+ let final_system_prompt = "";
339
+ const hasFixedSystem = fixed_system_prompt_content.length > 0;
340
+ const hasSystemHistory = system_prompt_history_content.length > 0;
341
+
342
+ if (hasFixedSystem && hasSystemHistory) {
343
+ final_system_prompt = fixed_system_prompt_content + SEPARATOR + system_prompt_history_content;
344
+ } else if (hasFixedSystem) {
345
+ final_system_prompt = fixed_system_prompt_content;
346
+ } else if (hasSystemHistory) {
347
+ final_system_prompt = system_prompt_history_content;
348
+ }
349
+
350
+ // 6. ** Crucially, always return an object **
351
+ const result = {
352
+ system_prompt: final_system_prompt,
353
+ prompt: final_prompt
354
+ };
355
+ // console.log("<<< Exiting convertMessagesToFalPrompt successfully."); // Optional success log
356
+ return result;
357
+
358
+ } catch (internalError) { // *** Catch any unexpected errors during processing ***
359
+ console.error("!!! CRITICAL ERROR inside convertMessagesToFalPrompt processing:", internalError);
360
+ // Log the input that caused the error for debugging
361
+ console.error("!!! Failing messages input was:", JSON.stringify(messages, null, 2));
362
+ // Re-throw the error so the main handler knows something went wrong during setup.
363
+ // This will be caught by the outer try...catch in the route handler.
364
+ throw new Error(`Failed to process messages internally: ${internalError.message}`);
365
+ }
366
+ }
367
+ // === End convertMessagesToFalPrompt function ===
368
 
369
 
370
  /**
371
+ * Makes a request to the Fal AI API, handling key rotation and retries on key-related errors.
372
  * For stream requests, returns the stream AND the key info used.
373
  * @param {object} falInput - The input object for the Fal AI API call.
374
  * @param {boolean} [stream=false] - Whether to make a streaming request.
 
402
  if (stream) {
403
  const falStream = await fal.stream("fal-ai/any-llm", { input: falInput });
404
  console.log(`Successfully initiated stream with key index ${keyInfo.index}.`);
405
+ // Return stream AND key info
406
  return { stream: falStream, keyUsed: keyInfo.key, indexUsed: keyInfo.index };
407
  } else {
408
+ // Non-stream logic
409
  console.log(`Executing non-stream request with key index ${keyInfo.index}...`);
410
  const result = await fal.subscribe("fal-ai/any-llm", { input: falInput, logs: true });
411
  console.log(`Successfully received non-stream result with key index ${keyInfo.index}.`);
 
422
  return result; // Return only result for non-stream
423
  }
424
  } catch (error) {
425
+ // This catch block handles errors during *request initiation*
426
  console.error(`Error caught during request initiation using Fal Key index ${keyInfo.index}:`, error.message || error);
427
  if (isKeyRelatedError(error)) {
428
  console.warn(`Marking Fal Key index ${keyInfo.index} as invalid due to caught initiation error.`);
 
445
  const { model, messages, stream = false, reasoning = false, ...restOpenAIParams } = req.body;
446
  console.log(`--> POST /v1/chat/completions | Model: ${model} | Stream: ${stream}`);
447
 
448
+ // --- Input validation for model and messages ---
449
  if (!model || !messages || !Array.isArray(messages) || messages.length === 0) {
450
  console.error("Invalid request: Missing 'model' or 'messages' array is empty/invalid.");
451
  return res.status(400).json({ error: 'Bad Request: `model` and a non-empty `messages` array are required.' });
452
  }
453
+ if (!FAL_SUPPORTED_MODELS.includes(model)) {
454
+ console.warn(`Warning: Requested model '${model}' is not in the explicitly supported list. Proxy will still attempt the request.`);
455
+ }
456
+ // --- End Input Validation ---
457
 
458
+ let keyUsedForRequest = null; // Variable to store the key used if initiation succeeds
459
  let indexUsedForRequest = null;
460
 
461
  try {
462
+ // --- Prepare Fal AI Input ---
463
+ // This might throw an error if convertMessagesToFalPrompt fails internally
464
  const { prompt, system_prompt } = convertMessagesToFalPrompt(messages);
465
+
466
+ const falInput = {
467
+ model: model,
468
+ prompt: prompt,
469
+ reasoning: !!reasoning,
470
+ };
471
  if (system_prompt && system_prompt.length > 0) {
472
  falInput.system_prompt = system_prompt;
473
  }
474
+ // --- End Prepare Input ---
475
 
476
  console.log("Attempting Fal request with key rotation/retry logic...");
477
  console.log(`Prepared Input Lengths - System Prompt: ${system_prompt?.length || 0}, Prompt: ${prompt?.length || 0}`);
478
 
479
+ // --- Handle Stream vs Non-Stream ---
480
  if (stream) {
481
+ // Set headers for Server-Sent Events (SSE)
482
  res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
 
483
  res.setHeader('Cache-Control', 'no-cache');
484
  res.setHeader('Connection', 'keep-alive');
485
+ res.setHeader('Access-Control-Allow-Origin', '*'); // Adjust CORS for production if needed
486
+ res.flushHeaders(); // Send headers immediately
487
 
488
+ let previousOutput = ''; // Track previous output for delta calculation
489
  let streamResult; // To hold the object { stream, keyUsed, indexUsed }
490
 
491
  try {
492
+ // **Initiate the stream using the retry helper**
493
+ // This can throw if initiation fails after all retries
494
  streamResult = await makeFalRequestWithRetry(falInput, true);
495
  const falStream = streamResult.stream;
496
  keyUsedForRequest = streamResult.keyUsed; // Store the key used for this stream
 
498
 
499
  // Process the stream events asynchronously
500
  for await (const event of falStream) {
501
+ // Safely extract data from the event
502
  const currentOutput = (event && typeof event.output === 'string') ? event.output : '';
503
  const isPartial = (event && typeof event.partial === 'boolean') ? event.partial : true;
504
  const errorInfo = (event && event.error) ? event.error : null;
505
 
506
+ // Handle errors reported *within* a stream event payload
507
  if (errorInfo) {
 
508
  console.error("Error received *within* fal stream event payload:", errorInfo);
509
+ const errorChunk = {
510
+ id: `chatcmpl-${Date.now()}-error`, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: model,
511
+ choices: [{ index: 0, delta: {}, finish_reason: "error", message: { role: 'assistant', content: `Fal Stream Event Error: ${JSON.stringify(errorInfo)}` } }]
512
+ };
513
  if (!res.writableEnded) { res.write(`data: ${JSON.stringify(errorChunk)}\n\n`); }
514
  else { console.warn("Stream ended before writing event error."); }
515
+ // Potentially break or add logic based on error type here
516
  }
517
+
518
+ // Calculate the delta (new content)
519
  let deltaContent = '';
520
  if (currentOutput.startsWith(previousOutput)) {
521
  deltaContent = currentOutput.substring(previousOutput.length);
 
525
  previousOutput = '';
526
  }
527
  previousOutput = currentOutput;
528
+
529
+ // Send OpenAI-compatible SSE chunk
530
  if (deltaContent || !isPartial) {
531
+ const openAIChunk = {
532
+ id: `chatcmpl-${Date.now()}`,
533
+ object: "chat.completion.chunk",
534
+ created: Math.floor(Date.now() / 1000),
535
+ model: model,
536
+ choices: [{
537
+ index: 0,
538
+ delta: { content: deltaContent },
539
+ finish_reason: isPartial === false ? "stop" : null
540
+ }]
541
+ };
542
  if (!res.writableEnded) { res.write(`data: ${JSON.stringify(openAIChunk)}\n\n`); }
543
  else { console.warn("Stream ended before writing data chunk."); }
544
  }
 
545
  } // End for-await loop
546
 
547
+ // Send the final [DONE] marker
548
  if (!res.writableEnded) {
549
  res.write(`data: [DONE]\n\n`);
550
+ res.end(); // Close the connection
551
  console.log("<-- Stream finished successfully and [DONE] sent.");
552
  } else {
553
+ console.log("<-- Stream processing finished, but connection was already ended before [DONE].");
554
  }
555
 
556
  } catch (streamError) {
557
+ // **Catch block for errors during stream processing OR initiation failure**
558
+ console.error('Error during stream request processing/initiation:', streamError.message || streamError);
559
+
560
+ // **Check if the error is key-related AND if initiation succeeded**
561
+ // This ensures we only invalidate the key if the error happened *during* processing
562
+ // using a key that successfully initiated the stream.
 
 
 
 
563
  if (keyUsedForRequest && isKeyRelatedError(streamError)) {
564
  console.warn(`Marking Fal Key index ${indexUsedForRequest} as invalid due to error during stream processing.`);
565
  invalidKeys.add(keyUsedForRequest);
566
  }
567
+ // else: Error was either non-key-related or happened during initiation (already handled/logged in makeFalRequestWithRetry).
568
 
569
+ // --- Report error back to the client ---
570
  try {
571
  if (!res.headersSent) {
572
  // Error likely during initiation (caught from makeFalRequestWithRetry)
 
577
  // Error during stream processing after headers sent
578
  const errorDetails = (streamError instanceof Error) ? streamError.message : JSON.stringify(streamError);
579
  res.write(`data: ${JSON.stringify({ error: { message: "Stream processing error after initiation", type: "proxy_error", details: errorDetails } })}\n\n`);
580
+ res.write(`data: [DONE]\n\n`); // Still send DONE for robust client handling
581
  res.end();
582
  console.log("<-- Stream error sent within stream, stream ended.");
583
  } else {
 
587
  console.error('Error sending stream error message to client:', finalError);
588
  if (!res.writableEnded) { res.end(); }
589
  }
590
+ // --- End error reporting ---
591
  }
592
 
593
  } else {
594
+ // --- Non-Stream Request ---
595
  try {
596
+ // Get the result using the retry helper
597
  const result = await makeFalRequestWithRetry(falInput, false);
598
+
599
+ // Construct OpenAI compatible response
600
+ const openAIResponse = {
601
+ id: `chatcmpl-${result.requestId || Date.now()}`,
602
+ object: "chat.completion",
603
+ created: Math.floor(Date.now() / 1000),
604
+ model: model,
605
+ choices: [{
606
+ index: 0,
607
+ message: {
608
+ role: "assistant",
609
+ content: result.output || "" // Ensure content is string
610
+ },
611
+ finish_reason: "stop"
612
+ }],
613
+ usage: { prompt_tokens: null, completion_tokens: null, total_tokens: null },
614
+ system_fingerprint: null,
615
+ ...(result.reasoning && { fal_reasoning: result.reasoning }),
616
+ };
617
 
618
  res.json(openAIResponse);
619
  console.log("<-- Non-stream response sent successfully.");
620
+
621
  } catch (error) {
622
+ // Catches errors from makeFalRequestWithRetry (e.g., all keys failed or non-key error)
623
+ console.error('Error during non-stream request processing:', error.message || error);
624
  if (!res.headersSent) {
625
  const errorMessage = (error instanceof Error) ? error.message : JSON.stringify(error);
626
  const finalMessage = errorMessage.includes("No valid Fal AI keys available") || errorMessage.includes("Request failed after trying")
 
633
  if (!res.writableEnded) { res.end(); }
634
  }
635
  }
636
+ } // --- End Stream/Non-Stream Logic ---
637
 
638
  } catch (error) {
639
+ // **Catch block for errors BEFORE Fal request attempt**
640
+ // (e.g., errors from convertMessagesToFalPrompt, JSON parsing errors)
641
  console.error('Unhandled error before initiating Fal request (likely setup or input conversion):', error.message || error);
642
  if (!res.headersSent) {
643
  const errorMessage = (error instanceof Error) ? error.message : JSON.stringify(error);
644
+ // Use 500 Internal Server Error for issues within the proxy itself during setup
645
  res.status(500).json({ error: 'Internal Server Error in Proxy Setup', details: errorMessage });
646
  console.log("<-- Proxy setup error response sent (500).");
647
  } else {
 
651
  }
652
  });
653
 
654
+ // Start the Express server
655
+ app.listen(PORT, () => {
656
+ console.log(`=====================================================================`);
657
+ console.log(` Fal OpenAI Proxy Server (Multi-Key Rotation & Failover)`);
658
+ console.log(`---------------------------------------------------------------------`);
659
+ console.log(` Listening on port : ${PORT}`);
660
+ console.log(` Reading Fal Keys from : FAL_KEY environment variable (comma-separated)`);
661
+ console.log(` Loaded Keys Count : ${falKeys.length}`);
662
+ console.log(` Invalid Keys Set : Initialized (size: ${invalidKeys.size})`);
663
+ console.log(` Proxy API Key Auth : ${API_KEY ? 'Enabled (using API_KEY env var)' : 'DISABLED'}`);
664
+ console.log(` Input Limits : System Prompt=${SYSTEM_PROMPT_LIMIT}, Prompt=${PROMPT_LIMIT}`);
665
+ console.log(` Concurrency Warning : Global Fal client reconfigured per request attempt!`);
666
+ console.log(`---------------------------------------------------------------------`);
667
+ console.log(` Endpoints Available:`);
668
+ console.log(` POST http://localhost:${PORT}/v1/chat/completions`);
669
+ console.log(` GET http://localhost:${PORT}/v1/models`);
670
+ console.log(`=====================================================================`);
671
+ });
672
+
673
+ // Root path handler for basic health check / info
674
+ app.get('/', (req, res) => {
675
+ res.send(`Fal OpenAI Proxy (Multi-Key Rotation/Failover from FAL_KEY) is running. Loaded ${falKeys.length} key(s). Currently ${invalidKeys.size} key(s) marked as invalid.`);
676
+ });