fallback changes from PR #35

This commit is contained in:
Badri Narayanan S
2026-01-03 18:01:21 +05:30
parent 9c4a712a9a
commit df6625b531
5 changed files with 86 additions and 7 deletions

View File

@@ -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<Object>} 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');
}
}

View File

@@ -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');
}
}

36
src/fallback-config.js Normal file
View File

@@ -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;
}

View File

@@ -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(`

View File

@@ -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);
}