feat: cherry-pick PR #183 — multi-provider support, PWA, dynamic models, UI improvements
Cherry-picked from decolua/9router PR #183. Note: open-sse changes included but need further review due to extensive modifications. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -28,7 +28,7 @@ export async function handleChat(request, clientRawRequest = null) {
|
||||
log.warn("CHAT", "Invalid JSON body");
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body");
|
||||
}
|
||||
|
||||
|
||||
// Build clientRawRequest for logging (if not provided)
|
||||
if (!clientRawRequest) {
|
||||
const url = new URL(request.url);
|
||||
@@ -42,7 +42,7 @@ export async function handleChat(request, clientRawRequest = null) {
|
||||
// Log request endpoint and model
|
||||
const url = new URL(request.url);
|
||||
const modelStr = body.model;
|
||||
|
||||
|
||||
// Count messages (support both messages[] and input[] formats)
|
||||
const msgCount = body.messages?.length || body.input?.length || 0;
|
||||
const toolCount = body.tools?.length || 0;
|
||||
@@ -99,7 +99,19 @@ export async function handleChat(request, clientRawRequest = null) {
|
||||
*/
|
||||
async function handleSingleModelChat(body, modelStr, clientRawRequest = null, request = null, apiKey = null) {
|
||||
const modelInfo = await getModelInfo(modelStr);
|
||||
|
||||
// If provider is null, this might be a combo name - check and handle
|
||||
if (!modelInfo.provider) {
|
||||
const comboModels = await getComboModels(modelStr);
|
||||
if (comboModels) {
|
||||
log.info("CHAT", `Combo "${modelStr}" with ${comboModels.length} models`);
|
||||
return handleComboChat({
|
||||
body,
|
||||
models: comboModels,
|
||||
handleSingleModel: (b, m) => handleSingleModelChat(b, m, clientRawRequest, request, apiKey, forceSourceFormat),
|
||||
log
|
||||
});
|
||||
}
|
||||
log.warn("CHAT", "Invalid model format", { model: modelStr });
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid model format");
|
||||
}
|
||||
@@ -178,12 +190,12 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re
|
||||
await clearAccountError(credentials.connectionId, credentials);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (result.success) return result.response;
|
||||
|
||||
// Mark account unavailable (auto-calculates cooldown with exponential backoff)
|
||||
const { shouldFallback } = await markAccountUnavailable(credentials.connectionId, result.status, result.error, provider, model);
|
||||
|
||||
|
||||
if (shouldFallback) {
|
||||
log.warn("AUTH", `Account ${accountId}... unavailable (${result.status}), trying fallback`);
|
||||
excludeConnectionId = credentials.connectionId;
|
||||
|
||||
@@ -268,11 +268,11 @@ export async function markAccountUnavailable(connectionId, status, errorText, pr
|
||||
export async function clearAccountError(connectionId, currentConnection) {
|
||||
// Only update if currently has error status
|
||||
const hasError = currentConnection.testStatus === "unavailable" ||
|
||||
currentConnection.lastError ||
|
||||
currentConnection.rateLimitedUntil;
|
||||
|
||||
currentConnection.lastError ||
|
||||
currentConnection.rateLimitedUntil;
|
||||
|
||||
if (!hasError) return; // Skip if already clean
|
||||
|
||||
|
||||
await updateProviderConnection(connectionId, {
|
||||
testStatus: "active",
|
||||
lastError: null,
|
||||
@@ -280,17 +280,25 @@ export async function clearAccountError(connectionId, currentConnection) {
|
||||
rateLimitedUntil: null,
|
||||
backoffLevel: 0
|
||||
});
|
||||
log.info("AUTH", `Account ${connectionId.slice(0,8)} error cleared`);
|
||||
log.info("AUTH", `Account ${connectionId.slice(0, 8)} error cleared`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract API key from request headers
|
||||
*/
|
||||
export function extractApiKey(request) {
|
||||
// Check Authorization header first
|
||||
const authHeader = request.headers.get("Authorization");
|
||||
if (authHeader?.startsWith("Bearer ")) {
|
||||
return authHeader.slice(7);
|
||||
}
|
||||
|
||||
// Check Anthropic x-api-key header
|
||||
const xApiKey = request.headers.get("x-api-key");
|
||||
if (xApiKey) {
|
||||
return xApiKey;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,15 @@ export async function getModelInfo(modelStr) {
|
||||
};
|
||||
}
|
||||
|
||||
// Check if this is a combo name before resolving as alias
|
||||
// This prevents combo names from being incorrectly routed to providers
|
||||
const combo = await getComboByName(parsed.model);
|
||||
if (combo) {
|
||||
// Return null provider to signal this should be handled as combo
|
||||
// The caller (handleChat) will detect this and handle it as combo
|
||||
return { provider: null, model: parsed.model };
|
||||
}
|
||||
|
||||
return getModelInfoCore(modelStr, getModelAliases);
|
||||
}
|
||||
|
||||
@@ -50,7 +59,7 @@ export async function getModelInfo(modelStr) {
|
||||
export async function getComboModels(modelStr) {
|
||||
// Only check if it's not in provider/model format
|
||||
if (modelStr.includes("/")) return null;
|
||||
|
||||
|
||||
const combo = await getComboByName(modelStr);
|
||||
if (combo && combo.models && combo.models.length > 0) {
|
||||
return combo.models;
|
||||
|
||||
Reference in New Issue
Block a user