diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 7ede79d326..061269661a 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -199,6 +199,13 @@ const CHAT_OPENAI_COMPAT_MODELS: Record = { 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 = { 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", diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index c80d5f4d89..ac24b454b8 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -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); diff --git a/src/app/api/providers/zed/import/route.ts b/src/app/api/providers/zed/import/route.ts index 792443ea3e..1ee8296d9e 100644 --- a/src/app/api/providers/zed/import/route.ts +++ b/src/app/api/providers/zed/import/route.ts @@ -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 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 ({ + 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({ diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index 50967e1528..328526ae98 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -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, diff --git a/src/lib/zed-oauth/importUtils.ts b/src/lib/zed-oauth/importUtils.ts new file mode 100644 index 0000000000..fecbf47218 --- /dev/null +++ b/src/lib/zed-oauth/importUtils.ts @@ -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(); + 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, + }; +} diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 6a66e4bac2..9df284e822 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -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", diff --git a/tests/unit/chat-openai-compat-providers.test.ts b/tests/unit/chat-openai-compat-providers.test.ts index 0ca61df745..f4a35f2654 100644 --- a/tests/unit/chat-openai-compat-providers.test.ts +++ b/tests/unit/chat-openai-compat-providers.test.ts @@ -27,6 +27,7 @@ const CHAT_OPENAI_COMPAT_PROVIDER_IDS = [ "databricks", "datarobot", "clarifai", + "poe", "azure-ai", "bedrock", "watsonx", diff --git a/tests/unit/provider-models-config.test.ts b/tests/unit/provider-models-config.test.ts index e3c722cbfb..6e7e106f67 100644 --- a/tests/unit/provider-models-config.test.ts +++ b/tests/unit/provider-models-config.test.ts @@ -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)); diff --git a/tests/unit/provider-models-route.test.ts b/tests/unit/provider-models-route.test.ts index ae0a99af59..10061fab55 100644 --- a/tests/unit/provider-models-route.test.ts +++ b/tests/unit/provider-models-route.test.ts @@ -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", diff --git a/tests/unit/provider-validation-specialty.test.ts b/tests/unit/provider-validation-specialty.test.ts index d8c4d8bfc2..e88a53bfc9 100644 --- a/tests/unit/provider-validation-specialty.test.ts +++ b/tests/unit/provider-validation-specialty.test.ts @@ -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; + 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; + 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); diff --git a/tests/unit/providers-page-utils.test.ts b/tests/unit/providers-page-utils.test.ts index 0dd7ec0d62..2a7476b97c 100644 --- a/tests/unit/providers-page-utils.test.ts +++ b/tests/unit/providers-page-utils.test.ts @@ -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 diff --git a/tests/unit/providers-route-managed-catalog.test.ts b/tests/unit/providers-route-managed-catalog.test.ts index 98e38c83d0..2bf013c437 100644 --- a/tests/unit/providers-route-managed-catalog.test.ts +++ b/tests/unit/providers-route-managed-catalog.test.ts @@ -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: { diff --git a/tests/unit/zed-import-utils.test.ts b/tests/unit/zed-import-utils.test.ts new file mode 100644 index 0000000000..765f794660 --- /dev/null +++ b/tests/unit/zed-import-utils.test.ts @@ -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); +});