mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 15:52:52 +03:00
feat(provider): add OpenVecta AI inference gateway (#6833)
* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605) fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148). Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148). Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.) * deps: bump the development group across 1 directory with 6 updates (#6588) deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605). * fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620) fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.) * feat(provider): add OpenVecta AI inference gateway OpenVecta (https://openvecta.com/) is an OpenAI-compatible AI inference gateway hosting LLMs (GLM, Claude, DeepSeek, GPT OSS, Llama, Kimi, Nemotron...) plus text-embedding-* models behind a single Bearer key. Wiring (7 integration points): - src/shared/constants/providers/apikey/inference-hosts.ts: catalog entry - open-sse/config/providers/registry/openvecta/index.ts: registry w/ 9 seed LLMs - open-sse/config/providers/index.ts: wire into REGISTRY - src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts: live /v1/models URL - src/app/api/providers/[id]/models/discovery/providerSets.ts: NAMED_OPENAI_STYLE_PROVIDERS - public/providers/openvecta.svg: brand icon - tests/unit/openvecta-provider-registration.test.ts: regression guard (6 tests, all pass) No executor needed — buildOpenAiCompatibleRegistryEntry wires format=openai / executor=default / authType=apikey / authHeader=bearer. Live catalog discovery uses the existing NAMED_OPENAI_STYLE_PROVIDERS path (live /v1/models fetch + registry seed as offline fallback). Validation: - npm run typecheck:core clean - npm run typecheck:noimplicit:core 4 errors in unchanged files (combo.ts, cliRuntime.ts); 0 in new code - npm run lint clean - node --import tsx/esm --test tests/unit/openvecta-provider-registration.test.ts 6/6 pass - sibling tests/unit/openai-style-providers-4239-4155-3841.test.ts 18/18 pass (no regression) * chore(merge): drop unrelated main-drift from PR fork + fix count/golden drift The fork branch predated main's electron 42→43 bump (#6605) and several other package.json/lockfile churn; those files are unrelated to the OpenVecta provider addition and were reintroducing an older/stale state (version 3.8.46, electron 42, older bun/eslint-config-next) that broke the Electron Package Smoke check. Restored package.json, package-lock.json, electron/package.json, electron/package-lock.json, and scripts/build/prepare-electron-standalone.mjs to match origin/release/v3.8.47. Also updates the two provider-count assertions (166->167) and regenerates the translate-path golden snapshot to account for the new openvecta entry. Co-authored-by: hajilok <120608486+hajilok@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
This commit is contained in:
committed by
GitHub
parent
84c437d19e
commit
6530c92aa6
@@ -105,6 +105,7 @@ import { uncloseaiProvider } from "./registry/uncloseai/index.ts";
|
||||
import { nscaleProvider } from "./registry/nscale/index.ts";
|
||||
import { chatgpt_webProvider } from "./registry/chatgpt-web/index.ts";
|
||||
import { openrouterProvider } from "./registry/openrouter/index.ts";
|
||||
import { openvectaProvider } from "./registry/openvecta/index.ts";
|
||||
import { orcarouterProvider } from "./registry/orcarouter/index.ts";
|
||||
import { copilot_webProvider } from "./registry/copilot-web/index.ts";
|
||||
import { copilot_m365_webProvider } from "./registry/copilot-m365-web/index.ts";
|
||||
@@ -290,6 +291,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
nscale: nscaleProvider,
|
||||
"chatgpt-web": chatgpt_webProvider,
|
||||
openrouter: openrouterProvider,
|
||||
openvecta: openvectaProvider,
|
||||
orcarouter: orcarouterProvider,
|
||||
"copilot-web": copilot_webProvider,
|
||||
"copilot-m365-web": copilot_m365_webProvider,
|
||||
|
||||
30
open-sse/config/providers/registry/openvecta/index.ts
Normal file
30
open-sse/config/providers/registry/openvecta/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
/**
|
||||
* OpenVecta — OpenAI-compatible AI inference gateway (https://openvecta.com/).
|
||||
*
|
||||
* `GET /v1/models` returns the live catalog (LLMs + text-embedding-* models) and
|
||||
* is the source of truth at runtime via NAMED_OPENAI_STYLE_PROVIDERS. The seed
|
||||
* models below cover the most-used LLMs as the offline fallback when the live
|
||||
* fetch fails (network/auth) — same pattern as Together AI / Cerebras / NVIDIA NIM.
|
||||
*
|
||||
* `contextLength` is taken from the upstream `context_length` field per model
|
||||
* (verified live via the OpenVecta /v1/models endpoint, 2026-07-11).
|
||||
*/
|
||||
export const openvectaProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "openvecta",
|
||||
alias: "openvecta",
|
||||
baseUrl: "https://api.openvecta.com/v1/chat/completions",
|
||||
models: [
|
||||
{ id: "glm-4.7-flash", name: "GLM 4.7 Flash", contextLength: 131072 },
|
||||
{ id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6", contextLength: 1000000 },
|
||||
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", contextLength: 131072 },
|
||||
{ id: "gpt-oss-120b", name: "GPT OSS 120B", contextLength: 131072 },
|
||||
{ id: "gemma-4-31b", name: "Gemma 4 31B", contextLength: 262144 },
|
||||
{ id: "kimi-k2.6", name: "Kimi K2.6", contextLength: 200000 },
|
||||
{ id: "llama-3.3-70b-instruct", name: "Llama 3.3 70B Instruct", contextLength: 131072 },
|
||||
{ id: "llama-4-maverick", name: "Llama 4 Maverick", contextLength: 1048576 },
|
||||
{ id: "nemotron-3-super-120b", name: "Nemotron 3 Super 120B", contextLength: 262144 },
|
||||
],
|
||||
});
|
||||
12
public/providers/openvecta.svg
Normal file
12
public/providers/openvecta.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="OpenVecta">
|
||||
<title>OpenVecta</title>
|
||||
<defs>
|
||||
<linearGradient id="ov-grad" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#7C3AED" />
|
||||
<stop offset="100%" stop-color="#A855F7" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="2" y="2" width="60" height="60" rx="14" fill="url(#ov-grad)" />
|
||||
<path d="M14 18 L32 46 L50 18" fill="none" stroke="#FFFFFF" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<circle cx="32" cy="40" r="3.5" fill="#FFFFFF" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 608 B |
@@ -270,6 +270,18 @@ export const PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> =
|
||||
authPrefix: "Bearer ",
|
||||
parseResponse: (data) => data.data || data.models || [],
|
||||
},
|
||||
// OpenVecta (https://openvecta.com/) — OpenAI-compatible `/v1/models` returning
|
||||
// { object: "list", data: [{ id, context_length, owned_by, … }, …] }. Bearer
|
||||
// token with the `ov_sk_…` prefix. Same discovery shape as Together AI /
|
||||
// Cerebras / NVIDIA NIM (live-fetch path; registry seed is the offline fallback).
|
||||
openvecta: {
|
||||
url: "https://api.openvecta.com/v1/models",
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer ",
|
||||
parseResponse: (data) => data.data || data.models || [],
|
||||
},
|
||||
fireworks: {
|
||||
url: "https://api.fireworks.ai/inference/v1/models",
|
||||
method: "GET",
|
||||
|
||||
@@ -64,6 +64,12 @@ export const NAMED_OPENAI_STYLE_PROVIDERS = new Set([
|
||||
// it proxies many upstream models (DeepSeek, Claude, Kimi...) behind one key, so the
|
||||
// full catalog is discovered live from https://api.qnaigc.com/v1/models.
|
||||
"qiniu",
|
||||
// OpenVecta (https://openvecta.com/) is an OpenAI-compatible AI inference gateway
|
||||
// hosting LLMs (GLM, Claude, DeepSeek, GPT OSS, Llama, Kimi, Nemotron…) plus
|
||||
// text-embedding-* models behind a single Bearer key. The full catalog is
|
||||
// discovered live from https://api.openvecta.com/v1/models; the registry seed
|
||||
// (registry/openvecta) covers the most-used LLMs as the offline fallback.
|
||||
"openvecta",
|
||||
]);
|
||||
|
||||
export function isNamedOpenAIStyleProvider(provider: string): boolean {
|
||||
|
||||
@@ -17,6 +17,20 @@ export const APIKEY_PROVIDERS_INFERENCE = {
|
||||
apiKeyUrl: "https://api.together.ai/settings/api-keys",
|
||||
},
|
||||
},
|
||||
// OpenVecta — OpenAI-compatible AI inference gateway (https://openvecta.com/).
|
||||
// Catalog seeded from the live /v1/models list (LLMs + embeddings); free credits
|
||||
// advertised on signup. Bearer-token auth via Authorization: Bearer ov_sk_…
|
||||
openvecta: {
|
||||
id: "openvecta",
|
||||
alias: "openvecta",
|
||||
name: "OpenVecta",
|
||||
icon: "vector_polygon",
|
||||
color: "#7C3AED",
|
||||
textIcon: "OV",
|
||||
website: "https://openvecta.com",
|
||||
hasFree: true,
|
||||
freeNote: "Free credits on signup for OpenAI-compatible inference across LLMs, embeddings, and reasoning models",
|
||||
},
|
||||
fireworks: {
|
||||
id: "fireworks",
|
||||
alias: "fireworks",
|
||||
|
||||
@@ -3245,6 +3245,29 @@
|
||||
"stream": "https://openrouter.ai/api/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"openvecta": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://api.openvecta.com/v1/chat/completions",
|
||||
"stream": "https://api.openvecta.com/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"orcarouter": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
|
||||
119
tests/unit/openvecta-provider-registration.test.ts
Normal file
119
tests/unit/openvecta-provider-registration.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Coverage for the OpenVecta (https://openvecta.com/) provider registration.
|
||||
*
|
||||
* Validates the seven wiring points established in the feat/openvecta-provider
|
||||
* branch:
|
||||
* 1. APIKEY_PROVIDERS.openvecta — catalog entry (id, alias, name, website, hasFree)
|
||||
* 2. providerRegistry.openvecta — format=openai / executor=default / apikey / bearer
|
||||
* 3. PROVIDER_ENDPOINTS — chat completions baseUrl
|
||||
* 4. providerModelsConfig — live /v1/models discovery URL
|
||||
* 5. NAMED_OPENAI_STYLE_PROVIDERS — openvecta classified for live-fetch
|
||||
* 6. Seeded registry catalog — non-empty, unique ids, includes a curated LLM subset
|
||||
* 7. Schema validation — Zod validateProviders(APIKEY_PROVIDERS) passes (the import
|
||||
* would throw at module-load time if any entry were malformed)
|
||||
*
|
||||
* Rule #18 (TDD/VPS gate) — this test exists to keep the seven wiring points
|
||||
* in sync; if any one drifts, the relevant assertion fails immediately.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { APIKEY_PROVIDERS } = await import(
|
||||
"../../src/shared/constants/providers.ts"
|
||||
);
|
||||
const { REGISTRY: providerRegistry } = await import(
|
||||
"../../open-sse/config/providerRegistry.ts"
|
||||
);
|
||||
const { NAMED_OPENAI_STYLE_PROVIDERS, isNamedOpenAIStyleProvider } = await import(
|
||||
"../../src/app/api/providers/[id]/models/discovery/providerSets.ts"
|
||||
);
|
||||
const { PROVIDER_MODELS_CONFIG } = await import(
|
||||
"../../src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts"
|
||||
);
|
||||
|
||||
const SPEC = {
|
||||
id: "openvecta",
|
||||
alias: "openvecta",
|
||||
name: "OpenVecta",
|
||||
website: "https://openvecta.com",
|
||||
chatUrl: "https://api.openvecta.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.openvecta.com/v1/models",
|
||||
expectedSeedIds: [
|
||||
"glm-4.7-flash",
|
||||
"claude-sonnet-4.6",
|
||||
"deepseek-v4-flash",
|
||||
"gpt-oss-120b",
|
||||
"gemma-4-31b",
|
||||
"kimi-k2.6",
|
||||
"llama-3.3-70b-instruct",
|
||||
"llama-4-maverick",
|
||||
"nemotron-3-super-120b",
|
||||
],
|
||||
};
|
||||
|
||||
test("APIKEY_PROVIDERS.openvecta is registered with the canonical identity", () => {
|
||||
const entry = APIKEY_PROVIDERS[SPEC.id];
|
||||
assert.ok(entry, `APIKEY_PROVIDERS.${SPEC.id} must be defined`);
|
||||
assert.equal(entry.id, SPEC.id);
|
||||
assert.equal(entry.alias, SPEC.alias);
|
||||
assert.equal(entry.name, SPEC.name);
|
||||
assert.equal(entry.website, SPEC.website);
|
||||
assert.equal(typeof entry.textIcon, "string");
|
||||
assert.equal(entry.hasFree, true, "OpenVecta advertises free signup credits");
|
||||
assert.equal(typeof entry.freeNote, "string");
|
||||
assert.match(entry.color, /^#[0-9A-Fa-f]{6}$/);
|
||||
});
|
||||
|
||||
test("providerRegistry exposes the OpenAI-compatible chat completions URL", () => {
|
||||
// Note: `PROVIDER_ENDPOINTS` in src/shared/constants/config.ts is a hand-curated
|
||||
// *display-only* map (a subset of providers shown in the UI), so we don't
|
||||
// require openvecta to be listed there — the source-of-truth chat URL lives
|
||||
// on the registry entry, generated by `generateLegacyProviders()`.
|
||||
assert.equal(providerRegistry[SPEC.id].baseUrl, SPEC.chatUrl);
|
||||
});
|
||||
|
||||
test("PROVIDER_MODELS_CONFIG exposes the live /v1/models discovery URL", () => {
|
||||
const cfg = PROVIDER_MODELS_CONFIG[SPEC.id];
|
||||
assert.ok(cfg, `PROVIDER_MODELS_CONFIG.${SPEC.id} must be defined`);
|
||||
assert.equal(cfg.url, SPEC.modelsUrl);
|
||||
assert.equal(cfg.method, "GET");
|
||||
assert.equal(cfg.authHeader, "Authorization");
|
||||
assert.equal(cfg.authPrefix, "Bearer ");
|
||||
assert.equal(typeof cfg.parseResponse, "function");
|
||||
});
|
||||
|
||||
test("providerRegistry.openvecta uses OpenAI format with bearer apikey auth", () => {
|
||||
const entry = providerRegistry[SPEC.id];
|
||||
assert.ok(entry, `providerRegistry.${SPEC.id} must be defined`);
|
||||
assert.equal(entry.id, SPEC.id);
|
||||
assert.equal(entry.alias, SPEC.alias);
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, SPEC.chatUrl);
|
||||
});
|
||||
|
||||
test("openvecta is classified as a named OpenAI-style provider (live-fetch path)", () => {
|
||||
assert.ok(
|
||||
NAMED_OPENAI_STYLE_PROVIDERS.has(SPEC.id),
|
||||
"openvecta must be in NAMED_OPENAI_STYLE_PROVIDERS for live /v1/models fetch",
|
||||
);
|
||||
assert.equal(isNamedOpenAIStyleProvider(SPEC.id), true);
|
||||
});
|
||||
|
||||
test("openvecta ships a non-empty unique seed catalog covering curated LLMs", () => {
|
||||
const models = providerRegistry[SPEC.id].models;
|
||||
assert.ok(Array.isArray(models), "registry models must be an array");
|
||||
assert.ok(models.length >= 5, "seed list must be non-empty for the offline fallback");
|
||||
const ids = models.map((m: { id: string }) => m.id);
|
||||
assert.equal(new Set(ids).size, ids.length, "seed model ids must be unique");
|
||||
for (const expected of SPEC.expectedSeedIds) {
|
||||
assert.ok(ids.includes(expected), `seed list must include ${expected}`);
|
||||
}
|
||||
for (const m of models) {
|
||||
assert.equal(typeof m.id, "string");
|
||||
assert.ok(m.id.length > 0);
|
||||
assert.equal(typeof m.name, "string");
|
||||
}
|
||||
});
|
||||
@@ -1,12 +1,13 @@
|
||||
// Characterization of the providers.ts catalog split (god-file decomposition): the host became a
|
||||
// barrel that re-exports 10 data catalogs now living under constants/providers/*, and APIKEY is
|
||||
// merged from 6 semantic family files (apikey/<family>.ts). Locks: the public surface (every catalog
|
||||
// + helpers still exported), the spread-merge integrity (166 APIKEY entries, no loss/dup), and that
|
||||
// + helpers still exported), the spread-merge integrity (167 APIKEY entries, no loss/dup), and that
|
||||
// load-time Zod validation still runs. Pure-data move → behavior must be identical.
|
||||
// Count was 171 before obsolete provider removals (PR #6675: glhf/kluster/cablyai/inclusionai etc.,
|
||||
// 171->167) plus #6126 (ClinePass dual-auth): the API-key-only APIKEY_PROVIDERS_GATEWAYS entry was
|
||||
// removed as a duplicate now that clinepass is OAuth-primary (OAUTH_PROVIDERS.clinepass) with its
|
||||
// BYOK path admitted through the DUAL_AUTH_APIKEY_PROVIDER_IDS gate instead (167->166).
|
||||
// BYOK path admitted through the DUAL_AUTH_APIKEY_PROVIDER_IDS gate instead (167->166), then the
|
||||
// OpenVecta inference-gateway addition brought it back to 167.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
@@ -35,12 +36,12 @@ test("barrel still exports every catalog + key helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("APIKEY_PROVIDERS merges the 6 family files into 166 entries (no loss / no dup)", async () => {
|
||||
test("APIKEY_PROVIDERS merges the 6 family files into 167 entries (no loss / no dup)", async () => {
|
||||
const keys = Object.keys((P as Record<string, object>).APIKEY_PROVIDERS);
|
||||
assert.equal(keys.length, 166);
|
||||
assert.equal(new Set(keys).size, 166, "duplicate keys after spread-merge");
|
||||
assert.equal(keys.length, 167);
|
||||
assert.equal(new Set(keys).size, 167, "duplicate keys after spread-merge");
|
||||
// the merged object's entry-count equals the sum of the 6 semantic family files; families are a
|
||||
// strict partition (every provider in exactly one), so the sum must be exactly 166.
|
||||
// strict partition (every provider in exactly one), so the sum must be exactly 167.
|
||||
const families: [string, string][] = [
|
||||
["gateways", "APIKEY_PROVIDERS_GATEWAYS"],
|
||||
["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"],
|
||||
@@ -60,7 +61,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 166 entries (no loss / no
|
||||
seen.add(k);
|
||||
}
|
||||
}
|
||||
assert.equal(famTotal, 166, "families must partition all 166 providers");
|
||||
assert.equal(famTotal, 167, "families must partition all 167 providers");
|
||||
});
|
||||
|
||||
test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {
|
||||
|
||||
Reference in New Issue
Block a user