Files
2026-07-13 13:39:12 +08:00

294 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { logToolCall } from "../audit.ts";
import { getMcpHttpAuthHeadersForInternalFetch } from "../httpAuthContext.ts";
import { normalizeQuotaResponse } from "../../../src/shared/contracts/quota.ts";
import { resolveOmniRouteBaseUrl } from "../../../src/shared/utils/resolveOmniRouteBaseUrl.ts";
import {
getComboModelProvider,
getComboModelString,
getComboStepTarget,
} from "../../../src/lib/combos/steps.ts";
import type { AutoRoutingStrategyValue } from "../../../src/shared/constants/routingStrategies.ts";
import { rankBySpeed, DEFAULT_SPEED_WEIGHTS } from "../../services/autoCombo/speedRanking.ts";
import type { SpeedCandidate } from "../../services/autoCombo/speedRanking.ts";
const OMNIROUTE_BASE_URL = resolveOmniRouteBaseUrl();
const OMNIROUTE_API_KEY = process.env.OMNIROUTE_API_KEY || "";
async function apiFetch(path: string, options: RequestInit = {}): Promise<unknown> {
const url = `${OMNIROUTE_BASE_URL}${path}`;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(OMNIROUTE_API_KEY ? { Authorization: `Bearer ${OMNIROUTE_API_KEY}` } : {}),
...getMcpHttpAuthHeadersForInternalFetch(),
...((options.headers as Record<string, string>) || {}),
};
const response = await fetch(url, { ...options, headers, signal: AbortSignal.timeout(30000) });
if (!response.ok) {
const text = await response.text().catch(() => "Unknown error");
throw new Error(`API [${response.status}]: ${text}`);
}
return response.json();
}
type JsonRecord = Record<string, unknown>;
interface ComboModel { provider: string; model: string; inputCostPer1M: number; }
interface PickFastestModelArgs {
comboId?: string;
/** When true, OPEN-circuit candidates are still scored (sorted to the bottom). */
includeUnhealthy?: boolean;
/** Optional weight overrides; merged onto DEFAULT_SPEED_WEIGHTS. */
weights?: Partial<{
ttft: number;
tps: number;
e2e: number;
p95: number;
health: number;
reliability: number;
stability: number;
}>;
/** When true + comboId present, sets the combo's autoRoutingStrategy to "latency". */
applyToCombo?: boolean;
/** Max number of ranked entries to return (default 10). */
limit?: number;
}
interface TelemetrySources {
combos: JsonRecord[];
breakers: JsonRecord[];
providers: ReturnType<typeof normalizeQuotaResponse>["providers"];
analyticsByProvider: JsonRecord;
analyticsTop: JsonRecord;
}
function isRecord(value: unknown): value is JsonRecord { return !!value && typeof value === "object" && !Array.isArray(value); }
function toRecord(value: unknown): JsonRecord { return isRecord(value) ? value : {}; }
function toArrayOfRecords(value: unknown): JsonRecord[] { return Array.isArray(value) ? value.filter(isRecord) : []; }
function toString(value: unknown, fallback = ""): string { return typeof value === "string" ? value : fallback; }
function toNumber(value: unknown, fallback = 0): number {
const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim().length > 0 ? Number(value) : Number.NaN;
return Number.isFinite(parsed) ? parsed : fallback;
}
function getComboModels(combo: JsonRecord): ComboModel[] {
const directModels = toArrayOfRecords(combo.models);
const nestedModels = toArrayOfRecords(toRecord(combo.data).models);
const sourceModels = directModels.length > 0 ? directModels : nestedModels;
return sourceModels.map((model) => ({
provider: getComboModelProvider(model) || (getComboModelString(model) ? "unknown" : "combo"),
model: getComboModelString(model) || getComboStepTarget(model) || "",
inputCostPer1M: toNumber(model.inputCostPer1M, 3.0),
}));
}
function normalizeCombosResponse(raw: unknown): JsonRecord[] {
if (Array.isArray(raw)) return raw.filter(isRecord);
const source = toRecord(raw);
return Array.isArray(source.combos) ? source.combos.filter(isRecord) : [];
}
function settledValue(result: PromiseSettledResult<unknown>): unknown {
return result.status === "fulfilled" ? result.value : undefined;
}
async function fetchTelemetrySources(): Promise<TelemetrySources> {
const [combosRaw, healthRaw, quotaRaw, analyticsRaw] = await Promise.allSettled([
apiFetch("/api/combos"),
apiFetch("/api/monitoring/health"),
apiFetch("/api/usage/quota"),
apiFetch("/api/usage/analytics?period=session"),
]);
const analytics = toRecord(settledValue(analyticsRaw));
return {
combos: normalizeCombosResponse(settledValue(combosRaw)),
breakers: toArrayOfRecords(toRecord(settledValue(healthRaw)).circuitBreakers),
providers: normalizeQuotaResponse(settledValue(quotaRaw) ?? {}).providers,
analyticsByProvider: toRecord(toRecord(analytics.byProvider)),
analyticsTop: analytics,
};
}
function selectComboScope(combos: JsonRecord[], comboId?: string) {
const targetCombo = comboId
? combos.find((combo) => toString(combo.id) === comboId || toString(combo.name) === comboId)
: undefined;
return {
targetCombo,
scopedCombos: targetCombo ? [targetCombo] : combos.filter((combo) => combo.enabled !== false),
};
}
function noCandidatesResult(error: string) {
return { content: [{ type: "text" as const, text: JSON.stringify({ error }) }], isError: true };
}
function providerAnalytics(sources: TelemetrySources, provider: string) {
const perProvider = toRecord(sources.analyticsByProvider[provider]);
return perProvider.requests
? perProvider
: toRecord(sources.analyticsTop.byProvider && toRecord(sources.analyticsTop.byProvider)[provider]);
}
function buildCandidate(model: ComboModel, sources: TelemetrySources): SpeedCandidate {
const cb = sources.breakers.find((breaker) => toString(breaker.provider) === model.provider);
const q = sources.providers.find((providerEntry) => providerEntry.provider === model.provider);
const analytics = providerAnalytics(sources, model.provider);
const cbState = toString(cb?.state, "CLOSED") as SpeedCandidate["circuitBreakerState"];
const p95 = toNumber(analytics.p95LatencyMs, NaN);
const errorRate = toNumber(analytics.errorRate, 0);
return {
provider: model.provider,
model: model.model,
circuitBreakerState: cbState,
avgE2ELatencyMs: toNumber(analytics.avgLatencyMs, NaN),
p95LatencyMs: Number.isFinite(p95) ? p95 : 0,
avgTokensPerSecond: toNumber(analytics.avgTokensPerSecond ?? analytics.tps, NaN),
avgTtftMs: toNumber(analytics.avgTtftMs ?? analytics.ttftMs, NaN),
latencyStdDev: toNumber(analytics.latencyStdDev, NaN),
errorRate: Number.isFinite(errorRate) ? errorRate : 0,
failureRate: Number.isFinite(errorRate) ? errorRate : 0,
quotaRemaining: q?.quotaUsed != null && q?.quotaTotal
? Math.max(0, 100 - q.quotaUsed / q.quotaTotal * 100)
: 100,
quotaTotal: q?.quotaTotal ?? 100,
costPer1MTokens: model.inputCostPer1M ?? 0,
};
}
function buildSpeedCandidates(scopedCombos: JsonRecord[], sources: TelemetrySources): SpeedCandidate[] {
const speedCandidates: SpeedCandidate[] = [];
for (const combo of scopedCombos) {
for (const model of getComboModels(combo)) {
if (model.provider && model.model) speedCandidates.push(buildCandidate(model, sources));
}
}
return speedCandidates;
}
function candidateCompletenessScore(candidate: SpeedCandidate): number {
return (candidate.circuitBreakerState ? 1 : 0) + (candidate.quotaRemaining != null ? 1 : 0);
}
function dedupeCandidates(candidates: SpeedCandidate[]): SpeedCandidate[] {
const deduped = new Map<string, SpeedCandidate>();
for (const candidate of candidates) {
const key = `${candidate.provider}::${candidate.model}`;
const existing = deduped.get(key);
if (!existing || candidateCompletenessScore(candidate) > candidateCompletenessScore(existing)) {
deduped.set(key, candidate);
}
}
return [...deduped.values()];
}
async function applyWinnerToCombo(targetCombo: JsonRecord, winner: { provider: string; model: string }) {
const comboId = toString(targetCombo.id);
const comboData = toRecord(targetCombo.data);
const baseConfig = toRecord(targetCombo.config);
const currentConfig = Object.keys(baseConfig).length > 0 ? baseConfig : toRecord(comboData.config);
const nextConfig = {
...currentConfig,
auto: {
...toRecord(currentConfig.auto),
routerStrategy: "latency" as AutoRoutingStrategyValue,
},
};
const updatedCombo = toRecord(
await apiFetch(`/api/combos/${encodeURIComponent(comboId)}`, {
method: "PUT",
body: JSON.stringify({ strategy: "auto", config: nextConfig }),
})
);
const updatedConfig = toRecord(updatedCombo.config);
return {
id: toString(updatedCombo.id, comboId),
name: toString(updatedCombo.name, toString(targetCombo.name, comboId)),
strategy: toString(updatedCombo.strategy, "auto"),
autoRoutingStrategy: toString(toRecord(updatedConfig.auto).routerStrategy, "latency"),
};
}
/**
* Speed-optimized "pick the fastest reliable provider×model" tool.
*
* Composes live telemetry from `/api/combos/metrics` (per-combo per-model
* avg latency + success rate), `/api/monitoring/health` (circuit-breaker
* state), `/api/usage/quota` (quota remaining) and `/api/usage/analytics`
* (per-provider p95 / errorRate) into SpeedCandidates, runs the same
* `rankBySpeed` engine that drives `LatencyStrategyImpl` and the latency-
* optimized playground preview, and returns:
* - the top-ranked (fastest) provider×model pair,
* - the full ranked list with per-factor scores (ttft / tps / e2e /
* p95 / health / reliability / stability) so callers can show
* "why this one wins" in dashboards,
* - optionally applies the choice to a target combo by flipping its
* strategy to "auto" + autoRoutingStrategy "latency", so the runtime
* router will keep using this ranking.
*/
export async function handlePickFastestModel(args: PickFastestModelArgs) {
const start = Date.now();
try {
const sources = await fetchTelemetrySources();
const { targetCombo, scopedCombos } = selectComboScope(sources.combos, args.comboId);
if (scopedCombos.length === 0) {
return noCandidatesResult("No matching combos available");
}
const finalCandidates = dedupeCandidates(buildSpeedCandidates(scopedCombos, sources));
if (finalCandidates.length === 0) {
return noCandidatesResult("No provider×model candidates available to rank");
}
const weights = args.weights ? { ...DEFAULT_SPEED_WEIGHTS, ...args.weights } : DEFAULT_SPEED_WEIGHTS;
const ranked = rankBySpeed(finalCandidates, weights, {
includeUnhealthy: args.includeUnhealthy === true,
});
const limit = Math.min(Math.max(toNumber(args.limit, 10), 1), 50);
const trimmed = ranked.slice(0, limit);
const winner = trimmed[0];
let appliedToCombo: JsonRecord | null = null;
if (args.applyToCombo && targetCombo && winner) {
appliedToCombo = await applyWinnerToCombo(targetCombo, winner);
}
const result = {
fastest: winner
? {
provider: winner.provider,
model: winner.model,
score: winner.score,
reason: winner.reason,
}
: null,
ranked: trimmed.map((entry) => ({
provider: entry.provider,
model: entry.model,
score: entry.score,
factors: entry.factors,
metrics: entry.metrics,
reason: entry.reason,
})),
weights,
comboScope: targetCombo
? { id: toString(targetCombo.id), name: toString(targetCombo.name) }
: null,
appliedToCombo,
};
await logToolCall("omniroute_pick_fastest_model", args, result, Date.now() - start, true);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall(
"omniroute_pick_fastest_model",
args,
null,
Date.now() - start,
false,
msg
);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}