161 lines
6.7 KiB
TypeScript
161 lines
6.7 KiB
TypeScript
// Split-guard for the provider-models discovery route decomposition
|
|
// (refactor: extract 4 pure leaves — helpers / normalizers / providerModelsConfig /
|
|
// providerSets — out of src/app/api/providers/[id]/models/route.ts). The leaves are
|
|
// DB-free and state-free; this guard pins their public surface, the host wiring, and
|
|
// the #5570 cablyai→aimlapi swap so a future edit that silently breaks the split fails.
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
import {
|
|
asRecord,
|
|
toNonEmptyString,
|
|
buildOptionalBearerHeaders,
|
|
buildNamedOpenAiStyleHeaders,
|
|
isLocalOpenAIStyleProvider,
|
|
mergeLocalCatalogModels,
|
|
getAzureOpenAIApiVersion,
|
|
} from "../../src/app/api/providers/[id]/models/discovery/helpers.ts";
|
|
import { normalizeOpenAiLikeModelsResponse } from "../../src/app/api/providers/[id]/models/discovery/normalizers.ts";
|
|
import {
|
|
NAMED_OPENAI_STYLE_PROVIDERS,
|
|
isNamedOpenAIStyleProvider,
|
|
} from "../../src/app/api/providers/[id]/models/discovery/providerSets.ts";
|
|
import { PROVIDER_MODELS_CONFIG } from "../../src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts";
|
|
|
|
// ── helpers leaf ─────────────────────────────────────────────────────────────
|
|
|
|
test("helpers.asRecord returns plain objects untouched, non-objects as {}", () => {
|
|
assert.deepEqual(asRecord({ a: 1 }), { a: 1 });
|
|
assert.deepEqual(asRecord([1, 2]), {});
|
|
assert.deepEqual(asRecord(null), {});
|
|
assert.deepEqual(asRecord("x"), {});
|
|
});
|
|
|
|
test("helpers.toNonEmptyString trims and rejects blanks/non-strings", () => {
|
|
assert.equal(toNonEmptyString(" hi "), "hi");
|
|
assert.equal(toNonEmptyString(" "), null);
|
|
assert.equal(toNonEmptyString(7), null);
|
|
});
|
|
|
|
test("helpers.buildOptionalBearerHeaders only adds Authorization when a token is present", () => {
|
|
assert.deepEqual(buildOptionalBearerHeaders("tok"), {
|
|
"Content-Type": "application/json",
|
|
Authorization: "Bearer tok",
|
|
});
|
|
assert.deepEqual(buildOptionalBearerHeaders(null), { "Content-Type": "application/json" });
|
|
});
|
|
|
|
test("helpers.buildNamedOpenAiStyleHeaders adds the reka X-Api-Key only for reka", () => {
|
|
assert.equal(buildNamedOpenAiStyleHeaders("reka", "tok")["X-Api-Key"], "tok");
|
|
assert.equal(buildNamedOpenAiStyleHeaders("openai", "tok")["X-Api-Key"], undefined);
|
|
});
|
|
|
|
test("helpers.mergeLocalCatalogModels dedupes by id, registry wins", () => {
|
|
const merged = mergeLocalCatalogModels(
|
|
[{ id: "a", name: "A" }],
|
|
[
|
|
{ id: "a", name: "dup" },
|
|
{ id: "b", name: "B" },
|
|
]
|
|
);
|
|
assert.deepEqual(
|
|
merged.map((m) => m.id),
|
|
["a", "b"]
|
|
);
|
|
assert.equal(merged.find((m) => m.id === "a")?.name, "A");
|
|
});
|
|
|
|
test("helpers.getAzureOpenAIApiVersion falls back to the pinned default", () => {
|
|
assert.equal(getAzureOpenAIApiVersion({}), "2024-12-01-preview");
|
|
assert.equal(getAzureOpenAIApiVersion({ apiVersion: "2025-01-01" }), "2025-01-01");
|
|
});
|
|
|
|
test("helpers.isLocalOpenAIStyleProvider is false for a hosted provider", () => {
|
|
assert.equal(isLocalOpenAIStyleProvider("openai"), false);
|
|
});
|
|
|
|
// ── normalizers leaf ─────────────────────────────────────────────────────────
|
|
|
|
test("normalizers.normalizeOpenAiLikeModelsResponse maps ids and applies the fallback owner", () => {
|
|
const out = normalizeOpenAiLikeModelsResponse(
|
|
{ data: [{ id: "m1" }, { id: "m2", display_name: "M2", owned_by: "x" }] },
|
|
"acme"
|
|
);
|
|
assert.deepEqual(out, [
|
|
{ id: "m1", name: "m1", owned_by: "acme" },
|
|
{ id: "m2", name: "M2", owned_by: "x" },
|
|
]);
|
|
});
|
|
|
|
test("normalizers.normalizeOpenAiLikeModelsResponse drops entries without an id", () => {
|
|
const out = normalizeOpenAiLikeModelsResponse({ data: [{}, { id: "ok" }] }, "acme");
|
|
assert.deepEqual(
|
|
out.map((m) => m.id),
|
|
["ok"]
|
|
);
|
|
});
|
|
|
|
// ── providerSets leaf ────────────────────────────────────────────────────────
|
|
|
|
test("providerSets.NAMED_OPENAI_STYLE_PROVIDERS is a populated Set with known members", () => {
|
|
assert.ok(NAMED_OPENAI_STYLE_PROVIDERS instanceof Set);
|
|
assert.ok(NAMED_OPENAI_STYLE_PROVIDERS.size >= 30);
|
|
for (const p of ["zenmux", "api-airforce", "together", "reka"]) {
|
|
assert.ok(NAMED_OPENAI_STYLE_PROVIDERS.has(p), `${p} must be a named OpenAI-style provider`);
|
|
}
|
|
});
|
|
|
|
test("providerSets.isNamedOpenAIStyleProvider matches Set membership", () => {
|
|
assert.equal(isNamedOpenAIStyleProvider("zenmux"), true);
|
|
assert.equal(isNamedOpenAIStyleProvider("definitely-not-a-provider"), false);
|
|
});
|
|
|
|
// ── providerModelsConfig leaf ────────────────────────────────────────────────
|
|
|
|
test("providerModelsConfig.PROVIDER_MODELS_CONFIG keeps core provider entries", () => {
|
|
assert.equal(PROVIDER_MODELS_CONFIG.claude.url, "https://api.anthropic.com/v1/models");
|
|
assert.equal(PROVIDER_MODELS_CONFIG["qwen-web"].url, "https://chat.qwen.ai/api/v2/models");
|
|
});
|
|
|
|
test("providerModelsConfig aimlapi entry replaced the removed cablyai entry (#5570)", () => {
|
|
assert.equal(PROVIDER_MODELS_CONFIG.aimlapi.url, "https://api.aimlapi.com/models");
|
|
assert.equal(PROVIDER_MODELS_CONFIG.cablyai, undefined);
|
|
});
|
|
|
|
test("providerModelsConfig aimlapi.parseResponse keeps only chat-completion models when present", () => {
|
|
const parsed = PROVIDER_MODELS_CONFIG.aimlapi.parseResponse([
|
|
{ id: "chat-1", type: "chat-completion", info: { name: "Chat 1" } },
|
|
{ id: "img-1", type: "image" },
|
|
]);
|
|
assert.deepEqual(parsed, [{ id: "chat-1", name: "Chat 1" }]);
|
|
});
|
|
|
|
// ── host wiring guard ────────────────────────────────────────────────────────
|
|
|
|
test("route.ts imports the discovery leaves and no longer declares the moved consts", () => {
|
|
const route = fs.readFileSync(
|
|
path.join("src", "app", "api", "providers", "[id]", "models", "route.ts"),
|
|
"utf-8"
|
|
);
|
|
for (const leaf of ["helpers", "normalizers", "providerSets", "providerModelsConfig"]) {
|
|
assert.match(
|
|
route,
|
|
new RegExp(`from "\\./discovery/${leaf}"`),
|
|
`route must import ./discovery/${leaf}`
|
|
);
|
|
}
|
|
assert.doesNotMatch(
|
|
route,
|
|
/const PROVIDER_MODELS_CONFIG\s*:/,
|
|
"PROVIDER_MODELS_CONFIG must live in the leaf, not route.ts"
|
|
);
|
|
assert.doesNotMatch(
|
|
route,
|
|
/const NAMED_OPENAI_STYLE_PROVIDERS\s*=/,
|
|
"NAMED_OPENAI_STYLE_PROVIDERS must live in the leaf, not route.ts"
|
|
);
|
|
});
|