mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(providers): add Empower and Poe gateway support
Register Empower and Poe as managed OpenAI-compatible providers with static metadata, example models, and remote model catalog support. Add Poe-specific credential validation against its balance endpoint and tighten Zed credential imports by skipping unsupported entries and deduplicating provider-token pairs before saving.
This commit is contained in:
@@ -199,6 +199,13 @@ const CHAT_OPENAI_COMPAT_MODELS: Record<string, RegistryModel[]> = {
|
||||
cablyai: buildModels(["gpt-4o", "gpt-4o-mini", "deepseek-chat"]),
|
||||
thebai: buildModels(["gpt-4o", "claude-3.5-sonnet", "llama-3.3-70b"]),
|
||||
fenayai: buildModels(["gpt-4o", "claude-3.5-sonnet", "deepseek-chat"]),
|
||||
empower: buildModels([
|
||||
"empower-functions",
|
||||
"mistralai/Mixtral-8x7B-Instruct-v0.1",
|
||||
"meta-llama/Meta-Llama-3-8B-Instruct",
|
||||
"meta-llama/Meta-Llama-3-70B-Instruct",
|
||||
]),
|
||||
poe: buildModels(["Claude-Sonnet-4.5", "GPT-5-Pro", "GPT-5-Codex", "Gemini-2.5-Pro"]),
|
||||
gitlab: [{ id: "gitlab-duo-code-suggestions", name: "GitLab Duo Code Suggestions" }],
|
||||
"gitlab-duo": [{ id: "gitlab-duo-code-suggestions", name: "GitLab Duo Code Suggestions" }],
|
||||
chutes: buildModels([
|
||||
@@ -2054,6 +2061,32 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
passthroughModels: true,
|
||||
},
|
||||
|
||||
empower: {
|
||||
id: "empower",
|
||||
alias: "empower",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://app.empower.dev/api/v1",
|
||||
modelsUrl: "https://app.empower.dev/api/v1/models",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
models: CHAT_OPENAI_COMPAT_MODELS.empower,
|
||||
passthroughModels: true,
|
||||
},
|
||||
|
||||
poe: {
|
||||
id: "poe",
|
||||
alias: "poe",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://api.poe.com/v1",
|
||||
modelsUrl: "https://api.poe.com/v1/models",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
models: CHAT_OPENAI_COMPAT_MODELS.poe,
|
||||
passthroughModels: true,
|
||||
},
|
||||
|
||||
gitlab: {
|
||||
id: "gitlab",
|
||||
alias: "gitlab",
|
||||
|
||||
@@ -80,7 +80,7 @@ function isLocalOpenAIStyleProvider(provider: string): boolean {
|
||||
return isSelfHostedChatProvider(provider);
|
||||
}
|
||||
|
||||
const NAMED_OPENAI_STYLE_PROVIDERS = new Set(["bedrock", "modal", "reka"]);
|
||||
const NAMED_OPENAI_STYLE_PROVIDERS = new Set(["bedrock", "modal", "reka", "empower", "poe"]);
|
||||
|
||||
function isNamedOpenAIStyleProvider(provider: string): boolean {
|
||||
return NAMED_OPENAI_STYLE_PROVIDERS.has(provider);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { discoverZedCredentials, isZedInstalled } from "@/lib/zed-oauth/keychain-reader";
|
||||
import { partitionZedCredentials } from "@/lib/zed-oauth/importUtils";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { createProviderConnection } from "@/lib/db/providers";
|
||||
|
||||
@@ -51,8 +52,14 @@ export async function POST(request: Request): Promise<NextResponse<ImportRespons
|
||||
// Discover credentials from keychain
|
||||
console.log("[Zed Import] Discovering Zed credentials from keychain...");
|
||||
const credentials = await discoverZedCredentials();
|
||||
const { importable, skipped, duplicatesDropped } = partitionZedCredentials(credentials);
|
||||
|
||||
if (credentials.length === 0) {
|
||||
if (importable.length === 0) {
|
||||
if (credentials.length > 0) {
|
||||
console.warn(
|
||||
`[Zed Import] Found ${credentials.length} keychain credential(s), but none mapped to supported OmniRoute providers`
|
||||
);
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
count: 0,
|
||||
@@ -64,9 +71,7 @@ export async function POST(request: Request): Promise<NextResponse<ImportRespons
|
||||
|
||||
// Save to database using OmniRoute's provider schema
|
||||
let savedCount = 0;
|
||||
for (const cred of credentials) {
|
||||
if (!cred.token) continue;
|
||||
|
||||
for (const cred of importable) {
|
||||
try {
|
||||
await createProviderConnection({
|
||||
provider: cred.provider,
|
||||
@@ -81,18 +86,24 @@ export async function POST(request: Request): Promise<NextResponse<ImportRespons
|
||||
}
|
||||
}
|
||||
|
||||
const credentialSummary = credentials.map((cred) => ({
|
||||
if (skipped.length > 0 || duplicatesDropped > 0) {
|
||||
console.log(
|
||||
`[Zed Import] Skipped ${skipped.length} unsupported credential(s) and dropped ${duplicatesDropped} duplicate credential(s)`
|
||||
);
|
||||
}
|
||||
|
||||
const credentialSummary = importable.map((cred) => ({
|
||||
provider: cred.provider,
|
||||
service: cred.service,
|
||||
account: cred.account,
|
||||
hasToken: Boolean(cred.token),
|
||||
}));
|
||||
|
||||
const importedProviders = credentials.map((c) => c.provider);
|
||||
const importedProviders = importable.map((c) => c.provider);
|
||||
const uniqueProviders = [...new Set(importedProviders)];
|
||||
|
||||
console.log(
|
||||
`[Zed Import] Discovered ${credentials.length} credentials and successfully saved ${savedCount} for ${uniqueProviders.length} providers`
|
||||
`[Zed Import] Discovered ${credentials.length} credentials, imported ${importable.length} supported credential(s), and successfully saved ${savedCount} for ${uniqueProviders.length} providers`
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -1579,6 +1579,43 @@ async function validateRunwayProvider({ apiKey, providerSpecificData = {} }: any
|
||||
return { valid: false, error: "Connection failed while testing Runway" };
|
||||
}
|
||||
|
||||
async function validatePoeProvider({ apiKey, providerSpecificData = {} }: any) {
|
||||
const baseUrl = normalizeBaseUrl(providerSpecificData.baseUrl) || "https://api.poe.com/v1";
|
||||
const balanceUrl = new URL("/usage/current_balance", baseUrl).toString();
|
||||
|
||||
try {
|
||||
const response = await validationRead(balanceUrl, {
|
||||
method: "GET",
|
||||
headers: buildBearerHeaders(apiKey, providerSpecificData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return { valid: true, error: null, method: "poe_current_balance" };
|
||||
}
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return { valid: false, error: "Invalid API key" };
|
||||
}
|
||||
|
||||
if (response.status === 429) {
|
||||
return {
|
||||
valid: true,
|
||||
error: null,
|
||||
method: "poe_current_balance",
|
||||
warning: "Rate limited, but credentials are valid",
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status >= 500) {
|
||||
return { valid: false, error: `Provider unavailable (${response.status})` };
|
||||
}
|
||||
} catch (error: any) {
|
||||
return toValidationErrorResult(error);
|
||||
}
|
||||
|
||||
return { valid: false, error: "Connection failed while testing Poe" };
|
||||
}
|
||||
|
||||
async function validateOpenAICompatibleProvider({ apiKey, providerSpecificData = {} }: any) {
|
||||
const baseUrl = normalizeBaseUrl(providerSpecificData.baseUrl);
|
||||
if (!baseUrl) {
|
||||
@@ -2511,6 +2548,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
|
||||
baseUrl: normalizeBaseUrl(providerSpecificData?.baseUrl || ""),
|
||||
modelId: "Qwen/Qwen3-4B-Thinking-2507-FP8",
|
||||
}),
|
||||
poe: validatePoeProvider,
|
||||
clarifai: validateClarifaiProvider,
|
||||
reka: validateRekaProvider,
|
||||
nlpcloud: validateNlpCloudProvider,
|
||||
|
||||
46
src/lib/zed-oauth/importUtils.ts
Normal file
46
src/lib/zed-oauth/importUtils.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { AI_PROVIDERS } from "@/shared/constants/providers";
|
||||
import type { ZedCredential } from "./keychain-reader";
|
||||
|
||||
export interface PartitionedZedCredentials {
|
||||
importable: ZedCredential[];
|
||||
skipped: ZedCredential[];
|
||||
duplicatesDropped: number;
|
||||
}
|
||||
|
||||
function isSupportedZedProvider(provider: string): boolean {
|
||||
return (
|
||||
typeof provider === "string" &&
|
||||
provider.length > 0 &&
|
||||
provider !== "unknown" &&
|
||||
!!AI_PROVIDERS[provider]
|
||||
);
|
||||
}
|
||||
|
||||
export function partitionZedCredentials(credentials: ZedCredential[]): PartitionedZedCredentials {
|
||||
const importable: ZedCredential[] = [];
|
||||
const skipped: ZedCredential[] = [];
|
||||
const seen = new Set<string>();
|
||||
let duplicatesDropped = 0;
|
||||
|
||||
for (const credential of credentials) {
|
||||
if (!credential?.token || !isSupportedZedProvider(credential.provider)) {
|
||||
skipped.push(credential);
|
||||
continue;
|
||||
}
|
||||
|
||||
const dedupeKey = `${credential.provider}:${credential.token}`;
|
||||
if (seen.has(dedupeKey)) {
|
||||
duplicatesDropped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
seen.add(dedupeKey);
|
||||
importable.push(credential);
|
||||
}
|
||||
|
||||
return {
|
||||
importable,
|
||||
skipped,
|
||||
duplicatesDropped,
|
||||
};
|
||||
}
|
||||
@@ -1127,6 +1127,32 @@ export const APIKEY_PROVIDERS = {
|
||||
authHint: "Bearer API key for the FenayAI OpenAI-compatible gateway.",
|
||||
passthroughModels: true,
|
||||
},
|
||||
empower: {
|
||||
id: "empower",
|
||||
alias: "empower",
|
||||
name: "Empower",
|
||||
icon: "hub",
|
||||
color: "#14B8A6",
|
||||
textIcon: "EM",
|
||||
website: "https://docs.empower.dev",
|
||||
authHint: "Bearer API key for the Empower OpenAI-compatible endpoint.",
|
||||
apiHint:
|
||||
"Empower exposes OpenAI-compatible chat on https://app.empower.dev/api/v1 with tool-calling support on empower-functions.",
|
||||
passthroughModels: true,
|
||||
},
|
||||
poe: {
|
||||
id: "poe",
|
||||
alias: "poe",
|
||||
name: "Poe",
|
||||
icon: "hub",
|
||||
color: "#F97316",
|
||||
textIcon: "PO",
|
||||
website: "https://creator.poe.com/api-reference",
|
||||
authHint: "Bearer API key for the Poe OpenAI-compatible API.",
|
||||
apiHint:
|
||||
"Poe exposes OpenAI-compatible chat and responses on https://api.poe.com/v1, with authenticated balance checks on /usage/current_balance.",
|
||||
passthroughModels: true,
|
||||
},
|
||||
gitlab: {
|
||||
id: "gitlab",
|
||||
alias: "gitlab",
|
||||
|
||||
@@ -27,6 +27,7 @@ const CHAT_OPENAI_COMPAT_PROVIDER_IDS = [
|
||||
"databricks",
|
||||
"datarobot",
|
||||
"clarifai",
|
||||
"poe",
|
||||
"azure-ai",
|
||||
"bedrock",
|
||||
"watsonx",
|
||||
|
||||
@@ -107,6 +107,16 @@ test("Clarifai registry exposes current OpenAI-compatible examples", () => {
|
||||
assert.ok(ids.has("gcp/generate/models/gemini-2_5-flash"));
|
||||
});
|
||||
|
||||
test("Poe registry exposes current OpenAI-compatible examples", () => {
|
||||
const poeModels = getProviderModels("poe");
|
||||
const ids = new Set(poeModels.map((model) => model.id));
|
||||
|
||||
assert.ok(ids.has("Claude-Sonnet-4.5"));
|
||||
assert.ok(ids.has("GPT-5-Pro"));
|
||||
assert.ok(ids.has("GPT-5-Codex"));
|
||||
assert.ok(ids.has("Gemini-2.5-Pro"));
|
||||
});
|
||||
|
||||
test("Azure AI Foundry registry exposes fallback marketplace examples", () => {
|
||||
const azureAiModels = getProviderModels("azure-ai");
|
||||
const ids = new Set(azureAiModels.map((model) => model.id));
|
||||
|
||||
@@ -335,6 +335,18 @@ test("provider models route fetches remote catalogs for new OpenAI-compatible ga
|
||||
expectedUrl: "https://fenayai.com/v1/models",
|
||||
model: { id: "deepseek-chat", name: "DeepSeek Chat via FenayAI" },
|
||||
},
|
||||
{
|
||||
provider: "empower",
|
||||
apiKey: "empower-key",
|
||||
expectedUrl: "https://app.empower.dev/api/v1/models",
|
||||
model: { id: "empower-functions", name: "Empower Functions", owned_by: "empower" },
|
||||
},
|
||||
{
|
||||
provider: "poe",
|
||||
apiKey: "poe-key",
|
||||
expectedUrl: "https://api.poe.com/v1/models",
|
||||
model: { id: "Claude-Sonnet-4.5", name: "Claude Sonnet 4.5", owned_by: "Anthropic" },
|
||||
},
|
||||
{
|
||||
provider: "chutes",
|
||||
apiKey: "chutes-key",
|
||||
|
||||
@@ -1189,6 +1189,49 @@ test("specialty validator rejects invalid Modal credentials", async () => {
|
||||
assert.equal(modal.error, "Invalid API key");
|
||||
});
|
||||
|
||||
test("specialty validator accepts Poe credentials on the current balance endpoint", async () => {
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
const target = String(url);
|
||||
|
||||
if (target === "https://api.poe.com/usage/current_balance") {
|
||||
const headers = init.headers as Record<string, string>;
|
||||
assert.equal(headers.Authorization, "Bearer poe-key");
|
||||
return new Response(JSON.stringify({ current_point_balance: 123456 }), { status: 200 });
|
||||
}
|
||||
|
||||
throw new Error(`unexpected fetch: ${target}`);
|
||||
};
|
||||
|
||||
const poe = await validateProviderApiKey({
|
||||
provider: "poe",
|
||||
apiKey: "poe-key",
|
||||
});
|
||||
|
||||
assert.equal(poe.valid, true);
|
||||
assert.equal(poe.method, "poe_current_balance");
|
||||
});
|
||||
|
||||
test("specialty validator rejects invalid Poe credentials", async () => {
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
const target = String(url);
|
||||
|
||||
if (target === "https://api.poe.com/usage/current_balance") {
|
||||
const headers = init.headers as Record<string, string>;
|
||||
assert.equal(headers.Authorization, "Bearer poe-bad");
|
||||
return new Response(JSON.stringify({ error: "unauthorized" }), { status: 401 });
|
||||
}
|
||||
|
||||
throw new Error(`unexpected fetch: ${target}`);
|
||||
};
|
||||
|
||||
const poe = await validateProviderApiKey({
|
||||
provider: "poe",
|
||||
apiKey: "poe-bad",
|
||||
});
|
||||
|
||||
assert.equal(poe.error, "Invalid API key");
|
||||
});
|
||||
|
||||
test("specialty validator accepts Clarifai credentials through the OpenAI-compatible models probe", async () => {
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
const target = String(url);
|
||||
|
||||
@@ -228,6 +228,8 @@ test("static catalog entries resolve local, search, audio, web-cookie and upstre
|
||||
const chutesProvider = providerPageUtils.resolveDashboardProviderInfo("chutes");
|
||||
const datarobotProvider = providerPageUtils.resolveDashboardProviderInfo("datarobot");
|
||||
const clarifaiProvider = providerPageUtils.resolveDashboardProviderInfo("clarifai");
|
||||
const empowerProvider = providerPageUtils.resolveDashboardProviderInfo("empower");
|
||||
const poeProvider = providerPageUtils.resolveDashboardProviderInfo("poe");
|
||||
const azureAiProvider = providerPageUtils.resolveDashboardProviderInfo("azure-ai");
|
||||
const watsonxProvider = providerPageUtils.resolveDashboardProviderInfo("watsonx");
|
||||
const ociProvider = providerPageUtils.resolveDashboardProviderInfo("oci");
|
||||
@@ -271,6 +273,10 @@ test("static catalog entries resolve local, search, audio, web-cookie and upstre
|
||||
assert.equal(datarobotProvider?.name, providers.APIKEY_PROVIDERS.datarobot.name);
|
||||
assert.equal(clarifaiProvider?.category, "apikey");
|
||||
assert.equal(clarifaiProvider?.name, providers.APIKEY_PROVIDERS.clarifai.name);
|
||||
assert.equal(empowerProvider?.category, "apikey");
|
||||
assert.equal(empowerProvider?.name, providers.APIKEY_PROVIDERS.empower.name);
|
||||
assert.equal(poeProvider?.category, "apikey");
|
||||
assert.equal(poeProvider?.name, providers.APIKEY_PROVIDERS.poe.name);
|
||||
assert.equal(azureAiProvider?.category, "apikey");
|
||||
assert.equal(azureAiProvider?.name, providers.APIKEY_PROVIDERS["azure-ai"].name);
|
||||
assert.equal(watsonxProvider?.category, "apikey");
|
||||
@@ -323,6 +329,8 @@ test("managed provider connection ids include supported static categories and ex
|
||||
assert.equal(providerCatalog.isManagedProviderConnectionId("chutes"), true);
|
||||
assert.equal(providerCatalog.isManagedProviderConnectionId("datarobot"), true);
|
||||
assert.equal(providerCatalog.isManagedProviderConnectionId("clarifai"), true);
|
||||
assert.equal(providerCatalog.isManagedProviderConnectionId("empower"), true);
|
||||
assert.equal(providerCatalog.isManagedProviderConnectionId("poe"), true);
|
||||
assert.equal(providerCatalog.isManagedProviderConnectionId("azure-ai"), true);
|
||||
assert.equal(providerCatalog.isManagedProviderConnectionId("bedrock"), true);
|
||||
assert.equal(providerCatalog.isManagedProviderConnectionId("watsonx"), true);
|
||||
@@ -372,6 +380,8 @@ test("grok-web taxonomy stays web-cookie only and does not leak into api-key ent
|
||||
assert.equal("chutes" in providers.APIKEY_PROVIDERS, true);
|
||||
assert.equal("datarobot" in providers.APIKEY_PROVIDERS, true);
|
||||
assert.equal("clarifai" in providers.APIKEY_PROVIDERS, true);
|
||||
assert.equal("empower" in providers.APIKEY_PROVIDERS, true);
|
||||
assert.equal("poe" in providers.APIKEY_PROVIDERS, true);
|
||||
assert.equal("azure-ai" in providers.APIKEY_PROVIDERS, true);
|
||||
assert.equal("bedrock" in providers.APIKEY_PROVIDERS, true);
|
||||
assert.equal("watsonx" in providers.APIKEY_PROVIDERS, true);
|
||||
@@ -446,6 +456,14 @@ test("grok-web taxonomy stays web-cookie only and does not leak into api-key ent
|
||||
apiKeyEntries.some((entry) => entry.providerId === "clarifai"),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
apiKeyEntries.some((entry) => entry.providerId === "empower"),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
apiKeyEntries.some((entry) => entry.providerId === "poe"),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
apiKeyEntries.some((entry) => entry.providerId === "azure-ai"),
|
||||
true
|
||||
|
||||
@@ -94,6 +94,22 @@ test("providers route accepts managed local, audio, web-cookie and search provid
|
||||
name: "Clarifai Primary",
|
||||
},
|
||||
},
|
||||
{
|
||||
provider: "empower",
|
||||
body: {
|
||||
provider: "empower",
|
||||
apiKey: "empower-key",
|
||||
name: "Empower Primary",
|
||||
},
|
||||
},
|
||||
{
|
||||
provider: "poe",
|
||||
body: {
|
||||
provider: "poe",
|
||||
apiKey: "poe-key",
|
||||
name: "Poe Primary",
|
||||
},
|
||||
},
|
||||
{
|
||||
provider: "azure-ai",
|
||||
body: {
|
||||
|
||||
74
tests/unit/zed-import-utils.test.ts
Normal file
74
tests/unit/zed-import-utils.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { partitionZedCredentials } = await import("../../src/lib/zed-oauth/importUtils.ts");
|
||||
|
||||
type ImportableCredential = {
|
||||
provider: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
test("partitionZedCredentials keeps only supported providers with tokens", () => {
|
||||
const result = partitionZedCredentials([
|
||||
{
|
||||
provider: "openai",
|
||||
service: "zed-openai",
|
||||
account: "api-key",
|
||||
token: "sk-openai",
|
||||
},
|
||||
{
|
||||
provider: "unknown",
|
||||
service: "zed-custom",
|
||||
account: "api-key",
|
||||
token: "custom-token",
|
||||
},
|
||||
{
|
||||
provider: "anthropic",
|
||||
service: "zed-anthropic",
|
||||
account: "api-key",
|
||||
token: "",
|
||||
},
|
||||
]);
|
||||
|
||||
assert.deepEqual(result.importable, [
|
||||
{
|
||||
provider: "openai",
|
||||
service: "zed-openai",
|
||||
account: "api-key",
|
||||
token: "sk-openai",
|
||||
},
|
||||
]);
|
||||
assert.equal(result.skipped.length, 2);
|
||||
assert.equal(result.duplicatesDropped, 0);
|
||||
});
|
||||
|
||||
test("partitionZedCredentials drops duplicate provider-token pairs from alias scans", () => {
|
||||
const result = partitionZedCredentials([
|
||||
{
|
||||
provider: "openai",
|
||||
service: "zed-openai",
|
||||
account: "api-key",
|
||||
token: "sk-openai",
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
service: "ai.zed.openai",
|
||||
account: "token",
|
||||
token: "sk-openai",
|
||||
},
|
||||
{
|
||||
provider: "anthropic",
|
||||
service: "zed-anthropic",
|
||||
account: "oauth",
|
||||
token: "sk-anthropic",
|
||||
},
|
||||
]);
|
||||
|
||||
assert.equal(result.importable.length, 2);
|
||||
assert.deepEqual(
|
||||
result.importable.map((entry: ImportableCredential) => `${entry.provider}:${entry.token}`),
|
||||
["openai:sk-openai", "anthropic:sk-anthropic"]
|
||||
);
|
||||
assert.equal(result.skipped.length, 0);
|
||||
assert.equal(result.duplicatesDropped, 1);
|
||||
});
|
||||
Reference in New Issue
Block a user