## 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
77 lines
3.3 KiB
JavaScript
77 lines
3.3 KiB
JavaScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
/**
|
|
* Next.js compiles `instrumentation.js` and every API route into SEPARATE server
|
|
* bundles, so one source file gets a distinct module instance per bundle. A
|
|
* reader installed into a plain module-local variable therefore lands in the
|
|
* startup copy while the chat route reads its own still-empty copy: the setter
|
|
* appears to succeed and the feature is silently dead.
|
|
*
|
|
* Distinct `?bundle=` suffixes give this file two instances of the same module in
|
|
* one process — it models "two bundles" without a real build.
|
|
*/
|
|
const boot = await import("../../open-sse/providers/capabilities.js?bundle=boot");
|
|
const route = await import("../../open-sse/providers/capabilities.js?bundle=route");
|
|
|
|
afterEach(() => {
|
|
boot.setUserCapsSource(null);
|
|
boot.setCatalogSource(null);
|
|
});
|
|
|
|
describe("capability sources across server bundles", () => {
|
|
it("keeps distinct module instances per bundle", () => {
|
|
// If these ever collapse into one instance the tests below prove nothing.
|
|
expect(route).not.toBe(boot);
|
|
});
|
|
|
|
it("honours a user-asserted caps source installed at startup", () => {
|
|
boot.setUserCapsSource(() => ({ vision: false }));
|
|
// qwen3-vl-plus is vision by name heuristic; the assertion must win in the
|
|
// bundle that actually serves the request.
|
|
expect(route.getCapabilitiesForModel("custom-node", "qwen3-vl-plus").vision).toBe(false);
|
|
|
|
boot.setUserCapsSource(() => ({ reasoning: true }));
|
|
expect(route.getCapabilitiesForModel("custom-node", "my-private-chatml-9").reasoning).toBe(true);
|
|
});
|
|
|
|
it("honours a catalog source installed at startup", () => {
|
|
boot.setCatalogSource({
|
|
getModalities: (model) => (model === "hand-rolled-7b" ? { vision: true } : null),
|
|
getLimits: (provider, model) => (model === "hand-rolled-7b" ? { contextWindow: 400000 } : null),
|
|
});
|
|
const caps = route.getCapabilitiesForModel("custom-node", "hand-rolled-7b");
|
|
expect(caps.vision).toBe(true);
|
|
expect(caps.contextWindow).toBe(400000);
|
|
});
|
|
|
|
it("detaches the catalog everywhere when one bundle detaches it", () => {
|
|
// modelCatalog/sync.js nulls the reader while computing a delta against the
|
|
// hand-written tables, then reinstalls it; both bundles must agree.
|
|
boot.setCatalogSource({ getModalities: () => ({ vision: true }), getLimits: () => null });
|
|
expect(route.getCapabilitiesForModel("custom-node", "hand-rolled-7b").vision).toBe(true);
|
|
|
|
boot.setCatalogSource(null);
|
|
expect(route.getCapabilitiesForModel("custom-node", "hand-rolled-7b").vision).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("user-caps snapshot across server bundles", () => {
|
|
const rows = [{ providerAlias: "node-abc", id: "chat-9", type: "llm", caps: { reasoning: true } }];
|
|
|
|
it("makes a rebuild started by the write route visible to the request route", async () => {
|
|
vi.doMock("@/lib/db/index.js", () => ({ getCustomModels: async () => rows }));
|
|
try {
|
|
const write = await import("../../src/lib/modelCaps/userCaps.js?bundle=write");
|
|
const request = await import("../../src/lib/modelCaps/userCaps.js?bundle=request");
|
|
|
|
write.invalidateUserCaps();
|
|
await vi.waitFor(() => {
|
|
expect(request.getUserCapsFor("node-abc", "chat-9")).toEqual({ reasoning: true });
|
|
});
|
|
} finally {
|
|
vi.doUnmock("@/lib/db/index.js");
|
|
vi.resetModules();
|
|
}
|
|
});
|
|
});
|