mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 06:12:17 +03:00
fix(providers): honor operator-set endpoint overrides for local models (#13078)
Merged. An operator-set endpoint override that is ignored for local models is the worst kind of setting — it looks applied and is not. Honouring it is the whole fix. Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run. Thank you.
This commit is contained in:
@@ -258,7 +258,7 @@ export async function PUT(request) {
|
||||
}
|
||||
}
|
||||
|
||||
const model = await updateCustomModel(provider, modelId, updates);
|
||||
const model = await updateCustomModel(provider, modelId, updates, { createIfMissing: true });
|
||||
|
||||
if (!model) {
|
||||
const rawKeys = Object.keys(raw);
|
||||
|
||||
@@ -686,20 +686,37 @@ function applyTriStateBooleanOverride(
|
||||
export async function updateCustomModel(
|
||||
providerId: string,
|
||||
modelId: string,
|
||||
updates: Record<string, unknown> = {}
|
||||
updates: Record<string, unknown> = {},
|
||||
options: { createIfMissing?: boolean } = {}
|
||||
) {
|
||||
const db = getDbInstance();
|
||||
const row = db
|
||||
.prepare("SELECT value FROM key_value WHERE namespace = 'customModels' AND key = ?")
|
||||
.get(providerId);
|
||||
if (!row) return null;
|
||||
|
||||
const value = getKeyValue(row).value;
|
||||
if (!value) return null;
|
||||
const value = row ? getKeyValue(row).value : null;
|
||||
const models: JsonRecord[] = value ? JSON.parse(value) : [];
|
||||
let index = models.findIndex((m: JsonRecord) => m.id === modelId);
|
||||
|
||||
const models = JSON.parse(value);
|
||||
const index = models.findIndex((m: JsonRecord) => m.id === modelId);
|
||||
if (index === -1) return null;
|
||||
if (index === -1) {
|
||||
if (!options.createIfMissing) return null;
|
||||
// A model discovered via sync/passthrough (syncedAvailableModels) has no
|
||||
// customModels row until an operator explicitly overrides one of its
|
||||
// fields -- PUT /api/provider-models is exactly that "set an override"
|
||||
// action, so upsert here (same default shape as addCustomModel()) instead
|
||||
// of 404ing on the very save it exists to serve. Observed live: a
|
||||
// llama.cpp connection's auto-discovered embedding model had no way to be
|
||||
// marked "supports embeddings" because it had never been explicitly
|
||||
// imported as a custom model first.
|
||||
models.push({
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
source: "manual",
|
||||
apiFormat: "chat-completions",
|
||||
supportedEndpoints: ["chat"],
|
||||
});
|
||||
index = models.length - 1;
|
||||
}
|
||||
|
||||
const current = models[index];
|
||||
const currentCompat = (current as JsonRecord).compatByProtocol as CompatByProtocolMap | undefined;
|
||||
@@ -770,10 +787,12 @@ export async function updateCustomModel(
|
||||
|
||||
models[index] = next;
|
||||
|
||||
db.prepare("UPDATE key_value SET value = ? WHERE namespace = 'customModels' AND key = ?").run(
|
||||
JSON.stringify(models),
|
||||
providerId
|
||||
);
|
||||
// INSERT OR REPLACE (not UPDATE): the createIfMissing path above may be
|
||||
// writing this provider's customModels row for the first time, and an
|
||||
// UPDATE...WHERE would silently match zero rows in that case.
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('customModels', ?, ?)"
|
||||
).run(providerId, JSON.stringify(models));
|
||||
|
||||
finishModelCatalogWriteWithBackup();
|
||||
return next;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getSyncedAvailableModelsByConnection } from "@/lib/db/models";
|
||||
import { getAllCustomModels, getSyncedAvailableModelsByConnection } from "@/lib/db/models";
|
||||
import { isSelfHostedChatProvider, resolveProviderId } from "@/shared/constants/providers";
|
||||
|
||||
export type LocalSyncedEndpointRoute = {
|
||||
@@ -14,18 +14,52 @@ export async function resolveLocalSyncedEndpointRoute(
|
||||
const slashIndex = modelStr.indexOf("/");
|
||||
if (slashIndex <= 0 || slashIndex === modelStr.length - 1) return null;
|
||||
|
||||
const provider = resolveProviderId(modelStr.slice(0, slashIndex));
|
||||
const model = modelStr.slice(slashIndex + 1);
|
||||
const providerPrefix = modelStr.slice(0, slashIndex);
|
||||
const provider = resolveProviderId(providerPrefix);
|
||||
if (!isSelfHostedChatProvider(provider)) return null;
|
||||
|
||||
const byConnection = await getSyncedAvailableModelsByConnection(provider);
|
||||
const connectionIds = Object.entries(byConnection)
|
||||
.filter(([, models]) =>
|
||||
models.some(
|
||||
(candidate) => candidate.id === model && candidate.supportedEndpoints?.includes(endpoint)
|
||||
)
|
||||
)
|
||||
.map(([connectionId]) => connectionId);
|
||||
const rawSuffix = modelStr.slice(slashIndex + 1);
|
||||
// Some self-hosted servers (llama.cpp included) report models by absolute
|
||||
// filesystem path, so the raw id itself already starts with "/" --
|
||||
// "<prefix>/<rawId>" then reads as "<prefix>//models/foo.gguf" (a double
|
||||
// slash). That IS the byte-for-byte round-trip-safe id the catalog
|
||||
// displays, but an operator who naturally collapses it to a single slash
|
||||
// ("<prefix>/models/foo.gguf") should still resolve -- try the raw model
|
||||
// id both as given and with a leading "/" restored.
|
||||
const modelCandidates = rawSuffix.startsWith("/") ? [rawSuffix] : [rawSuffix, `/${rawSuffix}`];
|
||||
|
||||
return connectionIds.length > 0 ? { provider, model, connectionIds } : null;
|
||||
// Most local servers' own /v1/models response carries no capability data at
|
||||
// all (llama.cpp included -- unlike Ollama's /api/show, there is nothing to
|
||||
// probe), so a discovered model's synced cache entry below often has no
|
||||
// supportedEndpoints of its own. An operator-set override (PUT
|
||||
// /api/provider-models, keyed by the exact catalog id the client used) is
|
||||
// the explicit "this model does support embeddings" declaration for
|
||||
// exactly that case -- honor it here the same way the /v1/models catalog
|
||||
// already merges customModels on top of synced entries, instead of only
|
||||
// trusting the un-annotated raw sync cache.
|
||||
const customModelsForProvider = (await getAllCustomModels())[provider];
|
||||
const byConnection = await getSyncedAvailableModelsByConnection(provider);
|
||||
|
||||
for (const model of modelCandidates) {
|
||||
const overrideEndpoints = Array.isArray(customModelsForProvider)
|
||||
? (
|
||||
customModelsForProvider as Array<{ id?: unknown; supportedEndpoints?: unknown }>
|
||||
).find((entry) => entry.id === modelStr || entry.id === `${providerPrefix}/${model}`)
|
||||
?.supportedEndpoints
|
||||
: undefined;
|
||||
const hasOverride = Array.isArray(overrideEndpoints) && overrideEndpoints.includes(endpoint);
|
||||
|
||||
const connectionIds = Object.entries(byConnection)
|
||||
.filter(([, models]) =>
|
||||
models.some(
|
||||
(candidate) =>
|
||||
candidate.id === model && (hasOverride || candidate.supportedEndpoints?.includes(endpoint))
|
||||
)
|
||||
)
|
||||
.map(([connectionId]) => connectionId);
|
||||
|
||||
if (connectionIds.length > 0) return { provider, model, connectionIds };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user