feat(providers): add Token Harbor provider

This commit is contained in:
Nikan Wystaf
2026-09-26 00:27:12 +03:30
parent 37a6b7e0f2
commit 737b1f4d0a
8 changed files with 143 additions and 5 deletions

View File

@@ -125,6 +125,7 @@ import p119 from "./selfhosted-embedding.js";
import p120 from "./fish-audio.js";
import p121 from "./alitp-intl.js";
import p122 from "./xquik.js";
import p125 from "./tokenharbor.js";
import p126 from "./dahl.js";
import p127 from "./atria.js";
import p129 from "./agnes.js";
@@ -254,6 +255,7 @@ export default [
p120,
p121,
p122,
p125,
p126,
p127,
p129,

View File

@@ -0,0 +1,49 @@
export default {
id: "tokenharbor",
priority: 120,
alias: "tokenharbor",
aliases: [
"th",
"thh",
],
uiAlias: "tokenharbor",
display: {
name: "Token Harbor",
icon: "anchor",
color: "#0F766E",
textIcon: "TH",
website: "https://tokenharbor.ai",
notice: {
text: "OpenAI-compatible aggregator. One API key reaches every model, billed per-token from a prepaid wallet. Model ids are bare (e.g. claude-opus-5.5, gpt-6-astra, deepseek-v4.1-flash:free) and are fetched live from the provider.",
apiKeyUrl: "https://tokenharbor.ai/dashboard",
},
},
category: "apikey",
authType: "apikey",
transport: {
// OpenAI-compatible. `format` is left at the shared "openai" default and
// `thinkingFormat` is deliberately NOT declared: Token Harbor forwards
// requests verbatim, so each model must resolve its own thinking wire
// format through providers/capabilities.js. Setting a provider-wide value
// would force one format (e.g. claude-adaptive) onto every model.
baseUrl: "https://tokenharbor.ai/v1/chat/completions",
validateUrl: "https://tokenharbor.ai/v1/models",
retry: {
429: 2,
},
},
// Curated seed; the live catalogue is fetched via modelsFetcher and any other
// id is accepted via passthroughModels. Their catalogue rotates (the :free set
// in particular), so this stays deliberately small and is only the offline
// fallback. Ids are bare — Token Harbor does not prefix them by upstream vendor.
models: [
{ id: "claude-opus-5.5", name: "Claude Opus 5.5" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "gpt-6-astra", name: "GPT-6 Astra" },
{ id: "gpt-6-sol", name: "GPT-6 Sol" },
{ id: "deepseek-v4.1-flash:free", name: "DeepSeek V4.1 Flash (Free)" },
{ id: "grok-4.7", name: "Grok 4.7" },
],
modelsFetcher: { url: "https://tokenharbor.ai/v1/models", type: "openai" },
passthroughModels: true,
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -302,9 +302,8 @@ const PROVIDER_MODELS_CONFIG = {
nvidia: createOpenAIModelsConfig("https://integrate.api.nvidia.com/v1/models"),
assemblyai: createOpenAIModelsConfig("https://api.assemblyai.com/v1/models"),
"vercel-ai-gateway": createOpenAIModelsConfig("https://ai-gateway.vercel.sh/v1/models"),
// OpenAI-compatible aggregators. All standard Bearer + OpenAI shape, so they
// reuse createOpenAIModelsConfig; Kira and Dahl serve their catalogue
// publicly, the rest need the connection API key.
// OpenAI-compatible aggregators.
tokenharbor: createOpenAIModelsConfig("https://tokenharbor.ai/v1/models"),
dahl: createOpenAIModelsConfig("https://inference.dahl.global/v1/models"),
atria: createOpenAIModelsConfig("https://api.atria-asi.ai/v1/models"),
agnes: createOpenAIModelsConfig("https://apihub.agnes-ai.com/v1/models"),

View File

@@ -696,8 +696,7 @@ async function testApiKeyConnection(connection, effectiveProxy = null) {
const res = await fetchWithConnectionProxy("https://api.hyperbolic.xyz/v1/models", { headers: { Authorization: `Bearer ${connection.apiKey}` } }, effectiveProxy);
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
}
// OpenAI-compatible aggregators. All standard Bearer + OpenAI shape, so the
// same /v1/models probe applies to each.
case "tokenharbor":
case "dahl":
case "atria":
case "agnes":

View File

@@ -196,6 +196,7 @@
"siliconflow": "siliconflow",
"tencent": "hunyuan",
"together": "together",
"tokenharbor": "tokenharbor",
"tokenrouter": "tokenrouter",
"venice": "venice",
"vercel-ai-gateway": "vercel-ai-gateway",
@@ -299,6 +300,7 @@
"siliconflow",
"stability-ai",
"together",
"tokenharbor",
"tokenrouter",
"venice",
"vertex",

View File

@@ -1105,5 +1105,13 @@
"baseUrl": "https://api.b.ai/v1/chat/completions",
"validateUrl": "https://api.b.ai/v1/models",
"format": "openai"
},
"tokenharbor": {
"baseUrl": "https://tokenharbor.ai/v1/chat/completions",
"validateUrl": "https://tokenharbor.ai/v1/models",
"retry": {
"429": 2
},
"format": "openai"
}
}

View File

@@ -0,0 +1,79 @@
import { describe, expect, it } from "vitest";
import REGISTRY from "../../open-sse/providers/registry/index.js";
import { PROVIDERS, PROVIDER_MODELS } from "../../open-sse/providers/index.js";
import { getCapabilitiesForModel } from "../../open-sse/providers/capabilities.js";
import { getExecutor } from "../../open-sse/executors/index.js";
import { DefaultExecutor } from "../../open-sse/executors/default.js";
describe("Token Harbor provider", () => {
const entry = REGISTRY.find((e) => e.id === "tokenharbor");
it("is registered as an OpenAI-compatible apikey provider", () => {
expect(entry).toBeDefined();
expect(entry.category).toBe("apikey");
expect(entry.authType).toBe("apikey");
expect(entry.alias).toBe("tokenharbor");
expect(entry.aliases).toContain("th");
});
it("points at the verified OpenAI-compatible base URL", () => {
expect(PROVIDERS.tokenharbor.baseUrl).toBe("https://tokenharbor.ai/v1/chat/completions");
expect(PROVIDERS.tokenharbor.validateUrl).toBe("https://tokenharbor.ai/v1/models");
// transport.format defaults to "openai" via the shared provider default
expect(PROVIDERS.tokenharbor.format).toBe("openai");
});
it("declares no provider-wide thinkingFormat so each model resolves its own", () => {
// Token Harbor forwards bodies verbatim. A provider-wide thinkingFormat
// would override capabilities.js and force one wire format (e.g.
// claude-adaptive) onto every model, which an OpenAI endpoint rejects.
expect(PROVIDERS.tokenharbor.thinkingFormat).toBeUndefined();
});
it("enables dynamic model discovery and passthrough", () => {
expect(entry.passthroughModels).toBe(true);
expect(entry.modelsFetcher).toMatchObject({
url: "https://tokenharbor.ai/v1/models",
type: "openai",
});
});
it("exposes a small seed of bare (unprefixed) model ids", () => {
const ids = (PROVIDER_MODELS.tokenharbor || []).map((m) => m.id);
expect(ids.length).toBeGreaterThan(0);
expect(ids).toContain("claude-opus-5.5");
// Token Harbor does not prefix ids by upstream vendor
expect(ids.every((id) => !id.includes("/"))).toBe(true);
});
it("routes through the shared DefaultExecutor (no custom adapter)", () => {
expect(getExecutor("tokenharbor")).toBeInstanceOf(DefaultExecutor);
});
it("resolves per-model capabilities from the shared tables", () => {
// Bare ids must still reach the canonical family patterns.
expect(getCapabilitiesForModel("tokenharbor", "claude-opus-5.5")).toMatchObject({
vision: true,
reasoning: true,
thinkingFormat: "claude-adaptive",
});
expect(getCapabilitiesForModel("tokenharbor", "gpt-6-astra")).toMatchObject({
reasoning: true,
thinkingFormat: "openai",
});
});
it("does not invent capabilities for an uncatalogued model", () => {
// Vision/reasoning must not be blanket-granted across the provider.
const caps = getCapabilitiesForModel("tokenharbor", "some-unknown-model-x");
expect(caps.vision).toBe(false);
expect(caps.reasoning).toBe(false);
expect(caps.thinkingFormat).toBeNull();
});
it("keeps every registry id unique after adding tokenharbor", () => {
const ids = REGISTRY.map((e) => e.id);
expect(new Set(ids).size).toBe(ids.length);
});
});