mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 04:42:10 +03:00
622 lines
22 KiB
TypeScript
622 lines
22 KiB
TypeScript
import { getSyncedAvailableModelsByConnection } from "@/lib/db/models";
|
|
import { getProviderConnections } from "@/lib/db/providers";
|
|
import { getDbInstance } from "@/lib/db/core";
|
|
import { getAllCircuitBreakerStatuses } from "@/shared/utils/circuitBreaker";
|
|
import { getAllModelLockouts } from "@omniroute/open-sse/services/accountFallback";
|
|
import { getWebSessionPoolHealth } from "@omniroute/open-sse/services/webSessionPoolHealth";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
|
|
export type ProviderHealthMatrixRange = "1h" | "24h" | "7d" | "30d";
|
|
export type ProviderHealthState = "healthy" | "degraded" | "down";
|
|
export type ProviderModelHealthStatus = "healthy" | "degraded" | "error" | "locked" | "idle";
|
|
|
|
export interface ProviderHealthMatrixOptions {
|
|
provider?: string | null;
|
|
range?: ProviderHealthMatrixRange | string | null;
|
|
includeHealthy?: boolean;
|
|
}
|
|
|
|
export interface ProviderHealthMatrixModel {
|
|
model: string;
|
|
status: ProviderModelHealthStatus;
|
|
isLockedOut: boolean;
|
|
lockoutReason: string | null;
|
|
lockoutRemainingMs: number;
|
|
requests: number;
|
|
successes: number;
|
|
successRate: number | null;
|
|
avgLatencyMs: number | null;
|
|
lastStatus: number | null;
|
|
lastErrorStatus: number | null;
|
|
lastRequestAt: string | null;
|
|
lastErrorAt: string | null;
|
|
}
|
|
|
|
export interface ProviderHealthMatrixAccount {
|
|
connectionId: string | null;
|
|
label: string;
|
|
isSynthetic: boolean;
|
|
isActive: boolean;
|
|
state: ProviderHealthState;
|
|
testStatus: string | null;
|
|
rateLimitedUntil: string | null;
|
|
cooldownRemainingMs: number;
|
|
lastErrorType: string | null;
|
|
errorCode: string | null;
|
|
backoffLevel: number;
|
|
modelCount: number;
|
|
issueCount: number;
|
|
models: ProviderHealthMatrixModel[];
|
|
}
|
|
|
|
export interface ProviderHealthMatrixProvider {
|
|
provider: string;
|
|
state: ProviderHealthState;
|
|
score: number;
|
|
circuitBreaker: {
|
|
state: string;
|
|
failureCount: number;
|
|
retryAfterMs: number;
|
|
lastFailureTime: number | null;
|
|
} | null;
|
|
connections: {
|
|
total: number;
|
|
active: number;
|
|
cooldown: number;
|
|
inactive: number;
|
|
terminal: number;
|
|
};
|
|
modelLockoutCount: number;
|
|
requests: number;
|
|
successRate: number | null;
|
|
avgLatencyMs: number | null;
|
|
lastRequestAt: string | null;
|
|
lastErrorAt: string | null;
|
|
issueCount: number;
|
|
accounts: ProviderHealthMatrixAccount[];
|
|
}
|
|
|
|
export interface ProviderHealthMatrixResponse {
|
|
checkedAt: string;
|
|
range: ProviderHealthMatrixRange;
|
|
cutoff: string;
|
|
summary: {
|
|
providerCount: number;
|
|
connectionCount: number;
|
|
modelCount: number;
|
|
issueCount: number;
|
|
healthyCount: number;
|
|
degradedCount: number;
|
|
downCount: number;
|
|
};
|
|
providers: ProviderHealthMatrixProvider[];
|
|
webSessionPools: {
|
|
totalPools: number;
|
|
healthyPools: number;
|
|
degradedPools: number;
|
|
downPools: number;
|
|
pools: Array<{
|
|
provider: string;
|
|
health: "healthy" | "degraded" | "down";
|
|
pool: Record<string, unknown> | null;
|
|
breaker: Record<string, unknown> | null;
|
|
sessions: Array<Record<string, unknown>>;
|
|
issues: string[];
|
|
}>;
|
|
};
|
|
}
|
|
|
|
interface CallLogTargetStats {
|
|
provider: string;
|
|
connectionId: string | null;
|
|
model: string;
|
|
requests: number;
|
|
successes: number;
|
|
avgLatencyMs: number | null;
|
|
lastRequestAt: string | null;
|
|
lastErrorAt: string | null;
|
|
lastStatus: number | null;
|
|
lastErrorStatus: number | null;
|
|
}
|
|
|
|
const RANGE_MS: Record<ProviderHealthMatrixRange, number> = {
|
|
"1h": 60 * 60 * 1000,
|
|
"24h": 24 * 60 * 60 * 1000,
|
|
"7d": 7 * 24 * 60 * 60 * 1000,
|
|
"30d": 30 * 24 * 60 * 60 * 1000,
|
|
};
|
|
|
|
const TERMINAL_STATUSES = new Set(["banned", "expired", "credits_exhausted"]);
|
|
|
|
function toString(value: unknown): string | null {
|
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
}
|
|
|
|
function toNumber(value: unknown): number {
|
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
if (typeof value === "string" && value.trim()) {
|
|
const parsed = Number(value);
|
|
if (Number.isFinite(parsed)) return parsed;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function parseTimeMs(value: unknown): number | null {
|
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
if (typeof value === "string" && value.trim()) {
|
|
const numeric = Number(value);
|
|
if (Number.isFinite(numeric)) return numeric;
|
|
const parsed = Date.parse(value);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function normalizeRange(value: ProviderHealthMatrixOptions["range"]): ProviderHealthMatrixRange {
|
|
return value === "1h" || value === "7d" || value === "30d" ? value : "24h";
|
|
}
|
|
|
|
function isActiveConnection(value: unknown): boolean {
|
|
return value !== false && value !== 0;
|
|
}
|
|
|
|
function isTerminalConnection(connection: JsonRecord): boolean {
|
|
const status = toString(connection.testStatus)?.toLowerCase();
|
|
const errorType = toString(connection.lastErrorType)?.toLowerCase();
|
|
return Boolean(
|
|
(status && TERMINAL_STATUSES.has(status)) || (errorType && TERMINAL_STATUSES.has(errorType))
|
|
);
|
|
}
|
|
|
|
function sanitizeConnectionLabel(connection: JsonRecord): string {
|
|
const label =
|
|
toString(connection.name) ||
|
|
toString(connection.displayName) ||
|
|
toString(connection.email) ||
|
|
toString(connection.id) ||
|
|
"connection";
|
|
const masked = label.includes("@") ? label.replace(/^(.).+(@.+)$/, "$1***$2") : label;
|
|
return masked.length > 80 ? `${masked.slice(0, 77)}…` : masked;
|
|
}
|
|
|
|
function statsKey(provider: string, connectionId: string | null, model: string): string {
|
|
return `${provider}\u0000${connectionId ?? ""}\u0000${model}`;
|
|
}
|
|
|
|
function accountKey(provider: string, connectionId: string | null): string {
|
|
return `${provider}\u0000${connectionId ?? ""}`;
|
|
}
|
|
|
|
function modelLockKey(provider: string, connectionId: string | null, model: string): string {
|
|
return `${provider}\u0000${connectionId ?? ""}\u0000${model}`;
|
|
}
|
|
|
|
function hasFailureStatus(status: number | null): boolean {
|
|
return status !== null && (status < 200 || status >= 400);
|
|
}
|
|
|
|
function computeSuccessRate(requests: number, successes: number): number | null {
|
|
return requests > 0 ? Math.round((successes / requests) * 100) : null;
|
|
}
|
|
|
|
function maxIso(left: string | null, right: string | null): string | null {
|
|
if (!left) return right;
|
|
if (!right) return left;
|
|
return Date.parse(right) > Date.parse(left) ? right : left;
|
|
}
|
|
|
|
function queryCallLogTargetStats(
|
|
cutoff: string,
|
|
providerFilter: string | null
|
|
): CallLogTargetStats[] {
|
|
const db = getDbInstance();
|
|
const providerClause = providerFilter ? "AND c.provider = @provider" : "";
|
|
const params = providerFilter ? { cutoff, provider: providerFilter } : { cutoff };
|
|
const rows = db
|
|
.prepare(
|
|
`WITH log_targets AS (
|
|
SELECT
|
|
c.provider,
|
|
COALESCE(c.connection_id, '') as connectionId,
|
|
COALESCE(c.model, c.requested_model, 'unknown') as model,
|
|
c.status,
|
|
c.duration,
|
|
c.timestamp,
|
|
c.id,
|
|
c.error_summary
|
|
FROM call_logs c
|
|
WHERE c.provider IS NOT NULL
|
|
AND c.provider != '-'
|
|
AND c.timestamp >= @cutoff
|
|
${providerClause}
|
|
), ranked AS (
|
|
SELECT
|
|
provider,
|
|
connectionId,
|
|
model,
|
|
status,
|
|
duration,
|
|
timestamp,
|
|
CASE
|
|
WHEN (status IS NOT NULL AND (status < 200 OR status >= 400))
|
|
OR error_summary IS NOT NULL
|
|
THEN 1
|
|
ELSE 0
|
|
END as isError,
|
|
ROW_NUMBER() OVER (
|
|
PARTITION BY provider, connectionId, model
|
|
ORDER BY timestamp DESC, id DESC
|
|
) as latestRank,
|
|
ROW_NUMBER() OVER (
|
|
PARTITION BY provider, connectionId, model,
|
|
CASE
|
|
WHEN (status IS NOT NULL AND (status < 200 OR status >= 400))
|
|
OR error_summary IS NOT NULL
|
|
THEN 1
|
|
ELSE 0
|
|
END
|
|
ORDER BY timestamp DESC, id DESC
|
|
) as errorRank
|
|
FROM log_targets
|
|
)
|
|
SELECT
|
|
provider,
|
|
connectionId,
|
|
model,
|
|
COUNT(*) as requests,
|
|
SUM(CASE WHEN status >= 200 AND status < 400 THEN 1 ELSE 0 END) as successes,
|
|
ROUND(AVG(duration)) as avgLatencyMs,
|
|
MAX(timestamp) as lastRequestAt,
|
|
MAX(
|
|
CASE
|
|
WHEN isError = 1
|
|
THEN timestamp
|
|
ELSE NULL
|
|
END
|
|
) as lastErrorAt,
|
|
MAX(CASE WHEN latestRank = 1 THEN status ELSE NULL END) as lastStatus,
|
|
MAX(CASE WHEN isError = 1 AND errorRank = 1 THEN status ELSE NULL END) as lastErrorStatus
|
|
FROM ranked
|
|
GROUP BY provider, connectionId, model`
|
|
)
|
|
.all(params) as JsonRecord[];
|
|
|
|
return rows.map((row) => {
|
|
const connectionId = toString(row.connectionId);
|
|
return {
|
|
provider: toString(row.provider) || "unknown",
|
|
connectionId: connectionId || null,
|
|
model: toString(row.model) || "unknown",
|
|
requests: toNumber(row.requests),
|
|
successes: toNumber(row.successes),
|
|
avgLatencyMs: row.avgLatencyMs == null ? null : toNumber(row.avgLatencyMs),
|
|
lastRequestAt: toString(row.lastRequestAt),
|
|
lastErrorAt: toString(row.lastErrorAt),
|
|
lastStatus: row.lastStatus == null ? null : toNumber(row.lastStatus),
|
|
lastErrorStatus: row.lastErrorStatus == null ? null : toNumber(row.lastErrorStatus),
|
|
};
|
|
});
|
|
}
|
|
|
|
function classifyModel(
|
|
stats: CallLogTargetStats | null,
|
|
locked: boolean
|
|
): ProviderModelHealthStatus {
|
|
if (locked) return "locked";
|
|
if (!stats || stats.requests === 0) return "idle";
|
|
if (hasFailureStatus(stats.lastStatus)) return "error";
|
|
const successRate = computeSuccessRate(stats.requests, stats.successes);
|
|
if (successRate !== null && successRate < 95) return "degraded";
|
|
return "healthy";
|
|
}
|
|
|
|
function classifyAccount(
|
|
connection: JsonRecord | null,
|
|
models: ProviderHealthMatrixModel[],
|
|
now: number
|
|
): ProviderHealthState {
|
|
if (connection) {
|
|
if (!isActiveConnection(connection.isActive) || isTerminalConnection(connection)) return "down";
|
|
const cooldownUntil = parseTimeMs(connection.rateLimitedUntil);
|
|
if (cooldownUntil && cooldownUntil > now) return "degraded";
|
|
}
|
|
if (models.some((model) => model.status === "error" || model.status === "locked")) {
|
|
return "degraded";
|
|
}
|
|
return "healthy";
|
|
}
|
|
|
|
function classifyProvider(
|
|
breaker: ProviderHealthMatrixProvider["circuitBreaker"],
|
|
accounts: ProviderHealthMatrixAccount[]
|
|
): ProviderHealthState {
|
|
if (breaker?.state === "OPEN") return "down";
|
|
if (breaker?.state === "HALF_OPEN") return "degraded";
|
|
if (accounts.length > 0 && accounts.every((account) => account.state === "down")) return "down";
|
|
if (accounts.some((account) => account.state !== "healthy")) return "degraded";
|
|
return "healthy";
|
|
}
|
|
|
|
export async function buildProviderHealthMatrix(
|
|
options: ProviderHealthMatrixOptions = {}
|
|
): Promise<ProviderHealthMatrixResponse> {
|
|
const now = Date.now();
|
|
const checkedAt = new Date(now).toISOString();
|
|
const range = normalizeRange(options.range);
|
|
const cutoff = new Date(now - RANGE_MS[range]).toISOString();
|
|
const providerFilter = toString(options.provider);
|
|
const includeHealthy = options.includeHealthy !== false;
|
|
|
|
const [connections, breakers, lockouts, stats] = await Promise.all([
|
|
getProviderConnections(providerFilter ? { provider: providerFilter } : {}),
|
|
getAllCircuitBreakerStatuses(),
|
|
getAllModelLockouts(),
|
|
Promise.resolve(queryCallLogTargetStats(cutoff, providerFilter)),
|
|
]);
|
|
|
|
const connectionRows = (connections as JsonRecord[]).filter((connection) => {
|
|
const provider = toString(connection.provider);
|
|
return provider && (!providerFilter || provider === providerFilter);
|
|
});
|
|
const breakerRows = (breakers as JsonRecord[]).filter((breaker) => {
|
|
const provider = toString(breaker.name);
|
|
return provider && (!providerFilter || provider === providerFilter);
|
|
});
|
|
const lockoutRows = (lockouts as JsonRecord[]).filter((lockout) => {
|
|
const provider = toString(lockout.provider);
|
|
return provider && (!providerFilter || provider === providerFilter);
|
|
});
|
|
|
|
const providerIds = new Set<string>();
|
|
for (const connection of connectionRows) {
|
|
const provider = toString(connection.provider);
|
|
if (provider) providerIds.add(provider);
|
|
}
|
|
for (const breaker of breakerRows) {
|
|
const provider = toString(breaker.name);
|
|
if (provider) providerIds.add(provider);
|
|
}
|
|
for (const lockout of lockoutRows) {
|
|
const provider = toString(lockout.provider);
|
|
if (provider) providerIds.add(provider);
|
|
}
|
|
for (const row of stats) providerIds.add(row.provider);
|
|
if (providerFilter) providerIds.add(providerFilter);
|
|
|
|
const syncedModelsByProvider = new Map<
|
|
string,
|
|
Awaited<ReturnType<typeof getSyncedAvailableModelsByConnection>>
|
|
>();
|
|
await Promise.all(
|
|
[...providerIds].map(async (provider) => {
|
|
syncedModelsByProvider.set(provider, await getSyncedAvailableModelsByConnection(provider));
|
|
})
|
|
);
|
|
|
|
const statsByTarget = new Map<string, CallLogTargetStats>();
|
|
const statsByAccount = new Map<string, CallLogTargetStats[]>();
|
|
for (const row of stats) {
|
|
statsByTarget.set(statsKey(row.provider, row.connectionId, row.model), row);
|
|
const key = accountKey(row.provider, row.connectionId);
|
|
const current = statsByAccount.get(key) ?? [];
|
|
current.push(row);
|
|
statsByAccount.set(key, current);
|
|
}
|
|
|
|
const lockoutsByTarget = new Map<string, JsonRecord>();
|
|
const lockoutCountByProvider = new Map<string, number>();
|
|
for (const lockout of lockoutRows) {
|
|
const provider = toString(lockout.provider);
|
|
const connectionId = toString(lockout.connectionId);
|
|
const model = toString(lockout.model);
|
|
if (!provider || !model) continue;
|
|
lockoutsByTarget.set(modelLockKey(provider, connectionId, model), lockout);
|
|
lockoutCountByProvider.set(provider, (lockoutCountByProvider.get(provider) ?? 0) + 1);
|
|
}
|
|
|
|
const providers: ProviderHealthMatrixProvider[] = [];
|
|
for (const provider of [...providerIds].sort()) {
|
|
const providerConnections = connectionRows.filter(
|
|
(connection) => toString(connection.provider) === provider
|
|
);
|
|
const providerStats = stats.filter((row) => row.provider === provider);
|
|
const providerBreaker = breakerRows.find((breaker) => toString(breaker.name) === provider);
|
|
const circuitBreaker = providerBreaker
|
|
? {
|
|
state: toString(providerBreaker.state) || "CLOSED",
|
|
failureCount: toNumber(providerBreaker.failureCount),
|
|
retryAfterMs: toNumber(providerBreaker.retryAfterMs),
|
|
lastFailureTime:
|
|
providerBreaker.lastFailureTime == null
|
|
? null
|
|
: toNumber(providerBreaker.lastFailureTime),
|
|
}
|
|
: null;
|
|
|
|
const accountRows = new Map<string, JsonRecord | null>();
|
|
for (const connection of providerConnections) {
|
|
accountRows.set(accountKey(provider, toString(connection.id)), connection);
|
|
}
|
|
for (const stat of providerStats) {
|
|
const key = accountKey(provider, stat.connectionId);
|
|
if (!accountRows.has(key)) accountRows.set(key, null);
|
|
}
|
|
for (const lockout of lockoutRows) {
|
|
if (toString(lockout.provider) !== provider) continue;
|
|
const key = accountKey(provider, toString(lockout.connectionId));
|
|
if (!accountRows.has(key)) accountRows.set(key, null);
|
|
}
|
|
|
|
const accounts: ProviderHealthMatrixAccount[] = [];
|
|
const providerSyncedModels = syncedModelsByProvider.get(provider) ?? {};
|
|
for (const [key, connection] of [...accountRows.entries()].sort()) {
|
|
const [, connectionPart] = key.split("\u0000");
|
|
const connectionId = connection ? toString(connection.id) : connectionPart || null;
|
|
const modelIds = new Set<string>();
|
|
if (connectionId && connection) {
|
|
const syncedModels = providerSyncedModels[connectionId] ?? [];
|
|
for (const model of syncedModels) {
|
|
if (model.id) modelIds.add(model.id);
|
|
}
|
|
const defaultModel = toString(connection.defaultModel);
|
|
if (defaultModel) modelIds.add(defaultModel);
|
|
}
|
|
for (const stat of statsByAccount.get(accountKey(provider, connectionId)) ?? []) {
|
|
modelIds.add(stat.model);
|
|
}
|
|
for (const lockout of lockoutRows) {
|
|
if (toString(lockout.provider) !== provider) continue;
|
|
if ((toString(lockout.connectionId) ?? "") !== (connectionId ?? "")) continue;
|
|
const model = toString(lockout.model);
|
|
if (model) modelIds.add(model);
|
|
}
|
|
|
|
const models = [...modelIds].sort().map((model): ProviderHealthMatrixModel => {
|
|
const stat = statsByTarget.get(statsKey(provider, connectionId, model)) ?? null;
|
|
const lockout = lockoutsByTarget.get(modelLockKey(provider, connectionId, model)) ?? null;
|
|
const isLockedOut = Boolean(lockout);
|
|
const status = classifyModel(stat, isLockedOut);
|
|
return {
|
|
model,
|
|
status,
|
|
isLockedOut,
|
|
lockoutReason: lockout ? toString(lockout.reason) : null,
|
|
lockoutRemainingMs: lockout ? Math.max(0, toNumber(lockout.remainingMs)) : 0,
|
|
requests: stat?.requests ?? 0,
|
|
successes: stat?.successes ?? 0,
|
|
successRate: stat ? computeSuccessRate(stat.requests, stat.successes) : null,
|
|
avgLatencyMs: stat?.avgLatencyMs ?? null,
|
|
lastStatus: stat?.lastStatus ?? null,
|
|
lastErrorStatus: stat?.lastErrorStatus ?? null,
|
|
lastRequestAt: stat?.lastRequestAt ?? null,
|
|
lastErrorAt: stat?.lastErrorAt ?? null,
|
|
};
|
|
});
|
|
|
|
const cooldownUntil = connection ? parseTimeMs(connection.rateLimitedUntil) : null;
|
|
const cooldownRemainingMs = cooldownUntil ? Math.max(0, cooldownUntil - now) : 0;
|
|
const state = classifyAccount(connection, models, now);
|
|
const issueCount =
|
|
(state === "healthy" ? 0 : 1) +
|
|
models.filter((model) => model.status === "error" || model.status === "locked").length;
|
|
accounts.push({
|
|
connectionId,
|
|
label: connection
|
|
? sanitizeConnectionLabel(connection)
|
|
: connectionId || "Unattributed traffic",
|
|
isSynthetic: !connection,
|
|
isActive: connection ? isActiveConnection(connection.isActive) : true,
|
|
state,
|
|
testStatus: connection ? toString(connection.testStatus) : null,
|
|
rateLimitedUntil: connection ? toString(connection.rateLimitedUntil) : null,
|
|
cooldownRemainingMs,
|
|
lastErrorType: connection ? toString(connection.lastErrorType) : null,
|
|
errorCode: connection ? toString(connection.errorCode) : null,
|
|
backoffLevel: connection ? toNumber(connection.backoffLevel) : 0,
|
|
modelCount: models.length,
|
|
issueCount,
|
|
models,
|
|
});
|
|
}
|
|
|
|
const requests = providerStats.reduce((sum, row) => sum + row.requests, 0);
|
|
const successes = providerStats.reduce((sum, row) => sum + row.successes, 0);
|
|
const weightedLatency = providerStats.reduce(
|
|
(sum, row) => sum + (row.avgLatencyMs ?? 0) * row.requests,
|
|
0
|
|
);
|
|
const state = classifyProvider(circuitBreaker, accounts);
|
|
const issueCount =
|
|
(state === "healthy" ? 0 : 1) +
|
|
accounts.reduce((sum, account) => sum + account.issueCount, 0);
|
|
const totalConnections = providerConnections.length;
|
|
const activeConnections = providerConnections.filter((connection) =>
|
|
isActiveConnection(connection.isActive)
|
|
).length;
|
|
const cooldownConnections = providerConnections.filter((connection) => {
|
|
const until = parseTimeMs(connection.rateLimitedUntil);
|
|
return Boolean(until && until > now);
|
|
}).length;
|
|
const terminalConnections = providerConnections.filter(isTerminalConnection).length;
|
|
const modelLockoutCount = lockoutCountByProvider.get(provider) ?? 0;
|
|
const scorePenalty =
|
|
(circuitBreaker?.state === "OPEN" ? 0.4 : circuitBreaker?.state === "HALF_OPEN" ? 0.2 : 0) +
|
|
(totalConnections > 0 ? (cooldownConnections / totalConnections) * 0.2 : 0) +
|
|
(totalConnections > 0 ? (terminalConnections / totalConnections) * 0.25 : 0) +
|
|
Math.min(0.2, modelLockoutCount * 0.05) +
|
|
Math.min(0.15, accounts.filter((account) => account.state === "down").length * 0.05);
|
|
|
|
const providerRow: ProviderHealthMatrixProvider = {
|
|
provider,
|
|
state,
|
|
score: Number(Math.max(0, Math.min(1, 1 - scorePenalty)).toFixed(2)),
|
|
circuitBreaker,
|
|
connections: {
|
|
total: totalConnections,
|
|
active: activeConnections,
|
|
cooldown: cooldownConnections,
|
|
inactive: providerConnections.filter(
|
|
(connection) => !isActiveConnection(connection.isActive)
|
|
).length,
|
|
terminal: terminalConnections,
|
|
},
|
|
modelLockoutCount,
|
|
requests,
|
|
successRate: computeSuccessRate(requests, successes),
|
|
avgLatencyMs: requests > 0 ? Math.round(weightedLatency / requests) : null,
|
|
lastRequestAt: providerStats.reduce<string | null>(
|
|
(latest, row) => maxIso(latest, row.lastRequestAt),
|
|
null
|
|
),
|
|
lastErrorAt: providerStats.reduce<string | null>(
|
|
(latest, row) => maxIso(latest, row.lastErrorAt),
|
|
null
|
|
),
|
|
issueCount,
|
|
accounts,
|
|
};
|
|
|
|
if (includeHealthy || providerRow.state !== "healthy" || providerRow.issueCount > 0) {
|
|
providers.push(providerRow);
|
|
}
|
|
}
|
|
|
|
const poolReport = getWebSessionPoolHealth(providerFilter ?? undefined);
|
|
|
|
return {
|
|
checkedAt,
|
|
range,
|
|
cutoff,
|
|
summary: {
|
|
providerCount: providers.length,
|
|
connectionCount: providers.reduce((sum, provider) => sum + provider.connections.total, 0),
|
|
modelCount: providers.reduce(
|
|
(sum, provider) =>
|
|
sum +
|
|
provider.accounts.reduce((accountSum, account) => accountSum + account.modelCount, 0),
|
|
0
|
|
),
|
|
issueCount: providers.reduce((sum, provider) => sum + provider.issueCount, 0),
|
|
healthyCount: providers.filter((provider) => provider.state === "healthy").length,
|
|
degradedCount: providers.filter((provider) => provider.state === "degraded").length,
|
|
downCount: providers.filter((provider) => provider.state === "down").length,
|
|
},
|
|
providers,
|
|
webSessionPools: {
|
|
totalPools: poolReport.providers.length,
|
|
healthyPools: poolReport.providers.filter((p) => p.health === "healthy").length,
|
|
degradedPools: poolReport.providers.filter((p) => p.health === "degraded").length,
|
|
downPools: poolReport.providers.filter((p) => p.health === "down").length,
|
|
pools: poolReport.providers.map((p) => ({
|
|
provider: p.provider,
|
|
health: p.health,
|
|
pool: p.pool as Record<string, unknown> | null,
|
|
breaker: p.breaker as Record<string, unknown> | null,
|
|
sessions: p.sessions as Array<Record<string, unknown>>,
|
|
issues: p.issues,
|
|
})),
|
|
},
|
|
};
|
|
}
|