Register two web search providers that reuse an existing chat provider's API key instead of requiring their own connection: - ollama-search (POST ollama.com/api/web_search) reuses the `ollama` key - zai-search (POST api.z.ai MCP web_search_prime) reuses the `glm` key A new `credentialFallback` registry field drives this: when a search provider has no connection of its own, the search handler falls back to the linked chat provider's credentials. Also teach getProviderIconSrc to serve .svg logos for providers that ship vector art.
166 lines
6.5 KiB
JavaScript
166 lines
6.5 KiB
JavaScript
// Provider definitions
|
|
import REGISTRY from "open-sse/providers/registry/index.js";
|
|
import { RISK_NOTICE } from "@/shared/constants/providersDisplay";
|
|
|
|
const MEDIA_ENTRY_KEYS = [
|
|
"serviceKinds", "ttsConfig", "sttConfig", "embeddingConfig",
|
|
"imageConfig", "imageToTextConfig", "videoConfig", "musicConfig",
|
|
"searchViaChat", "searchConfig", "fetchConfig", "credentialFallback",
|
|
"modelsFetcher", "mediaPriority", "hiddenKinds",
|
|
];
|
|
|
|
// Build provider UI object from registry entry
|
|
function buildProviderEntry(r) {
|
|
const mediaFields = {};
|
|
if (r.media) Object.assign(mediaFields, r.media);
|
|
for (const k of MEDIA_ENTRY_KEYS) {
|
|
if (r[k] !== undefined) mediaFields[k] = r[k];
|
|
}
|
|
const display = { ...(r.display || {}) };
|
|
if (display.deprecationNotice === "RISK_NOTICE") display.deprecationNotice = RISK_NOTICE;
|
|
return {
|
|
...display,
|
|
id: r.id,
|
|
alias: r.uiAlias || r.alias,
|
|
...(r.hidden ? { hidden: true } : {}),
|
|
...mediaFields,
|
|
...(r.priority !== undefined ? { priority: r.priority } : {}),
|
|
...(r.hasFree ? { hasFree: true } : {}),
|
|
...(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/videos/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.priority ?? a.mediaPriority ?? 999) - (b.priority ?? b.mediaPriority ?? 999));
|
|
}
|
|
|
|
// 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);
|