feat(providers): add Token Harbor provider
This commit is contained in:
@@ -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,
|
||||
|
||||
49
open-sse/providers/registry/tokenharbor.js
Normal file
49
open-sse/providers/registry/tokenharbor.js
Normal 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,
|
||||
};
|
||||
BIN
public/providers/tokenharbor.png
Normal file
BIN
public/providers/tokenharbor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
@@ -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"),
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
79
tests/unit/tokenharbor-provider.test.js
Normal file
79
tests/unit/tokenharbor-provider.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user