## 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
93 lines
3.7 KiB
JavaScript
93 lines
3.7 KiB
JavaScript
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
DEFAULT_CAPABILITIES,
|
|
getCapabilitiesForModel,
|
|
setUserCapsSource,
|
|
} from "../../open-sse/providers/capabilities.js";
|
|
import { applyThinking } from "../../open-sse/translator/concerns/thinkingUnified.js";
|
|
import { looksLikeVisionModel } from "../../open-sse/providers/visionPatterns.js";
|
|
|
|
// Dashboard capability toggles are the user's own assertion about a model the
|
|
// heuristics cannot know. They are final: false must beat a name heuristic,
|
|
// true must beat the safe floor, and applying them must never leak a mutation
|
|
// back into DEFAULT_CAPABILITIES.
|
|
afterEach(() => setUserCapsSource(null));
|
|
|
|
function assertSource(caps) {
|
|
setUserCapsSource(() => caps);
|
|
}
|
|
|
|
describe("user-asserted model capabilities", () => {
|
|
it("turns a heuristic-detected vision model back off", () => {
|
|
const model = "qwen3-vl-plus";
|
|
expect(looksLikeVisionModel(model)).toBe(true);
|
|
expect(getCapabilitiesForModel("custom-node", model).vision).toBe(true);
|
|
|
|
assertSource({ vision: false });
|
|
expect(getCapabilitiesForModel("custom-node", model).vision).toBe(false);
|
|
});
|
|
|
|
it("turns reasoning on for a model the tables consider text-only", () => {
|
|
const model = "my-private-chatml-9";
|
|
expect(getCapabilitiesForModel("custom-node", model).reasoning).toBe(false);
|
|
|
|
assertSource({ reasoning: true });
|
|
expect(getCapabilitiesForModel("custom-node", model).reasoning).toBe(true);
|
|
});
|
|
|
|
it("ignores keys outside the capability set and non-objects", () => {
|
|
const before = getCapabilitiesForModel("custom-node", "gpt-5.1");
|
|
assertSource({ vision: true, nonsense: true });
|
|
const after = getCapabilitiesForModel("custom-node", "gpt-5.1");
|
|
expect(after.vision).toBe(true);
|
|
expect("nonsense" in after).toBe(false);
|
|
|
|
for (const junk of [null, undefined, "nope", 42]) {
|
|
assertSource(junk);
|
|
expect(getCapabilitiesForModel("custom-node", "gpt-5.1")).toEqual(before);
|
|
}
|
|
});
|
|
|
|
it("only applies asserted keys, leaving the rest resolved normally", () => {
|
|
assertSource({ vision: false });
|
|
const caps = getCapabilitiesForModel("custom-node", "my-private-chatml-9");
|
|
expect(caps.vision).toBe(false);
|
|
expect(caps.tools).toBe(true);
|
|
expect(caps.contextWindow).toBe(DEFAULT_CAPABILITIES.contextWindow);
|
|
});
|
|
|
|
it("never mutates DEFAULT_CAPABILITIES", () => {
|
|
const floor = { ...DEFAULT_CAPABILITIES };
|
|
assertSource({ vision: true, reasoning: true, tools: false, contextWindow: 1234 });
|
|
getCapabilitiesForModel("custom-node", "anything");
|
|
expect(DEFAULT_CAPABILITIES).toEqual(floor);
|
|
});
|
|
|
|
it("keeps heuristics when no source is installed", () => {
|
|
assertSource({ vision: false });
|
|
expect(getCapabilitiesForModel("custom-node", "qwen3-vl-plus").vision).toBe(false);
|
|
setUserCapsSource(null);
|
|
expect(getCapabilitiesForModel("custom-node", "qwen3-vl-plus").vision).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("thinking follows the assertion", () => {
|
|
it("strips thinking fields when reasoning is asserted off", () => {
|
|
assertSource({ reasoning: false });
|
|
const body = { reasoning_effort: "high", messages: [{ role: "user", content: "hi" }] };
|
|
|
|
applyThinking("openai", "my-private-chatml-9", body, "custom-node");
|
|
expect(body.reasoning_effort).toBeUndefined();
|
|
expect(body.reasoning).toBeUndefined();
|
|
expect(body.thinking).toBeUndefined();
|
|
});
|
|
|
|
it("emits thinking fields when reasoning is asserted on", () => {
|
|
assertSource({ reasoning: true, thinkingFormat: "openai" });
|
|
const body = { reasoning_effort: "high", messages: [{ role: "user", content: "hi" }] };
|
|
|
|
applyThinking("openai", "my-private-chatml-9", body, "custom-node");
|
|
expect(body.reasoning_effort).toBe("high");
|
|
});
|
|
});
|