mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 04:42:10 +03:00
* fix(api): resolve compatible provider node by base type, not only exact id (#4421) * chore(quality): rebaseline db/providers.ts file-size 1050->1063 (#4421) * chore(quality): mark providerNodeSelect db-internal in db-rules allowlist (#4421) --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diego.souza@cdwasolutions.com.br>
This commit is contained in:
committed by
GitHub
parent
7ef34c2246
commit
f124806372
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.",
|
||||
"_rebaseline_2026_06_21_4421_node_lookup": "Issue #4421 own growth: src/lib/db/providers.ts 1050->1063 (+13 = resolveProviderNodeForConnection at the existing provider-node lookup — resolves a connection node by exact id OR the bare derived type when unambiguous, + import). Pure selection logic in the new src/lib/db/providerNodeSelect.ts (<cap, unit-tested); cohesive DB wiring next to getProviderNodeById, not extractable. Covered by tests/unit/provider-node-select-4421.test.ts.",
|
||||
"_rebaseline_2026_06_21_v3833_cycle_open_latent_filesize": "Abertura do ciclo v3.8.33: 4 arquivos cresceram no ciclo v3.8.32 sem bump de baseline e o drift escapou do fast-path do release (check:file-size não roda nas fast-gates p/ release/*, só no PR→main full CI, e o crescimento veio de commits entre fca66c644 e o head do merge 912239f46 — ex. #4475 targetFormat). Medido em origin/main (idêntico, cherry-picks deste ciclo NÃO tocam estes 4): open-sse/services/usage.ts 3408->3414, src/lib/db/core.ts 1820->1825, src/lib/usage/providerLimits.ts 949->950, src/shared/constants/providers.ts 3242->3243. Reconcílio ao valor real de main p/ abrir o .33 verde; shrink estrutural rastreado em #3501.",
|
||||
"_rebaseline_2026_06_21_4481_websearch_routing_ui": "Feature #4481 layer 2 UI own growth: src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx 1594->1629 (+35 = one Settings → Routing <Card> with a text <Input> bound to the `webSearchRouteModel` setting, placed next to the adjacent echo/LKGP cards; value/onChange via updateSetting, i18n labels with inline English fallbacks). Presentational wiring for the setting shipped in the same PR (#4509); the routing logic itself lives in the already-tested pure leaf open-sse/services/webSearchRouting.ts. Cohesive next to the existing routing cards; not extractable. Covered by tests/unit/web-search-tool-routing-4481.test.ts (UI source-guard + en.json key presence).",
|
||||
"_rebaseline_2026_06_21_4481_websearch_routing": "Feature #4481 layer 2 own growth: src/sse/handlers/chat.ts 1491->1513 (+22 = the CCR-style web-search routing hook at the request entrypoint, right after the T05 task-aware-routing block — a 5-line comment + one `if (hasNativeWebSearchTool(body)) { ... }` guard that reads getCachedSettings only when a web_search tool is present and overrides resolvedModelStr/body.model via the pure helper, plus a 4-line import). When a request carries a native web_search server tool and the operator set `webSearchRouteModel`, the whole request routes to that model instead of the default (some providers, e.g. MiniMax, don't implement Anthropic's web_search_20250305 server tool). The detection + override logic lives in the new pure leaf open-sse/services/webSearchRouting.ts (no DB, unit-testable); chat.ts is thin wiring mirroring the adjacent T05 override. Lands BEFORE auto/combo resolution + the layer-1 webSearchFallback so the target's own format/fallback handling applies. Not extractable further (it IS the entrypoint wiring). Covered by tests/unit/web-search-tool-routing-4481.test.ts. Structural shrink of this handler tracked separately.",
|
||||
@@ -184,7 +185,7 @@
|
||||
"src/lib/db/core.ts": 1825,
|
||||
"src/lib/db/migrationRunner.ts": 1125,
|
||||
"src/lib/db/models.ts": 1259,
|
||||
"src/lib/db/providers.ts": 1050,
|
||||
"src/lib/db/providers.ts": 1063,
|
||||
"src/lib/db/proxies.ts": 1048,
|
||||
"src/lib/db/settings.ts": 1149,
|
||||
"src/lib/db/usageAnalytics.ts": 873,
|
||||
|
||||
@@ -59,6 +59,7 @@ export const INTENTIONALLY_INTERNAL = new Set([
|
||||
"obsidian", // intentionally-internal: src/lib/obsidianSync.ts + settings/obsidian route + MCP obsidianTools.ts
|
||||
"pluginMetrics", // DEAD? (production): write path não foi conectado ainda (documentado no cabeçalho do módulo); testado por tests/unit/plugins-metrics.test.ts
|
||||
"prompts", // DEAD? (production): zero callers de produção encontrados; domínio domain/prompts.ts é independente; testado por tests/integration/proxy-pipeline.test.ts
|
||||
"providerNodeSelect", // db-internal: importado só por db/providers.ts (selectProviderNodeForConnection — lógica pura de seleção de provider node split do providers.ts, #4421)
|
||||
"providerStats", // intentionally-internal: src/app/api/provider-stats/route.ts
|
||||
"recovery", // intentionally-internal: bin/cli/runtime.mjs (import() dinâmico) + tests
|
||||
"secrets", // intentionally-internal: src/instrumentation-node.ts (import() dinâmico na inicialização)
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
createProviderConnection,
|
||||
deleteProviderConnections,
|
||||
updateProviderConnection,
|
||||
getProviderNodeById,
|
||||
resolveProviderNodeForConnection,
|
||||
isCloudEnabled,
|
||||
} from "@/models";
|
||||
import {
|
||||
@@ -110,7 +110,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
if (isOpenAICompatibleProvider(provider)) {
|
||||
const node: any = await getProviderNodeById(provider);
|
||||
const node: any = await resolveProviderNodeForConnection(provider);
|
||||
if (!node) {
|
||||
return NextResponse.json({ error: "OpenAI Compatible node not found" }, { status: 404 });
|
||||
}
|
||||
@@ -129,7 +129,7 @@ export async function POST(request: Request) {
|
||||
...(node.customHeaders ? { customHeaders: node.customHeaders } : {}),
|
||||
};
|
||||
} else if (isAnthropicCompatibleProvider(provider)) {
|
||||
const node: any = await getProviderNodeById(provider);
|
||||
const node: any = await resolveProviderNodeForConnection(provider);
|
||||
if (!node) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
48
src/lib/db/providerNodeSelect.ts
Normal file
48
src/lib/db/providerNodeSelect.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Pure provider-node selection helper (#4421).
|
||||
*
|
||||
* Kept dependency-free (no DB import) so it can be unit-tested without opening a
|
||||
* SQLite connection.
|
||||
*/
|
||||
|
||||
export interface SelectableNode {
|
||||
id?: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const TRAILING_UUID =
|
||||
/-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
/**
|
||||
* Recover the derived TYPE of a provider node from its id. Node ids are
|
||||
* "<type>-<uuid>" (e.g. "openai-compatible-responses-1715ed0f-..."), so stripping a
|
||||
* trailing UUID yields the type ("openai-compatible-responses"). Ids without a UUID
|
||||
* suffix are returned unchanged.
|
||||
*/
|
||||
export function nodeTypeFromId(id: unknown): string {
|
||||
const s = String(id ?? "");
|
||||
return TRAILING_UUID.test(s) ? s.replace(TRAILING_UUID, "") : s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the provider node a new connection should bind to, given either:
|
||||
* - the concrete node id ("<type>-<uuid>", what the dashboard sends), or
|
||||
* - the bare derived type ("openai-compatible-responses", what callers using the
|
||||
* /api/providers API directly often pass instead — #4421).
|
||||
*
|
||||
* When the exact id is not present, fall back to the SOLE node whose derived type
|
||||
* equals `idOrType` — but only when exactly one such node exists, so an ambiguous type
|
||||
* never silently picks the wrong node (returns null instead, preserving the 404). The
|
||||
* type is matched precisely (UUID-stripped), so "openai-compatible" never matches an
|
||||
* "openai-compatible-responses" node.
|
||||
*/
|
||||
export function selectProviderNodeForConnection<T extends SelectableNode>(
|
||||
idOrType: string,
|
||||
nodes: T[]
|
||||
): T | null {
|
||||
if (!idOrType) return null;
|
||||
const exact = nodes.find((n) => n.id === idOrType);
|
||||
if (exact) return exact;
|
||||
const ofType = nodes.filter((n) => typeof n.id === "string" && nodeTypeFromId(n.id) === idOrType);
|
||||
return ofType.length === 1 ? ofType[0] : null;
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { getDbInstance, rowToCamel, cleanNulls } from "./core";
|
||||
import { selectProviderNodeForConnection } from "./providerNodeSelect";
|
||||
import { backupDbFile } from "./backup";
|
||||
import {
|
||||
encryptConnectionFields,
|
||||
@@ -774,6 +775,18 @@ export async function getProviderNodeById(id: string) {
|
||||
return row ? rowToCamel(row) : null;
|
||||
}
|
||||
|
||||
// #4421: resolve the provider node for a new connection from either its concrete id
|
||||
// (what the dashboard sends, "<type>-<uuid>") OR the bare derived type (what callers
|
||||
// using the /api/providers API directly often pass, e.g. "openai-compatible-responses").
|
||||
// Falls back to the sole node of that type only when unambiguous; otherwise null (so the
|
||||
// caller still surfaces the existing 404).
|
||||
export async function resolveProviderNodeForConnection(idOrType: string) {
|
||||
const exact = await getProviderNodeById(idOrType);
|
||||
if (exact) return exact;
|
||||
const all = (await getProviderNodes()) as JsonRecord[];
|
||||
return selectProviderNodeForConnection(idOrType, all);
|
||||
}
|
||||
|
||||
export async function createProviderNode(data: JsonRecord) {
|
||||
const db = getDbInstance() as unknown as DbLike;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
@@ -21,6 +21,7 @@ export {
|
||||
// Provider Nodes
|
||||
getProviderNodes,
|
||||
getProviderNodeById,
|
||||
resolveProviderNodeForConnection,
|
||||
createProviderNode,
|
||||
updateProviderNode,
|
||||
deleteProviderNode,
|
||||
|
||||
@@ -8,6 +8,7 @@ export {
|
||||
deleteProviderConnections,
|
||||
getProviderNodes,
|
||||
getProviderNodeById,
|
||||
resolveProviderNodeForConnection,
|
||||
createProviderNode,
|
||||
updateProviderNode,
|
||||
deleteProviderNode,
|
||||
|
||||
53
tests/unit/provider-node-select-4421.test.ts
Normal file
53
tests/unit/provider-node-select-4421.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
// #4421: creating a connection with `provider: "openai-compatible-responses"` (the bare
|
||||
// derived type) returned 404 "OpenAI Compatible node not found" even though a node of
|
||||
// that type existed — the handler did an exact-id lookup, but node ids carry a UUID
|
||||
// suffix ("openai-compatible-responses-<uuid>"). selectProviderNodeForConnection now
|
||||
// also resolves the bare type to the sole matching node.
|
||||
|
||||
const { selectProviderNodeForConnection, nodeTypeFromId } = await import(
|
||||
"../../src/lib/db/providerNodeSelect.ts"
|
||||
);
|
||||
|
||||
const UUID = "1715ed0f-1111-2222-3333-444455556666";
|
||||
const UUID2 = "2222aaaa-1111-2222-3333-444455556666";
|
||||
|
||||
test("#4421 resolves by exact node id (dashboard path, unchanged)", () => {
|
||||
const nodes = [{ id: `openai-compatible-responses-${UUID}`, name: "fox" }];
|
||||
const r = selectProviderNodeForConnection(`openai-compatible-responses-${UUID}`, nodes);
|
||||
assert.equal(r?.name, "fox");
|
||||
});
|
||||
|
||||
test("#4421 resolves the bare base type to the sole matching node (direct-API path)", () => {
|
||||
// Before the fix the handler called getProviderNodeById("openai-compatible-responses")
|
||||
// — an exact-id lookup that returned null → 404.
|
||||
const nodes = [{ id: `openai-compatible-responses-${UUID}`, name: "fox" }];
|
||||
const r = selectProviderNodeForConnection("openai-compatible-responses", nodes);
|
||||
assert.equal(r?.name, "fox");
|
||||
});
|
||||
|
||||
test("#4421 returns null when the base type is ambiguous (more than one node)", () => {
|
||||
const nodes = [
|
||||
{ id: `openai-compatible-responses-${UUID}`, name: "a" },
|
||||
{ id: `openai-compatible-responses-${UUID2}`, name: "b" },
|
||||
];
|
||||
assert.equal(selectProviderNodeForConnection("openai-compatible-responses", nodes), null);
|
||||
});
|
||||
|
||||
test("#4421 a base type does not match a more-specific node type", () => {
|
||||
// "openai-compatible" (chat) must NOT resolve an "openai-compatible-responses" node.
|
||||
const nodes = [{ id: `openai-compatible-responses-${UUID}`, name: "fox" }];
|
||||
assert.equal(selectProviderNodeForConnection("openai-compatible", nodes), null);
|
||||
});
|
||||
|
||||
test("#4421 nodeTypeFromId strips a trailing UUID", () => {
|
||||
assert.equal(nodeTypeFromId(`openai-compatible-responses-${UUID}`), "openai-compatible-responses");
|
||||
assert.equal(nodeTypeFromId(`openai-compatible-${UUID}`), "openai-compatible");
|
||||
assert.equal(nodeTypeFromId("no-uuid-here"), "no-uuid-here");
|
||||
});
|
||||
|
||||
test("#4421 returns null when no node matches", () => {
|
||||
assert.equal(selectProviderNodeForConnection("anthropic-compatible", []), null);
|
||||
});
|
||||
Reference in New Issue
Block a user