mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
- 71 registry files: flat `media.*Config.models` → `models[]` with `kind` field - `media` wrapper removed → serviceKinds, *Config fields promoted top-level - `type` field renamed to `kind` (llm/image/tts/stt/embedding/embedding/video/music) - providers/index.js: PROVIDER_MEDIA now built from flat top-level media fields - shared/constants/providers.js: buildProviderEntry reads flat top-level media fields - route /v1/models: modelKind() uses kind||type; removed subConfig merge block - models/info route: removed sub-config fallback lookup (all models in PROVIDER_MODELS) - ttsProviders/index.js: synthesizeViaConfig reads tts models from PROVIDER_MODELS - test-models route, helpers.js, validate route: kind||type compat - Baselines: PROVIDERS 62/62 ✅, Alias 90/90 ✅ Co-authored-by: Cursor <cursoragent@cursor.com>
161 lines
6.2 KiB
JavaScript
161 lines
6.2 KiB
JavaScript
// Provider definitions
|
|
import REGISTRY from "open-sse/providers/registry/index.js";
|
|
|
|
const MEDIA_ENTRY_KEYS = [
|
|
"serviceKinds", "ttsConfig", "sttConfig", "embeddingConfig",
|
|
"imageConfig", "imageToTextConfig", "videoConfig", "musicConfig",
|
|
"searchViaChat", "searchConfig", "fetchConfig",
|
|
"modelsFetcher", "mediaPriority", "hiddenKinds",
|
|
];
|
|
|
|
// Build provider UI object from registry entry
|
|
function buildProviderEntry(r) {
|
|
const mediaFields = {};
|
|
// Support both legacy r.media wrapper and new flat top-level fields (post-migration)
|
|
if (r.media) Object.assign(mediaFields, r.media);
|
|
for (const k of MEDIA_ENTRY_KEYS) {
|
|
if (r[k] !== undefined) mediaFields[k] = r[k];
|
|
}
|
|
return {
|
|
...(r.display || {}),
|
|
id: r.id,
|
|
alias: r.uiAlias || r.alias,
|
|
...mediaFields,
|
|
...(r.thinkingConfig ? { thinkingConfig: r.thinkingConfig } : {}),
|
|
...(r.regions ? { regions: r.regions, defaultRegion: r.defaultRegion } : {}),
|
|
...(r.hasProviderSpecificData ? { hasProviderSpecificData: true } : {}),
|
|
...(r.noAuth ? { noAuth: true } : {}),
|
|
...(r.passthroughModels ? { passthroughModels: true } : {}),
|
|
...(r.hasOAuth ? { hasOAuth: true } : {}),
|
|
...(r.authModes ? { authModes: r.authModes } : {}),
|
|
...(r.authType ? { authType: r.authType } : {}),
|
|
...(r.authHint ? { authHint: r.authHint } : {}),
|
|
};
|
|
}
|
|
|
|
const byCategory = (cat) => Object.fromEntries(
|
|
REGISTRY.filter(r => r.category === cat).map(r => [r.id, buildProviderEntry(r)])
|
|
);
|
|
|
|
export const FREE_PROVIDERS = byCategory("free");
|
|
export const FREE_TIER_PROVIDERS = byCategory("freeTier");
|
|
|
|
// Thinking config definitions
|
|
// options: list of selectable modes ("auto" = no override from server)
|
|
// defaultMode: fallback when user hasn't configured
|
|
// extended: claude-style thinking (thinking.type + budget_tokens) — used by most providers
|
|
// effort: openai-style reasoning_effort — only openai + codex
|
|
export const THINKING_CONFIG = {
|
|
extended: {
|
|
options: ["auto", "on", "off"],
|
|
defaultMode: "auto",
|
|
defaultBudgetTokens: 10000
|
|
},
|
|
effort: {
|
|
options: ["auto", "none", "low", "medium", "high"],
|
|
defaultMode: "auto"
|
|
}
|
|
};
|
|
|
|
export const OAUTH_PROVIDERS = byCategory("oauth");
|
|
export const APIKEY_PROVIDERS = byCategory("apikey");
|
|
|
|
// Web Cookie Providers (use browser session cookie instead of API key)
|
|
export const WEB_COOKIE_PROVIDERS = byCategory("webCookie");
|
|
|
|
// Media provider kinds — each kind maps to a route and endpoint config
|
|
export const MEDIA_PROVIDER_KINDS = [
|
|
{ id: "embedding", label: "Embedding", icon: "data_array", endpoint: { method: "POST", path: "/v1/embeddings" } },
|
|
{ id: "image", label: "Text to Image", icon: "brush", endpoint: { method: "POST", path: "/v1/images/generations" } },
|
|
{ id: "imageToText", label: "Image to Text", icon: "image_search", endpoint: { method: "POST", path: "/v1/images/understanding" } },
|
|
{ id: "tts", label: "Text To Speech", icon: "record_voice_over", endpoint: { method: "POST", path: "/v1/audio/speech" } },
|
|
{ id: "stt", label: "Speech To Text", icon: "mic", endpoint: { method: "POST", path: "/v1/audio/transcriptions" } },
|
|
{ id: "webSearch", label: "Web Search", icon: "travel_explore", endpoint: { method: "POST", path: "/v1/search" } },
|
|
{ id: "webFetch", label: "Web Fetch", icon: "language", endpoint: { method: "POST", path: "/v1/web/fetch" } },
|
|
{ id: "video", label: "Video", icon: "movie", endpoint: { method: "POST", path: "/v1/video/generations" } },
|
|
{ id: "music", label: "Music", icon: "music_note", endpoint: { method: "POST", path: "/v1/audio/music" } },
|
|
];
|
|
|
|
export const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";
|
|
export const ANTHROPIC_COMPATIBLE_PREFIX = "anthropic-compatible-";
|
|
export const CUSTOM_EMBEDDING_PREFIX = "custom-embedding-";
|
|
|
|
export function isOpenAICompatibleProvider(providerId) {
|
|
return typeof providerId === "string" && providerId.startsWith(OPENAI_COMPATIBLE_PREFIX);
|
|
}
|
|
|
|
export function isAnthropicCompatibleProvider(providerId) {
|
|
return typeof providerId === "string" && providerId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX);
|
|
}
|
|
|
|
export function isCustomEmbeddingProvider(providerId) {
|
|
return typeof providerId === "string" && providerId.startsWith(CUSTOM_EMBEDDING_PREFIX);
|
|
}
|
|
|
|
// All providers (combined)
|
|
export const AI_PROVIDERS = { ...FREE_PROVIDERS, ...FREE_TIER_PROVIDERS, ...OAUTH_PROVIDERS, ...APIKEY_PROVIDERS, ...WEB_COOKIE_PROVIDERS };
|
|
|
|
// Auth methods
|
|
export const AUTH_METHODS = {
|
|
oauth: { id: "oauth" },
|
|
apikey: { id: "apikey" },
|
|
cookie: { id: "cookie" },
|
|
};
|
|
|
|
// Helper: Get provider by alias
|
|
export function getProviderByAlias(alias) {
|
|
for (const provider of Object.values(AI_PROVIDERS)) {
|
|
if (provider.alias === alias || provider.id === alias) {
|
|
return provider;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Helper: Get provider ID from alias
|
|
export function resolveProviderId(aliasOrId) {
|
|
const provider = getProviderByAlias(aliasOrId);
|
|
return provider?.id || aliasOrId;
|
|
}
|
|
|
|
// Helper: Get alias from provider ID
|
|
export function getProviderAlias(providerId) {
|
|
const provider = AI_PROVIDERS[providerId];
|
|
return provider?.alias || providerId;
|
|
}
|
|
|
|
// Alias to ID mapping (for quick lookup)
|
|
export const ALIAS_TO_ID = Object.values(AI_PROVIDERS).reduce((acc, p) => {
|
|
acc[p.alias] = p.id;
|
|
return acc;
|
|
}, {});
|
|
|
|
// ID to Alias mapping
|
|
export const ID_TO_ALIAS = Object.values(AI_PROVIDERS).reduce((acc, p) => {
|
|
acc[p.id] = p.alias;
|
|
return acc;
|
|
}, {});
|
|
|
|
// Helper: Get providers by service kind (e.g. "tts", "embedding", "image")
|
|
// Providers without serviceKinds default to ["llm"]
|
|
export function getProvidersByKind(kind) {
|
|
return Object.values(AI_PROVIDERS)
|
|
.filter((p) => {
|
|
const kinds = p.serviceKinds ?? ["llm"];
|
|
if (!kinds.includes(kind)) return false;
|
|
if (p.hidden) return false;
|
|
if (p.hiddenKinds?.includes(kind)) return false;
|
|
return true;
|
|
})
|
|
.sort((a, b) => (a.mediaPriority ?? 100) - (b.mediaPriority ?? 100));
|
|
}
|
|
|
|
// Derive từ registry features flags
|
|
export const USAGE_SUPPORTED_PROVIDERS = REGISTRY
|
|
.filter(r => r.features?.usage)
|
|
.map(r => r.id);
|
|
|
|
export const USAGE_APIKEY_PROVIDERS = REGISTRY
|
|
.filter(r => r.features?.usageApikey)
|
|
.map(r => r.id);
|