From df6625b531d0ff0816e7930af689f831dfe70c7c Mon Sep 17 00:00:00 2001 From: Badri Narayanan S Date: Sat, 3 Jan 2026 18:01:21 +0530 Subject: [PATCH] fallback changes from PR #35 --- src/cloudcode/message-handler.js | 13 ++++++++++- src/cloudcode/streaming-handler.js | 14 +++++++++++- src/fallback-config.js | 36 ++++++++++++++++++++++++++++++ src/index.js | 22 +++++++++++++++--- src/server.js | 8 +++++-- 5 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 src/fallback-config.js diff --git a/src/cloudcode/message-handler.js b/src/cloudcode/message-handler.js index 4491afc..beb6745 100644 --- a/src/cloudcode/message-handler.js +++ b/src/cloudcode/message-handler.js @@ -18,6 +18,7 @@ import { logger } from '../utils/logger.js'; import { parseResetTime } from './rate-limit-parser.js'; import { buildCloudCodeRequest, buildHeaders } from './request-builder.js'; import { parseThinkingSSEResponse } from './sse-parser.js'; +import { getFallbackModel } from '../fallback-config.js'; /** * Send a non-streaming request to Cloud Code with multi-account support @@ -32,7 +33,7 @@ import { parseThinkingSSEResponse } from './sse-parser.js'; * @returns {Promise} Anthropic-format response object * @throws {Error} If max retries exceeded or no accounts available */ -export async function sendMessage(anthropicRequest, accountManager) { +export async function sendMessage(anthropicRequest, accountManager, fallbackEnabled = false) { const model = anthropicRequest.model; const isThinking = isThinkingModel(model); @@ -76,6 +77,16 @@ export async function sendMessage(anthropicRequest, accountManager) { } if (!account) { + // Check if fallback is enabled and available + if (fallbackEnabled) { + const fallbackModel = getFallbackModel(model); + if (fallbackModel) { + logger.warn(`[CloudCode] All accounts exhausted for ${model}. Attempting fallback to ${fallbackModel}`); + // Retry with fallback model + const fallbackRequest = { ...anthropicRequest, model: fallbackModel }; + return await sendMessage(fallbackRequest, accountManager, false); // Disable fallback for recursive call + } + } throw new Error('No accounts available'); } } diff --git a/src/cloudcode/streaming-handler.js b/src/cloudcode/streaming-handler.js index f3af687..f33945b 100644 --- a/src/cloudcode/streaming-handler.js +++ b/src/cloudcode/streaming-handler.js @@ -16,6 +16,7 @@ import { logger } from '../utils/logger.js'; import { parseResetTime } from './rate-limit-parser.js'; import { buildCloudCodeRequest, buildHeaders } from './request-builder.js'; import { streamSSEResponse } from './sse-streamer.js'; +import { getFallbackModel } from '../fallback-config.js'; /** @@ -31,7 +32,7 @@ import { streamSSEResponse } from './sse-streamer.js'; * @yields {Object} Anthropic-format SSE events (message_start, content_block_start, content_block_delta, etc.) * @throws {Error} If max retries exceeded or no accounts available */ -export async function* sendMessageStream(anthropicRequest, accountManager) { +export async function* sendMessageStream(anthropicRequest, accountManager, fallbackEnabled = false) { const model = anthropicRequest.model; // Retry loop with account failover @@ -74,6 +75,17 @@ export async function* sendMessageStream(anthropicRequest, accountManager) { } if (!account) { + // Check if fallback is enabled and available + if (fallbackEnabled) { + const fallbackModel = getFallbackModel(model); + if (fallbackModel) { + logger.warn(`[CloudCode] All accounts exhausted for ${model}. Attempting fallback to ${fallbackModel} (streaming)`); + // Retry with fallback model + const fallbackRequest = { ...anthropicRequest, model: fallbackModel }; + yield* sendMessageStream(fallbackRequest, accountManager, false); // Disable fallback for recursive call + return; + } + } throw new Error('No accounts available'); } } diff --git a/src/fallback-config.js b/src/fallback-config.js new file mode 100644 index 0000000..880e5ac --- /dev/null +++ b/src/fallback-config.js @@ -0,0 +1,36 @@ +/** + * Model Fallback Configuration + * + * Defines fallback mappings for when a model's quota is exhausted across all accounts. + * Enables graceful degradation to alternative models with similar capabilities. + */ + +/** + * Model fallback mapping + * Maps primary model ID to fallback model ID + */ +export const MODEL_FALLBACK_MAP = { + 'gemini-3-pro-high': 'claude-sonnet-4-5-thinking', + 'gemini-3-pro-low': 'claude-sonnet-4-5', + 'claude-opus-4-5-thinking': 'gemini-3-pro-high', + 'claude-sonnet-4-5-thinking': 'gemini-3-pro-high', + 'claude-sonnet-4-5': 'gemini-3-pro-low' +}; + +/** + * Get fallback model for a given model ID + * @param {string} model - Primary model ID + * @returns {string|null} Fallback model ID or null if no fallback exists + */ +export function getFallbackModel(model) { + return MODEL_FALLBACK_MAP[model] || null; +} + +/** + * Check if a model has a fallback configured + * @param {string} model - Model ID to check + * @returns {boolean} True if fallback exists + */ +export function hasFallback(model) { + return model in MODEL_FALLBACK_MAP; +} diff --git a/src/index.js b/src/index.js index 3c71759..b439884 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ import os from 'os'; // Parse command line arguments const args = process.argv.slice(2); const isDebug = args.includes('--debug') || process.env.DEBUG === 'true'; +const isFallbackEnabled = args.includes('--fallback') || process.env.FALLBACK === 'true'; // Initialize logger logger.setDebug(isDebug); @@ -20,6 +21,13 @@ if (isDebug) { logger.debug('Debug mode enabled'); } +if (isFallbackEnabled) { + logger.info('Model fallback mode enabled'); +} + +// Export fallback flag for server to use +export const FALLBACK_ENABLED = isFallbackEnabled; + const PORT = process.env.PORT || DEFAULT_PORT; // Home directory for account storage @@ -40,14 +48,22 @@ app.listen(PORT, () => { if (!isDebug) { controlSection += '║ --debug Enable debug logging ║\n'; } + if (!isFallbackEnabled) { + controlSection += '║ --fallback Enable model fallback on quota exhaust ║\n'; + } controlSection += '║ Ctrl+C Stop server ║'; - // Build status section if debug mode is active + // Build status section if any modes are active let statusSection = ''; - if (isDebug) { + if (isDebug || isFallbackEnabled) { statusSection = '║ ║\n'; statusSection += '║ Active Modes: ║\n'; - statusSection += '║ ✓ Debug mode enabled ║\n'; + if (isDebug) { + statusSection += '║ ✓ Debug mode enabled ║\n'; + } + if (isFallbackEnabled) { + statusSection += '║ ✓ Model fallback enabled ║\n'; + } } logger.log(` diff --git a/src/server.js b/src/server.js index f0ccb5d..587b393 100644 --- a/src/server.js +++ b/src/server.js @@ -13,6 +13,10 @@ import { AccountManager } from './account-manager/index.js'; import { formatDuration } from './utils/helpers.js'; import { logger } from './utils/logger.js'; +// Parse fallback flag directly from command line args to avoid circular dependency +const args = process.argv.slice(2); +const FALLBACK_ENABLED = args.includes('--fallback') || process.env.FALLBACK === 'true'; + const app = express(); // Initialize account manager (will be fully initialized on first request or startup) @@ -595,7 +599,7 @@ app.post('/v1/messages', async (req, res) => { try { // Use the streaming generator with account manager - for await (const event of sendMessageStream(request, accountManager)) { + for await (const event of sendMessageStream(request, accountManager, FALLBACK_ENABLED)) { res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`); // Flush after each event for real-time streaming if (res.flush) res.flush(); @@ -616,7 +620,7 @@ app.post('/v1/messages', async (req, res) => { } else { // Handle non-streaming response - const response = await sendMessage(request, accountManager); + const response = await sendMessage(request, accountManager, FALLBACK_ENABLED); res.json(response); }