Files
OmniRoute/tests/unit/newapi-gateway-providers.test.ts
Yawar eb6f319712 feat(providers): add tabitoken gateway and serve hcnsec's four protocols (#10668)
Obrigado — PR muito bem documentado e verificado. Adiciona o gateway TabiToken (Anthropic-first, /v1/messages, x-api-key) e estende hcnsec de 1 para 4 protocolos (Chat, Responses, Anthropic Messages, Gemini). AlternateFormat ganha o hook urlBuilder opcional (necessário para o path model-scoped do Gemini), compartilhado com o provider gemini nativo em vez de duplicado.

Reconciliado nesta sessão contra o release tip atualizado (base drift real: 343→345 canônicos entre quando o PR foi criado e o merge, mais os PRs #10673/#10658 mergeados nesse meio-tempo). Conflitos em contagens de providers (docs, file-size baseline, teste de partição) resolvidos additivamente.

Validação (reconciliação a partir de origin/release/v3.8.50):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- npm run check:provider-consistency — OK (266 REGISTRY entries, 346 providers canônicos, 0 exceções)
- 40/40 testes passando (newapi-gateway-providers, hcnsec-provider, providers-constants-split, alternate-formats)
2026-08-20 20:24:42 -03:00

300 lines
14 KiB
TypeScript

// Coverage for the two NewAPI-based gateways touched alongside the AlternateFormat.urlBuilder
// hook: tabitoken (new — Claude-first, 2 protocols) and hcnsec (already shipped as an
// OpenAI-only regional entry, now declaring the 3 further protocols it actually serves).
//
// hcnsec is the first registry entry to expose the **Gemini** protocol as an alternate, and the
// Gemini route embeds the model in the path (`{base}/{model}:generateContent`) instead of a
// constant suffix. `chatPath`/`urlSuffix` cannot express that, so AlternateFormat grew an
// optional `urlBuilder` and shared.ts grew `buildGeminiGenerateContentUrl` — the same builder the
// native `gemini` provider now uses. The identity assertion at the bottom of this file is what
// keeps those two consumers from drifting apart.
//
// hcnsec's pre-existing guarantees (openai format, bearer auth, no static model seed) keep their
// own guard in tests/unit/hcnsec-provider.test.ts; the shape test here re-asserts them only to
// prove the alternates were added *without* moving the defaults.
//
// Everything is asserted through the real DefaultExecutor (not a reimplementation of the
// precedence rules) because both providers exist in the static registry — the limitation the
// older Task 3/4 tests in alternate-formats.test.ts had to work around no longer applies.
import { test } from "node:test";
import assert from "node:assert/strict";
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
import { getTargetFormat } from "../../open-sse/services/provider.ts";
import { getRegistryEntry } from "../../open-sse/config/providerRegistry.ts";
import { buildGeminiGenerateContentUrl } from "../../open-sse/config/providers/shared.ts";
import { geminiProvider } from "../../open-sse/config/providers/registry/gemini/index.ts";
import { getAlternateFormats } from "../../src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts";
import { AI_PROVIDERS, AGGREGATOR_PROVIDER_IDS } from "../../src/shared/constants/providers.ts";
import { APIKEY_PROVIDERS_GATEWAYS } from "../../src/shared/constants/providers/apikey/gateways.ts";
import { APIKEY_PROVIDERS_REGIONAL } from "../../src/shared/constants/providers/apikey/regional.ts";
import { PROVIDER_ENDPOINTS } from "../../src/shared/constants/config.ts";
const KEY = { apiKey: "sk-test" } as never;
const withFormat = (targetFormat: string) =>
({ apiKey: "sk-test", providerSpecificData: { targetFormat } }) as never;
// ── tabitoken ─────────────────────────────────────────────────────────────────
test("tabitoken registry entry is Claude-first with an OpenAI alternate", () => {
const entry = getRegistryEntry("tabitoken");
assert.equal(entry?.format, "claude");
assert.equal(entry?.executor, "default");
assert.equal(entry?.authType, "apikey");
assert.equal(entry?.authHeader, "x-api-key");
assert.equal(entry?.baseUrl, "https://tabitoken.com/v1/messages");
assert.equal(entry?.modelsUrl, "https://tabitoken.com/v1/models");
assert.equal(entry?.passthroughModels, true);
// The generic claude-format path in default.ts::buildHeaders only defaults
// anthropic-version for `anthropic-compatible-*` ids, so the entry carries it.
assert.equal(entry?.headers?.["Anthropic-Version"], "2023-06-01");
// Only the two protocols tabitoken's own /api/pricing reports per model
// (supported_endpoint_types: ["anthropic","openai"]).
assert.deepEqual(
(entry?.alternateFormats || []).map((a) => a.format),
["openai"]
);
});
test("tabitoken catalog matches the four Claude models its public pricing endpoint lists", () => {
const entry = getRegistryEntry("tabitoken");
assert.deepEqual(
(entry?.models || []).map((m) => m.id),
["claude-opus-5", "claude-opus-5-thinking", "claude-opus-4-8", "claude-opus-4-8-thinking"]
);
for (const model of entry?.models || []) {
assert.equal(typeof model.name, "string");
assert.ok(model.name.length > 0, `${model.id} must carry a display name`);
}
});
test("tabitoken defaults to /v1/messages + x-api-key and switches to Bearer on the OpenAI alternate", () => {
const executor = new DefaultExecutor("tabitoken");
assert.equal(getTargetFormat("tabitoken", null), "claude");
assert.equal(
executor.buildUrl("claude-opus-5", true, 0, KEY),
"https://tabitoken.com/v1/messages"
);
const claudeHeaders = executor.buildHeaders(KEY, true) as Record<string, string>;
assert.equal(claudeHeaders["x-api-key"], "sk-test");
assert.equal(claudeHeaders["Authorization"], undefined);
assert.equal(claudeHeaders["Anthropic-Version"], "2023-06-01");
assert.equal(getTargetFormat("tabitoken", { targetFormat: "openai" }), "openai");
const openaiCreds = withFormat("openai");
assert.equal(
executor.buildUrl("claude-opus-5", true, 0, openaiCreds),
"https://tabitoken.com/v1/chat/completions"
);
const openaiHeaders = executor.buildHeaders(openaiCreds, true) as Record<string, string>;
assert.equal(openaiHeaders["Authorization"], "Bearer sk-test");
assert.equal(openaiHeaders["x-api-key"], undefined);
});
test("tabitoken ignores a targetFormat it does not declare", () => {
const executor = new DefaultExecutor("tabitoken");
// Unknown alternate → resolveAlternateFormat returns null → default format/route/auth.
assert.equal(getTargetFormat("tabitoken", { targetFormat: "gemini" }), "claude");
const creds = withFormat("gemini");
assert.equal(
executor.buildUrl("claude-opus-5", true, 0, creds),
"https://tabitoken.com/v1/messages"
);
const headers = executor.buildHeaders(creds, true) as Record<string, string>;
assert.equal(headers["x-api-key"], "sk-test");
assert.equal(headers["x-goog-api-key"], undefined);
});
// ── hcnsec ────────────────────────────────────────────────────────────────────
test("hcnsec keeps its OpenAI-first defaults and adds the three further protocols it serves", () => {
const entry = getRegistryEntry("hcnsec");
// Unchanged by this extension — the guard that adding alternates moved no default.
assert.equal(entry?.format, "openai");
assert.equal(entry?.executor, "default");
assert.equal(entry?.authHeader, "bearer");
assert.equal(entry?.baseUrl, "https://api.hcnsec.cn/v1/chat/completions");
assert.equal(entry?.modelsUrl, "https://api.hcnsec.cn/v1/models");
assert.deepEqual(entry?.models, []);
assert.equal(entry?.passthroughModels, true);
// Added: the Responses route plus one alternate per further protocol.
assert.equal(entry?.responsesBaseUrl, "https://api.hcnsec.cn/v1/responses");
assert.deepEqual(
(entry?.alternateFormats || []).map((a) => a.format),
["claude", "openai-responses", "gemini"]
);
});
test("hcnsec routes each protocol to its own endpoint with the matching auth scheme", () => {
const executor = new DefaultExecutor("hcnsec");
assert.equal(getTargetFormat("hcnsec", null), "openai");
assert.equal(
executor.buildUrl("gpt-5", true, 0, KEY),
"https://api.hcnsec.cn/v1/chat/completions"
);
assert.equal(
(executor.buildHeaders(KEY, true) as Record<string, string>)["Authorization"],
"Bearer sk-test"
);
const claudeCreds = withFormat("claude");
assert.equal(getTargetFormat("hcnsec", { targetFormat: "claude" }), "claude");
assert.equal(
executor.buildUrl("claude-opus-5", true, 0, claudeCreds),
"https://api.hcnsec.cn/v1/messages"
);
const claudeHeaders = executor.buildHeaders(claudeCreds, true) as Record<string, string>;
assert.equal(claudeHeaders["x-api-key"], "sk-test");
assert.equal(claudeHeaders["Authorization"], undefined);
assert.equal(claudeHeaders["Anthropic-Version"], "2023-06-01");
const responsesCreds = withFormat("openai-responses");
assert.equal(getTargetFormat("hcnsec", { targetFormat: "openai-responses" }), "openai-responses");
assert.equal(
executor.buildUrl("gpt-5", true, 0, responsesCreds),
"https://api.hcnsec.cn/v1/responses"
);
assert.equal(
(executor.buildHeaders(responsesCreds, true) as Record<string, string>)["Authorization"],
"Bearer sk-test"
);
});
test("hcnsec's Gemini alternate builds the model-scoped generateContent route in both forms", () => {
const executor = new DefaultExecutor("hcnsec");
const creds = withFormat("gemini");
assert.equal(getTargetFormat("hcnsec", { targetFormat: "gemini" }), "gemini");
// Unary: the model lands in the path, which is exactly what chatPath/urlSuffix
// (both constants) could not express before urlBuilder existed.
assert.equal(
executor.buildUrl("gemini-3.7-flash", false, 0, creds),
"https://api.hcnsec.cn/v1beta/models/gemini-3.7-flash:generateContent"
);
// Streaming keeps the `?alt=sse` suffix the Gemini protocol requires.
assert.equal(
executor.buildUrl("gemini-3.7-flash", true, 0, creds),
"https://api.hcnsec.cn/v1beta/models/gemini-3.7-flash:streamGenerateContent?alt=sse"
);
const headers = executor.buildHeaders(creds, true) as Record<string, string>;
assert.equal(headers["x-goog-api-key"], "sk-test");
assert.equal(headers["Authorization"], undefined);
assert.equal(headers["x-api-key"], undefined);
});
test("hcnsec ignores a targetFormat it does not declare", () => {
const executor = new DefaultExecutor("hcnsec");
assert.equal(getTargetFormat("hcnsec", { targetFormat: "codex" }), "openai");
const creds = withFormat("codex");
assert.equal(
executor.buildUrl("gpt-5", true, 0, creds),
"https://api.hcnsec.cn/v1/chat/completions"
);
assert.equal(
(executor.buildHeaders(creds, true) as Record<string, string>)["Authorization"],
"Bearer sk-test"
);
});
// ── shared Gemini URL builder ─────────────────────────────────────────────────
test("the Gemini route builder is shared with the native gemini provider, not duplicated", () => {
// Identity, not equality: if someone re-inlines an arrow function on either side the
// `?alt=sse` suffix is free to drift between the two consumers. This is the guard.
assert.equal(geminiProvider.urlBuilder, buildGeminiGenerateContentUrl);
const geminiAlternate = (getRegistryEntry("hcnsec")?.alternateFormats || []).find(
(a) => a.format === "gemini"
);
assert.equal(geminiAlternate?.urlBuilder, buildGeminiGenerateContentUrl);
assert.equal(
buildGeminiGenerateContentUrl("https://example.com/v1beta/models", "m", false),
"https://example.com/v1beta/models/m:generateContent"
);
assert.equal(
buildGeminiGenerateContentUrl("https://example.com/v1beta/models", "m", true),
"https://example.com/v1beta/models/m:streamGenerateContent?alt=sse"
);
});
test("the native gemini provider still resolves its own generateContent route", () => {
const executor = new DefaultExecutor("gemini");
assert.equal(
executor.buildUrl("gemini-3.7-flash", false, 0, KEY),
"https://generativelanguage.googleapis.com/v1beta/models/gemini-3.7-flash:generateContent"
);
assert.equal(
executor.buildUrl("gemini-3.7-flash", true, 0, KEY),
"https://generativelanguage.googleapis.com/v1beta/models/gemini-3.7-flash:streamGenerateContent?alt=sse"
);
});
// ── dashboard / catalog registration ──────────────────────────────────────────
test("both hosts surface in the dashboard alternate-protocol picker", () => {
assert.deepEqual(
getAlternateFormats("tabitoken").map((a) => a.format),
["openai"]
);
assert.deepEqual(
getAlternateFormats("hcnsec").map((a) => a.format),
["claude", "openai-responses", "gemini"]
);
// Every alternate needs a label — it is the string the picker renders.
for (const id of ["tabitoken", "hcnsec"]) {
for (const alternate of getAlternateFormats(id)) {
assert.ok(alternate.label, `${id}/${alternate.format} must declare a label`);
}
}
});
test("tabitoken is catalogued as an aggregator gateway with a display endpoint", () => {
const provider = (AI_PROVIDERS as Record<string, Record<string, unknown>>).tabitoken;
assert.ok(provider, "tabitoken must be present in AI_PROVIDERS");
assert.equal(provider.id, "tabitoken");
assert.equal(provider.alias, "tabitoken");
assert.equal(provider.passthroughModels, true);
assert.equal(typeof provider.name, "string");
assert.equal(typeof provider.website, "string");
assert.equal(typeof provider.apiHint, "string");
assert.ok(AGGREGATOR_PROVIDER_IDS.has("tabitoken"), "tabitoken must be an aggregator");
// The display endpoint must name the protocol the gateway defaults to.
assert.equal(
(PROVIDER_ENDPOINTS as Record<string, string>).tabitoken,
"https://tabitoken.com/v1/messages"
);
});
test("extending hcnsec's protocols leaves its existing catalog classification alone", () => {
// hcnsec shipped before this change as an API-key **regional** provider (its own guard:
// tests/unit/hcnsec-provider.test.ts). Declaring three more protocols on the registry entry
// is an engine-level capability change — it must not silently reclassify the catalog entry
// as a gateway/aggregator, which would move it in the dashboard and in the generated
// provider reference. This test is the guard against that drift.
const provider = (AI_PROVIDERS as Record<string, Record<string, unknown>>).hcnsec;
assert.ok(provider, "hcnsec must remain present in AI_PROVIDERS");
assert.equal(provider.id, "hcnsec");
assert.equal(provider.alias, "hcnsec");
assert.equal(provider.passthroughModels, true);
assert.equal(provider.name, "Huancheng Public API");
assert.equal(provider.website, "https://api.hcnsec.cn");
assert.equal(typeof provider.authHint, "string");
assert.equal(
AGGREGATOR_PROVIDER_IDS.has("hcnsec"),
false,
"hcnsec stays a regional provider — the protocol extension must not reclassify it"
);
const regional = APIKEY_PROVIDERS_REGIONAL as Record<string, unknown>;
assert.ok(regional.hcnsec, "hcnsec must stay in the regional family file");
const gateways = APIKEY_PROVIDERS_GATEWAYS as Record<string, unknown>;
assert.equal(
gateways.hcnsec,
undefined,
"hcnsec must not be duplicated into gateways — the families are a strict partition"
);
});