mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 21:32:10 +03:00
Register 8 new advanced MCP tools (simulate_route, set_budget_guard, set_resilience_profile, test_combo, get_provider_metrics, best_combo_for_task, explain_route, get_session_snapshot) with their handler implementations. Add A2A smart routing skill that routes prompts through the OmniRoute pipeline with routing explanation, cost envelope, and resilience trace metadata.
117 lines
3.0 KiB
TypeScript
117 lines
3.0 KiB
TypeScript
/**
|
|
* A2A Routing Decision Logger
|
|
*
|
|
* Records every routing decision to the `routing_decisions` SQLite table.
|
|
* Used by `omniroute_explain_route` (T03) and future learning router.
|
|
* Retention: 7 days default.
|
|
*/
|
|
|
|
import { randomUUID } from "crypto";
|
|
|
|
export interface RoutingFactor {
|
|
name: string; // "quota", "health", "cost", "latency", "task_fit"
|
|
value: number;
|
|
weight: number;
|
|
contribution: number;
|
|
}
|
|
|
|
export interface FallbackEntry {
|
|
provider: string;
|
|
reason: string; // "circuit_breaker_open", "quota_exceeded", "timeout"
|
|
}
|
|
|
|
export interface RoutingDecision {
|
|
requestId: string;
|
|
taskType: string;
|
|
comboId: string;
|
|
providerSelected: string;
|
|
modelUsed: string;
|
|
score: number;
|
|
factors: RoutingFactor[];
|
|
fallbacksTriggered: FallbackEntry[];
|
|
success: boolean;
|
|
latencyMs: number;
|
|
cost: number;
|
|
timestamp: string;
|
|
}
|
|
|
|
// In-memory log (production would use SQLite via routing_decisions table)
|
|
const decisions: RoutingDecision[] = [];
|
|
const MAX_DECISIONS = 1000;
|
|
const RETENTION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
|
|
/**
|
|
* Log a routing decision.
|
|
*/
|
|
export function logRoutingDecision(
|
|
params: Omit<RoutingDecision, "requestId" | "timestamp">
|
|
): RoutingDecision {
|
|
const decision: RoutingDecision = {
|
|
...params,
|
|
requestId: randomUUID(),
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
decisions.push(decision);
|
|
|
|
// Cleanup: cap + TTL
|
|
if (decisions.length > MAX_DECISIONS) {
|
|
const cutoff = new Date(Date.now() - RETENTION_MS);
|
|
const validIdx = decisions.findIndex((d) => new Date(d.timestamp) > cutoff);
|
|
if (validIdx > 0) decisions.splice(0, validIdx);
|
|
else if (decisions.length > MAX_DECISIONS)
|
|
decisions.splice(0, decisions.length - MAX_DECISIONS);
|
|
}
|
|
|
|
return decision;
|
|
}
|
|
|
|
/**
|
|
* Get a specific routing decision by request ID.
|
|
*/
|
|
export function getRoutingDecision(requestId: string): RoutingDecision | undefined {
|
|
return decisions.find((d) => d.requestId === requestId);
|
|
}
|
|
|
|
/**
|
|
* Get recent routing decisions.
|
|
*/
|
|
export function getRecentDecisions(limit: number = 20): RoutingDecision[] {
|
|
return decisions.slice(-limit).reverse();
|
|
}
|
|
|
|
/**
|
|
* Get routing decision stats.
|
|
*/
|
|
export function getDecisionStats(): {
|
|
total: number;
|
|
successRate: number;
|
|
avgLatencyMs: number;
|
|
topProviders: Array<{ provider: string; count: number }>;
|
|
} {
|
|
if (decisions.length === 0) {
|
|
return { total: 0, successRate: 1, avgLatencyMs: 0, topProviders: [] };
|
|
}
|
|
|
|
const successful = decisions.filter((d) => d.success);
|
|
const providerCounts = new Map<string, number>();
|
|
let totalLatency = 0;
|
|
|
|
for (const d of decisions) {
|
|
totalLatency += d.latencyMs;
|
|
providerCounts.set(d.providerSelected, (providerCounts.get(d.providerSelected) || 0) + 1);
|
|
}
|
|
|
|
const topProviders = [...providerCounts.entries()]
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, 5)
|
|
.map(([provider, count]) => ({ provider, count }));
|
|
|
|
return {
|
|
total: decisions.length,
|
|
successRate: successful.length / decisions.length,
|
|
avgLatencyMs: Math.round(totalLatency / decisions.length),
|
|
topProviders,
|
|
};
|
|
}
|