mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 04:12:17 +03:00
Rebased onto the release tip after #13024 landed: both PRs extend the same three registration files, so the sibling merge turned this into a conflict. The resolution is additive — both catalog entries kept, both registry imports kept, both base URLs kept — and EURouter stays in AGGREGATOR_PROVIDER_IDS while GreenPT stays out, exactly as each PR argued. 14 provider tests pass on the rebased branch and the file-size gate is green under the annotated rebaseline. Thank you for re-checking the endpoint live instead of trusting the report, and for the sovereignty caveat. Naming the upstreams from EURouter's own catalog — Claude Sonnet served by AWS Bedrock, 19 models owned by openai — and then writing an apiHint that says routing rather than residency is the kind of care that keeps a provider entry honest. The test asserting the copy contains none of "residency", "stays in the EU", "EU-hosted" or "sovereign" is a good guard against that drifting later.
This commit is contained in:
1
changelog.d/features/12985-eurouter-provider.md
Normal file
1
changelog.d/features/12985-eurouter-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(providers):** Added EURouter as an OpenAI-compatible API-key gateway (`https://api.eurouter.ai/v1`), with live model discovery via `passthroughModels`. Its copy states that models are served by third-party upstreams listed per model, so an EU-based router is not read as EU data residency for inference.
|
||||
@@ -250,6 +250,7 @@ import { llmgatewayProvider } from "./registry/llmgateway/index.ts";
|
||||
import { llmKiwiProvider } from "./registry/llm-kiwi/index.ts";
|
||||
import { literouterProvider } from "./registry/literouter/index.ts";
|
||||
import { greenptProvider } from "./registry/greenpt/index.ts";
|
||||
import { eurouterProvider } from "./registry/eurouter/index.ts";
|
||||
import { mnnAiProvider } from "./registry/mnn-ai/index.ts";
|
||||
import { meganovaAiProvider } from "./registry/meganova-ai/index.ts";
|
||||
import { mixlayerProvider } from "./registry/mixlayer/index.ts";
|
||||
@@ -526,6 +527,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
"llm-kiwi": llmKiwiProvider,
|
||||
literouter: literouterProvider,
|
||||
greenpt: greenptProvider,
|
||||
eurouter: eurouterProvider,
|
||||
"mnn-ai": mnnAiProvider,
|
||||
"meganova-ai": meganovaAiProvider,
|
||||
mixlayer: mixlayerProvider,
|
||||
|
||||
11
open-sse/config/providers/registry/eurouter/index.ts
Normal file
11
open-sse/config/providers/registry/eurouter/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const eurouterProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "eurouter",
|
||||
alias: "eurouter",
|
||||
baseUrl: "https://api.eurouter.ai/v1/chat/completions",
|
||||
modelsUrl: "https://api.eurouter.ai/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -18,6 +18,7 @@ export const PROVIDER_ENDPOINTS = {
|
||||
"llm-kiwi": "https://api.llm.kiwi/v1/chat/completions",
|
||||
literouter: "https://api.literouter.com/v1/chat/completions",
|
||||
greenpt: "https://api.greenpt.ai/v1/chat/completions",
|
||||
eurouter: "https://api.eurouter.ai/v1/chat/completions",
|
||||
"mnn-ai": "https://api.mnnai.ru/v1/chat/completions",
|
||||
"meganova-ai": "https://api.meganova.ai/v1/chat/completions",
|
||||
mixlayer: "https://models.mixlayer.ai/v1/chat/completions",
|
||||
|
||||
@@ -123,6 +123,7 @@ export const AGGREGATOR_PROVIDER_IDS = new Set([
|
||||
"llmgateway",
|
||||
"llm-kiwi",
|
||||
"literouter",
|
||||
"eurouter",
|
||||
"mnn-ai",
|
||||
"meganova-ai",
|
||||
"mixlayer",
|
||||
|
||||
@@ -285,6 +285,27 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
|
||||
apiHint:
|
||||
"Create a GreenPT API key, then use https://api.greenpt.ai/v1 as the OpenAI-compatible base URL. Review jurisdiction, privacy and regional data-transfer requirements before use.",
|
||||
},
|
||||
eurouter: {
|
||||
id: "eurouter",
|
||||
serviceKinds: ["llm"],
|
||||
alias: "eurouter",
|
||||
name: "EURouter",
|
||||
icon: "router",
|
||||
color: "#1D4ED8",
|
||||
textIcon: "EUR",
|
||||
passthroughModels: true,
|
||||
website: "https://eurouter.ai",
|
||||
// No free allowance is published, so no badge. A key was accepted but the
|
||||
// account had no credits, so nothing about pricing tiers is claimed here.
|
||||
hasFree: false,
|
||||
// Deliberately says routing, not residency. EURouter is a router: its own
|
||||
// catalog names the upstream that serves each model (claude-sonnet-5 ->
|
||||
// AWS Bedrock, and 19 models owned by openai, 9 by anthropic, 7 by amazon).
|
||||
// An EU-based router is a routing layer in the EU; where a model actually
|
||||
// executes, and under whose terms, is a per-upstream property (#12985).
|
||||
apiHint:
|
||||
"Create an EURouter API key, then use https://api.eurouter.ai/v1 as the OpenAI-compatible base URL. Models are served by third-party upstreams listed per model in the EURouter catalog; check each upstream jurisdiction, privacy and data-transfer terms before use.",
|
||||
},
|
||||
"mnn-ai": {
|
||||
id: "mnn-ai",
|
||||
serviceKinds: ["llm"],
|
||||
|
||||
85
tests/unit/eurouter-provider.test.ts
Normal file
85
tests/unit/eurouter-provider.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { eurouterProvider } from "../../open-sse/config/providers/registry/eurouter/index.ts";
|
||||
|
||||
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { DefaultExecutor, getExecutor } = await import("../../open-sse/executors/index.ts");
|
||||
const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts");
|
||||
const { isValidModel } = await import("../../src/shared/constants/models.ts");
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts");
|
||||
const { AGGREGATOR_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts");
|
||||
|
||||
const CHAT_URL = "https://api.eurouter.ai/v1/chat/completions";
|
||||
const MODELS_URL = "https://api.eurouter.ai/v1/models";
|
||||
|
||||
test("eurouter is an OpenAI-compatible Bearer registry entry", () => {
|
||||
assert.equal(eurouterProvider.id, "eurouter");
|
||||
assert.equal(eurouterProvider.alias, "eurouter");
|
||||
assert.equal(eurouterProvider.format, "openai");
|
||||
assert.equal(eurouterProvider.executor, "default");
|
||||
assert.equal(eurouterProvider.authType, "apikey");
|
||||
assert.equal(eurouterProvider.authHeader, "bearer");
|
||||
assert.equal(eurouterProvider.baseUrl, CHAT_URL);
|
||||
assert.equal(eurouterProvider.modelsUrl, MODELS_URL);
|
||||
assert.equal(eurouterProvider.passthroughModels, true);
|
||||
});
|
||||
|
||||
test("eurouter leaves its 147-model catalog to live discovery", () => {
|
||||
assert.deepEqual(eurouterProvider.models, []);
|
||||
});
|
||||
|
||||
test("eurouter is wired through registry, metadata, endpoint and default executor", async () => {
|
||||
assert.equal(REGISTRY.eurouter?.baseUrl, CHAT_URL);
|
||||
assert.equal(PROVIDER_ENDPOINTS.eurouter, CHAT_URL);
|
||||
assert.equal(APIKEY_PROVIDERS.eurouter?.id, "eurouter");
|
||||
assert.equal(APIKEY_PROVIDERS.eurouter?.alias, "eurouter");
|
||||
assert.ok((await getExecutor("eurouter")) instanceof DefaultExecutor);
|
||||
assert.equal(isValidModel("eurouter", "future/live-catalog-model"), true);
|
||||
});
|
||||
|
||||
test("eurouter is listed as an aggregator", () => {
|
||||
// It routes to third-party upstreams rather than serving its own inference,
|
||||
// which is what that set means -- the opposite call from GreenPT (#12986).
|
||||
assert.equal(AGGREGATOR_PROVIDER_IDS.has("eurouter"), true);
|
||||
});
|
||||
|
||||
test("eurouter advertises no free allowance", () => {
|
||||
// A key was accepted (HTTP 402 Insufficient balance) but the account had no
|
||||
// credits, so no pricing tier was observed and none is claimed.
|
||||
assert.equal(APIKEY_PROVIDERS.eurouter?.hasFree, false);
|
||||
assert.equal(APIKEY_PROVIDERS.eurouter?.freeNote, undefined);
|
||||
});
|
||||
|
||||
test("eurouter copy does not imply EU residency for inference", () => {
|
||||
// The name invites that reading and the catalog contradicts it: models are
|
||||
// served by upstreams such as AWS Bedrock. Being EU-based is a property of
|
||||
// the routing layer, not of where a model executes (#12985).
|
||||
const hint = String(APIKEY_PROVIDERS.eurouter?.apiHint ?? "");
|
||||
assert.ok(hint.length > 0, "an apiHint is required to carry the caveat");
|
||||
for (const claim of [
|
||||
"data residency",
|
||||
"residency",
|
||||
"stays in the EU",
|
||||
"EU-hosted",
|
||||
"sovereign",
|
||||
]) {
|
||||
assert.ok(
|
||||
!hint.toLowerCase().includes(claim.toLowerCase()),
|
||||
`apiHint must not claim "${claim}"`
|
||||
);
|
||||
}
|
||||
assert.ok(
|
||||
hint.toLowerCase().includes("third-party upstream"),
|
||||
"apiHint must say the models are served by third-party upstreams"
|
||||
);
|
||||
});
|
||||
|
||||
test("eurouter claims no capability that was not exercised", () => {
|
||||
// Streaming SSE conformance was not exercised -- the usual place these
|
||||
// gateways diverge, and a passthrough entry breaks there silently.
|
||||
const metadata = APIKEY_PROVIDERS.eurouter as Record<string, unknown>;
|
||||
for (const key of ["supportsTools", "supportsVision", "capabilities"]) {
|
||||
assert.equal(metadata[key], undefined, `${key} must not be declared unverified`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user