mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 14:12:59 +03:00
* feat(dashboard): replace free-text model inputs with hidePaid-aware Selects (#6540) Swap RoutingTab.webSearchRouteModel, ComboDefaultsTab.handoffModel, and BackgroundDegradationTab's from/to fields from free-text inputs to a new shared ModelSelectField fed by the already hidePaidModels-aware GET /api/models, with an off-catalog "(custom)" fallback so an existing saved value is never silently dropped. ModelRoutingSection's glob pattern field gets a fail-open "matches only paid models" warning instead, since it's a wildcard matcher rather than a single model id. Adds save-time paid-target rejection (PAID_MODEL_TARGET_BLOCKED, 400) on PATCH /api/settings, PATCH /api/settings/combo-defaults, and PUT /api/settings/background-degradation when hidePaidModels is on, failing open for aliases/combo names/unrecognized providers. globToRegex is extracted from lib/db/modelComboMappings.ts into a new dependency-free shared/utils/globPattern.ts so both the DB module and the new client-side pattern heuristic reuse the same regex-building logic. * refactor(6540): extract paid-target guard in background-degradation PUT The inline hidePaid check nested if>if>for>if inside the PUT handler, pushing its cognitive complexity to 16 (>15) — a NEW violation that broke the check:complexity-ratchets gate (891 > baseline 890). Extracted the check into a module-local hasBlockedPaidTarget() helper; behaviour and response body are unchanged. cognitive-complexity back to 890 = baseline. * fix(i18n): mirror paidModelPatternWarning into pt-BR.json (#6540) The new key landed only in en.json; the i18n pt-BR integrity test (no drift, #6695) requires pt-BR.json to carry every en.json key.
143 lines
4.7 KiB
TypeScript
143 lines
4.7 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { getSettings, updateSettings } from "@/lib/localDb";
|
|
import { updateComboDefaultsSchema } from "@/shared/validation/schemas";
|
|
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
|
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
|
import { isPaidModelTarget } from "@/shared/utils/freeModels";
|
|
|
|
const LEGACY_COMBO_RESILIENCE_KEYS = new Set([
|
|
"timeoutMs",
|
|
"healthCheckEnabled",
|
|
"healthCheckTimeoutMs",
|
|
]);
|
|
|
|
function sanitizeComboRuntimeConfig(config?: Record<string, any> | null) {
|
|
if (!config || typeof config !== "object") return {};
|
|
return Object.fromEntries(
|
|
Object.entries(config).filter(
|
|
([key, value]) =>
|
|
value !== undefined && value !== null && !LEGACY_COMBO_RESILIENCE_KEYS.has(key)
|
|
)
|
|
);
|
|
}
|
|
|
|
function sanitizeProviderOverrides(overrides?: Record<string, any> | null) {
|
|
if (!overrides || typeof overrides !== "object") return {};
|
|
return Object.fromEntries(
|
|
Object.entries(overrides).map(([providerId, config]) => [
|
|
providerId,
|
|
sanitizeComboRuntimeConfig(config),
|
|
])
|
|
);
|
|
}
|
|
|
|
/**
|
|
* GET /api/settings/combo-defaults
|
|
* Returns the current combo global defaults and provider overrides
|
|
*/
|
|
export async function GET(request: Request) {
|
|
const authError = await requireManagementAuth(request);
|
|
if (authError) return authError;
|
|
try {
|
|
const settings: any = await getSettings();
|
|
const comboDefaults = sanitizeComboRuntimeConfig(settings.comboDefaults);
|
|
const providerOverrides = sanitizeProviderOverrides(settings.providerOverrides);
|
|
return NextResponse.json({
|
|
comboDefaults:
|
|
Object.keys(comboDefaults).length > 0
|
|
? comboDefaults
|
|
: {
|
|
strategy: "priority",
|
|
maxRetries: 1,
|
|
retryDelayMs: 2000,
|
|
fallbackDelayMs: 0,
|
|
handoffThreshold: 0.85,
|
|
handoffModel: "",
|
|
maxMessagesForSummary: 30,
|
|
maxComboDepth: 3,
|
|
trackMetrics: true,
|
|
reasoningTokenBufferEnabled: true,
|
|
zeroLatencyOptimizationsEnabled: false,
|
|
},
|
|
providerOverrides,
|
|
});
|
|
} catch (error) {
|
|
console.log("Error fetching combo defaults:", error);
|
|
return NextResponse.json({ error: "Failed to fetch combo defaults" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PATCH /api/settings/combo-defaults
|
|
* Update combo global defaults and/or provider overrides
|
|
* Body: { comboDefaults?: {...}, providerOverrides?: {...} }
|
|
*/
|
|
export async function PATCH(request: Request) {
|
|
const authError = await requireManagementAuth(request);
|
|
if (authError) return authError;
|
|
let rawBody;
|
|
try {
|
|
rawBody = await request.json();
|
|
} catch {
|
|
return NextResponse.json(
|
|
{
|
|
error: {
|
|
message: "Invalid request",
|
|
details: [{ field: "body", message: "Invalid JSON body" }],
|
|
},
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
try {
|
|
const validation = validateBody(updateComboDefaultsSchema, rawBody);
|
|
if (isValidationFailure(validation)) {
|
|
return NextResponse.json({ error: validation.error }, { status: 400 });
|
|
}
|
|
const body = validation.data;
|
|
|
|
// #6540: reject a paid-only handoffModel target when hidePaidModels is on.
|
|
// Fails open on "unknown" (aliases/combo names) — mirrors the settings
|
|
// route's PAID_MODEL_TARGET_BLOCKED check.
|
|
if (
|
|
typeof body.comboDefaults?.handoffModel === "string" &&
|
|
body.comboDefaults.handoffModel.trim() !== ""
|
|
) {
|
|
const currentSettings: any = await getSettings();
|
|
if (currentSettings?.hidePaidModels === true) {
|
|
if (isPaidModelTarget(body.comboDefaults.handoffModel) === "paid") {
|
|
return NextResponse.json(
|
|
{
|
|
error: {
|
|
code: "PAID_MODEL_TARGET_BLOCKED",
|
|
message:
|
|
"This field cannot target a paid-only model while 'Hide paid models' is enabled.",
|
|
},
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
const updates: Record<string, any> = {};
|
|
|
|
if (body.comboDefaults) {
|
|
updates.comboDefaults = sanitizeComboRuntimeConfig(body.comboDefaults);
|
|
}
|
|
if (body.providerOverrides) {
|
|
updates.providerOverrides = sanitizeProviderOverrides(body.providerOverrides);
|
|
}
|
|
|
|
const settings: any = await updateSettings(updates);
|
|
return NextResponse.json({
|
|
comboDefaults: sanitizeComboRuntimeConfig(settings.comboDefaults),
|
|
providerOverrides: sanitizeProviderOverrides(settings.providerOverrides),
|
|
});
|
|
} catch (error) {
|
|
console.log("Error updating combo defaults:", error);
|
|
return NextResponse.json({ error: "Failed to update combo defaults" }, { status: 500 });
|
|
}
|
|
}
|