mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(dashboard-api): apply hidePaidModels to /api/models + openrouter-catalog + test endpoints (#6328) (#6552)
fix(dashboard-api): apply hidePaidModels to /api/models + openrouter-catalog + test endpoints (#6328). Integrated into release/v3.8.47. (thanks @chirag127)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_comment": "Catraca de complexidade (check-complexity.mjs, ESLint core rules complexity>=15 e max-lines-per-function>80 sobre src+open-sse+electron+bin via eslint.complexity.config.mjs). Conta total de violacoes; so pode cair. --update ratcheta.",
|
||||
"count": 2050,
|
||||
"count": 2052,
|
||||
"_rebaseline_2026_07_07_v3846_release_close": "2035->2050 (+15). v3.8.46 release close (generate-release Phase 0 pre-flight): drift herdado do merge burst do ciclo (39 commits do dia + campanha /review-*). Trust-but-verify: os fixes de base-red do captain (agentSkills path.resolve #6366, catalogo cache #6408, tipagem de teste no-explicit-any, MitmProxyTab suppression) sao complexity-net-zero — check:complexity mede 2050 identico com e sem os fixes (a catraca NAO roda no fast-path PR->release, entao o ramo acumulou sem rebaselinar). Tighten via --update next cycle.",
|
||||
"_rebaseline_2026_07_04_v3844_release_close": "2026->2028 (+2). v3.8.44 release close (generate-release Phase 0/1): drift residual do fim do ciclo medido no tip pos-#6155 (merge burst final: #6155 cooling-panel + #6104 Kenari + #6139/#6128 provider-limits). Trust-but-verify: os 2 fixes de codigo do release-captain (model.ts alias boundary, auggie.ts stdin error handlers) adicionam 0 violacoes NOVAS — eslint.complexity direto nos 2 arquivos flagra apenas funcoes que ja estouravam o limite antes (runStreaming/start ja >80 linhas; resolveModelByProviderInference/getModelInfoCore pre-existentes de #5918), e resolveProviderAlias segue abaixo de 15. Logo o +2 e drift herdado do burst. Tighten via --update next cycle.",
|
||||
"_rebaseline_2026_07_03_v3844_ipfilter_release_green": "2015->2026 (+11). v3.8.44 cycle drift measured on release tip 32e4c906e during the #6131/#5975 release-green rebaseline. Inherited from the merge burst (Quality Ratchet does not run on PR->release fast-gates). route-edge-coverage +7 is my #5975 test comment; the rest is parallel-session drift. Tighten via --update next cycle.",
|
||||
@@ -35,5 +35,6 @@
|
||||
"_rebaseline_2026_06_10": "Re-baseline consciente: 1739 foi medido na branch das Fases 0-6 (base ~v3.8.17); a v3.8.18 publicada ja carrega 1746 (provado: o commit-base 5f2722bd6, anterior a qualquer commit do ciclo v3.8.19, mede 1746 — funcoes complexas dos reworks RequestLoggerV2/stream/combo). Mesma familia dos re-baselines de eslintWarnings/file-size. Reducao = Fase 6A (2026-06-16).",
|
||||
"_rebaseline_2026_06_13_6a11": "Re-baseline consciente Task 6A.11: escopo ampliado para src+open-sse+electron+bin (electron/bin contribuem 0 violacoes novas — todos os 4 arquivos .ts em bin/ estao abaixo dos thresholds). Drift 1746→1794 pre-existente de features mergeadas nos ciclos v3.8.22/v3.8.23 (nao causado por esta task). Congelado no valor real medido para destrancar o gate.",
|
||||
"_rebaseline_2026_06_26_v3837_release": "1950->1963 (+13). v3.8.37 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings/complexity accrued unmeasured across this cycle's 76 commits — provider adds DGrid/Pioneer/xAI, headroom proxy lifecycle #4649, ~50 SSE/translator fixes, Engine Combos #5062). Trust-but-verify: this release-finalize working tree touches ONLY CHANGELOG.md, docs/i18n/*/CHANGELOG.md mirrors, and these baselines — 0 production-code change, so all drift is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
|
||||
"_rebaseline_2026_07_06_v3845_release_close": "2028->2035 (+7). v3.8.45 release close (generate-release Phase 0): drift herdado do merge burst final do ciclo (#6216 streaming fixes, #6251/#6253 dashboard UX, #6292 zero-width, fixes do pre-flight ce897453 — todos test/config/workflow-neutros em complexidade nova, verificado pelo validador no tip 5ecca12aa5). Tighten via --update next cycle."
|
||||
}
|
||||
"_rebaseline_2026_07_06_v3845_release_close": "2028->2035 (+7). v3.8.45 release close (generate-release Phase 0): drift herdado do merge burst final do ciclo (#6216 streaming fixes, #6251/#6253 dashboard UX, #6292 zero-width, fixes do pre-flight ce897453 — todos test/config/workflow-neutros em complexidade nova, verificado pelo validador no tip 5ecca12aa5). Tighten via --update next cycle.",
|
||||
"_rebaseline_2026_07_07_6552_chirag_api_models_filter": "2050->2052 (+2). PR #6552 (@chirag127, #6328): hidePaidModels filter across the 4 dashboard /api/models endpoints adds 2 functions over the complexity threshold. Owner-approved rebaseline (contributor own-growth). Tighten via --update next cycle."
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { getOpenRouterCatalog, refreshOpenRouterCatalog } from "@/lib/catalog/openrouterCatalog";
|
||||
import { getSettings } from "@/lib/db/settings";
|
||||
import { isFreeModel } from "@/shared/utils/freeModels";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
// Require authentication (dashboard/API key)
|
||||
@@ -19,30 +21,42 @@ export async function GET(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// #6328 (follow-up to #6495): REMOVE — not just hide — paid models from the
|
||||
// OpenRouter catalog echo when hidePaidModels is on. Fail open on settings read.
|
||||
let hidePaid = false;
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
hidePaid = settings?.hidePaidModels === true;
|
||||
} catch {}
|
||||
const applyFilter = <T extends { id?: string }>(data: T[]): T[] =>
|
||||
hidePaid ? data.filter((m) => isFreeModel("or", m as { id: string; pricing?: unknown })) : data;
|
||||
|
||||
const forceRefresh = req.nextUrl.searchParams.get("refresh") === "true";
|
||||
|
||||
if (forceRefresh) {
|
||||
const result = await refreshOpenRouterCatalog();
|
||||
const data = applyFilter(result.data);
|
||||
return NextResponse.json({
|
||||
object: "list",
|
||||
data: result.data,
|
||||
data,
|
||||
meta: {
|
||||
source: result.ok ? "fresh" : "error",
|
||||
count: result.data.length,
|
||||
count: data.length,
|
||||
error: result.error ?? undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const result = await getOpenRouterCatalog();
|
||||
const data = applyFilter(result.data);
|
||||
return NextResponse.json({
|
||||
object: "list",
|
||||
data: result.data,
|
||||
data,
|
||||
meta: {
|
||||
source: result.fromCache ? (result.stale ? "stale-cache" : "cache") : "fresh",
|
||||
cachedAt: result.cachedAt ?? undefined,
|
||||
stale: result.stale,
|
||||
count: result.data.length,
|
||||
count: data.length,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { AI_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models";
|
||||
import { updateModelAliasSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { hasEligibleConnectionForModel } from "@/domain/connectionModelRules";
|
||||
import { getSettings } from "@/lib/db/settings";
|
||||
import { isFreeModel, providerHasFreeModels } from "@/shared/utils/freeModels";
|
||||
|
||||
// GET /api/models - Get models with aliases (only from active providers by default)
|
||||
export async function GET(request: Request) {
|
||||
@@ -85,7 +87,22 @@ export async function GET(request: Request) {
|
||||
};
|
||||
}).filter((m: any) => showAll || m.available);
|
||||
|
||||
return NextResponse.json({ models });
|
||||
// #6328 (follow-up to #6495): REMOVE — not just hide — paid models from the
|
||||
// dashboard model picker when the operator opts into hidePaidModels. Mirrors
|
||||
// the `shouldHidePaid` guard in `src/app/api/v1/models/catalog.ts` (public
|
||||
// catalog). Settings read fails open: on error we preserve pre-patch behavior.
|
||||
let hidePaid = false;
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
hidePaid = settings?.hidePaidModels === true;
|
||||
} catch {}
|
||||
const filtered = hidePaid
|
||||
? models.filter(
|
||||
(m: { provider: string; model: string }) => providerHasFreeModels(m.provider) && isFreeModel(m.provider, { id: m.model })
|
||||
)
|
||||
: models;
|
||||
|
||||
return NextResponse.json({ models: filtered });
|
||||
} catch (error) {
|
||||
console.log("Error fetching models:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch models" }, { status: 500 });
|
||||
|
||||
@@ -16,6 +16,8 @@ import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { runSingleModelTest } from "@/lib/api/modelTestRunner";
|
||||
import { setModelIsHidden } from "@/lib/localDb";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
import { getSettings } from "@/lib/db/settings";
|
||||
import { isFreeModel, providerHasFreeModels } from "@/shared/utils/freeModels";
|
||||
import * as log from "@/sse/utils/logger";
|
||||
|
||||
const PER_MODEL_TIMEOUT_MS = 20_000;
|
||||
@@ -80,6 +82,16 @@ export async function POST(request: Request) {
|
||||
}
|
||||
const { providerId, modelIds, connectionId, respectRateLimit, autoHideFailed } = validation.data;
|
||||
|
||||
// #6328 (follow-up to #6495): REMOVE — not just hide — paid Test-all dispatches
|
||||
// when hidePaidModels is on. Paid ids are skipped inside the loop with a
|
||||
// "Skipped" entry; the skip does NOT touch the consecutive-rate-limit halt
|
||||
// counter (skips are not upstream failures). Fail open on settings read.
|
||||
let hidePaid = false;
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
hidePaid = settings?.hidePaidModels === true;
|
||||
} catch {}
|
||||
|
||||
log.info(
|
||||
"MODEL_TEST_ALL",
|
||||
`Starting batch test for ${modelIds.length} model(s) on provider ${providerId}`,
|
||||
@@ -98,6 +110,20 @@ export async function POST(request: Request) {
|
||||
let stopReason: "consecutive_rate_limits" | undefined;
|
||||
|
||||
for (const modelId of modelIds) {
|
||||
// #6328: skip paid ids without dispatching; do not increment
|
||||
// consecutiveRateLimits — skip is not a rate-limited failure.
|
||||
if (
|
||||
hidePaid &&
|
||||
!(providerHasFreeModels(providerId) && isFreeModel(providerId, { id: modelId }))
|
||||
) {
|
||||
results[modelId] = {
|
||||
status: "error",
|
||||
latencyMs: 0,
|
||||
error: "Skipped: paid model with hidePaidModels enabled",
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
let entry: BatchTestResultEntry;
|
||||
try {
|
||||
// `runSingleModelTest` only engages the Bottleneck rate limiter when
|
||||
|
||||
@@ -3,6 +3,8 @@ import { z } from "zod";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { runSingleModelTest } from "@/lib/api/modelTestRunner";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
|
||||
import { getSettings } from "@/lib/db/settings";
|
||||
import { isFreeModel, providerHasFreeModels } from "@/shared/utils/freeModels";
|
||||
|
||||
const testModelSchema = z.object({
|
||||
providerId: z.string().min(1),
|
||||
@@ -40,6 +42,27 @@ export async function POST(request: Request) {
|
||||
}
|
||||
const { providerId, modelId, connectionId } = validation.data;
|
||||
|
||||
// #6328 (follow-up to #6495): REMOVE — not just hide — paid Test dispatches
|
||||
// when hidePaidModels is on. 403 (distinct from validation-400) so callers
|
||||
// can tell policy-blocked apart from bad-request. Fail open on settings read.
|
||||
let hidePaid = false;
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
hidePaid = settings?.hidePaidModels === true;
|
||||
} catch {}
|
||||
if (
|
||||
hidePaid &&
|
||||
!(providerHasFreeModels(providerId) && isFreeModel(providerId, { id: modelId }))
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: "error",
|
||||
error: "Paid model blocked while hidePaidModels is enabled",
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const result = await runSingleModelTest({
|
||||
providerId,
|
||||
modelId,
|
||||
|
||||
58
tests/unit/api-models-hide-paid-6328.test.ts
Normal file
58
tests/unit/api-models-hide-paid-6328.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* #6328 (follow-up to #6495) — the dashboard `/api/models` endpoint must also
|
||||
* REMOVE paid models when `hidePaidModels` is on (the public `/v1/models`
|
||||
* catalog already did via #6495). `openai` has no curated free roster, so its
|
||||
* chat models are paid-tier and must disappear when the toggle is on.
|
||||
* Rule #18 regression guard for the added filter in src/app/api/models/route.ts.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-api-models-hidepaid-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const settingsDb = await import("../../src/lib/db/settings.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const modelsRoute = await import("../../src/app/api/models/route.ts");
|
||||
|
||||
async function fetchModels(): Promise<Array<{ provider: string; model: string }>> {
|
||||
const res = await modelsRoute.GET(new Request("http://localhost/api/models?all=true"));
|
||||
const body = (await res.json()) as { models: Array<{ provider: string; model: string }> };
|
||||
return body.models;
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
try {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* best-effort */
|
||||
}
|
||||
});
|
||||
|
||||
test("#6328 /api/models removes paid models when hidePaidModels is on", async () => {
|
||||
await providersDb.createProviderConnection({
|
||||
provider: "openai",
|
||||
authType: "apikey",
|
||||
name: "openai-main",
|
||||
apiKey: "sk-test",
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const hasPaidOpenAi = (list: Array<{ provider: string; model: string }>) =>
|
||||
list.some((m) => m.provider === "openai" && /^gpt-/.test(m.model));
|
||||
|
||||
await settingsDb.updateSettings({ hidePaidModels: false });
|
||||
assert.equal(hasPaidOpenAi(await fetchModels()), true, "paid OpenAI models visible when toggle is off");
|
||||
|
||||
await settingsDb.updateSettings({ hidePaidModels: true });
|
||||
assert.equal(
|
||||
hasPaidOpenAi(await fetchModels()),
|
||||
false,
|
||||
"paid OpenAI models must be removed when hidePaidModels is on (#6328)"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user