import { useEffect, useState } from "react"; import { useSettingStore } from "@/store/setting"; import { GEMINI_BASE_URL, OPENROUTER_BASE_URL, OPENAI_BASE_URL, ANTHROPIC_BASE_URL, DEEPSEEK_BASE_URL, XAI_BASE_URL, MISTRAL_BASE_URL, POLLINATIONS_BASE_URL, OLLAMA_BASE_URL, } from "@/constants/urls"; import { multiApiKeyPolling } from "@/utils/model"; import { generateSignature } from "@/utils/signature"; import { completePath } from "@/utils/url"; interface GeminiModel { name: string; description: string; displayName: string; inputTokenLimit: number; maxTemperature?: number; outputTokenLimit: number; temperature?: number; topK?: number; topP?: number; supportedGenerationMethods: string[]; version: string; } interface OllamaModel { name: string; modified_at: string; size: number; digest: string; details: { format?: string; family?: string; families?: string | null; parameter_size?: string; quantization_level?: string; }; } interface OpenAIModel { id: string; object: string; created: number; owned_by: string; } interface MistralModel { id: string; capabilities: { completion_chat: boolean; }; } function useModelList() { const [modelList, setModelList] = useState([]); const provider = useSettingStore((state) => state.provider); useEffect(() => { setModelList([]); }, [provider]); async function fetchWithRetry(url: string, options: RequestInit, retries = 2): Promise { try { const response = await fetch(url, options); if (!response.ok && retries > 0) { await new Promise(resolve => setTimeout(resolve, 1000)); return fetchWithRetry(url, options, retries - 1); } return response; } catch (err) { if (retries > 0) { await new Promise(resolve => setTimeout(resolve, 1000)); return fetchWithRetry(url, options, retries - 1); } throw err; } } async function refresh(provider: string): Promise { try { const state = useSettingStore.getState(); const { accessPassword, mode } = state; const password = accessPassword || process.env.NEXT_PUBLIC_ACCESS_PASSWORD || ""; const accessKey = password ? generateSignature(password, Date.now()) : ""; let url = ""; let headers: Record = {}; if (provider === "google") { const { apiKey = "", apiProxy } = state; if (mode === "local" && !apiKey) return []; const key = multiApiKeyPolling(apiKey); url = mode === "local" ? completePath(apiProxy || GEMINI_BASE_URL, "/v1beta") + "/models" : "/api/ai/google/v1beta/models"; headers = { "x-goog-api-key": mode === "local" ? key : accessKey, }; } else if (provider === "openrouter") { const { openRouterApiKey = "", openRouterApiProxy } = state; if (mode === "local" && !openRouterApiKey) return []; const apiKey = multiApiKeyPolling(openRouterApiKey); url = mode === "local" ? completePath(openRouterApiProxy || OPENROUTER_BASE_URL, "/api/v1") + "/models" : "/api/ai/openrouter/v1/models"; headers = { Authorization: `Bearer ${mode === "local" ? apiKey : accessKey}`, }; } else if (provider === "openai") { const { openAIApiKey = "", openAIApiProxy } = state; if (mode === "local" && !openAIApiKey) return []; const apiKey = multiApiKeyPolling(openAIApiKey); url = mode === "local" ? completePath(openAIApiProxy || OPENAI_BASE_URL, "/v1") + "/models" : "/api/ai/openai/v1/models"; headers = { Authorization: `Bearer ${mode === "local" ? apiKey : accessKey}`, }; } else if (provider === "anthropic") { const { anthropicApiKey = "", anthropicApiProxy } = state; if (mode === "local" && !anthropicApiKey) return []; const apiKey = multiApiKeyPolling(anthropicApiKey); url = mode === "local" ? completePath(anthropicApiProxy || ANTHROPIC_BASE_URL, "/v1") + "/models" : "/api/ai/anthropic/v1/models"; headers = { "x-api-key": mode === "local" ? apiKey : accessKey, "Anthropic-Version": "2023-06-01", }; } else if (provider === "deepseek") { const { deepseekApiKey = "", deepseekApiProxy } = state; if (mode === "local" && !deepseekApiKey) return []; const apiKey = multiApiKeyPolling(deepseekApiKey); url = mode === "local" ? completePath(deepseekApiProxy || DEEPSEEK_BASE_URL, "/v1") + "/models" : "/api/ai/deepseek/v1/models"; headers = { Authorization: `Bearer ${mode === "local" ? apiKey : accessKey}`, }; } else if (provider === "xai") { const { xAIApiKey = "", xAIApiProxy } = state; if (mode === "local" && !xAIApiKey) return []; const apiKey = multiApiKeyPolling(xAIApiKey); url = mode === "local" ? completePath(xAIApiProxy || XAI_BASE_URL, "/v1") + "/models" : "/api/ai/xai/v1/models"; headers = { Authorization: `Bearer ${mode === "local" ? apiKey : accessKey}`, }; } else if (provider === "mistral") { const { mistralApiKey = "", mistralApiProxy } = state; if (mode === "local" && !mistralApiKey) return []; const apiKey = multiApiKeyPolling(mistralApiKey); url = mode === "local" ? completePath(mistralApiProxy || MISTRAL_BASE_URL, "/v1") + "/models" : "/api/ai/mistral/v1/models"; headers = { Authorization: `Bearer ${mode === "local" ? apiKey : accessKey}`, }; } else if (provider === "openaicompatible") { const { openAICompatibleApiKey = "", openAICompatibleApiProxy } = state; if (mode === "local" && !openAICompatibleApiKey) return []; url = mode === "local" ? (openAICompatibleApiProxy ? completePath(openAICompatibleApiProxy, "/v1") + "/models" : "") : "/api/ai/openaicompatible/v1/models"; if (!url) return []; const apiKey = multiApiKeyPolling(openAICompatibleApiKey); headers = { authorization: `Bearer ${mode === "local" ? apiKey : accessKey}`, }; } else if (provider === "pollinations") { const { pollinationsApiProxy } = state; url = mode === "proxy" ? "/api/ai/pollinations/v1/models" : completePath(pollinationsApiProxy || POLLINATIONS_BASE_URL, "/v1") + "/models"; if (mode === "proxy") headers.Authorization = `Bearer ${accessKey}`; } else if (provider === "ollama") { const { ollamaApiProxy } = state; url = mode === "proxy" ? "/api/ai/ollama/api/tags" : completePath(ollamaApiProxy || OLLAMA_BASE_URL, "/api") + "/tags"; if (mode === "proxy") headers.Authorization = `Bearer ${accessKey}`; } if (!url) return []; const response = await fetchWithRetry(url, { headers }).catch((err) => { console.error(`Fetch error for ${provider} at ${url}:`, err); throw err; }); if (!response.ok) { const text = await response.text().catch(() => "No error body"); console.warn(`Fetch failed for ${provider} at ${url} with status ${response.status}: ${text}`); return []; } const rawData = await response.json().catch((err) => { console.error(`JSON parse error for ${provider} at ${url}:`, err); return null; }); if (!rawData) return []; let newModelList: string[] = []; if (provider === "google") { const models = Array.isArray(rawData.models) ? rawData.models : []; newModelList = (models as GeminiModel[]) .filter( (item) => item.name && item.name.startsWith("models/gemini") && Array.isArray(item.supportedGenerationMethods) && item.supportedGenerationMethods.includes("generateContent") ) .map((item) => item.name.replace("models/", "")); } else if (provider === "ollama") { const models = Array.isArray(rawData.models) ? rawData.models : []; newModelList = (models as OllamaModel[]).map((item) => item.name); } else if (["openrouter", "openai", "anthropic", "deepseek", "xai", "mistral", "openaicompatible", "pollinations"].includes(provider)) { const data = Array.isArray(rawData.data) ? rawData.data : []; if (provider === "openai") { newModelList = (data as OpenAIModel[]) .map((item) => item.id) .filter((id) => !(id.startsWith("text") || id.startsWith("tts") || id.startsWith("whisper") || id.startsWith("dall-e"))); } else if (provider === "mistral") { newModelList = (data as MistralModel[]) .filter((item) => item.capabilities?.completion_chat) .map((item) => item.id); } else if (provider === "pollinations") { newModelList = (data as OpenAIModel[]) .map((item) => item.id) .filter((name) => !name.includes("audio")); } else if (provider === "xai") { newModelList = (data as OpenAIModel[]) .map((item) => item.id) .filter((id) => !id.includes("image")); } else { newModelList = (data as { id: string }[]).map((item) => item.id); } } setModelList(newModelList); return newModelList; } catch (e) { console.error(`Failed to refresh model list for ${provider}:`, e); return []; } } return { modelList, refresh, }; } export default useModelList;