# 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 since 0532f00d
- **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 by 86112cee, 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:
2026-09-10 11:11:24 +07:00
parent 302795a613
commit a84ba559f3
40 changed files with 1562 additions and 802 deletions

View File

@@ -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);
}