Spaces:
Paused
Paused
Update server.js
Browse files
server.js
CHANGED
|
@@ -2,11 +2,13 @@ import express from 'express';
|
|
| 2 |
import { fal } from '@fal-ai/client';
|
| 3 |
|
| 4 |
// --- Multi-Key Configuration ---
|
| 5 |
-
|
|
|
|
| 6 |
const API_KEY = process.env.API_KEY; // Custom API Key for proxy auth remains the same
|
| 7 |
|
| 8 |
if (!rawFalKeys) {
|
| 9 |
-
|
|
|
|
| 10 |
process.exit(1);
|
| 11 |
}
|
| 12 |
|
|
@@ -26,18 +28,19 @@ let falKeys = rawFalKeys.split(',')
|
|
| 26 |
}));
|
| 27 |
|
| 28 |
if (falKeys.length === 0) {
|
| 29 |
-
|
|
|
|
| 30 |
process.exit(1);
|
| 31 |
}
|
| 32 |
|
| 33 |
let currentKeyIndex = 0;
|
| 34 |
const failedKeyCooldown = 60 * 1000; // Cooldown period in milliseconds (e.g., 60 seconds) before retrying a failed key
|
| 35 |
|
| 36 |
-
|
|
|
|
| 37 |
console.log(`Failed key cooldown period: ${failedKeyCooldown / 1000} seconds.`);
|
| 38 |
|
| 39 |
// NOTE: We will configure fal client per request now, so initial global config is removed.
|
| 40 |
-
// fal.config({ ... }); // Removed
|
| 41 |
|
| 42 |
// --- Key Management Functions ---
|
| 43 |
|
|
@@ -126,7 +129,6 @@ const PORT = process.env.PORT || 3000;
|
|
| 126 |
|
| 127 |
// API Key 鉴权中间件 (Remains the same, checks custom API_KEY)
|
| 128 |
const apiKeyAuth = (req, res, next) => {
|
| 129 |
-
// ... (Keep existing apiKeyAuth middleware code) ...
|
| 130 |
const authHeader = req.headers['authorization'];
|
| 131 |
|
| 132 |
if (!authHeader) {
|
|
@@ -157,7 +159,7 @@ const SYSTEM_PROMPT_LIMIT = 4800;
|
|
| 157 |
// === 限制定义结束 ===
|
| 158 |
|
| 159 |
// 定义 fal-ai/any-llm 支持的模型列表 (Remains the same)
|
| 160 |
-
const FAL_SUPPORTED_MODELS = [
|
| 161 |
"anthropic/claude-3.7-sonnet",
|
| 162 |
"anthropic/claude-3.5-sonnet",
|
| 163 |
"anthropic/claude-3-5-haiku",
|
|
@@ -178,7 +180,7 @@ const FAL_SUPPORTED_MODELS = [ /* ... model list ... */
|
|
| 178 |
];
|
| 179 |
|
| 180 |
// Helper function to get owner from model ID (Remains the same)
|
| 181 |
-
const getOwner = (modelId) => {
|
| 182 |
if (modelId && modelId.includes('/')) {
|
| 183 |
return modelId.split('/')[0];
|
| 184 |
}
|
|
@@ -186,7 +188,7 @@ const getOwner = (modelId) => { /* ... */
|
|
| 186 |
};
|
| 187 |
|
| 188 |
// GET /v1/models endpoint (Remains the same)
|
| 189 |
-
app.get('/v1/models', (req, res) => {
|
| 190 |
console.log("Received request for GET /v1/models");
|
| 191 |
try {
|
| 192 |
const modelsData = FAL_SUPPORTED_MODELS.map(modelId => ({
|
|
@@ -201,7 +203,7 @@ app.get('/v1/models', (req, res) => { /* ... */
|
|
| 201 |
});
|
| 202 |
|
| 203 |
// === convertMessagesToFalPrompt 函数 (Remains the same) ===
|
| 204 |
-
function convertMessagesToFalPrompt(messages) {
|
| 205 |
let fixed_system_prompt_content = "";
|
| 206 |
const conversation_message_blocks = [];
|
| 207 |
// console.log(`Original messages count: ${messages.length}`); // Less verbose logging
|
|
@@ -347,48 +349,35 @@ async function tryFalCallWithFailover(operation, functionId, params) {
|
|
| 347 |
// --- Configure fal client with the selected key for this attempt ---
|
| 348 |
// WARNING: This global config change might have concurrency issues in high-load scenarios
|
| 349 |
// if the fal client library doesn't isolate requests properly.
|
| 350 |
-
// A better approach would be per-request credentials if the library supported it.
|
| 351 |
fal.config({ credentials: currentFalKey });
|
| 352 |
|
| 353 |
if (operation === 'stream') {
|
| 354 |
-
// For streams, the retry logic primarily applies to *initiating* the stream.
|
| 355 |
-
// If the stream starts but fails later, this loop won't restart it.
|
| 356 |
const streamResult = await fal.stream(functionId, params);
|
| 357 |
console.log(`Successfully initiated stream with key ending in ...${currentFalKey.slice(-4)}`);
|
| 358 |
-
// If successful, return the stream iterator
|
| 359 |
return streamResult;
|
| 360 |
} else { // 'subscribe' (non-stream)
|
| 361 |
const result = await fal.subscribe(functionId, params);
|
| 362 |
console.log(`Successfully completed subscribe request with key ending in ...${currentFalKey.slice(-4)}`);
|
| 363 |
|
| 364 |
-
// Check for application-level errors *returned* by fal within the result object
|
| 365 |
-
// These are usually model errors, not key errors. Let them propagate.
|
| 366 |
if (result && result.error) {
|
| 367 |
console.warn(`Fal-ai returned an application error (non-stream) with key ...${currentFalKey.slice(-4)}: ${JSON.stringify(result.error)}`);
|
| 368 |
-
// Don't mark key as failed for application errors unless specifically known.
|
| 369 |
}
|
| 370 |
-
// Return the result object (which might contain an error)
|
| 371 |
return result;
|
| 372 |
}
|
| 373 |
} catch (error) {
|
| 374 |
console.error(`Error using key ending in ...${currentFalKey.slice(-4)}:`, error.message || error);
|
| 375 |
-
lastError = error;
|
| 376 |
|
| 377 |
-
// Check if the error is likely related to the key itself
|
| 378 |
if (isKeyRelatedError(error)) {
|
| 379 |
markKeyFailed(keyInfo);
|
| 380 |
console.log(`Key marked as failed. Trying next key if available...`);
|
| 381 |
-
// Continue the loop to try the next key
|
| 382 |
} else {
|
| 383 |
-
// If the error is not key-related (e.g., network issue, fal internal error),
|
| 384 |
-
// stop retrying and propagate the error immediately.
|
| 385 |
console.error("Non-key related error occurred. Aborting retries.");
|
| 386 |
-
throw error;
|
| 387 |
}
|
| 388 |
}
|
| 389 |
}
|
| 390 |
|
| 391 |
-
// If the loop finishes, all keys were tried and failed with key-related errors.
|
| 392 |
console.error("All FAL keys failed after attempting each one.");
|
| 393 |
throw new Error(lastError ? `All FAL keys failed. Last error: ${lastError.message}` : "All FAL API keys failed.");
|
| 394 |
}
|
|
@@ -402,7 +391,6 @@ app.post('/v1/chat/completions', async (req, res) => {
|
|
| 402 |
|
| 403 |
if (!FAL_SUPPORTED_MODELS.includes(model)) {
|
| 404 |
console.warn(`Warning: Requested model '${model}' is not in the explicitly supported list.`);
|
| 405 |
-
// Allow proceeding, maybe fal-ai/any-llm supports it dynamically
|
| 406 |
}
|
| 407 |
if (!model || !messages || !Array.isArray(messages) || messages.length === 0) {
|
| 408 |
console.error("Invalid request parameters:", { model, messages: Array.isArray(messages) ? messages.length : typeof messages });
|
|
@@ -416,31 +404,24 @@ app.post('/v1/chat/completions', async (req, res) => {
|
|
| 416 |
model: model,
|
| 417 |
prompt: prompt,
|
| 418 |
...(system_prompt && { system_prompt: system_prompt }),
|
| 419 |
-
reasoning: !!reasoning,
|
| 420 |
-
// Spread any other OpenAI compatible params if needed, though fal might ignore them
|
| 421 |
-
// ...restOpenAIParams // Be careful with spreading unknown params
|
| 422 |
};
|
| 423 |
|
| 424 |
console.log("Prepared Fal Input (lengths):", { system_prompt: system_prompt?.length, prompt: prompt?.length });
|
| 425 |
-
// Optional: Log full input for debugging (can be verbose)
|
| 426 |
-
// console.log("Full Fal Input:", JSON.stringify(falInput, null, 2));
|
| 427 |
|
| 428 |
-
// --- Use the failover wrapper for the Fal API call ---
|
| 429 |
if (stream) {
|
| 430 |
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
| 431 |
res.setHeader('Cache-Control', 'no-cache');
|
| 432 |
res.setHeader('Connection', 'keep-alive');
|
| 433 |
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
| 434 |
res.flushHeaders();
|
| 435 |
|
| 436 |
let previousOutput = '';
|
| 437 |
let falStream;
|
| 438 |
|
| 439 |
try {
|
| 440 |
-
// --- Initiate stream using failover ---
|
| 441 |
falStream = await tryFalCallWithFailover('stream', "fal-ai/any-llm", { input: falInput });
|
| 442 |
|
| 443 |
-
// --- Process the stream (existing logic) ---
|
| 444 |
for await (const event of falStream) {
|
| 445 |
const currentOutput = (event && typeof event.output === 'string') ? event.output : '';
|
| 446 |
const isPartial = (event && typeof event.partial === 'boolean') ? event.partial : true;
|
|
@@ -448,11 +429,9 @@ app.post('/v1/chat/completions', async (req, res) => {
|
|
| 448 |
|
| 449 |
if (errorInfo) {
|
| 450 |
console.error("Error received *during* fal stream:", errorInfo);
|
| 451 |
-
// Note: This error happened *after* successful stream initiation.
|
| 452 |
-
// We send an error chunk, but don't mark the key failed here as the connection worked initially.
|
| 453 |
const errorChunk = { id: `chatcmpl-${Date.now()}-error`, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: model, choices: [{ index: 0, delta: {}, finish_reason: "error", message: { role: 'assistant', content: `Fal Stream Error: ${JSON.stringify(errorInfo)}` } }] };
|
| 454 |
res.write(`data: ${JSON.stringify(errorChunk)}\n\n`);
|
| 455 |
-
break;
|
| 456 |
}
|
| 457 |
|
| 458 |
let deltaContent = '';
|
|
@@ -461,11 +440,11 @@ app.post('/v1/chat/completions', async (req, res) => {
|
|
| 461 |
} else if (currentOutput.length > 0) {
|
| 462 |
console.warn("Fal stream output mismatch detected. Sending full current output as delta.", { previousLength: previousOutput.length, currentLength: currentOutput.length });
|
| 463 |
deltaContent = currentOutput;
|
| 464 |
-
previousOutput = '';
|
| 465 |
}
|
| 466 |
-
previousOutput = currentOutput;
|
| 467 |
|
| 468 |
-
if (deltaContent || !isPartial) {
|
| 469 |
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 }] };
|
| 470 |
res.write(`data: ${JSON.stringify(openAIChunk)}\n\n`);
|
| 471 |
}
|
|
@@ -475,12 +454,9 @@ app.post('/v1/chat/completions', async (req, res) => {
|
|
| 475 |
console.log("Stream finished successfully.");
|
| 476 |
|
| 477 |
} catch (streamError) {
|
| 478 |
-
// This catch handles errors from tryFalCallWithFailover OR the stream processing loop
|
| 479 |
console.error('Error during stream processing:', streamError);
|
| 480 |
-
// Don't try to write to response if headers already sent and stream failed mid-way uncleanly
|
| 481 |
if (!res.writableEnded) {
|
| 482 |
try {
|
| 483 |
-
// Send a final error chunk if possible
|
| 484 |
const errorDetails = (streamError instanceof Error) ? streamError.message : JSON.stringify(streamError);
|
| 485 |
const finalErrorChunk = { error: { message: "Stream failed", type: "proxy_error", details: errorDetails } };
|
| 486 |
res.write(`data: ${JSON.stringify(finalErrorChunk)}\n\n`);
|
|
@@ -488,65 +464,57 @@ app.post('/v1/chat/completions', async (req, res) => {
|
|
| 488 |
res.end();
|
| 489 |
} catch (finalError) {
|
| 490 |
console.error('Error sending final stream error message to client:', finalError);
|
| 491 |
-
if (!res.writableEnded) { res.end(); }
|
| 492 |
}
|
| 493 |
}
|
| 494 |
}
|
| 495 |
|
| 496 |
} else { // Non-stream
|
| 497 |
console.log("Executing non-stream request with failover...");
|
| 498 |
-
// --- Call subscribe using failover ---
|
| 499 |
const result = await tryFalCallWithFailover('subscribe', "fal-ai/any-llm", { input: falInput, logs: true });
|
| 500 |
|
| 501 |
console.log("Received non-stream result from fal-ai via failover wrapper.");
|
| 502 |
-
// Optional: Log full result for debugging
|
| 503 |
-
// console.log("Full non-stream result:", JSON.stringify(result, null, 2));
|
| 504 |
|
| 505 |
-
// Check for application-level errors *within* the successful response
|
| 506 |
if (result && result.error) {
|
| 507 |
console.error("Fal-ai returned an application error in non-stream mode (after successful API call):", result.error);
|
| 508 |
-
// Return a 500 status but format it like OpenAI error if possible
|
| 509 |
return res.status(500).json({
|
| 510 |
object: "error",
|
| 511 |
message: `Fal-ai application error: ${JSON.stringify(result.error)}`,
|
| 512 |
type: "fal_ai_error",
|
| 513 |
param: null,
|
| 514 |
-
code: result.error.code || null
|
| 515 |
});
|
| 516 |
}
|
| 517 |
|
| 518 |
-
// --- Format successful non-stream response (existing logic) ---
|
| 519 |
const openAIResponse = {
|
| 520 |
-
id: `chatcmpl-${result?.requestId || Date.now()}`,
|
| 521 |
object: "chat.completion",
|
| 522 |
created: Math.floor(Date.now() / 1000),
|
| 523 |
-
model: model,
|
| 524 |
choices: [{
|
| 525 |
index: 0,
|
| 526 |
message: {
|
| 527 |
role: "assistant",
|
| 528 |
-
content: result?.output || ""
|
| 529 |
},
|
| 530 |
-
finish_reason: "stop"
|
| 531 |
}],
|
| 532 |
-
usage: {
|
| 533 |
prompt_tokens: null,
|
| 534 |
completion_tokens: null,
|
| 535 |
total_tokens: null
|
| 536 |
},
|
| 537 |
-
system_fingerprint: null,
|
| 538 |
-
...(result?.reasoning && { fal_reasoning: result.reasoning }),
|
| 539 |
};
|
| 540 |
res.json(openAIResponse);
|
| 541 |
console.log("Returned non-stream response successfully.");
|
| 542 |
}
|
| 543 |
|
| 544 |
} catch (error) {
|
| 545 |
-
// This catches errors from setup, convertMessagesToFalPrompt, or tryFalCallWithFailover (if all keys failed or non-key error occurred)
|
| 546 |
console.error('Unhandled error in /v1/chat/completions:', error);
|
| 547 |
if (!res.headersSent) {
|
| 548 |
const errorMessage = (error instanceof Error) ? error.message : JSON.stringify(error);
|
| 549 |
-
// Provide a more informative error message
|
| 550 |
const errorType = error.message?.includes("All FAL keys failed") ? "api_key_error" : "proxy_internal_error";
|
| 551 |
res.status(500).json({
|
| 552 |
error: {
|
|
@@ -557,7 +525,7 @@ app.post('/v1/chat/completions', async (req, res) => {
|
|
| 557 |
});
|
| 558 |
} else if (!res.writableEnded) {
|
| 559 |
console.error("Headers already sent, attempting to end response after error.");
|
| 560 |
-
res.end();
|
| 561 |
}
|
| 562 |
}
|
| 563 |
});
|
|
@@ -567,7 +535,8 @@ app.listen(PORT, () => {
|
|
| 567 |
console.log(`===========================================================`);
|
| 568 |
console.log(` Fal OpenAI Proxy Server (Multi-Key Failover)`);
|
| 569 |
console.log(` Listening on port: ${PORT}`);
|
| 570 |
-
|
|
|
|
| 571 |
console.log(` API Key Auth Enabled: ${API_KEY ? 'Yes' : 'No'}`);
|
| 572 |
console.log(` Limits: System Prompt=${SYSTEM_PROMPT_LIMIT}, Prompt=${PROMPT_LIMIT}`);
|
| 573 |
console.log(` Chat Completions: POST http://localhost:${PORT}/v1/chat/completions`);
|
|
@@ -575,7 +544,7 @@ app.listen(PORT, () => {
|
|
| 575 |
console.log(`===========================================================`);
|
| 576 |
});
|
| 577 |
|
| 578 |
-
// Root path response
|
| 579 |
app.get('/', (req, res) => {
|
| 580 |
res.send('Fal OpenAI Proxy (Multi-Key Failover) is running.');
|
| 581 |
});
|
|
|
|
| 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 |
|
|
|
|
| 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 |
|
|
|
|
| 129 |
|
| 130 |
// API Key 鉴权中间件 (Remains the same, checks custom API_KEY)
|
| 131 |
const apiKeyAuth = (req, res, next) => {
|
|
|
|
| 132 |
const authHeader = req.headers['authorization'];
|
| 133 |
|
| 134 |
if (!authHeader) {
|
|
|
|
| 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",
|
|
|
|
| 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 |
}
|
|
|
|
| 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");
|
| 193 |
try {
|
| 194 |
const modelsData = FAL_SUPPORTED_MODELS.map(modelId => ({
|
|
|
|
| 203 |
});
|
| 204 |
|
| 205 |
// === convertMessagesToFalPrompt 函数 (Remains the same) ===
|
| 206 |
+
function convertMessagesToFalPrompt(messages) {
|
| 207 |
let fixed_system_prompt_content = "";
|
| 208 |
const conversation_message_blocks = [];
|
| 209 |
// console.log(`Original messages count: ${messages.length}`); // Less verbose logging
|
|
|
|
| 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 |
}
|
|
|
|
| 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 });
|
|
|
|
| 404 |
model: model,
|
| 405 |
prompt: prompt,
|
| 406 |
...(system_prompt && { system_prompt: system_prompt }),
|
| 407 |
+
reasoning: !!reasoning,
|
|
|
|
|
|
|
| 408 |
};
|
| 409 |
|
| 410 |
console.log("Prepared Fal Input (lengths):", { system_prompt: system_prompt?.length, prompt: prompt?.length });
|
|
|
|
|
|
|
| 411 |
|
|
|
|
| 412 |
if (stream) {
|
| 413 |
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
| 414 |
res.setHeader('Cache-Control', 'no-cache');
|
| 415 |
res.setHeader('Connection', 'keep-alive');
|
| 416 |
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
| 417 |
res.flushHeaders();
|
| 418 |
|
| 419 |
let previousOutput = '';
|
| 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;
|
|
|
|
| 429 |
|
| 430 |
if (errorInfo) {
|
| 431 |
console.error("Error received *during* fal stream:", errorInfo);
|
|
|
|
|
|
|
| 432 |
const errorChunk = { id: `chatcmpl-${Date.now()}-error`, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: model, choices: [{ index: 0, delta: {}, finish_reason: "error", message: { role: 'assistant', content: `Fal Stream Error: ${JSON.stringify(errorInfo)}` } }] };
|
| 433 |
res.write(`data: ${JSON.stringify(errorChunk)}\n\n`);
|
| 434 |
+
break;
|
| 435 |
}
|
| 436 |
|
| 437 |
let deltaContent = '';
|
|
|
|
| 440 |
} else if (currentOutput.length > 0) {
|
| 441 |
console.warn("Fal stream output mismatch detected. Sending full current output as delta.", { previousLength: previousOutput.length, currentLength: currentOutput.length });
|
| 442 |
deltaContent = currentOutput;
|
| 443 |
+
previousOutput = '';
|
| 444 |
}
|
| 445 |
+
previousOutput = currentOutput;
|
| 446 |
|
| 447 |
+
if (deltaContent || !isPartial) {
|
| 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 |
}
|
|
|
|
| 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`);
|
|
|
|
| 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: {
|
|
|
|
| 525 |
});
|
| 526 |
} else if (!res.writableEnded) {
|
| 527 |
console.error("Headers already sent, attempting to end response after error.");
|
| 528 |
+
res.end();
|
| 529 |
}
|
| 530 |
}
|
| 531 |
});
|
|
|
|
| 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`);
|
|
|
|
| 544 |
console.log(`===========================================================`);
|
| 545 |
});
|
| 546 |
|
| 547 |
+
// Root path response
|
| 548 |
app.get('/', (req, res) => {
|
| 549 |
res.send('Fal OpenAI Proxy (Multi-Key Failover) is running.');
|
| 550 |
});
|