diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index ab5b6931db..ae083ecf34 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -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 = { nscale: nscaleProvider, "chatgpt-web": chatgpt_webProvider, openrouter: openrouterProvider, + openvecta: openvectaProvider, orcarouter: orcarouterProvider, "copilot-web": copilot_webProvider, "copilot-m365-web": copilot_m365_webProvider, diff --git a/open-sse/config/providers/registry/openvecta/index.ts b/open-sse/config/providers/registry/openvecta/index.ts new file mode 100644 index 0000000000..25753438b5 --- /dev/null +++ b/open-sse/config/providers/registry/openvecta/index.ts @@ -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 }, + ], +}); \ No newline at end of file diff --git a/public/providers/openvecta.svg b/public/providers/openvecta.svg new file mode 100644 index 0000000000..8c843c9f79 --- /dev/null +++ b/public/providers/openvecta.svg @@ -0,0 +1,12 @@ + + OpenVecta + + + + + + + + + + \ No newline at end of file diff --git a/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts b/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts index 7f7a64e595..ab0b1de1cb 100644 --- a/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts +++ b/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts @@ -270,6 +270,18 @@ export const PROVIDER_MODELS_CONFIG: Record = 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", diff --git a/src/app/api/providers/[id]/models/discovery/providerSets.ts b/src/app/api/providers/[id]/models/discovery/providerSets.ts index 4bf76ecb19..1cc2049a22 100644 --- a/src/app/api/providers/[id]/models/discovery/providerSets.ts +++ b/src/app/api/providers/[id]/models/discovery/providerSets.ts @@ -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 { diff --git a/src/shared/constants/providers/apikey/inference-hosts.ts b/src/shared/constants/providers/apikey/inference-hosts.ts index 9f0069872b..c51a3285a6 100644 --- a/src/shared/constants/providers/apikey/inference-hosts.ts +++ b/src/shared/constants/providers/apikey/inference-hosts.ts @@ -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", diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index f92c7c0e2c..65269d491f 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -3245,6 +3245,29 @@ "stream": "https://openrouter.ai/api/v1/chat/completions" } }, + "openvecta": { + "format": "openai", + "headers": { + "apiKey": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "nonStream": { + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "oauth": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "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": { diff --git a/tests/unit/openvecta-provider-registration.test.ts b/tests/unit/openvecta-provider-registration.test.ts new file mode 100644 index 0000000000..58734a3c9b --- /dev/null +++ b/tests/unit/openvecta-provider-registration.test.ts @@ -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"); + } +}); \ No newline at end of file diff --git a/tests/unit/providers-constants-split.test.ts b/tests/unit/providers-constants-split.test.ts index 51cfd53754..dd7143b53d 100644 --- a/tests/unit/providers-constants-split.test.ts +++ b/tests/unit/providers-constants-split.test.ts @@ -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/.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).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", () => {