refactor(open-sse): B1 consolidate media endpoint URLs into registry

- image/embed/tts/search base URLs derive from media.*Config.baseUrl /
  searchViaChat.endpoint (single source); handlers read PROVIDER_MEDIA.
- ~18 hardcoded endpoints moved: bfl/fal/stability/runway/hf/gemini/
  cloudflare/recraft/sdwebui/comfyui/nanobanana image, voyage embed,
  gemini/openrouter tts, chatSearch endpoints.
- Byte-identical: PROVIDERS 62 + alias 90 baselines, image buildUrl outputs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
decolua
2026-06-14 13:58:12 +07:00
parent bb9e9aa91f
commit e53ce79abb
32 changed files with 96 additions and 38 deletions

View File

@@ -4,7 +4,6 @@ import { PROVIDER_MEDIA } from "../../providers/index.js";
// media-only providers without a registry file keep URL here; rest derive from registry media.embeddingConfig.baseUrl
const ENDPOINTS = {
"voyage-ai": "https://api.voyageai.com/v1/embeddings",
"jina-ai": "https://api.jina.ai/v1/embeddings",
};

View File

@@ -1,7 +1,8 @@
// Black Forest Labs (FLUX) — async submit + polling_url
import { sleep, nowSec, POLL_INTERVAL_MS, POLL_TIMEOUT_MS } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = "https://api.bfl.ai/v1";
const BASE_URL = PROVIDER_MEDIA["black-forest-labs"]?.imageConfig?.baseUrl;
export default {
async: true,

View File

@@ -1,6 +1,7 @@
import { nowSec, urlToBase64 } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = "https://api.cloudflare.com/client/v4/accounts";
const BASE_URL = PROVIDER_MEDIA["cloudflare-ai"]?.imageConfig?.baseUrl;
const MULTIPART_MODELS = new Set([
"@cf/black-forest-labs/flux-2-dev",

View File

@@ -1,7 +1,11 @@
// ComfyUI — local, noAuth (placeholder; full graph workflow not implemented)
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = PROVIDER_MEDIA["comfyui"]?.imageConfig?.baseUrl;
export default {
noAuth: true,
buildUrl: () => "http://localhost:8188",
buildUrl: () => BASE_URL,
buildHeaders: () => ({ "Content-Type": "application/json" }),
buildBody: (_model, body) => ({ prompt: body.prompt }),
normalize: (responseBody) => responseBody,

View File

@@ -1,7 +1,8 @@
// Fal.ai — async submit + queue polling
import { sleep, nowSec, sizeToAspectRatio, POLL_INTERVAL_MS, POLL_TIMEOUT_MS } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = "https://queue.fal.run";
const BASE_URL = PROVIDER_MEDIA["fal-ai"]?.imageConfig?.baseUrl;
export default {
async: true,

View File

@@ -1,7 +1,8 @@
// Google Gemini adapter (Nano Banana models)
import { nowSec } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models";
const BASE_URL = PROVIDER_MEDIA["gemini"]?.imageConfig?.baseUrl;
export default {
buildUrl: (model, creds) => {

View File

@@ -1,7 +1,8 @@
// HuggingFace Inference API — returns binary image
import { nowSec } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = "https://api-inference.huggingface.co/models";
const BASE_URL = PROVIDER_MEDIA["huggingface"]?.imageConfig?.baseUrl;
export default {
buildUrl: (model) => `${BASE_URL}/${model}`,

View File

@@ -1,8 +1,10 @@
// NanoBanana API — async submit + poll record-info
import { sleep, nowSec, sizeToAspectRatio, POLL_INTERVAL_MS, POLL_TIMEOUT_MS } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const SUBMIT_URL = "https://api.nanobananaapi.ai/api/v1/nanobanana/generate";
const POLL_BASE = "https://api.nanobananaapi.ai/api/v1/nanobanana/record-info";
const IMG_CFG = PROVIDER_MEDIA["nanobanana"]?.imageConfig || {};
const SUBMIT_URL = IMG_CFG.baseUrl;
const POLL_BASE = IMG_CFG.pollUrl;
export default {
async: true,

View File

@@ -1,13 +1,8 @@
// OpenAI-compatible adapter (used by openai, minimax, openrouter, recraft)
import { PROVIDER_MEDIA } from "../../providers/index.js";
// media-only providers without a registry file keep their URL here; rest derive from registry media.imageConfig.baseUrl
const ENDPOINTS = {
recraft: "https://external.api.recraft.ai/v1/images/generations",
};
const imageCfg = (id) => PROVIDER_MEDIA[id]?.imageConfig || {};
const imageUrl = (id) => imageCfg(id).baseUrl || ENDPOINTS[id];
const imageUrl = (id) => imageCfg(id).baseUrl;
export default function createOpenAIAdapter(providerId) {
const cfg = imageCfg(providerId);

View File

@@ -1,7 +1,8 @@
// Runway ML — async submit + /tasks/{id} polling
import { sleep, nowSec, sizeToAspectRatio, POLL_INTERVAL_MS, POLL_TIMEOUT_MS } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = "https://api.dev.runwayml.com/v1";
const BASE_URL = PROVIDER_MEDIA["runwayml"]?.imageConfig?.baseUrl;
export default {
async: true,

View File

@@ -1,9 +1,12 @@
// SD WebUI (AUTOMATIC1111) — local, noAuth
import { nowSec } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = PROVIDER_MEDIA["sdwebui"]?.imageConfig?.baseUrl;
export default {
noAuth: true,
buildUrl: () => "http://localhost:7860/sdapi/v1/txt2img",
buildUrl: () => BASE_URL,
buildHeaders: () => ({ "Content-Type": "application/json" }),
buildBody: (_model, body) => {
const { prompt, n = 1, size = "1024x1024" } = body;

View File

@@ -1,7 +1,8 @@
// Stability AI v2 — sync, returns { image: "<b64>" }
import { nowSec, sizeToAspectRatio } from "./_base.js";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const BASE_URL = "https://api.stability.ai/v2beta/stable-image/generate";
const BASE_URL = PROVIDER_MEDIA["stability-ai"]?.imageConfig?.baseUrl;
// Map model id → endpoint segment
function modelToEndpoint(model) {

View File

@@ -4,8 +4,10 @@
*/
import { PROVIDER_MEDIA } from "../../providers/index.js";
// Default search model derives from registry searchViaChat (single source)
// Default search model + endpoint derive from registry searchViaChat (single source)
const searchModel = (id) => PROVIDER_MEDIA[id]?.searchViaChat?.defaultModel;
const searchEndpoint = (id, model) =>
(PROVIDER_MEDIA[id]?.searchViaChat?.endpoint || "").replace("{model}", model || "");
const REQUEST_TIMEOUT_MS = 15000;
const DEFAULT_MAX_RESULTS = 10;
@@ -47,8 +49,7 @@ function normalizeCitation(c) {
*/
const CHAT_SEARCH_CONFIG = {
gemini: {
endpoint: (model) =>
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
endpoint: (model) => searchEndpoint("gemini", model),
buildBody: (query) => ({
contents: [{ role: "user", parts: [{ text: query }] }],
tools: [{ google_search: {} }]
@@ -73,7 +74,7 @@ const CHAT_SEARCH_CONFIG = {
},
openai: {
endpoint: () => "https://api.openai.com/v1/chat/completions",
endpoint: () => searchEndpoint("openai"),
buildBody: (query, model) => {
const body = {
model,
@@ -107,7 +108,7 @@ const CHAT_SEARCH_CONFIG = {
},
xai: {
endpoint: () => "https://api.x.ai/v1/responses",
endpoint: () => searchEndpoint("xai"),
buildBody: (query, model) => ({
model,
input: [{ role: "user", content: query }],
@@ -146,7 +147,7 @@ const CHAT_SEARCH_CONFIG = {
},
kimi: {
endpoint: () => "https://api.moonshot.cn/v1/chat/completions",
endpoint: () => searchEndpoint("kimi"),
buildBody: (query, model) => ({
model,
messages: [{ role: "user", content: query }],
@@ -195,7 +196,7 @@ const CHAT_SEARCH_CONFIG = {
},
minimax: {
endpoint: () => "https://api.minimaxi.com/v1/text/chatcompletion_v2",
endpoint: () => searchEndpoint("minimax"),
buildBody: (query, model) => ({
model,
messages: [{ role: "user", content: query }],
@@ -253,7 +254,7 @@ const CHAT_SEARCH_CONFIG = {
},
perplexity: {
endpoint: () => "https://api.perplexity.ai/chat/completions",
endpoint: () => searchEndpoint("perplexity"),
buildBody: (query, model) => ({
model,
messages: [{ role: "user", content: query }]

View File

@@ -2,7 +2,9 @@
import { Buffer } from "node:buffer";
import { PROVIDER_MEDIA } from "../../providers/index.js";
const KNOWN_MODELS = (PROVIDER_MEDIA["gemini"]?.ttsConfig?.models || []).map((m) => m.id);
const TTS_CFG = PROVIDER_MEDIA["gemini"]?.ttsConfig || {};
const TTS_BASE = TTS_CFG.baseUrl;
const KNOWN_MODELS = (TTS_CFG.models || []).map((m) => m.id);
const DEFAULT_MODEL = KNOWN_MODELS[0];
const DEFAULT_VOICE = "Kore";
@@ -52,7 +54,7 @@ export default {
async synthesize(text, model, credentials, _responseFormat, opts = {}) {
if (!credentials?.apiKey) throw new Error("No Gemini API key configured");
const { modelId, voiceId } = parseGeminiModelVoice(model);
const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelId}:generateContent?key=${credentials.apiKey}`;
const url = `${TTS_BASE}/${modelId}:generateContent?key=${credentials.apiKey}`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },

View File

@@ -1,10 +1,14 @@
// OpenRouter TTS — via chat completions + audio modality (SSE stream)
import { PROVIDER_MEDIA } from "../../providers/index.js";
const TTS_CFG = PROVIDER_MEDIA["openrouter"]?.ttsConfig || {};
export default {
async synthesize(text, model, credentials) {
if (!credentials?.apiKey) throw new Error("No OpenRouter API key configured");
// model format: "tts-model/voice" e.g. "openai/gpt-4o-mini-tts/alloy"
let ttsModel = "openai/gpt-4o-mini-tts";
let ttsModel = TTS_CFG.defaultModel;
let voice = "alloy";
if (model && model.includes("/")) {
const lastSlash = model.lastIndexOf("/");
@@ -20,13 +24,12 @@ export default {
voice = model;
}
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
const res = await fetch(TTS_CFG.baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${credentials.apiKey}`,
"HTTP-Referer": "https://endpoint-proxy.local",
"X-Title": "Endpoint Proxy",
...(TTS_CFG.headers || {}),
},
body: JSON.stringify({
model: ttsModel,