diff --git a/changelog.d/fixes/8082-poe-baseurl.md b/changelog.d/fixes/8082-poe-baseurl.md new file mode 100644 index 0000000000..c2627162d9 --- /dev/null +++ b/changelog.d/fixes/8082-poe-baseurl.md @@ -0,0 +1 @@ +- **fix(providers):** add the missing `poe` REGISTRY entry so built-in Poe model discovery resolves a base URL instead of failing with "No base URL configured for provider" ([#8082](https://github.com/diegosouzapw/OmniRoute/issues/8082)) diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index 3fb8d957a4..d34c977faf 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -101,6 +101,7 @@ import { t3_webProvider } from "./registry/t3-web/index.ts"; import { iflytekProvider } from "./registry/iflytek/index.ts"; import { crofProvider } from "./registry/crof/index.ts"; import { moonshotProvider } from "./registry/moonshot/index.ts"; +import { poeProvider } from "./registry/poe/index.ts"; import { bazaarlinkProvider } from "./registry/bazaarlink/index.ts"; import { perplexityProvider } from "./registry/perplexity/index.ts"; import { perplexity_webProvider } from "./registry/perplexity/web/index.ts"; @@ -311,6 +312,7 @@ export const REGISTRY: Record = { iflytek: iflytekProvider, crof: crofProvider, moonshot: moonshotProvider, + poe: poeProvider, bazaarlink: bazaarlinkProvider, perplexity: perplexityProvider, "perplexity-web": perplexity_webProvider, diff --git a/open-sse/config/providers/registry/poe/index.ts b/open-sse/config/providers/registry/poe/index.ts new file mode 100644 index 0000000000..b80d612fa5 --- /dev/null +++ b/open-sse/config/providers/registry/poe/index.ts @@ -0,0 +1,25 @@ +import type { RegistryEntry } from "../../shared.ts"; + +// Poe (creator.poe.com) — OpenAI-compatible chat/responses gateway. #8082: the +// built-in `poe` provider (NAMED_OPENAI_STYLE_PROVIDERS, passthroughModels:true) +// had no REGISTRY entry, so model discovery's `getRegistryEntry("poe")?.baseUrl` +// resolved to undefined and every request failed with "No base URL configured +// for provider" even though credentials/inference worked fine. This base URL is +// the single source of truth other Poe code paths should read from (see +// src/lib/providers/validation/audioMiscProviders.ts::validatePoeProvider). +export const POE_DEFAULT_BASE_URL = "https://api.poe.com/v1"; + +export const poeProvider: RegistryEntry = { + id: "poe", + alias: "poe", + format: "openai", + executor: "default", + baseUrl: `${POE_DEFAULT_BASE_URL}/chat/completions`, + authType: "apikey", + authHeader: "bearer", + models: [ + { id: "gpt-5.2", name: "GPT-5.2" }, + { id: "claude-opus-4.8", name: "Claude Opus 4.8" }, + { id: "gemini-3.0-pro", name: "Gemini 3.0 Pro" }, + ], +}; diff --git a/src/lib/providers/validation/audioMiscProviders.ts b/src/lib/providers/validation/audioMiscProviders.ts index 4553bfddbc..be02b84704 100644 --- a/src/lib/providers/validation/audioMiscProviders.ts +++ b/src/lib/providers/validation/audioMiscProviders.ts @@ -3,6 +3,7 @@ // poe. Extracted from validation.ts (god-file decomposition) — top-level functions with no // dispatcher-state captures; behavior is byte-identical to the original inline defs. import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts"; +import { POE_DEFAULT_BASE_URL } from "@omniroute/open-sse/config/providers/registry/poe/index.ts"; import { normalizeBaseUrl } from "./urlHelpers"; import { applyCustomUserAgent, @@ -608,7 +609,7 @@ export async function validateNousResearchProvider({ apiKey, providerSpecificDat } export async function validatePoeProvider({ apiKey, providerSpecificData = {} }: any) { - const baseUrl = normalizeBaseUrl(providerSpecificData.baseUrl) || "https://api.poe.com/v1"; + const baseUrl = normalizeBaseUrl(providerSpecificData.baseUrl) || POE_DEFAULT_BASE_URL; const balanceUrl = new URL("/usage/current_balance", baseUrl).toString(); try { diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index 929dbd02cc..de5d5fe6a3 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -3857,6 +3857,29 @@ "stream": "https://api.pioneer.ai/v1/chat/completions" } }, + "poe": { + "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.poe.com/v1/chat/completions", + "stream": "https://api.poe.com/v1/chat/completions" + } + }, "pollinations": { "format": "openai", "headers": { diff --git a/tests/unit/poe-provider-models-baseurl.test.ts b/tests/unit/poe-provider-models-baseurl.test.ts new file mode 100644 index 0000000000..7043e4ae44 --- /dev/null +++ b/tests/unit/poe-provider-models-baseurl.test.ts @@ -0,0 +1,73 @@ +// #8082: the built-in `poe` provider (passthroughModels:true, no baseUrl field +// exposed in the UI) is routed through the generic OpenAI-style model-discovery +// branch, which resolves its base URL ONLY from +// `connection.providerSpecificData.baseUrl` OR `getRegistryEntry("poe")?.baseUrl`. +// There was no REGISTRY entry for "poe", so discovery always failed with +// {"error":"No base URL configured for provider"} even though the actual +// inference/validation path (src/lib/providers/validation/audioMiscProviders.ts) +// has always had a hardcoded "https://api.poe.com/v1" fallback and worked fine. +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-poe-models-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const providerModelsRoute = await import("../../src/app/api/providers/[id]/models/route.ts"); + +const originalFetch = globalThis.fetch; + +async function resetStorage() { + globalThis.fetch = originalFetch; + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +test.beforeEach(async () => { + await resetStorage(); +}); + +test.after(async () => { + globalThis.fetch = originalFetch; + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("provider models route resolves the built-in Poe registry base URL instead of failing with 'No base URL configured for provider' (#8082)", async () => { + const connection = await providersDb.createProviderConnection({ + provider: "poe", + authType: "apikey", + name: "poe-8082", + apiKey: "poe-test-key", + isActive: true, + testStatus: "active", + // Mirrors the built-in provider's UI: no baseUrl field is exposed for Poe, + // so providerSpecificData never carries one. + providerSpecificData: {}, + }); + + const seenRequests: Array<{ url: string; authorization: string | null }> = []; + globalThis.fetch = async (url, init) => { + const headers = new Headers(init?.headers as HeadersInit | undefined); + seenRequests.push({ url: String(url), authorization: headers.get("authorization") }); + return Response.json({ data: [{ id: "gpt-5.2", name: "GPT-5.2" }] }); + }; + + const response = await providerModelsRoute.GET( + new Request(`http://localhost/api/providers/${connection.id}/models?refresh=true`), + { params: { id: connection.id } } + ); + const body = (await response.json()) as { error?: string }; + + assert.notEqual(body.error, "No base URL configured for provider"); + assert.equal(response.status, 200); + assert.ok( + seenRequests.some((req) => req.url.startsWith("https://api.poe.com/v1")), + `expected a request against the Poe registry base URL, got: ${JSON.stringify(seenRequests)}` + ); +});