# v0.5.70 (2026-09-08)
## Features - **Providers**: creating a compatible / custom-embedding node now registers the endpoint only — the API key is added afterwards from the node's page, like the built-in providers. The create dialogs drop the API Key / Model ID / Check fields, and `POST /api/provider-nodes` no longer accepts credentials at all, so a node can never be half-created - **Providers**: compatible nodes now use the same model rows as built-in providers — capability badges, copy, per-model test, alias handling and the Add/Edit Model modal with vision + reasoning toggles, replacing the weaker read-only list - **Providers**: compatible nodes get the built-in bulk toolbars: Test All / Disable / Active / Select All over connections, and Test All Models / Disable All / Active All over models, with per-row disable and a restore strip for disabled models - **Providers**: wire the dead "Fetch Models" button on compatible nodes to the live upstream catalog, de-duplicating against already-added models ## Fixes - **Models**: persist per-model capability assertions for custom and compatible providers and honor them everywhere — unsupported media is stripped on the chat path, `/v1/models` and `/api/models` report what the user asserted, and thinking translation follows it (asserting `reasoning:false` now actually strips thinking fields, `reasoning:true` emits them) - **Models**: partial capability edits merge instead of overwriting, so toggling vision off no longer erases a stored reasoning assertion - **Capabilities**: keep server-injected readers (synced catalog, user-asserted capabilities) in process-wide state — Next.js compiles startup and each API route into separate bundles with their own module instances, so a boot-time install was invisible to every request handler and the models.dev catalog contributed nothing to upstream requests since0532f00d- **Dashboard**: thinking-level picker and model-row suffix reflect user-asserted reasoning on compatible nodes - **Providers**: `Default Model` is optional when adding an API key to a compatible node — the node's own model list (and the picker in the test modals) already determine what gets probed, and the built-in fallback still covers connection checks - **Providers**: restore the `useCopyToClipboard` import dropped from the provider detail page, which crashed the route with `ReferenceError` for every provider - **DB**: restore `getModelAliases` / `setModelAlias` / `deleteModelAlias` re-exports dropped from the `localDb` shim by86112cee, which broke `GET /api/models` and `GET /v1/models` at import time - **Providers**: remove dead `PassthroughModelsSection` (never passed props, superseded by the shared model rows) - **Media Providers**: creating a custom embedding node reports that a key still has to be added, instead of claiming a key was saved; the edit dialog keeps its API Key + Check affordance since a stored key already exists there - **Build**: self-host Inter instead of fetching it through `next/font/google` at build time — a Docker / mirrored builder with no route to `fonts.googleapis.com` failed the entire image build on `Failed to fetch 'Inter' from Google Fonts`. The seven `@font-face` rules and their `unicode-range`s copy what `next/font` emitted (a `latin`-only file would have dropped Vietnamese diacritics) and the latin subset is preloaded as before, so rendered metrics are unchanged
This commit is contained in:
@@ -414,16 +414,60 @@ const TRUST_UPSTREAM_VISION = new Set(["openrouter"]);
|
||||
*/
|
||||
const MODALITY_KEYS = ["vision", "pdf", "audioInput", "videoInput"];
|
||||
|
||||
// Catalog lookups, installed by the server at startup. Left as no-ops in the
|
||||
// browser bundle, where there is no file to read.
|
||||
let catalogSource = null;
|
||||
// ── Server-injected readers ──────────────────────────────────────────
|
||||
// Next.js compiles instrumentation and each API route into SEPARATE server
|
||||
// bundles, so a plain module-local `let` would give every bundle its own copy of
|
||||
// this file and a source installed at boot would be invisible to the request
|
||||
// handlers (silently: the setters still "succeed"). The slots therefore live on
|
||||
// globalThis, which IS shared across server bundles in the same process.
|
||||
// Same reason the browser bundle is safe: it never calls a setter, so the slots
|
||||
// stay empty and every consumer below short-circuits.
|
||||
const SOURCE_SLOTS = (globalThis.__9R_CAPABILITY_SOURCES ||= {
|
||||
catalog: null, // { getModalities, getLimits } — synced models.dev catalog
|
||||
userCaps: null, // (provider, model) => asserted caps — dashboard toggles
|
||||
});
|
||||
|
||||
/**
|
||||
* Install the synced catalog reader (server only).
|
||||
* @param {{ getModalities: Function, getLimits: Function } | null} source
|
||||
*/
|
||||
export function setCatalogSource(source) {
|
||||
catalogSource = source;
|
||||
SOURCE_SLOTS.catalog = source || null;
|
||||
}
|
||||
|
||||
// Capabilities the user asserted per provider+model (dashboard "Add/Edit Model"
|
||||
// toggles), installed by the server from the custom-model store. Unlike the
|
||||
// catalog and name heuristics this is authoritative and two-directional: it can
|
||||
// turn a capability OFF as well as on.
|
||||
const USER_CAPS_KEYS = ["vision", "pdf", "audioInput", "videoInput", "imageOutput", "audioOutput", "search", "tools", "reasoning", "thinkingFormat", "contextWindow", "maxOutput"];
|
||||
// (slot lives in SOURCE_SLOTS above — see the cross-bundle note)
|
||||
|
||||
/**
|
||||
* Install the user-asserted caps reader (server only).
|
||||
* @param {(provider: string|null, model: string) => object|null} source sync lookup
|
||||
*/
|
||||
export function setUserCapsSource(source) {
|
||||
SOURCE_SLOTS.userCaps = typeof source === "function" ? source : null;
|
||||
}
|
||||
|
||||
// Last step of every resolution path: the user's own assertion wins over any
|
||||
// heuristic, including the tables above (a hand-typed model id can collide with
|
||||
// a pattern entry that describes a different product).
|
||||
function applyUserCaps(result, provider, model) {
|
||||
const userCapsSource = SOURCE_SLOTS.userCaps;
|
||||
if (!userCapsSource) return result;
|
||||
let asserted = null;
|
||||
try {
|
||||
asserted = userCapsSource(provider, model);
|
||||
} catch {
|
||||
return result;
|
||||
}
|
||||
if (!asserted || typeof asserted !== "object") return result;
|
||||
for (const key of USER_CAPS_KEYS) {
|
||||
if (asserted[key] === undefined) continue;
|
||||
result[key] = asserted[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Apply the synced catalog + name heuristic on top of a table-resolved result.
|
||||
@@ -431,7 +475,7 @@ export function setCatalogSource(source) {
|
||||
// flips when an outside source positively declares support.
|
||||
function refine(base, provider, model) {
|
||||
const result = { ...DEFAULT_CAPABILITIES, ...base };
|
||||
|
||||
const catalogSource = SOURCE_SLOTS.catalog;
|
||||
if (catalogSource) {
|
||||
const modalities = catalogSource.getModalities(model);
|
||||
if (modalities) {
|
||||
@@ -457,28 +501,31 @@ export function getCapabilitiesForModel(provider, model) {
|
||||
|
||||
// Canonical exact lookup strips vendor prefix: "anthropic/claude-opus-4.7" -> "claude-opus-4.7".
|
||||
const baseModel = model.includes("/") ? model.split("/").pop() : model;
|
||||
|
||||
// 1. Provider-specific override
|
||||
if (provider) {
|
||||
const providerCaps = PROVIDER_CAPABILITIES[provider];
|
||||
if (providerCaps?.[model]) return { ...DEFAULT_CAPABILITIES, ...providerCaps[model] };
|
||||
if (providerCaps?.[baseModel]) return { ...DEFAULT_CAPABILITIES, ...providerCaps[baseModel] };
|
||||
}
|
||||
|
||||
// 2. Canonical exact
|
||||
if (MODEL_CAPABILITIES[baseModel]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[baseModel] };
|
||||
if (MODEL_CAPABILITIES[model]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[model] };
|
||||
|
||||
// 3. Pattern match (first match wins), refined by catalog + name heuristic
|
||||
for (const { pattern, caps } of PATTERN_CAPABILITIES) {
|
||||
if (matchPattern(pattern, baseModel) || matchPattern(pattern, model)) {
|
||||
return refine(caps, provider, model);
|
||||
const resolve = () => {
|
||||
// 1. Provider-specific override
|
||||
if (provider) {
|
||||
const providerCaps = PROVIDER_CAPABILITIES[provider];
|
||||
if (providerCaps?.[model]) return { ...DEFAULT_CAPABILITIES, ...providerCaps[model] };
|
||||
if (providerCaps?.[baseModel]) return { ...DEFAULT_CAPABILITIES, ...providerCaps[baseModel] };
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Floor (upstream-validated gateways keep vision on for unknown models)
|
||||
if (provider && TRUST_UPSTREAM_VISION.has(provider)) {
|
||||
return { ...refine(null, provider, model), vision: true };
|
||||
}
|
||||
return refine(null, provider, model);
|
||||
// 2. Canonical exact
|
||||
if (MODEL_CAPABILITIES[baseModel]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[baseModel] };
|
||||
if (MODEL_CAPABILITIES[model]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[model] };
|
||||
|
||||
// 3. Pattern match (first match wins), refined by catalog + name heuristic
|
||||
for (const { pattern, caps } of PATTERN_CAPABILITIES) {
|
||||
if (matchPattern(pattern, baseModel) || matchPattern(pattern, model)) {
|
||||
return refine(caps, provider, model);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Floor (upstream-validated gateways keep vision on for unknown models)
|
||||
if (provider && TRUST_UPSTREAM_VISION.has(provider)) {
|
||||
return { ...refine(null, provider, model), vision: true };
|
||||
}
|
||||
return refine(null, provider, model);
|
||||
};
|
||||
|
||||
return applyUserCaps(resolve(), provider, model);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user