ezeinet commited on
Commit
4370b4d
·
verified ·
1 Parent(s): 98572f6

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +140 -255
server.js CHANGED
@@ -1,101 +1,49 @@
1
  import express from 'express';
2
  import { fal } from '@fal-ai/client';
3
 
4
- // --- Multi-Key Configuration ---
5
- // *** 使用 FAL_KEY 环境变量读取逗号分隔的密钥 ***
6
- const rawFalKeys = process.env.FAL_KEY; // Expect comma-separated keys: key1,key2,key3 in FAL_KEY
7
- const API_KEY = process.env.API_KEY; // Custom API Key for proxy auth remains the same
8
-
9
- if (!rawFalKeys) {
10
- // *** 更新错误信息以引用 FAL_KEY ***
11
- console.error("Error: FAL_KEY environment variable is not set (should be comma-separated).");
12
- process.exit(1);
13
- }
14
-
15
- if (!API_KEY) {
16
- console.error("Error: API_KEY environment variable is not set.");
17
- process.exit(1);
18
- }
19
-
20
- // Parse and prepare the keys
21
- let falKeys = rawFalKeys.split(',')
22
- .map(key => key.trim())
23
- .filter(key => key.length > 0)
24
- .map(key => ({
25
- key: key,
26
- failed: false, // Track if the key is currently considered failed
27
- failedTimestamp: 0 // Timestamp when the key was marked as failed
28
- }));
29
-
30
- if (falKeys.length === 0) {
31
- // *** 更新错误信息以引用 FAL_KEY ***
32
- console.error("Error: No valid keys found in FAL_KEY after processing the environment variable.");
33
- process.exit(1);
34
- }
35
-
36
- let currentKeyIndex = 0;
37
- const failedKeyCooldown = 60 * 1000; // Cooldown period in milliseconds (e.g., 60 seconds) before retrying a failed key
38
-
39
- // *** 更新日志信息以引用 FAL_KEY ***
40
- console.log(`Loaded ${falKeys.length} FAL API Key(s) from FAL_KEY environment variable.`);
41
- console.log(`Failed key cooldown period: ${failedKeyCooldown / 1000} seconds.`);
42
-
43
- // NOTE: We will configure fal client per request now, so initial global config is removed.
44
 
45
- // --- Key Management Functions ---
46
 
47
- /**
48
- * Selects the next available FAL key using round-robin and skipping recently failed keys.
49
- * @returns {object | null} Key info object { key, failed, failedTimestamp } or null if all keys are failed.
50
- */
51
- function getNextKey() {
52
- const totalKeys = falKeys.length;
53
- if (totalKeys === 0) return null;
54
-
55
- let attempts = 0;
56
- while (attempts < totalKeys) {
57
- const keyIndex = currentKeyIndex % totalKeys;
58
- const keyInfo = falKeys[keyIndex];
59
- // Increment index for the *next* call, ensuring round-robin
60
- currentKeyIndex = (currentKeyIndex + 1) % totalKeys;
61
-
62
- // Check if key is marked as failed and if cooldown has passed
63
- if (keyInfo.failed) {
64
- const now = Date.now();
65
- if (now - keyInfo.failedTimestamp < failedKeyCooldown) {
66
- // console.log(`Key index ${keyIndex} is in cooldown. Skipping.`);
67
- attempts++;
68
- continue; // Skip this key, it's still in cooldown
69
- } else {
70
- console.log(`Cooldown finished for key index ${keyIndex}. Resetting failure status.`);
71
- keyInfo.failed = false; // Cooldown expired, reset status
72
- keyInfo.failedTimestamp = 0;
73
- }
74
- }
75
- // console.log(`Selected key index: ${keyIndex}`);
76
- return keyInfo; // Return the valid key info object
77
- }
78
 
79
- console.warn("All FAL keys are currently marked as failed and in cooldown.");
80
- return null; // All keys are currently failed and within cooldown
81
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- /**
84
- * Marks a specific key as failed.
85
- * @param {object} keyInfo - The key info object to mark as failed.
86
- */
87
- function markKeyFailed(keyInfo) {
88
- if (keyInfo && !keyInfo.failed) { // Only mark if not already marked
89
- keyInfo.failed = true;
90
- keyInfo.failedTimestamp = Date.now();
91
- const keyIndex = falKeys.findIndex(k => k.key === keyInfo.key);
92
- console.warn(`Marking key index ${keyIndex} (ending ...${keyInfo.key.slice(-4)}) as failed.`);
93
  }
94
- }
 
95
 
96
  /**
97
  * Determines if an error likely indicates an API key issue (auth, quota, etc.).
98
- * This needs refinement based on actual errors from fal.ai.
99
  * @param {Error} error - The error object caught from the fal client.
100
  * @returns {boolean} - True if the error suggests a key failure, false otherwise.
101
  */
@@ -115,19 +63,10 @@ function isKeyRelatedError(error) {
115
  errorMessage.includes('quota exceeded')) {
116
  return true;
117
  }
118
- // Add more specific error messages or codes from fal.ai if known
119
- // console.log("Error does not appear to be key-related:", error); // Debugging
120
  return false;
121
  }
122
 
123
- // --- Express App Setup ---
124
- const app = express();
125
- app.use(express.json({ limit: '50mb' }));
126
- app.use(express.urlencoded({ extended: true, limit: '50mb' }));
127
-
128
- const PORT = process.env.PORT || 3000;
129
-
130
- // API Key 鉴权中间件 (Remains the same, checks custom API_KEY)
131
  const apiKeyAuth = (req, res, next) => {
132
  const authHeader = req.headers['authorization'];
133
 
@@ -143,50 +82,19 @@ const apiKeyAuth = (req, res, next) => {
143
  }
144
 
145
  const providedKey = authParts[1];
146
- if (providedKey !== API_KEY) {
147
- console.warn('Unauthorized: Invalid API Key');
148
- return res.status(401).json({ error: 'Unauthorized: Invalid API Key' });
149
  }
150
 
 
 
 
151
  next();
152
  };
153
 
154
  app.use(['/v1/models', '/v1/chat/completions'], apiKeyAuth);
155
 
156
- // === 全局定义限制 === (Remains the same)
157
- const PROMPT_LIMIT = 4800;
158
- const SYSTEM_PROMPT_LIMIT = 4800;
159
- // === 限制定义结束 ===
160
-
161
- // 定义 fal-ai/any-llm 支持的模型列表 (Remains the same)
162
- const FAL_SUPPORTED_MODELS = [
163
- "anthropic/claude-3.7-sonnet",
164
- "anthropic/claude-3.5-sonnet",
165
- "anthropic/claude-3-5-haiku",
166
- "anthropic/claude-3-haiku",
167
- "google/gemini-pro-1.5",
168
- "google/gemini-flash-1.5",
169
- "google/gemini-flash-1.5-8b",
170
- "google/gemini-2.0-flash-001",
171
- "meta-llama/llama-3.2-1b-instruct",
172
- "meta-llama/llama-3.2-3b-instruct",
173
- "meta-llama/llama-3.1-8b-instruct",
174
- "meta-llama/llama-3.1-70b-instruct",
175
- "openai/gpt-4o-mini",
176
- "openai/gpt-4o",
177
- "deepseek/deepseek-r1",
178
- "meta-llama/llama-4-maverick",
179
- "meta-llama/llama-4-scout"
180
- ];
181
-
182
- // Helper function to get owner from model ID (Remains the same)
183
- const getOwner = (modelId) => {
184
- if (modelId && modelId.includes('/')) {
185
- return modelId.split('/')[0];
186
- }
187
- return 'fal-ai';
188
- };
189
-
190
  // GET /v1/models endpoint (Remains the same)
191
  app.get('/v1/models', (req, res) => {
192
  console.log("Received request for GET /v1/models");
@@ -323,74 +231,53 @@ function convertMessagesToFalPrompt(messages) {
323
  }
324
  // === convertMessagesToFalPrompt 函数结束 ===
325
 
326
-
327
  /**
328
- * Wraps the fal.ai API call with retry logic using available keys.
329
  * @param {'stream' | 'subscribe'} operation - The fal operation to perform.
330
  * @param {string} functionId - The fal function ID (e.g., "fal-ai/any-llm").
331
  * @param {object} params - The parameters for the fal function call (input, logs, etc.).
332
- * @returns {Promise<any>} - The result from the successful fal call (stream or subscription result).
333
- * @throws {Error} - Throws an error if all keys fail or a non-key-related error occurs.
 
334
  */
335
- async function tryFalCallWithFailover(operation, functionId, params) {
336
- const maxRetries = falKeys.length; // Try each key at most once per request cycle
337
- let lastError = null;
338
-
339
- for (let i = 0; i < maxRetries; i++) {
340
- const keyInfo = getNextKey();
341
- if (!keyInfo) {
342
- throw new Error(lastError ? `All FAL keys failed. Last error: ${lastError.message}` : "All FAL keys are currently unavailable (failed or in cooldown).");
343
- }
344
-
345
- const currentFalKey = keyInfo.key;
346
- console.log(`Attempt ${i + 1}/${maxRetries}: Using key ending in ...${currentFalKey.slice(-4)}`);
347
-
348
- try {
349
- // --- Configure fal client with the selected key for this attempt ---
350
- // WARNING: This global config change might have concurrency issues in high-load scenarios
351
- // if the fal client library doesn't isolate requests properly.
352
- fal.config({ credentials: currentFalKey });
353
-
354
- if (operation === 'stream') {
355
- const streamResult = await fal.stream(functionId, params);
356
- console.log(`Successfully initiated stream with key ending in ...${currentFalKey.slice(-4)}`);
357
- return streamResult;
358
- } else { // 'subscribe' (non-stream)
359
- const result = await fal.subscribe(functionId, params);
360
- console.log(`Successfully completed subscribe request with key ending in ...${currentFalKey.slice(-4)}`);
361
-
362
- if (result && result.error) {
363
- console.warn(`Fal-ai returned an application error (non-stream) with key ...${currentFalKey.slice(-4)}: ${JSON.stringify(result.error)}`);
364
- }
365
- return result;
366
- }
367
- } catch (error) {
368
- console.error(`Error using key ending in ...${currentFalKey.slice(-4)}:`, error.message || error);
369
- lastError = error;
370
-
371
- if (isKeyRelatedError(error)) {
372
- markKeyFailed(keyInfo);
373
- console.log(`Key marked as failed. Trying next key if available...`);
374
- } else {
375
- console.error("Non-key related error occurred. Aborting retries.");
376
- throw error;
377
  }
 
378
  }
 
 
 
 
 
 
 
 
379
  }
380
-
381
- console.error("All FAL keys failed after attempting each one.");
382
- throw new Error(lastError ? `All FAL keys failed. Last error: ${lastError.message}` : "All FAL API keys failed.");
383
  }
384
 
385
-
386
- // POST /v1/chat/completions endpoint (Modified to use tryFalCallWithFailover)
387
  app.post('/v1/chat/completions', async (req, res) => {
388
  const { model, messages, stream = false, reasoning = false, ...restOpenAIParams } = req.body;
 
389
 
390
  console.log(`Received chat completion request for model: ${model}, stream: ${stream}`);
391
 
392
  if (!FAL_SUPPORTED_MODELS.includes(model)) {
393
- console.warn(`Warning: Requested model '${model}' is not in the explicitly supported list.`);
394
  }
395
  if (!model || !messages || !Array.isArray(messages) || messages.length === 0) {
396
  console.error("Invalid request parameters:", { model, messages: Array.isArray(messages) ? messages.length : typeof messages });
@@ -420,9 +307,9 @@ app.post('/v1/chat/completions', async (req, res) => {
420
  let falStream;
421
 
422
  try {
423
- falStream = await tryFalCallWithFailover('stream', "fal-ai/any-llm", { input: falInput });
424
 
425
- for await (const event of falStream) {
426
  const currentOutput = (event && typeof event.output === 'string') ? event.output : '';
427
  const isPartial = (event && typeof event.partial === 'boolean') ? event.partial : true;
428
  const errorInfo = (event && event.error) ? event.error : null;
@@ -448,84 +335,84 @@ app.post('/v1/chat/completions', async (req, res) => {
448
  const openAIChunk = { id: `chatcmpl-${Date.now()}`, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: model, choices: [{ index: 0, delta: { content: deltaContent }, finish_reason: isPartial === false ? "stop" : null }] };
449
  res.write(`data: ${JSON.stringify(openAIChunk)}\n\n`);
450
  }
451
- }
452
- res.write(`data: [DONE]\n\n`);
453
- res.end();
454
- console.log("Stream finished successfully.");
455
 
456
  } catch (streamError) {
457
  console.error('Error during stream processing:', streamError);
458
  if (!res.writableEnded) {
459
- try {
460
- const errorDetails = (streamError instanceof Error) ? streamError.message : JSON.stringify(streamError);
461
- const finalErrorChunk = { error: { message: "Stream failed", type: "proxy_error", details: errorDetails } };
462
- res.write(`data: ${JSON.stringify(finalErrorChunk)}\n\n`);
463
- res.write(`data: [DONE]\n\n`);
464
- res.end();
465
- } catch (finalError) {
466
- console.error('Error sending final stream error message to client:', finalError);
467
- if (!res.writableEnded) { res.end(); }
468
- }
469
- }
470
  }
471
 
472
  } else { // Non-stream
473
- console.log("Executing non-stream request with failover...");
474
- const result = await tryFalCallWithFailover('subscribe', "fal-ai/any-llm", { input: falInput, logs: true });
475
-
476
- console.log("Received non-stream result from fal-ai via failover wrapper.");
477
-
478
- if (result && result.error) {
479
- console.error("Fal-ai returned an application error in non-stream mode (after successful API call):", result.error);
480
- return res.status(500).json({
481
- object: "error",
482
- message: `Fal-ai application error: ${JSON.stringify(result.error)}`,
483
- type: "fal_ai_error",
484
- param: null,
485
- code: result.error.code || null
486
- });
487
- }
488
-
489
- const openAIResponse = {
490
- id: `chatcmpl-${result?.requestId || Date.now()}`,
491
- object: "chat.completion",
492
- created: Math.floor(Date.now() / 1000),
493
- model: model,
494
- choices: [{
495
- index: 0,
496
- message: {
497
- role: "assistant",
498
- content: result?.output || ""
499
- },
500
- finish_reason: "stop"
501
- }],
502
- usage: {
503
- prompt_tokens: null,
504
- completion_tokens: null,
505
- total_tokens: null
506
- },
507
- system_fingerprint: null,
508
- ...(result?.reasoning && { fal_reasoning: result.reasoning }),
509
- };
510
- res.json(openAIResponse);
511
- console.log("Returned non-stream response successfully.");
512
  }
513
 
514
  } catch (error) {
515
  console.error('Unhandled error in /v1/chat/completions:', error);
516
  if (!res.headersSent) {
517
  const errorMessage = (error instanceof Error) ? error.message : JSON.stringify(error);
518
- const errorType = error.message?.includes("All FAL keys failed") ? "api_key_error" : "proxy_internal_error";
519
  res.status(500).json({
520
- error: {
521
- message: `Internal Server Error in Proxy: ${errorMessage}`,
522
- type: errorType,
523
- details: error.stack // Optional: include stack in dev/debug mode
524
- }
525
  });
526
  } else if (!res.writableEnded) {
527
- console.error("Headers already sent, attempting to end response after error.");
528
- res.end();
529
  }
530
  }
531
  });
@@ -533,11 +420,9 @@ app.post('/v1/chat/completions', async (req, res) => {
533
  // --- Server Start ---
534
  app.listen(PORT, () => {
535
  console.log(`===========================================================`);
536
- console.log(` Fal OpenAI Proxy Server (Multi-Key Failover)`);
537
  console.log(` Listening on port: ${PORT}`);
538
- // *** 更新日志信息以引用 FAL_KEY ***
539
- console.log(` Loaded ${falKeys.length} FAL API Key(s) from FAL_KEY.`);
540
- console.log(` API Key Auth Enabled: ${API_KEY ? 'Yes' : 'No'}`);
541
  console.log(` Limits: System Prompt=${SYSTEM_PROMPT_LIMIT}, Prompt=${PROMPT_LIMIT}`);
542
  console.log(` Chat Completions: POST http://localhost:${PORT}/v1/chat/completions`);
543
  console.log(` Models Endpoint: GET http://localhost:${PORT}/v1/models`);
@@ -546,5 +431,5 @@ app.listen(PORT, () => {
546
 
547
  // Root path response
548
  app.get('/', (req, res) => {
549
- res.send('Fal OpenAI Proxy (Multi-Key Failover) is running.');
550
  });
 
1
  import express from 'express';
2
  import { fal } from '@fal-ai/client';
3
 
4
+ // --- Express App Setup ---
5
+ const app = express();
6
+ app.use(express.json({ limit: '50mb' }));
7
+ app.use(express.urlencoded({ extended: true, limit: '50mb' }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
+ const PORT = process.env.PORT || 3000;
10
 
11
+ // === 全局定义限制 === (Remains the same)
12
+ const PROMPT_LIMIT = 4800;
13
+ const SYSTEM_PROMPT_LIMIT = 4800;
14
+ // === 限制定义结束 ===
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ // 定义 fal-ai/any-llm 支持的模型列表 (Remains the same)
17
+ const FAL_SUPPORTED_MODELS = [
18
+ "anthropic/claude-3.7-sonnet",
19
+ "anthropic/claude-3.5-sonnet",
20
+ "anthropic/claude-3-5-haiku",
21
+ "anthropic/claude-3-haiku",
22
+ "google/gemini-pro-1.5",
23
+ "google/gemini-flash-1.5",
24
+ "google/gemini-flash-1.5-8b",
25
+ "google/gemini-2.0-flash-001",
26
+ "meta-llama/llama-3.2-1b-instruct",
27
+ "meta-llama/llama-3.2-3b-instruct",
28
+ "meta-llama/llama-3.1-8b-instruct",
29
+ "meta-llama/llama-3.1-70b-instruct",
30
+ "openai/gpt-4o-mini",
31
+ "openai/gpt-4o",
32
+ "deepseek/deepseek-r1",
33
+ "meta-llama/llama-4-maverick",
34
+ "meta-llama/llama-4-scout"
35
+ ];
36
 
37
+ // Helper function to get owner from model ID (Remains the same)
38
+ const getOwner = (modelId) => {
39
+ if (modelId && modelId.includes('/')) {
40
+ return modelId.split('/')[0];
 
 
 
 
 
 
41
  }
42
+ return 'fal-ai';
43
+ };
44
 
45
  /**
46
  * Determines if an error likely indicates an API key issue (auth, quota, etc.).
 
47
  * @param {Error} error - The error object caught from the fal client.
48
  * @returns {boolean} - True if the error suggests a key failure, false otherwise.
49
  */
 
63
  errorMessage.includes('quota exceeded')) {
64
  return true;
65
  }
 
 
66
  return false;
67
  }
68
 
69
+ // API Key 鉴权中间件 (Modified to extract FAL key directly)
 
 
 
 
 
 
 
70
  const apiKeyAuth = (req, res, next) => {
71
  const authHeader = req.headers['authorization'];
72
 
 
82
  }
83
 
84
  const providedKey = authParts[1];
85
+ if (!providedKey || providedKey.trim() === '') {
86
+ console.warn('Unauthorized: Empty API Key');
87
+ return res.status(401).json({ error: 'Unauthorized: Empty API Key' });
88
  }
89
 
90
+ // Store the FAL key in the request object for later use
91
+ req.falKey = providedKey;
92
+ console.log(`Received request with FAL key ending in ...${providedKey.slice(-4)}`);
93
  next();
94
  };
95
 
96
  app.use(['/v1/models', '/v1/chat/completions'], apiKeyAuth);
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  // GET /v1/models endpoint (Remains the same)
99
  app.get('/v1/models', (req, res) => {
100
  console.log("Received request for GET /v1/models");
 
231
  }
232
  // === convertMessagesToFalPrompt 函数结束 ===
233
 
 
234
  /**
235
+ * Makes a call to the fal.ai API using the provided key.
236
  * @param {'stream' | 'subscribe'} operation - The fal operation to perform.
237
  * @param {string} functionId - The fal function ID (e.g., "fal-ai/any-llm").
238
  * @param {object} params - The parameters for the fal function call (input, logs, etc.).
239
+ * @param {string} falKey - The FAL API key to use for this request.
240
+ * @returns {Promise<any>} - The result from the fal call (stream or subscription result).
241
+ * @throws {Error} - Throws an error if the call fails.
242
  */
243
+ async function callFalApi(operation, functionId, params, falKey) {
244
+ try {
245
+ // Configure fal client with the provided key
246
+ fal.config({ credentials: falKey });
247
+
248
+ if (operation === 'stream') {
249
+ const streamResult = await fal.stream(functionId, params);
250
+ console.log(`Successfully initiated stream with key ending in ...${falKey.slice(-4)}`);
251
+ return streamResult;
252
+ } else { // 'subscribe' (non-stream)
253
+ const result = await fal.subscribe(functionId, params);
254
+ console.log(`Successfully completed subscribe request with key ending in ...${falKey.slice(-4)}`);
255
+
256
+ if (result && result.error) {
257
+ console.warn(`Fal-ai returned an application error (non-stream) with key ...${falKey.slice(-4)}: ${JSON.stringify(result.error)}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  }
259
+ return result;
260
  }
261
+ } catch (error) {
262
+ console.error(`Error using key ending in ...${falKey.slice(-4)}:`, error.message || error);
263
+
264
+ if (isKeyRelatedError(error)) {
265
+ console.error(`Key-related error detected with key ending in ...${falKey.slice(-4)}`);
266
+ }
267
+
268
+ throw error;
269
  }
 
 
 
270
  }
271
 
272
+ // POST /v1/chat/completions endpoint (Modified to use client-provided key)
 
273
  app.post('/v1/chat/completions', async (req, res) => {
274
  const { model, messages, stream = false, reasoning = false, ...restOpenAIParams } = req.body;
275
+ const falKey = req.falKey; // Get the FAL key from the request object
276
 
277
  console.log(`Received chat completion request for model: ${model}, stream: ${stream}`);
278
 
279
  if (!FAL_SUPPORTED_MODELS.includes(model)) {
280
+ console.warn(`Warning: Requested model '${model}' is not in the explicitly supported list.`);
281
  }
282
  if (!model || !messages || !Array.isArray(messages) || messages.length === 0) {
283
  console.error("Invalid request parameters:", { model, messages: Array.isArray(messages) ? messages.length : typeof messages });
 
307
  let falStream;
308
 
309
  try {
310
+ falStream = await callFalApi('stream', "fal-ai/any-llm", { input: falInput }, falKey);
311
 
312
+ for await (const event of falStream) {
313
  const currentOutput = (event && typeof event.output === 'string') ? event.output : '';
314
  const isPartial = (event && typeof event.partial === 'boolean') ? event.partial : true;
315
  const errorInfo = (event && event.error) ? event.error : null;
 
335
  const openAIChunk = { id: `chatcmpl-${Date.now()}`, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: model, choices: [{ index: 0, delta: { content: deltaContent }, finish_reason: isPartial === false ? "stop" : null }] };
336
  res.write(`data: ${JSON.stringify(openAIChunk)}\n\n`);
337
  }
338
+ }
339
+ res.write(`data: [DONE]\n\n`);
340
+ res.end();
341
+ console.log("Stream finished successfully.");
342
 
343
  } catch (streamError) {
344
  console.error('Error during stream processing:', streamError);
345
  if (!res.writableEnded) {
346
+ try {
347
+ const errorDetails = (streamError instanceof Error) ? streamError.message : JSON.stringify(streamError);
348
+ const finalErrorChunk = { error: { message: "Stream failed", type: "proxy_error", details: errorDetails } };
349
+ res.write(`data: ${JSON.stringify(finalErrorChunk)}\n\n`);
350
+ res.write(`data: [DONE]\n\n`);
351
+ res.end();
352
+ } catch (finalError) {
353
+ console.error('Error sending final stream error message to client:', finalError);
354
+ if (!res.writableEnded) { res.end(); }
355
+ }
356
+ }
357
  }
358
 
359
  } else { // Non-stream
360
+ console.log("Executing non-stream request...");
361
+ const result = await callFalApi('subscribe', "fal-ai/any-llm", { input: falInput, logs: true }, falKey);
362
+
363
+ console.log("Received non-stream result from fal-ai.");
364
+
365
+ if (result && result.error) {
366
+ console.error("Fal-ai returned an application error in non-stream mode (after successful API call):", result.error);
367
+ return res.status(500).json({
368
+ object: "error",
369
+ message: `Fal-ai application error: ${JSON.stringify(result.error)}`,
370
+ type: "fal_ai_error",
371
+ param: null,
372
+ code: result.error.code || null
373
+ });
374
+ }
375
+
376
+ const openAIResponse = {
377
+ id: `chatcmpl-${result?.requestId || Date.now()}`,
378
+ object: "chat.completion",
379
+ created: Math.floor(Date.now() / 1000),
380
+ model: model,
381
+ choices: [{
382
+ index: 0,
383
+ message: {
384
+ role: "assistant",
385
+ content: result?.output || ""
386
+ },
387
+ finish_reason: "stop"
388
+ }],
389
+ usage: {
390
+ prompt_tokens: null,
391
+ completion_tokens: null,
392
+ total_tokens: null
393
+ },
394
+ system_fingerprint: null,
395
+ ...(result?.reasoning && { fal_reasoning: result.reasoning }),
396
+ };
397
+ res.json(openAIResponse);
398
+ console.log("Returned non-stream response successfully.");
399
  }
400
 
401
  } catch (error) {
402
  console.error('Unhandled error in /v1/chat/completions:', error);
403
  if (!res.headersSent) {
404
  const errorMessage = (error instanceof Error) ? error.message : JSON.stringify(error);
405
+ const errorType = isKeyRelatedError(error) ? "api_key_error" : "proxy_internal_error";
406
  res.status(500).json({
407
+ error: {
408
+ message: `Internal Server Error in Proxy: ${errorMessage}`,
409
+ type: errorType,
410
+ details: error.stack // Optional: include stack in dev/debug mode
411
+ }
412
  });
413
  } else if (!res.writableEnded) {
414
+ console.error("Headers already sent, attempting to end response after error.");
415
+ res.end();
416
  }
417
  }
418
  });
 
420
  // --- Server Start ---
421
  app.listen(PORT, () => {
422
  console.log(`===========================================================`);
423
+ console.log(` Fal OpenAI Proxy Server (Direct Key Mode)`);
424
  console.log(` Listening on port: ${PORT}`);
425
+ console.log(` Using client-provided FAL API keys directly`);
 
 
426
  console.log(` Limits: System Prompt=${SYSTEM_PROMPT_LIMIT}, Prompt=${PROMPT_LIMIT}`);
427
  console.log(` Chat Completions: POST http://localhost:${PORT}/v1/chat/completions`);
428
  console.log(` Models Endpoint: GET http://localhost:${PORT}/v1/models`);
 
431
 
432
  // Root path response
433
  app.get('/', (req, res) => {
434
+ res.send('Fal OpenAI Proxy (Direct Key Mode) is running.');
435
  });