feat(oauth): add Openference OAuth and API key provider integration

Wire Openference as a first-party OAuth gateway (PKCE, rotating refresh)
and an API-key catalog entry on api.openference.com, with live model
discovery, connection testing, free-tier badges, and regression tests.
This commit is contained in:
Anh Tran
2026-08-07 18:32:46 +01:00
committed by diegosouzapw
parent aae408f585
commit 0e6e37f4f3
21 changed files with 664 additions and 11 deletions

View File

@@ -65,27 +65,27 @@ export const PROVIDERS: Record<string, LegacyProvider> = new Proxy(
{} as Record<string, LegacyProvider>,
{
get(_, prop) {
if (typeof prop === 'symbol') return undefined;
if (typeof prop === "symbol") return undefined;
return Reflect.get(initProviders(), prop, _providers);
},
has(_, prop) {
if (typeof prop === 'symbol') return false;
if (typeof prop === "symbol") return false;
return Reflect.has(initProviders(), prop);
},
ownKeys() {
return Reflect.ownKeys(initProviders());
},
getOwnPropertyDescriptor(_, prop) {
if (typeof prop === 'symbol') return undefined;
if (typeof prop === "symbol") return undefined;
return Object.getOwnPropertyDescriptor(initProviders(), prop);
},
set(_, prop, value) {
if (typeof prop === 'symbol') return false;
if (typeof prop === "symbol") return false;
(initProviders() as Record<string, LegacyProvider>)[prop] = value;
return true;
},
deleteProperty(_, prop) {
if (typeof prop === 'symbol') return false;
if (typeof prop === "symbol") return false;
return Reflect.deleteProperty(initProviders(), prop);
},
}
@@ -124,6 +124,11 @@ export const OAUTH_ENDPOINTS = {
auth: "https://github.com/login/oauth/authorize",
deviceCode: "https://github.com/login/device/code",
},
openference: {
token: "https://openference.com/oauth/token",
auth: "https://openference.com/app/oauth/authorize",
clientId: "omniroute",
},
};
// Cache TTLs (seconds)

View File

@@ -121,6 +121,8 @@ import { chatgpt_webProvider } from "./registry/chatgpt-web/index.ts";
import { openrouterProvider } from "./registry/openrouter/index.ts";
import { cheaperinferenceProvider } from "./registry/cheaperinference/index.ts";
import { openvectaProvider } from "./registry/openvecta/index.ts";
import { openferenceProvider } from "./registry/openference/index.ts";
import { openference_apiProvider } from "./registry/openference-api/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";
@@ -345,6 +347,8 @@ export const REGISTRY: Record<string, RegistryEntry> = {
openrouter: openrouterProvider,
cheaperinference: cheaperinferenceProvider,
openvecta: openvectaProvider,
openference: openferenceProvider,
"openference-api": openference_apiProvider,
orcarouter: orcarouterProvider,
"copilot-web": copilot_webProvider,
"copilot-m365-web": copilot_m365_webProvider,

View File

@@ -0,0 +1,18 @@
import type { RegistryEntry } from "../../shared.ts";
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
/**
* Openference API key — OpenAI-compatible gateway (https://openference.com/).
*
* Bearer API keys (`sk-…`) hit the same api.openference.com/v1/* surface as OAuth
* JWTs. Live model discovery uses NAMED_OPENAI_STYLE_PROVIDERS; the seed below is
* the offline fallback when the live fetch fails.
*/
export const openference_apiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
id: "openference-api",
alias: "ofa",
baseUrl: "https://api.openference.com/v1/chat/completions",
responsesBaseUrl: "https://api.openference.com/v1/responses",
passthroughModels: true,
models: [{ id: "GLM-5.2", name: "GLM 5.2", contextLength: 850000 }],
});

View File

@@ -0,0 +1,25 @@
import type { RegistryEntry } from "../../shared.ts";
/**
* Openference — OpenAI-compatible AI inference gateway (https://openference.com/).
*
* OAuth access tokens are ES256 JWTs accepted as Bearer credentials on
* api.openference.com/v1/*. Live model discovery uses NAMED_OPENAI_STYLE_PROVIDERS;
* seed models below are the offline fallback when the live fetch fails.
*/
export const openferenceProvider: RegistryEntry = {
id: "openference",
alias: "of",
format: "openai",
executor: "default",
baseUrl: "https://api.openference.com/v1/chat/completions",
responsesBaseUrl: "https://api.openference.com/v1/responses",
authType: "oauth",
authHeader: "bearer",
passthroughModels: true,
oauth: {
clientIdDefault: "omniroute",
tokenUrl: "https://openference.com/oauth/token",
},
models: [{ id: "GLM-5.2", name: "GLM 5.2", contextLength: 850000 }],
};

View File

@@ -48,6 +48,7 @@ import { refreshGoogleToken } from "./tokenRefresh/providers/google.ts";
import { ensureAntigravityProjectAssigned } from "./antigravityProjectBootstrap.ts";
import { persistDiscoveredAntigravityProjectId } from "./antigravityProjectPersist.ts";
import { refreshCodexToken } from "./tokenRefresh/providers/codex.ts";
import { refreshOpenferenceToken } from "./tokenRefresh/providers/openference.ts";
import { refreshKiroToken } from "./tokenRefresh/providers/kiro.ts";
import { refreshQoderToken } from "./tokenRefresh/providers/qoder.ts";
import { refreshGitHubToken } from "./tokenRefresh/providers/github.ts";
@@ -62,6 +63,7 @@ export {
refreshClaudeOAuthToken,
refreshGoogleToken,
refreshCodexToken,
refreshOpenferenceToken,
refreshKiroToken,
refreshQoderToken,
refreshGitHubToken,
@@ -339,10 +341,7 @@ async function _getAccessTokenInternal(provider, credentials, log, proxyConfig:
!(credentials.projectId || credentials.providerSpecificData?.projectId)
) {
try {
const discovered = await ensureAntigravityProjectAssigned(
result.accessToken,
fetch
);
const discovered = await ensureAntigravityProjectAssigned(result.accessToken, fetch);
if (discovered) {
result.projectId = discovered;
result.providerSpecificData = {
@@ -362,7 +361,8 @@ async function _getAccessTokenInternal(provider, credentials, log, proxyConfig:
});
}
} catch (discoveryError) {
const msg = discoveryError instanceof Error ? discoveryError.message : String(discoveryError);
const msg =
discoveryError instanceof Error ? discoveryError.message : String(discoveryError);
log?.warn?.("TOKEN", `Antigravity projectId discovery failed: ${msg}`);
}
}
@@ -376,6 +376,9 @@ async function _getAccessTokenInternal(provider, credentials, log, proxyConfig:
case "codex":
return await refreshCodexToken(credentials.refreshToken, log, proxyConfig);
case "openference":
return await refreshOpenferenceToken(credentials.refreshToken, log, proxyConfig);
case "qoder":
return await refreshQoderToken(credentials.refreshToken, log, proxyConfig);
@@ -439,6 +442,7 @@ export function supportsTokenRefresh(provider) {
"agy",
"claude",
"codex",
"openference",
"qoder",
"github",
"kiro",

View File

@@ -0,0 +1,92 @@
// @ts-nocheck
import { OAUTH_ENDPOINTS } from "../../../config/constants.ts";
import { runWithProxyContext } from "../../../utils/proxyFetch.ts";
import { buildFormParams } from "../shared.ts";
/**
* Specialized refresh for Openference OAuth tokens.
* Openference uses rotating (one-time-use) oar_* refresh tokens.
*/
export async function refreshOpenferenceToken(refreshToken, log, proxyConfig: unknown = null) {
try {
const response = await runWithProxyContext(proxyConfig, () =>
fetch(OAUTH_ENDPOINTS.openference.token, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: buildFormParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: OAUTH_ENDPOINTS.openference.clientId,
}),
})
);
if (!response.ok) {
const errorText = await response.text();
let errorCode = null;
try {
const parsed = JSON.parse(errorText);
errorCode =
parsed?.error?.code || (typeof parsed?.error === "string" ? parsed.error : null);
} catch {
// not JSON, ignore
}
if (
errorCode === "invalid_grant" ||
errorCode === "token_expired" ||
errorCode === "invalid_token"
) {
log?.error?.(
"TOKEN_REFRESH",
"Openference refresh token already used or invalid. Re-authentication required.",
{
status: response.status,
errorCode,
}
);
return { error: "unrecoverable_refresh_error", code: errorCode };
}
if (response.status === 401) {
const code = errorCode || "unauthorized";
log?.error?.(
"TOKEN_REFRESH",
"Openference OAuth token endpoint returned 401. Re-authentication required.",
{
status: response.status,
errorCode: code,
}
);
return { error: "unrecoverable_refresh_error", code };
}
log?.error?.("TOKEN_REFRESH", "Failed to refresh Openference token", {
status: response.status,
error: errorText,
});
return null;
}
const tokens = await response.json();
log?.info?.("TOKEN_REFRESH", "Successfully refreshed Openference token", {
hasNewAccessToken: !!tokens.access_token,
hasNewRefreshToken: !!tokens.refresh_token,
expiresIn: tokens.expires_in,
});
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || refreshToken,
expiresIn: tokens.expires_in,
};
} catch (error) {
log?.error?.("TOKEN_REFRESH", `Network error refreshing Openference token: ${error.message}`);
return null;
}
}

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="128" height="128">
<title>Openference</title>
<path fill="#6366f1" fill-rule="evenodd" clip-rule="evenodd" d="M12 5C15 5 18 7 20 13C18 19 15 21 12 21C9 21 6 19 4 13C6 7 9 5 12 5ZM8.4 11.6A1 1 0 0 1 10.4 11.6L10.4 14.4A1 1 0 0 1 8.4 14.4ZM13.6 11.6A1 1 0 0 1 15.6 11.6L15.6 14.4A1 1 0 0 1 13.6 14.4Z"/>
<circle cx="12" cy="4.5" r="1.3" fill="#6366f1"/>
</svg>

After

Width:  |  Height:  |  Size: 432 B

View File

@@ -47,7 +47,7 @@ if (!globalThis.__pkceCallbackStates) {
}
/** Providers that use the PKCE browser callback flow (like Codex). */
const PKCE_CALLBACK_PROVIDERS = new Set(["codex", "xai-oauth", "grok-cli"]);
const PKCE_CALLBACK_PROVIDERS = new Set(["codex", "xai-oauth", "grok-cli", "openference"]);
/**
* Providers whose device flow runs in the user's browser (auth.openai.com blocks

View File

@@ -606,6 +606,22 @@ export const PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> =
authPrefix: "Bearer ",
parseResponse: (data) => data.data || data.models || [],
},
openference: {
url: "https://api.openference.com/v1/models",
method: "GET",
headers: { "Content-Type": "application/json" },
authHeader: "Authorization",
authPrefix: "Bearer ",
parseResponse: (data) => data.data || data.models || [],
},
"openference-api": {
url: "https://api.openference.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",

View File

@@ -71,6 +71,10 @@ export const NAMED_OPENAI_STYLE_PROVIDERS = new Set([
// discovered live from https://api.openvecta.com/v1/models; the registry seed
// (registry/openvecta) covers the most-used LLMs as the offline fallback.
"openvecta",
// Openference (https://openference.com/) — OAuth JWT or API key on the same
// OpenAI-compatible gateway. Live catalog from api.openference.com/v1/models.
"openference",
"openference-api",
// Typhoon (SCB 10X, Thailand) and Inception Labs (Mercury diffusion models) are
// OpenAI-compatible providers whose /v1/models endpoint exists and is used for
// catalog discovery/key validation (verified 2026-07-22).

View File

@@ -172,4 +172,22 @@ export const OAUTH_TEST_CONFIG = {
extraHeaders: { "User-Agent": "OmniRoute", Accept: "application/vnd.github+json" },
refreshable: true,
},
// Openference: first-party OAuth gateway — list models to verify the JWT without
// consuming inference quota. 402 (no active plan) still means auth succeeded.
openference: {
url: "https://api.openference.com/v1/models",
method: "GET",
authHeader: "Authorization",
authPrefix: "Bearer ",
refreshable: true,
acceptStatuses: [402],
},
of: {
url: "https://api.openference.com/v1/models",
method: "GET",
authHeader: "Authorization",
authPrefix: "Bearer ",
refreshable: true,
acceptStatuses: [402],
},
};

View File

@@ -152,6 +152,19 @@ export const XAI_OAUTH_CONFIG = {
callbackHost: "127.0.0.1",
};
// Openference OAuth Configuration (Authorization Code Flow with PKCE)
export const OPENFERENCE_CONFIG = {
clientId: "omniroute",
authorizeUrl: "https://openference.com/app/oauth/authorize",
tokenUrl: "https://openference.com/oauth/token",
userinfoUrl: "https://openference.com/oauth/userinfo",
scope: "openid profile email model:invoke offline_access",
codeChallengeMethod: "S256",
loopbackPort: 56123,
callbackPath: "/callback",
callbackHost: "127.0.0.1",
};
// Kimi Coding OAuth Configuration (Device Code Flow)
export const KIMI_CODING_CONFIG = {
clientId: resolvePublicCred("kimi_id", "KIMI_CODING_OAUTH_CLIENT_ID"),
@@ -544,6 +557,7 @@ export const PROVIDERS = {
CODEBUDDY_CN: "codebuddy-cn",
GROK_CLI: "grok-cli",
XAI_OAUTH: "xai-oauth",
OPENFERENCE: "openference",
ZED: "zed",
ZED_HOSTED: "zed-hosted",
};

View File

@@ -28,6 +28,7 @@ import { cline } from "./cline";
import { windsurf } from "./windsurf";
import { grokCli } from "./grok-cli";
import { xaiOauth } from "./xai-oauth";
import { openference } from "./openference";
import { codebuddyCn } from "./codebuddy-cn";
import { zed } from "./zed";
import { zedHosted } from "./zed-hosted";
@@ -60,6 +61,7 @@ export const PROVIDERS = {
// under this one entry (#7013) — see grok-cli.ts's mapTokens for the dispatch.
"grok-cli": grokCli,
"xai-oauth": xaiOauth,
openference,
"codebuddy-cn": codebuddyCn,
// Zed IDE credential bridge — uses keychain import, not standard OAuth
zed,

View File

@@ -0,0 +1,125 @@
import { OPENFERENCE_CONFIG } from "../constants/oauth";
const BASE64_BLOCK_SIZE = 4;
/** Extract display metadata from an Openference id_token (OIDC). */
export function decodeOpenferenceIdTokenIdentity(idToken: unknown): {
email: string | null;
name: string | null;
} {
if (typeof idToken !== "string") return { email: null, name: null };
const parts = idToken.split(".");
if (parts.length !== 3) return { email: null, name: null };
try {
const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
const padding = (BASE64_BLOCK_SIZE - (base64.length % BASE64_BLOCK_SIZE)) % BASE64_BLOCK_SIZE;
const payload = JSON.parse(
Buffer.from(base64 + "=".repeat(padding), "base64").toString("utf8")
);
return {
email: payload.email || payload.preferred_username || null,
name: payload.name || null,
};
} catch {
return { email: null, name: null };
}
}
function getOpenferenceUserEmail(userInfo: Record<string, unknown>): string | null {
const candidates = [userInfo.email, userInfo.preferred_username];
for (const candidate of candidates) {
if (typeof candidate === "string" && candidate.trim().length > 0) {
return candidate.trim();
}
}
return null;
}
function getOpenferenceUserName(userInfo: Record<string, unknown>): string | null {
const candidates = [userInfo.name, userInfo.email, userInfo.preferred_username];
for (const candidate of candidates) {
if (typeof candidate === "string" && candidate.trim().length > 0) {
return candidate.trim();
}
}
return null;
}
export const openference = {
config: OPENFERENCE_CONFIG,
flowType: "authorization_code_pkce" as const,
fixedPort: OPENFERENCE_CONFIG.loopbackPort,
callbackPath: OPENFERENCE_CONFIG.callbackPath,
callbackHost: OPENFERENCE_CONFIG.callbackHost,
buildAuthUrl: (config, redirectUri, state, codeChallenge) => {
const params = new URLSearchParams({
response_type: "code",
client_id: config.clientId,
redirect_uri: redirectUri,
scope: config.scope,
code_challenge: codeChallenge,
code_challenge_method: config.codeChallengeMethod,
state,
});
return `${config.authorizeUrl}?${params.toString()}`;
},
exchangeToken: async (config, code, redirectUri, codeVerifier) => {
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: config.clientId,
code,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Openference token exchange failed: ${error}`);
}
return response.json();
},
postExchange: async (tokens) => {
const userinfoUrl = OPENFERENCE_CONFIG.userinfoUrl;
const headers = {
Authorization: `Bearer ${tokens.access_token}`,
Accept: "application/json",
};
const userRes = await fetch(userinfoUrl, { headers });
const userInfo = userRes.ok ? ((await userRes.json()) as Record<string, unknown>) : {};
return { userInfo };
},
mapTokens: (tokens, extra) => {
const identity = decodeOpenferenceIdTokenIdentity(tokens.id_token);
const userInfo = (extra?.userInfo ?? {}) as Record<string, unknown>;
const email = identity.email || getOpenferenceUserEmail(userInfo);
const name = identity.name || getOpenferenceUserName(userInfo) || email;
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
idToken: tokens.id_token,
expiresIn: tokens.expires_in,
email,
name,
providerSpecificData: {
scope: tokens.scope || OPENFERENCE_CONFIG.scope,
tokenType: tokens.token_type || "Bearer",
},
};
},
};

View File

@@ -723,6 +723,7 @@ export async function checkConnection(conn) {
"amazon-q",
"gitlab-duo",
"claude",
"openference",
]);
const isRotatingProvider = ROTATING_REFRESH_PROVIDERS.has(
String(conn.provider || "").toLowerCase()

View File

@@ -171,6 +171,7 @@ const KNOWN_SVGS = new Set([
"openadapter",
"openai",
"openclaw",
"openference",
"opencode",
"openrouter",
"orcarouter",

View File

@@ -32,6 +32,19 @@ export const APIKEY_PROVIDERS_INFERENCE = {
freeNote:
"Free credits on signup for OpenAI-compatible inference across LLMs, embeddings, and reasoning models",
},
// Openference — OpenAI-compatible AI inference gateway (https://openference.com/).
// API-key auth via Authorization: Bearer sk-… on the same gateway as OAuth JWTs.
"openference-api": {
id: "openference-api",
alias: "ofa",
name: "Openference API",
icon: "openference",
color: "#6366F1",
textIcon: "OF",
website: "https://openference.com",
hasFree: true,
freeNote: "Free plan: 3-day trial with open-source models — no credit card required",
},
fireworks: {
id: "fireworks",
alias: "fireworks",

View File

@@ -29,6 +29,19 @@ export const OAUTH_PROVIDERS = {
authHint:
"Sign in with xAI to use api.x.ai models such as Grok 4.5. This is separate from Grok Build JWT sessions, which use cli-chat-proxy.grok.com and grok-build model aliases.",
},
openference: {
id: "openference",
alias: "of",
name: "Openference",
icon: "openference",
color: "#6366F1",
textIcon: "OF",
website: "https://openference.com",
hasFree: true,
freeNote: "Free plan: 3-day trial with open-source models — no credit card required",
authHint:
"Sign in with your Openference account to route requests through api.openference.com. An active plan is required for inference — OAuth may authenticate but return 402 without one.",
},
"grok-cli": {
id: "grok-cli",
alias: "gc",

View File

@@ -44,6 +44,7 @@ const {
TRAE_CONFIG,
WINDSURF_CONFIG,
XAI_OAUTH_CONFIG,
OPENFERENCE_CONFIG,
ZED_HOSTED_CONFIG,
} = oauthModule;
const { getAntigravityLoadCodeAssistMetadata } = antigravityHeadersModule;
@@ -72,6 +73,7 @@ const EXPECTED_PROVIDER_KEYS = [
"devin-cli",
"grok-cli",
"xai-oauth",
"openference",
"codebuddy-cn",
"zed",
"zed-hosted",
@@ -106,6 +108,7 @@ const EXPECTED_CONFIG_BY_PROVIDER = {
trae: TRAE_CONFIG,
"grok-cli": GROK_BUILD_OAUTH_CONFIG,
"xai-oauth": XAI_OAUTH_CONFIG,
openference: OPENFERENCE_CONFIG,
"codebuddy-cn": CODEBUDDY_CN_CONFIG,
zed: ZED_CONFIG,
"zed-hosted": ZED_HOSTED_CONFIG,
@@ -154,6 +157,8 @@ const REQUIRED_FIELDS_BY_PROVIDER = {
// prettier-ignore
"xai-oauth": ["authorizeUrl", "tokenUrl", "scope", "codeChallengeMethod", "clientId", "loopbackPort", "callbackPath", "callbackHost"],
// prettier-ignore
openference: ["authorizeUrl", "tokenUrl", "userinfoUrl", "scope", "codeChallengeMethod", "clientId", "loopbackPort", "callbackPath", "callbackHost"],
// prettier-ignore
"grok-cli": ["authorizeUrl", "tokenUrl", "scope", "codeChallengeMethod", "clientId", "loopbackPort", "callbackPath", "callbackHost"],
// prettier-ignore
"zed-hosted": ["webBaseUrl", "cloudBaseUrl", "llmBaseUrl", "userInfoUrl", "llmTokenUrl", "modelsUrl"],

View File

@@ -0,0 +1,89 @@
/**
* Coverage for the Openference API key provider (https://openference.com/).
*
* Validates wiring alongside the OAuth `openference` entry:
* 1. APIKEY_PROVIDERS["openference-api"] — catalog entry (id, alias, name, website, hasFree)
* 2. providerRegistry["openference-api"] — format=openai / executor=default / apikey / bearer
* 3. PROVIDER_MODELS_CONFIG — live /v1/models discovery URL
* 4. NAMED_OPENAI_STYLE_PROVIDERS — classified for live-fetch
* 5. Seeded registry catalog — non-empty, unique ids
*/
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: "openference-api",
alias: "ofa",
name: "Openference API",
website: "https://openference.com",
chatUrl: "https://api.openference.com/v1/chat/completions",
modelsUrl: "https://api.openference.com/v1/models",
expectedSeedIds: ["GLM-5.2"],
};
test("APIKEY_PROVIDERS.openference-api 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(entry.icon, "openference");
assert.equal(typeof entry.textIcon, "string");
assert.equal(entry.hasFree, true);
assert.equal(typeof entry.freeNote, "string");
assert.match(entry.color, /^#[0-9A-Fa-f]{6}$/);
});
test("providerRegistry exposes the OpenAI-compatible chat completions URL", () => {
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.openference-api 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);
assert.equal(entry.passthroughModels, true);
});
test("openference-api is classified as a named OpenAI-style provider (live-fetch path)", () => {
assert.ok(
NAMED_OPENAI_STYLE_PROVIDERS.has(SPEC.id),
"openference-api must be in NAMED_OPENAI_STYLE_PROVIDERS for live /v1/models fetch"
);
assert.equal(isNamedOpenAIStyleProvider(SPEC.id), true);
});
test("openference-api ships a non-empty unique seed catalog", () => {
const models = providerRegistry[SPEC.id].models;
assert.ok(Array.isArray(models), "registry models must be an array");
assert.ok(models.length >= 1, "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}`);
}
});

View File

@@ -0,0 +1,199 @@
import test from "node:test";
import assert from "node:assert/strict";
import { generateAuthData } from "../../src/lib/oauth/providers.ts";
import {
openference,
decodeOpenferenceIdTokenIdentity,
} from "../../src/lib/oauth/providers/openference.ts";
import { OPENFERENCE_CONFIG } from "../../src/lib/oauth/constants/oauth.ts";
import { getExecutor, hasSpecializedExecutor } from "../../open-sse/executors/index.ts";
import { openferenceProvider } from "../../open-sse/config/providers/registry/openference/index.ts";
import { refreshOpenferenceToken } from "../../open-sse/services/tokenRefresh/providers/openference.ts";
import { OAUTH_TEST_CONFIG } from "../../src/app/api/providers/[id]/test/oauthTestConfig.ts";
import { testOAuthConnection } from "../../src/app/api/providers/[id]/test/route.ts";
import { supportsTokenRefresh } from "../../open-sse/services/tokenRefresh.ts";
import { NAMED_OPENAI_STYLE_PROVIDERS } from "../../src/app/api/providers/[id]/models/discovery/providerSets.ts";
import { OAUTH_PROVIDERS } from "../../src/shared/constants/providers/oauth.ts";
import PROVIDERS from "../../src/lib/oauth/providers/index.ts";
const originalFetch = globalThis.fetch;
function createJwt(payload: Record<string, unknown>) {
const encode = (value: Record<string, unknown>) =>
Buffer.from(JSON.stringify(value)).toString("base64url");
return `${encode({ alg: "none" })}.${encode(payload)}.signature`;
}
test.afterEach(() => {
globalThis.fetch = originalFetch;
});
test("Openference OAuth builds the PKCE authorization request", () => {
const authData = generateAuthData("openference", "http://127.0.0.1:56123/callback");
const url = new URL(authData.authUrl);
assert.equal(url.origin, "https://openference.com");
assert.equal(url.pathname, "/app/oauth/authorize");
assert.equal(url.searchParams.get("client_id"), OPENFERENCE_CONFIG.clientId);
assert.equal(url.searchParams.get("scope"), OPENFERENCE_CONFIG.scope);
assert.equal(url.searchParams.get("code_challenge_method"), "S256");
assert.ok(url.searchParams.get("code_challenge"));
assert.equal(authData.fixedPort, 56123);
assert.equal(authData.callbackPath, "/callback");
assert.equal(authData.callbackHost, "127.0.0.1");
});
test("Openference OAuth exchanges a code with form-urlencoded PKCE fields", async () => {
globalThis.fetch = async (input, init) => {
assert.equal(String(input), OPENFERENCE_CONFIG.tokenUrl);
assert.equal(init?.method, "POST");
assert.equal(init?.headers?.["Content-Type"], "application/x-www-form-urlencoded");
const body = init?.body as URLSearchParams;
assert.equal(body.get("grant_type"), "authorization_code");
assert.equal(body.get("client_id"), OPENFERENCE_CONFIG.clientId);
assert.equal(body.get("code"), "auth-code");
assert.equal(body.get("redirect_uri"), "http://127.0.0.1:56123/callback");
assert.equal(body.get("code_verifier"), "verifier");
return Response.json({
access_token: "access",
refresh_token: "oar_refresh",
expires_in: 3600,
id_token: createJwt({ email: "user@openference.com", name: "Openference User" }),
});
};
const tokens = await openference.exchangeToken(
OPENFERENCE_CONFIG,
"auth-code",
"http://127.0.0.1:56123/callback",
"verifier"
);
assert.equal(tokens.access_token, "access");
});
test("Openference OAuth maps refreshable tokens and id_token display metadata", () => {
const idToken = createJwt({ email: "user@openference.com", name: "Openference User" });
assert.deepEqual(decodeOpenferenceIdTokenIdentity(idToken), {
email: "user@openference.com",
name: "Openference User",
});
const mapped = openference.mapTokens({
access_token: "access",
refresh_token: "oar_refresh",
id_token: idToken,
expires_in: 3600,
scope: OPENFERENCE_CONFIG.scope,
});
assert.equal(mapped.accessToken, "access");
assert.equal(mapped.refreshToken, "oar_refresh");
assert.equal(mapped.email, "user@openference.com");
assert.equal(mapped.name, "Openference User");
});
test("Openference OAuth postExchange fetches userinfo when id_token lacks email", async () => {
globalThis.fetch = async (input) => {
assert.equal(String(input), OPENFERENCE_CONFIG.userinfoUrl);
return Response.json({ email: "from-userinfo@openference.com", name: "Userinfo Name" });
};
const extra = await openference.postExchange({ access_token: "access" });
const mapped = openference.mapTokens(
{ access_token: "access", refresh_token: "oar_refresh", expires_in: 3600 },
extra
);
assert.equal(mapped.email, "from-userinfo@openference.com");
assert.equal(mapped.name, "Userinfo Name");
});
test("Openference is registered as an OAuth gateway with default executor", () => {
assert.ok(OAUTH_PROVIDERS.openference);
assert.equal(OAUTH_PROVIDERS.openference.alias, "of");
assert.equal(OAUTH_PROVIDERS.openference.color, "#6366F1");
assert.equal(OAUTH_PROVIDERS.openference.hasFree, true);
assert.equal(typeof OAUTH_PROVIDERS.openference.freeNote, "string");
assert.ok(PROVIDERS.openference);
assert.equal(openferenceProvider.authType, "oauth");
assert.equal(openferenceProvider.executor, "default");
assert.equal(openferenceProvider.baseUrl, "https://api.openference.com/v1/chat/completions");
assert.deepEqual(
openferenceProvider.models?.map((model) => model.id),
["GLM-5.2"]
);
assert.equal(hasSpecializedExecutor("openference"), false);
const headers = getExecutor("openference").buildHeaders({ accessToken: "oauth-access" }, false);
assert.equal(headers.Authorization, "Bearer oauth-access");
});
test("Openference is classified for live OpenAI-style model discovery", () => {
assert.ok(NAMED_OPENAI_STYLE_PROVIDERS.has("openference"));
});
test("OAUTH_TEST_CONFIG covers openference and alias of", () => {
assert.ok((OAUTH_TEST_CONFIG as Record<string, unknown>).openference);
assert.ok((OAUTH_TEST_CONFIG as Record<string, unknown>).of);
});
test("Openference Test Connection probes /v1/models instead of reporting unsupported", async () => {
let calledUrl = "";
globalThis.fetch = async (url) => {
calledUrl = String(url);
return new Response(JSON.stringify({ data: [{ id: "GLM-5.2" }] }), {
status: 200,
headers: { "content-type": "application/json" },
});
};
const result = await testOAuthConnection({
provider: "openference",
accessToken: "healthy-access-token",
refreshToken: "oar_refresh",
tokenExpiresAt: new Date(Date.now() + 3600_000).toISOString(),
});
assert.notEqual(result.diagnosis?.type, "unsupported");
assert.notEqual(result.error, "Provider test not supported");
assert.equal(result.valid, true);
assert.equal(calledUrl, "https://api.openference.com/v1/models");
});
test("Openference Test Connection treats 402 as authenticated (plan required for inference)", async () => {
globalThis.fetch = async () =>
new Response(JSON.stringify({ error: "payment_required" }), {
status: 402,
headers: { "content-type": "application/json" },
});
const result = await testOAuthConnection({
provider: "openference",
accessToken: "healthy-access-token",
refreshToken: "oar_refresh",
tokenExpiresAt: new Date(Date.now() + 3600_000).toISOString(),
});
assert.equal(result.valid, true);
});
test("Openference refresh rotates oar_* tokens", async () => {
assert.equal(supportsTokenRefresh("openference"), true);
globalThis.fetch = async (input, init) => {
assert.equal(String(input), OPENFERENCE_CONFIG.tokenUrl);
const body = init?.body as URLSearchParams;
assert.equal(body.get("grant_type"), "refresh_token");
assert.equal(body.get("client_id"), OPENFERENCE_CONFIG.clientId);
assert.equal(body.get("refresh_token"), "oar_old");
return Response.json({
access_token: "new-access",
refresh_token: "oar_new",
expires_in: 3600,
});
};
const refreshed = await refreshOpenferenceToken("oar_old", null, null);
assert.equal(refreshed?.accessToken, "new-access");
assert.equal(refreshed?.refreshToken, "oar_new");
});