mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-23 15:42:12 +03:00
604 lines
21 KiB
TypeScript
604 lines
21 KiB
TypeScript
import { getComboById } from "@/lib/db/combos";
|
|
import {
|
|
getReasoningRoutingRules,
|
|
type ReasoningBudgetAction,
|
|
type ReasoningEffort,
|
|
type ReasoningRoutingRule,
|
|
type ReasoningSourceEffort,
|
|
} from "@/lib/db/reasoningRoutingRules";
|
|
import { getResolvedModelCapabilities } from "@/lib/modelCapabilities";
|
|
import { normalizeRoutingTags } from "@/domain/tagRouter";
|
|
import { splitClaudeEffortSuffix } from "@omniroute/open-sse/config/providerModels.ts";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
const EFFORTS = new Set<ReasoningEffort>([
|
|
"none",
|
|
"low",
|
|
"medium",
|
|
"high",
|
|
"xhigh",
|
|
"max",
|
|
"ultra",
|
|
]);
|
|
const SCOPE_RANK: Record<ReasoningRoutingRule["scope"], number> = {
|
|
apiKey: 4,
|
|
combo: 3,
|
|
model: 2,
|
|
global: 1,
|
|
connection: 0,
|
|
};
|
|
|
|
export interface ExtractedReasoningIntent {
|
|
model: string;
|
|
effort: ReasoningEffort | null;
|
|
sourceEffort: Exclude<ReasoningSourceEffort, "any"> | "signal";
|
|
hasReasoningSignal: boolean;
|
|
hasThinkingBudget: boolean;
|
|
}
|
|
|
|
export interface ReasoningRuleDecision {
|
|
rule: ReasoningRoutingRule;
|
|
sourceModel: string;
|
|
sourceEffort: Exclude<ReasoningSourceEffort, "any"> | "signal";
|
|
targetModel: string;
|
|
targetCombo: JsonRecord | null;
|
|
targetEffort: ReasoningEffort | null;
|
|
capability: "supported" | "unsupported" | "unknown";
|
|
requiresReasoning: boolean;
|
|
warnings: string[];
|
|
}
|
|
|
|
function asRecord(value: unknown): JsonRecord {
|
|
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
|
|
}
|
|
|
|
function effort(value: unknown): ReasoningEffort | null {
|
|
if (typeof value !== "string") return null;
|
|
const normalized = value.trim().toLowerCase() as ReasoningEffort;
|
|
return EFFORTS.has(normalized) ? normalized : null;
|
|
}
|
|
|
|
function thinkingLevelEffort(value: unknown): ReasoningEffort | null {
|
|
if (value === false) return "none";
|
|
if (typeof value !== "string") return null;
|
|
const normalized = value.trim().toLowerCase();
|
|
if (["disabled", "off", "false"].includes(normalized)) return "none";
|
|
return effort(normalized);
|
|
}
|
|
|
|
function splitGenericEffortSuffix(model: string): {
|
|
model: string;
|
|
effort: ReasoningEffort | null;
|
|
} {
|
|
const claude = model.toLowerCase().includes("claude") ? splitClaudeEffortSuffix(model) : null;
|
|
if (claude?.effort) return { model: claude.baseModel, effort: claude.effort };
|
|
return splitCodexEffortSuffix(model) ?? { model, effort: null };
|
|
}
|
|
|
|
function supportsCodexSuffix(candidate: string, normalizedBase: string): boolean {
|
|
if (candidate === "max") return /^gpt-5\.6-(?:sol|terra|luna)$/.test(normalizedBase);
|
|
if (candidate === "ultra") return /^gpt-5\.6-(?:sol|terra)$/.test(normalizedBase);
|
|
return true;
|
|
}
|
|
|
|
function splitCodexEffortSuffix(model: string): { model: string; effort: ReasoningEffort } | null {
|
|
const lower = model.toLowerCase();
|
|
for (const candidate of ["ultra", "xhigh", "medium", "high", "none", "low", "max"] as const) {
|
|
if (!lower.endsWith(`-${candidate}`)) continue;
|
|
const baseModel = model.slice(0, -(candidate.length + 1));
|
|
const normalizedBase = baseModel.toLowerCase().replace(/^(?:codex|cx)\//, "");
|
|
if (/^gpt-[\w.-]+$/.test(normalizedBase) && supportsCodexSuffix(candidate, normalizedBase)) {
|
|
return { model: baseModel, effort: candidate };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function firstDefinedEffort(
|
|
body: JsonRecord,
|
|
reasoning: JsonRecord,
|
|
thinking: JsonRecord,
|
|
thinkingConfig: JsonRecord,
|
|
suffixEffort: ReasoningEffort | null
|
|
): ReasoningEffort | null {
|
|
const output = asRecord(body.output_config);
|
|
const values = [
|
|
effort(reasoning.effort),
|
|
effort(body.reasoning_effort),
|
|
effort(body.reasoningEffort),
|
|
effort(body.effort),
|
|
effort(output.effort),
|
|
thinkingLevelEffort(body.thinkingLevel),
|
|
thinkingLevelEffort(body.thinking_level),
|
|
thinkingLevelEffort(thinkingConfig.thinkingLevel ?? thinkingConfig.thinking_level),
|
|
body.thinking === false || thinking.type === "disabled" ? "none" : null,
|
|
suffixEffort,
|
|
];
|
|
return values.find((value): value is ReasoningEffort => value !== null) ?? null;
|
|
}
|
|
|
|
function hasNumericField(record: JsonRecord, key: string): boolean {
|
|
return typeof record[key] === "number";
|
|
}
|
|
|
|
function detectThinkingBudget(
|
|
body: JsonRecord,
|
|
reasoning: JsonRecord,
|
|
thinking: JsonRecord,
|
|
thinkingConfig: JsonRecord
|
|
): boolean {
|
|
const budgetFields: Array<[JsonRecord, string]> = [
|
|
[thinking, "budget_tokens"],
|
|
[thinking, "budgetTokens"],
|
|
[reasoning, "budget_tokens"],
|
|
[reasoning, "budgetTokens"],
|
|
[reasoning, "max_tokens"],
|
|
[body, "thinking_budget"],
|
|
[body, "thinkingBudget"],
|
|
[thinkingConfig, "thinkingBudget"],
|
|
[thinkingConfig, "thinking_budget"],
|
|
];
|
|
return budgetFields.some(([record, key]) => hasNumericField(record, key));
|
|
}
|
|
|
|
function hasReasoningSignal(
|
|
body: JsonRecord,
|
|
reasoning: JsonRecord,
|
|
thinkingConfig: JsonRecord,
|
|
explicitEffort: ReasoningEffort | null,
|
|
hasBudget: boolean
|
|
): boolean {
|
|
return (
|
|
explicitEffort !== null ||
|
|
[
|
|
body.thinking,
|
|
body.thinkingLevel,
|
|
body.thinking_level,
|
|
reasoning.effort,
|
|
body.reasoning_effort,
|
|
body.reasoningEffort,
|
|
body.effort,
|
|
asRecord(body.output_config).effort,
|
|
thinkingConfig.thinkingLevel,
|
|
thinkingConfig.thinking_level,
|
|
].some((value) => value !== undefined) ||
|
|
hasBudget
|
|
);
|
|
}
|
|
|
|
export function extractReasoningIntent(
|
|
modelInput: unknown,
|
|
bodyInput: unknown
|
|
): ExtractedReasoningIntent {
|
|
const body = asRecord(bodyInput);
|
|
const rawModel = typeof modelInput === "string" ? modelInput.trim() : "";
|
|
const suffixed = splitGenericEffortSuffix(rawModel);
|
|
const reasoning = asRecord(body.reasoning);
|
|
const thinking = asRecord(body.thinking);
|
|
const generationConfig = asRecord(body.generationConfig);
|
|
const thinkingConfig = asRecord(
|
|
generationConfig.thinkingConfig ?? generationConfig.thinking_config
|
|
);
|
|
const explicitEffort = firstDefinedEffort(
|
|
body,
|
|
reasoning,
|
|
thinking,
|
|
thinkingConfig,
|
|
suffixed.effort
|
|
);
|
|
const hasThinkingBudget = detectThinkingBudget(body, reasoning, thinking, thinkingConfig);
|
|
const hasReasoningSignalValue = hasReasoningSignal(
|
|
body,
|
|
reasoning,
|
|
thinkingConfig,
|
|
explicitEffort,
|
|
hasThinkingBudget
|
|
);
|
|
return {
|
|
model: suffixed.model,
|
|
effort: explicitEffort,
|
|
sourceEffort: explicitEffort ?? (hasReasoningSignalValue ? "signal" : "missing"),
|
|
hasReasoningSignal: hasReasoningSignalValue,
|
|
hasThinkingBudget,
|
|
};
|
|
}
|
|
|
|
export function globMatches(pattern: string, value: string): boolean {
|
|
const escaped = pattern
|
|
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
.replace(/\*/g, ".*")
|
|
.replace(/\?/g, ".");
|
|
return new RegExp(`^${escaped}$`, "i").test(value);
|
|
}
|
|
|
|
export async function resolveReasoningSourceModels(
|
|
model: string,
|
|
resolve: (model: string) => Promise<{ provider?: string | null; model?: string | null }>
|
|
): Promise<{ normalized: string; aliases: string[] }> {
|
|
try {
|
|
const resolved = await resolve(model);
|
|
const resolvedModel =
|
|
typeof resolved?.model === "string" && resolved.model.trim() ? resolved.model.trim() : null;
|
|
const provider =
|
|
typeof resolved?.provider === "string" && resolved.provider.trim()
|
|
? resolved.provider.trim()
|
|
: null;
|
|
const normalized =
|
|
provider && resolvedModel ? `${provider}/${resolvedModel}` : resolvedModel || model;
|
|
return {
|
|
normalized,
|
|
aliases: normalized.toLowerCase() === model.toLowerCase() ? [] : [model],
|
|
};
|
|
} catch {
|
|
return { normalized: model, aliases: [] };
|
|
}
|
|
}
|
|
|
|
function tagsMatch(rule: ReasoningRoutingRule, requestTags: string[]): boolean {
|
|
if (rule.requestTags.length === 0) return true;
|
|
const available = new Set(normalizeRoutingTags(requestTags));
|
|
return rule.tagMatchMode === "all"
|
|
? rule.requestTags.every((tag) => available.has(tag))
|
|
: rule.requestTags.some((tag) => available.has(tag));
|
|
}
|
|
|
|
function modelMatches(rule: ReasoningRoutingRule, model: string): boolean {
|
|
if (!rule.modelPattern) return true;
|
|
return globMatches(rule.modelPattern, model);
|
|
}
|
|
|
|
function isExactModelMatch(rule: ReasoningRoutingRule, model: string): boolean {
|
|
return (
|
|
!!rule.modelPattern &&
|
|
!/[?*]/.test(rule.modelPattern) &&
|
|
rule.modelPattern.toLowerCase() === model.toLowerCase()
|
|
);
|
|
}
|
|
|
|
function capabilityFor(
|
|
model: string,
|
|
targetEffort: ReasoningEffort | null,
|
|
requiresReasoning = Boolean(targetEffort && targetEffort !== "none")
|
|
) {
|
|
if (!requiresReasoning || targetEffort === "none") return "supported" as const;
|
|
const capabilities = getResolvedModelCapabilities(model);
|
|
if (capabilities.supportsThinking === false) return "unsupported" as const;
|
|
if (targetEffort === "max" || targetEffort === "ultra") {
|
|
const normalized = model.toLowerCase().replace(/^(?:codex|cx)\//, "");
|
|
const supported =
|
|
targetEffort === "ultra"
|
|
? /^gpt-5\.6-(?:sol|terra)(?:-|$)/.test(normalized)
|
|
: /^gpt-5\.6-(?:sol|terra|luna)(?:-|$)/.test(normalized);
|
|
if (supported) return "supported" as const;
|
|
if (capabilities.supportsThinking === null) return "unknown" as const;
|
|
return "unsupported" as const;
|
|
}
|
|
return capabilities.supportsThinking === null ? ("unknown" as const) : ("supported" as const);
|
|
}
|
|
|
|
type RoutingInput = {
|
|
sourceModel: string;
|
|
sourceModelAliases?: string[];
|
|
sourceEffort: Exclude<ReasoningSourceEffort, "any"> | "signal";
|
|
hasReasoningSignal: boolean;
|
|
hasThinkingBudget?: boolean;
|
|
apiKeyId?: string | null;
|
|
comboId?: string | null;
|
|
connectionId?: string | null;
|
|
requestTags?: string[];
|
|
connectionOnly?: boolean;
|
|
capabilityModel?: string | null;
|
|
};
|
|
|
|
function inputModels(input: RoutingInput): string[] {
|
|
return [input.sourceModel, ...(input.sourceModelAliases ?? [])];
|
|
}
|
|
|
|
function scopeMatches(rule: ReasoningRoutingRule, input: RoutingInput): boolean {
|
|
if (input.connectionOnly ? rule.scope !== "connection" : rule.scope === "connection") {
|
|
return false;
|
|
}
|
|
if (rule.scope === "apiKey" && rule.apiKeyId !== input.apiKeyId) return false;
|
|
if (rule.scope === "combo" && rule.comboId !== input.comboId) return false;
|
|
if (rule.scope === "connection" && rule.connectionId !== input.connectionId) return false;
|
|
return rule.sourceEffort === "any" || rule.sourceEffort === input.sourceEffort;
|
|
}
|
|
|
|
function ruleMatches(rule: ReasoningRoutingRule, input: RoutingInput): boolean {
|
|
return (
|
|
scopeMatches(rule, input) &&
|
|
inputModels(input).some((model) => modelMatches(rule, model)) &&
|
|
tagsMatch(rule, input.requestTags ?? [])
|
|
);
|
|
}
|
|
|
|
function exactMatchScore(rule: ReasoningRoutingRule, input: RoutingInput): number {
|
|
return Number(inputModels(input).some((model) => isExactModelMatch(rule, model)));
|
|
}
|
|
|
|
function compareRules(input: RoutingInput) {
|
|
return (a: ReasoningRoutingRule, b: ReasoningRoutingRule) =>
|
|
SCOPE_RANK[b.scope] - SCOPE_RANK[a.scope] ||
|
|
b.priority - a.priority ||
|
|
exactMatchScore(b, input) - exactMatchScore(a, input) ||
|
|
a.createdAt.localeCompare(b.createdAt) ||
|
|
a.id.localeCompare(b.id);
|
|
}
|
|
|
|
function resolveTargetModel(
|
|
rule: ReasoningRoutingRule,
|
|
sourceModel: string,
|
|
combo: JsonRecord | null
|
|
) {
|
|
if (combo) return typeof combo.name === "string" ? combo.name : rule.targetComboId;
|
|
return rule.targetKind === "model" && rule.targetModel ? rule.targetModel : sourceModel;
|
|
}
|
|
|
|
async function resolveTarget(rule: ReasoningRoutingRule, sourceModel: string) {
|
|
if (rule.targetKind !== "combo" || !rule.targetComboId) {
|
|
return { targetCombo: null, targetModel: resolveTargetModel(rule, sourceModel, null) };
|
|
}
|
|
const targetCombo = (await getComboById(rule.targetComboId)) as JsonRecord | null;
|
|
if (!targetCombo) throw new Error("Reasoning routing target combo does not exist");
|
|
return { targetCombo, targetModel: resolveTargetModel(rule, sourceModel, targetCombo) };
|
|
}
|
|
|
|
function resolveTargetEffort(
|
|
rule: ReasoningRoutingRule,
|
|
input: RoutingInput
|
|
): ReasoningEffort | null {
|
|
const inherits =
|
|
rule.effortMode === "inherit" || (rule.effortMode === "default" && input.hasReasoningSignal);
|
|
if (!inherits) return rule.targetEffort;
|
|
return input.sourceEffort === "missing" || input.sourceEffort === "signal"
|
|
? null
|
|
: input.sourceEffort;
|
|
}
|
|
|
|
function resolveCapability(
|
|
targetCombo: JsonRecord | null,
|
|
targetModel: string,
|
|
targetEffort: ReasoningEffort | null,
|
|
requiresReasoning: boolean
|
|
): { capability: ReasoningRuleDecision["capability"]; warnings: string[] } {
|
|
if (!targetCombo) {
|
|
return {
|
|
capability: capabilityFor(targetModel, targetEffort, requiresReasoning),
|
|
warnings: [],
|
|
};
|
|
}
|
|
const models = Array.isArray(targetCombo.models) ? targetCombo.models : [];
|
|
const statuses = models.map((entry) => {
|
|
const record = asRecord(entry);
|
|
const model =
|
|
typeof entry === "string"
|
|
? entry
|
|
: typeof record.model === "string"
|
|
? String(record.model)
|
|
: null;
|
|
return model ? capabilityFor(model, targetEffort, requiresReasoning) : "unknown";
|
|
});
|
|
const capability =
|
|
statuses.length > 0 && statuses.every((status) => status === "unsupported")
|
|
? "unsupported"
|
|
: statuses.some((status) => status === "unknown")
|
|
? "unknown"
|
|
: "supported";
|
|
const unsupportedCount = statuses.filter((status) => status === "unsupported").length;
|
|
const warnings =
|
|
unsupportedCount > 0 && capability !== "unsupported"
|
|
? [`${unsupportedCount} incompatible combo target(s) will be skipped`]
|
|
: [];
|
|
return { capability, warnings };
|
|
}
|
|
|
|
export async function resolveReasoningRoutingRule(
|
|
input: RoutingInput
|
|
): Promise<ReasoningRuleDecision | null> {
|
|
const rules = await getReasoningRoutingRules({ enabledOnly: true });
|
|
const candidates = rules.filter((rule) => ruleMatches(rule, input)).sort(compareRules(input));
|
|
const rule = candidates[0];
|
|
if (!rule) return null;
|
|
|
|
const { targetCombo, targetModel } = await resolveTarget(rule, input.sourceModel);
|
|
const targetEffort = resolveTargetEffort(rule, input);
|
|
const budgetAction = targetEffort === "none" ? "remove" : rule.budgetAction;
|
|
const requiresReasoning =
|
|
targetEffort !== "none" &&
|
|
(Boolean(targetEffort) ||
|
|
budgetAction === "set" ||
|
|
(budgetAction === "preserve" && input.hasThinkingBudget === true));
|
|
const capabilityResult = resolveCapability(
|
|
targetCombo,
|
|
input.capabilityModel || targetModel,
|
|
targetEffort,
|
|
requiresReasoning
|
|
);
|
|
const capability = capabilityResult.capability;
|
|
const warnings = capabilityResult.warnings;
|
|
if (capability === "unknown") warnings.push("Reasoning capability could not be verified");
|
|
return {
|
|
rule,
|
|
sourceModel: input.sourceModel,
|
|
sourceEffort: input.sourceEffort,
|
|
targetModel,
|
|
targetCombo,
|
|
targetEffort,
|
|
capability,
|
|
requiresReasoning,
|
|
warnings,
|
|
};
|
|
}
|
|
|
|
function clearReasoning(body: JsonRecord): void {
|
|
delete body.reasoning_effort;
|
|
delete body.reasoningEffort;
|
|
delete body.reasoning;
|
|
delete body.effort;
|
|
delete body.thinking;
|
|
delete body.thinkingLevel;
|
|
delete body.thinking_level;
|
|
delete body.thinking_budget;
|
|
delete body.thinkingBudget;
|
|
if (body.output_config && typeof body.output_config === "object") {
|
|
const output = { ...asRecord(body.output_config) };
|
|
delete output.effort;
|
|
body.output_config = output;
|
|
}
|
|
}
|
|
|
|
function clearDiscreteReasoning(body: JsonRecord): void {
|
|
delete body.reasoning_effort;
|
|
delete body.reasoningEffort;
|
|
delete body.effort;
|
|
delete body.thinkingLevel;
|
|
delete body.thinking_level;
|
|
if (body.reasoning && typeof body.reasoning === "object") {
|
|
const reasoning = { ...asRecord(body.reasoning) };
|
|
delete reasoning.effort;
|
|
if (Object.keys(reasoning).length > 0) body.reasoning = reasoning;
|
|
else delete body.reasoning;
|
|
}
|
|
if (body.output_config && typeof body.output_config === "object") {
|
|
const output = { ...asRecord(body.output_config) };
|
|
delete output.effort;
|
|
if (Object.keys(output).length > 0) body.output_config = output;
|
|
else delete body.output_config;
|
|
}
|
|
const thinking = asRecord(body.thinking);
|
|
if (body.thinking !== undefined && typeof thinking.budget_tokens !== "number") {
|
|
delete body.thinking;
|
|
}
|
|
}
|
|
|
|
function removeBudgets(body: JsonRecord): void {
|
|
delete body.thinking_budget;
|
|
delete body.thinkingBudget;
|
|
const thinking = { ...asRecord(body.thinking) };
|
|
delete thinking.budget_tokens;
|
|
delete thinking.budgetTokens;
|
|
if (Object.keys(thinking).length > 0 && body.thinking !== undefined) body.thinking = thinking;
|
|
else delete body.thinking;
|
|
const reasoning = { ...asRecord(body.reasoning) };
|
|
delete reasoning.budget_tokens;
|
|
delete reasoning.budgetTokens;
|
|
delete reasoning.max_tokens;
|
|
if (Object.keys(reasoning).length > 0 && body.reasoning !== undefined) body.reasoning = reasoning;
|
|
else if (body.reasoning !== undefined) delete body.reasoning;
|
|
const generationConfig = { ...asRecord(body.generationConfig) };
|
|
delete generationConfig.thinkingConfig;
|
|
delete generationConfig.thinking_config;
|
|
if (Object.keys(generationConfig).length > 0) body.generationConfig = generationConfig;
|
|
else delete body.generationConfig;
|
|
}
|
|
|
|
function applyBudget(body: JsonRecord, action: ReasoningBudgetAction, budget: number | null): void {
|
|
if (action === "preserve") return;
|
|
removeBudgets(body);
|
|
if (action === "remove") return;
|
|
body.thinking = { type: "enabled", budget_tokens: budget };
|
|
}
|
|
|
|
export function attachReasoningRuleDirective(
|
|
bodyInput: unknown,
|
|
decision: ReasoningRuleDecision
|
|
): JsonRecord {
|
|
const source = asRecord(bodyInput);
|
|
const body: JsonRecord = {
|
|
...source,
|
|
model: decision.rule.scope === "connection" ? source.model : decision.targetModel,
|
|
};
|
|
body._omnirouteReasoningRule = {
|
|
id: decision.rule.id,
|
|
effortMode: decision.rule.effortMode,
|
|
targetEffort: decision.targetEffort,
|
|
budgetAction: decision.targetEffort === "none" ? "remove" : decision.rule.budgetAction,
|
|
budgetTokens: decision.rule.budgetTokens,
|
|
};
|
|
body._omnirouteReasoningRouteTrace = {
|
|
ruleId: decision.rule.id,
|
|
ruleName: decision.rule.name,
|
|
scope: decision.rule.scope,
|
|
sourceModel: decision.sourceModel,
|
|
targetModel: decision.targetModel,
|
|
sourceEffort: decision.sourceEffort,
|
|
targetEffort: decision.targetEffort,
|
|
effortMode: decision.rule.effortMode,
|
|
budgetAction: decision.targetEffort === "none" ? "remove" : decision.rule.budgetAction,
|
|
capability: decision.capability,
|
|
warnings: decision.warnings,
|
|
};
|
|
return body;
|
|
}
|
|
|
|
export function applyReasoningRuleDirective(bodyInput: unknown): unknown {
|
|
const source = asRecord(bodyInput);
|
|
const directive = asRecord(source._omnirouteReasoningRule);
|
|
if (!directive.id) return bodyInput;
|
|
const body = { ...source };
|
|
delete body._omnirouteReasoningRule;
|
|
const effortMode = directive.effortMode;
|
|
const targetEffort = effort(directive.targetEffort);
|
|
if (effortMode === "force" && targetEffort === "none") clearReasoning(body);
|
|
else if ((effortMode === "force" || effortMode === "default") && targetEffort) {
|
|
if (effortMode === "force") clearDiscreteReasoning(body);
|
|
body.reasoning_effort = targetEffort;
|
|
body.reasoning = { ...asRecord(body.reasoning), effort: targetEffort };
|
|
body.output_config = { ...asRecord(body.output_config), effort: targetEffort };
|
|
}
|
|
applyBudget(
|
|
body,
|
|
directive.budgetAction as ReasoningBudgetAction,
|
|
typeof directive.budgetTokens === "number" ? directive.budgetTokens : null
|
|
);
|
|
return body;
|
|
}
|
|
|
|
export function filterComboForReasoningDecision(
|
|
comboInput: unknown,
|
|
decision: ReasoningRuleDecision
|
|
): { combo: JsonRecord | null; removed: string[] } {
|
|
const combo = { ...asRecord(comboInput) };
|
|
if (!Array.isArray(combo.models) || !decision.requiresReasoning) return { combo, removed: [] };
|
|
const removed: string[] = [];
|
|
combo.models = combo.models.filter((entry) => {
|
|
const model =
|
|
typeof entry === "string"
|
|
? entry
|
|
: typeof asRecord(entry).model === "string"
|
|
? String(asRecord(entry).model)
|
|
: null;
|
|
if (!model) return true;
|
|
if (capabilityFor(model, decision.targetEffort, true) !== "unsupported") return true;
|
|
removed.push(model);
|
|
return false;
|
|
});
|
|
return { combo: (combo.models as unknown[]).length > 0 ? combo : null, removed };
|
|
}
|
|
|
|
export function isCodexTarget(model: string): boolean {
|
|
return !model.includes("/") || /^(?:codex|cx)\//i.test(model);
|
|
}
|
|
|
|
export function validateCodexWsDecision(decision: ReasoningRuleDecision): string | null {
|
|
if (decision.targetCombo) {
|
|
const models = Array.isArray(decision.targetCombo.models) ? decision.targetCombo.models : [];
|
|
const allCodex =
|
|
models.length > 0 &&
|
|
models.every((entry) => {
|
|
const model =
|
|
typeof entry === "string"
|
|
? entry
|
|
: typeof asRecord(entry).model === "string"
|
|
? String(asRecord(entry).model)
|
|
: "";
|
|
return !!model && isCodexTarget(model);
|
|
});
|
|
if (!allCodex) return "Codex WebSocket reasoning rules require a Codex-only target";
|
|
return "Codex WebSocket transport cannot execute combo targets";
|
|
}
|
|
return isCodexTarget(decision.targetModel)
|
|
? null
|
|
: "Codex WebSocket reasoning rules require a Codex target model";
|
|
}
|