kyush-llm-router/server/src/services/RouterService.ts
2026-03-07 04:05:56 +09:00

114 lines
3.8 KiB
TypeScript

import { Backend } from '../../../shared/types';
import { BackendModel } from '../models/Backend';
export class RouterService {
static selectBackend(allowedBackendIds: number[]): Backend | null {
if (allowedBackendIds.length === 0) {
return null;
}
const allBackends = BackendModel.findAll();
const backends = allBackends
.filter(b => (b.is_active === true || b.is_active === 1) && allowedBackendIds.includes(b.id));
if (backends.length === 0) {
return null;
}
const roundRobinIndex = Math.floor(Math.random() * backends.length);
return backends[roundRobinIndex];
}
static async forwardRequest(
backend: Backend,
path: string,
method: string,
headers: Record<string, string>,
body?: unknown
): Promise<{ status: number; data: unknown }> {
let backendPath = path;
if (backend.base_url.includes('/v1')) {
backendPath = path.replace(/^\/v1/, '');
}
const backendUrl = backend.base_url.replace(/\/$/, '') + backendPath;
const fetchHeaders: Record<string, string> = {
'Content-Type': 'application/json',
...headers,
};
if (backend.api_key) {
fetchHeaders['Authorization'] = `Bearer ${backend.api_key}`;
}
try {
const response = await fetch(backendUrl, {
method,
headers: fetchHeaders,
body: body ? JSON.stringify(body) : undefined,
});
const data = await response.json().catch(() => ({}));
return {
status: response.status,
data,
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
// Extract detailed error information from error cause
let cause: string | undefined;
let errorType: string;
if (error instanceof Error && error.cause) {
const causeError = error.cause as any;
const causeCode = causeError.code || causeError.errno;
const causeSyscall = causeError.syscall;
const causeAddress = causeError.address || causeError.hostname;
const causePort = causeError.port;
if (causeCode === 'ECONNREFUSED') {
errorType = 'Backend connection refused';
cause = causeAddress && causePort
? `Backend server at ${causeAddress}:${causePort} is not accepting connections`
: 'Backend server is not accepting connections';
} else if (causeCode === 'ETIMEDOUT' || causeCode === 'ECONNABORTED') {
errorType = 'Backend request timeout';
cause = 'Connection to backend timed out';
} else if (causeCode === 'ENOTFOUND') {
errorType = 'Backend unreachable';
cause = causeAddress ? `Could not resolve hostname: ${causeAddress}` : 'Could not resolve backend hostname';
} else if (causeCode === 'EPIPE' || causeSyscall === 'write') {
errorType = 'Backend connection lost';
cause = causeSyscall ? `Connection broken during ${causeSyscall} operation` : 'Connection broken during operation';
} else {
errorType = 'Backend connection error';
cause = `${causeCode || 'Unknown error'} during ${causeSyscall || 'connection'}`;
}
} else if (errorMsg.includes('ETIMEDOUT') || errorMsg.includes('ECONNABORTED')) {
errorType = 'Backend request timeout';
cause = 'Connection timed out after 30s';
} else if (errorMsg.includes('aborted')) {
errorType = 'Request aborted';
cause = 'Request was aborted before completion';
} else {
errorType = 'Failed to forward request to backend';
cause = errorMsg;
}
const detailedError = {
error: errorType,
cause: cause,
backend: backend.base_url,
path: path,
};
return {
status: 502,
data: detailedError,
};
}
}
}