mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 10:12:11 +03:00
* feat(routing): add reasoning-based model and effort routing * refactor(routing): modularize reasoning and auto-routing pipeline * fix(routing): remove redundant DB re-export and prevent SQL scan false positives * fix(routing): resolve reasoning routing review blockers * fix(i18n): keep release ranking fallbacks outside reasoning * fix(db): renumber reasoning-routing migration past release tip (124→125) 124_generic_session_affinity_ttl.sql (#7274) has since landed on release/v3.8.49 at version 124, colliding with this PR's own 124_reasoning_routing_rules.sql. Renumbers to 125 (the next free slot past the current release tip) and updates the one filename reference in docs/routing/REASONING_ROUTING.md. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(db): renumber reasoning-routing migration 125→126 (slot taken by #7360) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * refactor(api): compact temp-path decls in exportAll GET (complexity-ratchet lines budget) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * refactor(api): single-statement auth guard in exportAll GET (function under 80-line cap) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
244 lines
9.1 KiB
TypeScript
244 lines
9.1 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { getDbInstance } from "./core";
|
|
import { registerDbStateResetter } from "./stateReset";
|
|
|
|
export type ReasoningRuleScope = "global" | "apiKey" | "combo" | "model" | "connection";
|
|
export type ReasoningEffort = "none" | "low" | "medium" | "high" | "xhigh" | "max" | "ultra";
|
|
export type ReasoningSourceEffort = "any" | "missing" | ReasoningEffort;
|
|
export type ReasoningEffortMode = "inherit" | "default" | "force";
|
|
export type ReasoningTargetKind = "keep" | "model" | "combo";
|
|
export type ReasoningBudgetAction = "preserve" | "remove" | "set";
|
|
|
|
export interface ReasoningRoutingRule {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
scope: ReasoningRuleScope;
|
|
apiKeyId: string | null;
|
|
comboId: string | null;
|
|
connectionId: string | null;
|
|
modelPattern: string | null;
|
|
sourceEffort: ReasoningSourceEffort;
|
|
requestTags: string[];
|
|
tagMatchMode: "any" | "all";
|
|
effortMode: ReasoningEffortMode;
|
|
targetEffort: ReasoningEffort | null;
|
|
targetKind: ReasoningTargetKind;
|
|
targetModel: string | null;
|
|
targetComboId: string | null;
|
|
budgetAction: ReasoningBudgetAction;
|
|
budgetTokens: number | null;
|
|
priority: number;
|
|
enabled: boolean;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export type ReasoningRoutingRuleInput = Omit<
|
|
ReasoningRoutingRule,
|
|
"id" | "createdAt" | "updatedAt"
|
|
>;
|
|
|
|
type RuleRow = Record<string, unknown>;
|
|
let cache: { version: number; rules: ReasoningRoutingRule[] } | null = null;
|
|
let version = 0;
|
|
|
|
function parseTags(value: unknown): string[] {
|
|
try {
|
|
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
return Array.isArray(parsed)
|
|
? [
|
|
...new Set(
|
|
parsed
|
|
.filter((tag): tag is string => typeof tag === "string")
|
|
.map((tag) => tag.trim().toLowerCase())
|
|
.filter(Boolean)
|
|
),
|
|
]
|
|
: [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function rowToRule(row: RuleRow): ReasoningRoutingRule {
|
|
return {
|
|
id: String(row.id),
|
|
name: String(row.name),
|
|
description: typeof row.description === "string" ? row.description : "",
|
|
scope: row.scope as ReasoningRuleScope,
|
|
apiKeyId: typeof row.api_key_id === "string" ? row.api_key_id : null,
|
|
comboId: typeof row.combo_id === "string" ? row.combo_id : null,
|
|
connectionId: typeof row.connection_id === "string" ? row.connection_id : null,
|
|
modelPattern: typeof row.model_pattern === "string" ? row.model_pattern : null,
|
|
sourceEffort: row.source_effort as ReasoningSourceEffort,
|
|
requestTags: parseTags(row.request_tags),
|
|
tagMatchMode: row.tag_match_mode === "all" ? "all" : "any",
|
|
effortMode: row.effort_mode as ReasoningEffortMode,
|
|
targetEffort:
|
|
typeof row.target_effort === "string" ? (row.target_effort as ReasoningEffort) : null,
|
|
targetKind: row.target_kind as ReasoningTargetKind,
|
|
targetModel: typeof row.target_model === "string" ? row.target_model : null,
|
|
targetComboId: typeof row.target_combo_id === "string" ? row.target_combo_id : null,
|
|
budgetAction: row.budget_action as ReasoningBudgetAction,
|
|
budgetTokens: typeof row.budget_tokens === "number" ? row.budget_tokens : null,
|
|
priority: typeof row.priority === "number" ? row.priority : 0,
|
|
enabled: row.enabled === 1 || row.enabled === true,
|
|
createdAt: String(row.created_at),
|
|
updatedAt: String(row.updated_at),
|
|
};
|
|
}
|
|
|
|
function invalidate(): void {
|
|
version += 1;
|
|
cache = null;
|
|
}
|
|
|
|
function referenceExists(table: string, id: string | null): boolean {
|
|
if (!id) return false;
|
|
const allowedTables = new Set(["api_keys", "combos", "provider_connections"]);
|
|
if (!allowedTables.has(table)) return false;
|
|
return Boolean(getDbInstance().prepare(`SELECT 1 FROM ${table} WHERE id = ?`).get(id));
|
|
}
|
|
|
|
export function getReasoningRoutingRuleReferenceErrors(rule: ReasoningRoutingRuleInput): string[] {
|
|
const errors: string[] = [];
|
|
if (rule.scope === "apiKey" && !referenceExists("api_keys", rule.apiKeyId)) {
|
|
errors.push("The referenced API key does not exist");
|
|
}
|
|
if (rule.scope === "combo" && !referenceExists("combos", rule.comboId)) {
|
|
errors.push("The referenced source combo does not exist");
|
|
}
|
|
if (rule.scope === "connection" && !referenceExists("provider_connections", rule.connectionId)) {
|
|
errors.push("The referenced provider connection does not exist");
|
|
}
|
|
if (rule.targetKind === "combo" && !referenceExists("combos", rule.targetComboId)) {
|
|
errors.push("The referenced target combo does not exist");
|
|
}
|
|
return errors;
|
|
}
|
|
|
|
function assertReferences(rule: ReasoningRoutingRuleInput): void {
|
|
const errors = getReasoningRoutingRuleReferenceErrors(rule);
|
|
if (errors.length > 0) throw new Error(errors.join("; "));
|
|
}
|
|
|
|
export async function getReasoningRoutingRules(
|
|
options: { enabledOnly?: boolean } = {}
|
|
): Promise<ReasoningRoutingRule[]> {
|
|
if (!cache || cache.version !== version) {
|
|
const rows = getDbInstance()
|
|
.prepare(
|
|
"SELECT * FROM reasoning_routing_rules ORDER BY priority DESC, created_at ASC, id ASC"
|
|
)
|
|
.all() as RuleRow[];
|
|
cache = { version, rules: rows.map(rowToRule) };
|
|
}
|
|
const rules = options.enabledOnly ? cache.rules.filter((rule) => rule.enabled) : cache.rules;
|
|
return rules.map((rule) => ({ ...rule, requestTags: [...rule.requestTags] }));
|
|
}
|
|
|
|
export async function getReasoningRoutingRuleById(
|
|
id: string
|
|
): Promise<ReasoningRoutingRule | null> {
|
|
const row = getDbInstance()
|
|
.prepare("SELECT * FROM reasoning_routing_rules WHERE id = ?")
|
|
.get(id) as RuleRow | undefined;
|
|
return row ? rowToRule(row) : null;
|
|
}
|
|
|
|
function ruleParams(
|
|
rule: ReasoningRoutingRuleInput,
|
|
id: string,
|
|
createdAt: string,
|
|
updatedAt: string
|
|
) {
|
|
return {
|
|
id,
|
|
name: rule.name,
|
|
description: rule.description || "",
|
|
scope: rule.scope,
|
|
apiKeyId: rule.apiKeyId ?? null,
|
|
comboId: rule.comboId ?? null,
|
|
connectionId: rule.connectionId ?? null,
|
|
modelPattern: rule.modelPattern ?? null,
|
|
sourceEffort: rule.sourceEffort,
|
|
requestTags: JSON.stringify(parseTags(rule.requestTags)),
|
|
tagMatchMode: rule.tagMatchMode,
|
|
effortMode: rule.effortMode,
|
|
targetEffort: rule.effortMode === "inherit" ? null : rule.targetEffort,
|
|
targetKind: rule.targetKind,
|
|
targetModel: rule.targetKind === "model" ? rule.targetModel : null,
|
|
targetComboId: rule.targetKind === "combo" ? rule.targetComboId : null,
|
|
budgetAction: rule.targetEffort === "none" ? "remove" : rule.budgetAction,
|
|
budgetTokens: rule.budgetAction === "set" ? rule.budgetTokens : null,
|
|
priority: rule.priority,
|
|
enabled: rule.enabled ? 1 : 0,
|
|
createdAt,
|
|
updatedAt,
|
|
};
|
|
}
|
|
|
|
export async function createReasoningRoutingRule(
|
|
rule: ReasoningRoutingRuleInput
|
|
): Promise<ReasoningRoutingRule> {
|
|
assertReferences(rule);
|
|
const id = randomUUID();
|
|
const now = new Date().toISOString();
|
|
getDbInstance()
|
|
.prepare(
|
|
`INSERT INTO reasoning_routing_rules (
|
|
id, name, description, scope, api_key_id, combo_id, connection_id, model_pattern,
|
|
source_effort, request_tags, tag_match_mode, effort_mode, target_effort, target_kind,
|
|
target_model, target_combo_id, budget_action, budget_tokens, priority, enabled, created_at, updated_at
|
|
) VALUES (
|
|
@id, @name, @description, @scope, @apiKeyId, @comboId, @connectionId, @modelPattern,
|
|
@sourceEffort, @requestTags, @tagMatchMode, @effortMode, @targetEffort, @targetKind,
|
|
@targetModel, @targetComboId, @budgetAction, @budgetTokens, @priority, @enabled, @createdAt, @updatedAt
|
|
)`
|
|
)
|
|
.run(ruleParams(rule, id, now, now));
|
|
invalidate();
|
|
return (await getReasoningRoutingRuleById(id))!;
|
|
}
|
|
|
|
export async function updateReasoningRoutingRule(
|
|
id: string,
|
|
patch: Partial<ReasoningRoutingRuleInput>
|
|
): Promise<ReasoningRoutingRule | null> {
|
|
const existing = await getReasoningRoutingRuleById(id);
|
|
if (!existing) return null;
|
|
const { id: _id, createdAt, updatedAt: _updatedAt, ...base } = existing;
|
|
const next = { ...base, ...patch } as ReasoningRoutingRuleInput;
|
|
assertReferences(next);
|
|
const now = new Date().toISOString();
|
|
getDbInstance()
|
|
.prepare(
|
|
`UPDATE reasoning_routing_rules SET
|
|
name=@name, description=@description, scope=@scope, api_key_id=@apiKeyId,
|
|
combo_id=@comboId, connection_id=@connectionId, model_pattern=@modelPattern,
|
|
source_effort=@sourceEffort, request_tags=@requestTags, tag_match_mode=@tagMatchMode,
|
|
effort_mode=@effortMode, target_effort=@targetEffort, target_kind=@targetKind,
|
|
target_model=@targetModel, target_combo_id=@targetComboId, budget_action=@budgetAction,
|
|
budget_tokens=@budgetTokens, priority=@priority, enabled=@enabled, updated_at=@updatedAt
|
|
WHERE id=@id`
|
|
)
|
|
.run(ruleParams(next, id, createdAt, now));
|
|
invalidate();
|
|
return getReasoningRoutingRuleById(id);
|
|
}
|
|
|
|
export async function deleteReasoningRoutingRule(id: string): Promise<boolean> {
|
|
const result = getDbInstance()
|
|
.prepare("DELETE FROM reasoning_routing_rules WHERE id = ?")
|
|
.run(id);
|
|
if ((result.changes ?? 0) > 0) invalidate();
|
|
return (result.changes ?? 0) > 0;
|
|
}
|
|
|
|
export function invalidateReasoningRoutingRuleCache(): void {
|
|
invalidate();
|
|
}
|
|
|
|
registerDbStateResetter(invalidateReasoningRoutingRuleCache);
|