From 9da0bd6243c7a75d3efd0d82b5029ee0c4e8a7b6 Mon Sep 17 00:00:00 2001 From: Kariseven323 <2724283447@qq.com> Date: Fri, 28 Nov 2025 23:54:53 +0800 Subject: [PATCH 1/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20fetchProviderMo?= =?UTF-8?q?dels=20=E4=B8=AD=E7=9A=84=20TypeScript=20=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 logger.error 中 unknown 类型参数问题 - 为 isClientAbortError 添加类型检查 --- src/actions/providers.ts | 315 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 314 insertions(+), 1 deletion(-) diff --git a/src/actions/providers.ts b/src/actions/providers.ts index 62aae4c8e..0c790f7a4 100644 --- a/src/actions/providers.ts +++ b/src/actions/providers.ts @@ -2449,7 +2449,7 @@ export async function testProviderUnified(data: UnifiedTestArgs): Promise; +}; + +/** + * Gemini 模型列表响应类型 + */ +type GeminiModelsResponse = { + models: Array<{ + name: string; + displayName?: string; + description?: string; + supportedGenerationMethods?: string[]; + }>; +}; + +/** + * 从供应商 API 获取可用模型列表 + * + * 支持: + * - OpenAI Compatible / Codex: GET /v1/models + * - Claude / Claude-Auth: GET /v1/models (部分中继服务支持) + * - Gemini / Gemini-CLI: GET /v1beta/models + */ +export async function fetchProviderModels( + data: FetchModelsArgs +): Promise> { + const session = await getSession(); + if (!session || session.user.role !== "admin") { + return { ok: false, error: "无权限执行此操作" }; + } + + // 验证 URL + const urlValidation = validateProviderUrlForConnectivity(data.providerUrl); + if (!urlValidation.valid) { + return { + ok: false, + error: urlValidation.error.message, + }; + } + + // 如果提供了代理 URL,验证代理 URL + if (data.proxyUrl && !isValidProxyUrl(data.proxyUrl)) { + return { + ok: false, + error: "代理地址格式无效,支持格式: http://, https://, socks5://, socks4://", + }; + } + + const normalizedUrl = urlValidation.normalizedUrl.replace(/\/$/, ""); + + try { + // 根据供应商类型构建请求配置 + const { endpoint, headers } = getModelsApiConfig( + data.providerType, + data.apiKey, + normalizedUrl + ); + + const url = normalizedUrl + endpoint; + + // 如需要,创建代理代理 + const tempProvider: ProviderProxyConfig = { + id: -1, + name: "fetch-models", + proxyUrl: data.proxyUrl ?? null, + proxyFallbackToDirect: data.proxyFallbackToDirect ?? false, + }; + + const proxyConfig = createProxyAgentForProvider(tempProvider, url); + + interface UndiciFetchOptions extends RequestInit { + dispatcher?: unknown; + } + + const init: UndiciFetchOptions = { + method: "GET", + headers: { + ...headers, + Accept: "application/json", + }, + signal: AbortSignal.timeout(API_TEST_CONFIG.TIMEOUT_MS), + }; + + if (proxyConfig) { + init.dispatcher = proxyConfig.agent; + } + + logger.debug("fetchProviderModels: Fetching models", { + providerType: data.providerType, + endpoint, + url: url.replace(/:\/\/[^@]*@/, "://***@"), + }); + + const response = await fetch(url, init); + + if (!response.ok) { + const errorText = await response.text(); + let errorDetail: string | undefined; + + try { + const errorJson = JSON.parse(errorText); + errorDetail = extractErrorMessage(errorJson); + } catch { + errorDetail = errorText.substring(0, 200); + } + + logger.warn("fetchProviderModels: API error", { + status: response.status, + errorDetail, + }); + + return { + ok: false, + error: `获取模型列表失败: HTTP ${response.status}${errorDetail ? ` - ${errorDetail}` : ""}`, + }; + } + + const responseData = await response.json(); + + // 根据供应商类型解析模型 + const models = parseModelsResponse(data.providerType, responseData); + + if (models.length === 0) { + return { + ok: false, + error: "未找到可用模型", + }; + } + + logger.info("fetchProviderModels: Success", { + providerType: data.providerType, + modelCount: models.length, + }); + + return { + ok: true, + data: { models }, + }; + } catch (error) { + logger.error("fetchProviderModels error", { + error: error instanceof Error ? error.message : String(error) + }); + + if (error instanceof Error && isClientAbortError(error)) { + return { + ok: false, + error: "请求超时,请检查网络连接或供应商地址", + }; + } + + return { + ok: false, + error: error instanceof Error ? error.message : "获取模型列表失败", + }; + } +} + +/** + * 根据供应商类型获取模型 API 配置 + */ +function getModelsApiConfig( + providerType: ProviderType, + apiKey: string, + providerUrl: string +): { endpoint: string; headers: Record } { + switch (providerType) { + case "gemini": + case "gemini-cli": { + // Gemini 使用 /v1beta/models 端点 + // 检查 API 密钥是否为 JSON 凭证(服务账户) + const isJsonCreds = apiKey.trim().startsWith("{"); + if (isJsonCreds) { + // 对于 JSON 凭证,我们需要先获取访问令牌 + // 为简单起见,现在假设使用 API 密钥认证 + return { + endpoint: "/v1beta/models", + headers: { + "x-goog-api-key": apiKey, + }, + }; + } + return { + endpoint: `/v1beta/models?key=${apiKey}`, + headers: {}, + }; + } + + case "claude": + case "claude-auth": { + // Claude 中继服务可能支持 /v1/models + const hostname = getHostnameFromUrl(providerUrl); + const isOfficialAnthropic = hostname + ? hostname.endsWith("anthropic.com") || hostname.endsWith("claude.ai") + : false; + + const headers: Record = { + "anthropic-version": "2023-06-01", + }; + + if (isOfficialAnthropic) { + headers["x-api-key"] = apiKey; + } else { + // 对于中继服务,尝试两种认证方法 + headers["x-api-key"] = apiKey; + headers["Authorization"] = `Bearer ${apiKey}`; + } + + return { + endpoint: "/v1/models", + headers, + }; + } + + case "openai-compatible": + case "codex": + default: { + // OpenAI 兼容服务使用 /v1/models + return { + endpoint: "/v1/models", + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }; + } + } +} + +/** + * 根据供应商类型解析模型响应 + */ +function parseModelsResponse(providerType: ProviderType, responseData: unknown): string[] { + if (!responseData || typeof responseData !== "object") { + return []; + } + + const data = responseData as Record; + + // Gemini 格式: { models: [{ name: "models/gemini-pro", ... }] } + if (providerType === "gemini" || providerType === "gemini-cli") { + const geminiResponse = data as GeminiModelsResponse; + if (Array.isArray(geminiResponse.models)) { + return geminiResponse.models + .map((model) => { + // Gemini 模型名称类似于 "models/gemini-pro",仅提取模型名称 + const name = model.name || ""; + return name.startsWith("models/") ? name.slice(7) : name; + }) + .filter((name) => name.length > 0) + .sort(); + } + return []; + } + + // OpenAI 格式: { object: "list", data: [{ id: "gpt-4", ... }] } + const openaiResponse = data as OpenAIModelsResponse; + if (Array.isArray(openaiResponse.data)) { + return openaiResponse.data + .map((model) => model.id) + .filter((id) => typeof id === "string" && id.length > 0) + .sort(); + } + + // 尝试处理其他格式 + // 部分供应商返回 { models: ["model1", "model2"] } + if (Array.isArray(data.models)) { + return (data.models as unknown[]) + .filter((m): m is string => typeof m === "string") + .sort(); + } + + // 部分供应商直接返回数组 + if (Array.isArray(data)) { + return (data as unknown[]) + .map((item) => { + if (typeof item === "string") return item; + if (typeof item === "object" && item !== null) { + const obj = item as Record; + return (obj.id || obj.name || obj.model) as string; + } + return ""; + }) + .filter((name) => typeof name === "string" && name.length > 0) + .sort(); + } + + return []; +} From bdd2186a8b055d9df4250a6c0ad37137ce684366 Mon Sep 17 00:00:00 2001 From: Kariseven323 <2724283447@qq.com> Date: Fri, 28 Nov 2025 23:54:57 +0800 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20FetchModelsBut?= =?UTF-8?q?ton=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现从 provider API 获取模型列表的功能 - 支持代理配置和错误处理 - 添加状态管理和用户反馈 - 注释本地化为中文,保留关键字英文 --- .../_components/forms/fetch-models-button.tsx | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 src/app/[locale]/settings/providers/_components/forms/fetch-models-button.tsx diff --git a/src/app/[locale]/settings/providers/_components/forms/fetch-models-button.tsx b/src/app/[locale]/settings/providers/_components/forms/fetch-models-button.tsx new file mode 100644 index 000000000..aaa4d8ca2 --- /dev/null +++ b/src/app/[locale]/settings/providers/_components/forms/fetch-models-button.tsx @@ -0,0 +1,174 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Loader2, Download, CheckCircle2, XCircle } from "lucide-react"; +import { fetchProviderModels, getUnmaskedProviderKey } from "@/actions/providers"; +import { toast } from "sonner"; +import { useTranslations } from "next-intl"; +import { isValidUrl } from "@/lib/utils/validation"; +import type { ProviderType } from "@/types/provider"; + +interface FetchModelsButtonProps { + providerUrl: string; + apiKey: string; + providerType: ProviderType; + proxyUrl?: string | null; + proxyFallbackToDirect?: boolean; + disabled?: boolean; + providerId?: number; + onModelsLoaded: (models: string[]) => void; +} + +type FetchStatus = "idle" | "loading" | "success" | "error"; + +/** + * 从 provider API 获取可用模型的 Button 组件 + * + * 从 provider 的 /models 端点获取模型列表,并通过 onModelsLoaded + * callback 将结果传递给父组件。 + */ +export function FetchModelsButton({ + providerUrl, + apiKey, + providerType, + proxyUrl, + proxyFallbackToDirect = false, + disabled = false, + providerId, + onModelsLoaded, +}: FetchModelsButtonProps) { + const t = useTranslations("settings.providers.form.fetchModels"); + const [status, setStatus] = useState("idle"); + const [lastFetchCount, setLastFetchCount] = useState(0); + + const handleFetch = async () => { + // 验证 URL + if (!providerUrl.trim()) { + toast.error(t("fillUrlFirst")); + return; + } + + if (!isValidUrl(providerUrl.trim()) || !/^https?:\/\//.test(providerUrl.trim())) { + toast.error(t("invalidUrl")); + return; + } + + setStatus("loading"); + + try { + // 解析 API key:优先使用表单输入,如果提供了 providerId 则回退到数据库 + let resolvedKey = apiKey.trim(); + + if (!resolvedKey && providerId) { + const result = await getUnmaskedProviderKey(providerId); + if (!result.ok) { + toast.error(result.error || t("fillKeyFirst")); + setStatus("error"); + return; + } + + if (!result.data?.key) { + toast.error(t("fillKeyFirst")); + setStatus("error"); + return; + } + + resolvedKey = result.data.key; + } + + if (!resolvedKey) { + toast.error(t("fillKeyFirst")); + setStatus("error"); + return; + } + + // 从 provider 获取模型 + const response = await fetchProviderModels({ + providerUrl: providerUrl.trim(), + apiKey: resolvedKey, + providerType, + proxyUrl: proxyUrl?.trim() || null, + proxyFallbackToDirect, + }); + + if (!response.ok) { + toast.error(response.error || t("fetchFailed")); + setStatus("error"); + return; + } + + if (!response.data?.models || response.data.models.length === 0) { + toast.warning(t("noModelsFound")); + setStatus("error"); + return; + } + + const { models } = response.data; + setLastFetchCount(models.length); + setStatus("success"); + + // 通知父组件 + onModelsLoaded(models); + + toast.success(t("fetchSuccess"), { + description: t("modelsFound", { count: models.length }), + }); + + // 3 秒后重置状态 + setTimeout(() => { + setStatus("idle"); + }, 3000); + } catch (error) { + console.error("Fetch models failed:", error); + toast.error(t("fetchFailed")); + setStatus("error"); + } + }; + + const getButtonContent = () => { + switch (status) { + case "loading": + return ( + <> + + {t("fetching")} + + ); + case "success": + return ( + <> + + {t("fetchedCount", { count: lastFetchCount })} + + ); + case "error": + return ( + <> + + {t("fetchFailed")} + + ); + default: + return ( + <> + + {t("fetchModels")} + + ); + } + }; + + return ( + + ); +} From 4c2879fb573b293c21d053eabf3e31ca6e89f091 Mon Sep 17 00:00:00 2001 From: Kariseven323 <2724283447@qq.com> Date: Fri, 28 Nov 2025 23:55:02 +0800 Subject: [PATCH 3/9] =?UTF-8?q?i18n:=20=E6=B7=BB=E5=8A=A0=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E8=8E=B7=E5=8F=96=E5=8A=9F=E8=83=BD=E7=9A=84=E5=9B=BD?= =?UTF-8?q?=E9=99=85=E5=8C=96=E6=96=87=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 FetchModelsButton 组件添加中英文翻译 - 包含加载状态、成功和错误消息 --- messages/en/settings.json | 12 ++++++++++++ messages/zh-CN/settings.json | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/messages/en/settings.json b/messages/en/settings.json index 23541ad95..54ba69d59 100644 --- a/messages/en/settings.json +++ b/messages/en/settings.json @@ -788,6 +788,18 @@ "openai": "OpenAI", "gemini": "Gemini" }, + "fetchModels": { + "fetchModels": "Fetch Models", + "fetching": "Fetching...", + "fetchSuccess": "Fetch Success", + "fetchFailed": "Fetch Failed", + "fetchedCount": "Fetched {count}", + "modelsFound": "Found {count} available models", + "noModelsFound": "No models found", + "fillUrlFirst": "Please fill in provider URL first", + "invalidUrl": "Invalid provider URL, only http/https supported", + "fillKeyFirst": "Please fill in API key first" + }, "modelRedirect": { "currentRules": "Current Rules ({count})", "addNewRule": "Add New Rule", diff --git a/messages/zh-CN/settings.json b/messages/zh-CN/settings.json index 7fd6f8212..1c8436d36 100644 --- a/messages/zh-CN/settings.json +++ b/messages/zh-CN/settings.json @@ -393,6 +393,18 @@ "openai": "OpenAI", "gemini": "Gemini" }, + "fetchModels": { + "fetchModels": "获取模型", + "fetching": "获取中...", + "fetchSuccess": "获取成功", + "fetchFailed": "获取失败", + "fetchedCount": "已获取 {count} 个", + "modelsFound": "找到 {count} 个可用模型", + "noModelsFound": "未找到可用模型", + "fillUrlFirst": "请先填写供应商 URL", + "invalidUrl": "供应商 URL 无效,仅支持 http/https", + "fillKeyFirst": "请先填写 API 密钥" + }, "modelRedirect": { "currentRules": "当前规则 ({count})", "addNewRule": "添加新规则", From 3b13c282569ec8836e0d070e111ad890129e26b0 Mon Sep 17 00:00:00 2001 From: Kariseven323 <2724283447@qq.com> Date: Fri, 28 Nov 2025 23:55:07 +0800 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=E9=9B=86=E6=88=90=20FetchModelsBut?= =?UTF-8?q?ton=20=E5=88=B0=20provider=20=E8=A1=A8=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 provider-form.tsx 中添加模型获取按钮 - 更新 model-multi-select.tsx 支持动态模型加载 - 完善用户交互流程 --- .../_components/forms/provider-form.tsx | 45 +++++++++++++------ .../_components/model-multi-select.tsx | 12 ++++- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form.tsx b/src/app/[locale]/settings/providers/_components/forms/provider-form.tsx index d48c8f451..8f3636be9 100644 --- a/src/app/[locale]/settings/providers/_components/forms/provider-form.tsx +++ b/src/app/[locale]/settings/providers/_components/forms/provider-form.tsx @@ -39,6 +39,7 @@ import { ModelMultiSelect } from "../model-multi-select"; import { ModelRedirectEditor } from "../model-redirect-editor"; import { ProxyTestButton } from "./proxy-test-button"; import { ApiTestButton } from "./api-test-button"; +import { FetchModelsButton } from "./fetch-models-button"; import { UrlPreview } from "./url-preview"; import { ChevronDown } from "lucide-react"; import { useTranslations } from "next-intl"; @@ -706,19 +707,37 @@ export function ProviderForm({ - +
+
+ +
+ { + // Merge with existing models, avoiding duplicates + const merged = [...new Set([...allowedModels, ...models])]; + setAllowedModels(merged); + }} + /> +
{allowedModels.length > 0 && (
diff --git a/src/app/[locale]/settings/providers/_components/model-multi-select.tsx b/src/app/[locale]/settings/providers/_components/model-multi-select.tsx index 534313dd1..2ec3af4f6 100644 --- a/src/app/[locale]/settings/providers/_components/model-multi-select.tsx +++ b/src/app/[locale]/settings/providers/_components/model-multi-select.tsx @@ -160,9 +160,17 @@ export function ModelMultiSelect({
- {/* 模型列表(不分组,字母排序) */} + {/* 模型列表(已选中的优先显示) */} - {availableModels.map((model) => ( + {[...availableModels] + .sort((a, b) => { + const aSelected = selectedModels.includes(a); + const bSelected = selectedModels.includes(b); + if (aSelected && !bSelected) return -1; + if (!aSelected && bSelected) return 1; + return 0; + }) + .map((model) => ( Date: Sat, 29 Nov 2025 13:44:37 +0800 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20fetchProviderMo?= =?UTF-8?q?dels=20=E7=9B=B8=E5=85=B3=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 Gemini API key 日志脱敏 (key=xxx 参数) - 修复错误状态不重置问题,3秒后自动恢复 - 修复空模型列表显示错误状态的问题 - 修复模型排序不稳定,添加字母顺序排序 - Gemini JSON 凭证返回明确错误提示 - parseModelsResponse 兼容对象数组格式 --- src/actions/providers.ts | 21 +++++++++---------- .../_components/forms/fetch-models-button.tsx | 9 +++++++- .../_components/model-multi-select.tsx | 2 +- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/actions/providers.ts b/src/actions/providers.ts index 0c790f7a4..16c84dfd3 100644 --- a/src/actions/providers.ts +++ b/src/actions/providers.ts @@ -2683,7 +2683,7 @@ export async function fetchProviderModels( logger.debug("fetchProviderModels: Fetching models", { providerType: data.providerType, endpoint, - url: url.replace(/:\/\/[^@]*@/, "://***@"), + url: url.replace(/key=([^&]*)/gi, "key=***").replace(/:\/\/[^@]*@/, "://***@"), }); const response = await fetch(url, init); @@ -2765,14 +2765,7 @@ function getModelsApiConfig( // 检查 API 密钥是否为 JSON 凭证(服务账户) const isJsonCreds = apiKey.trim().startsWith("{"); if (isJsonCreds) { - // 对于 JSON 凭证,我们需要先获取访问令牌 - // 为简单起见,现在假设使用 API 密钥认证 - return { - endpoint: "/v1beta/models", - headers: { - "x-goog-api-key": apiKey, - }, - }; + throw new Error("Gemini JSON 凭证暂不支持获取模型列表,请使用 API Key"); } return { endpoint: `/v1beta/models?key=${apiKey}`, @@ -2856,10 +2849,16 @@ function parseModelsResponse(providerType: ProviderType, responseData: unknown): } // 尝试处理其他格式 - // 部分供应商返回 { models: ["model1", "model2"] } + // 部分供应商返回 { models: ["model1", "model2"] } 或 { models: [{id: "model1"}, ...] } if (Array.isArray(data.models)) { return (data.models as unknown[]) - .filter((m): m is string => typeof m === "string") + .map((m) => { + if (typeof m === "string") return m; + if (m && typeof m === "object" && "id" in m) return (m as { id: string }).id; + if (m && typeof m === "object" && "name" in m) return (m as { name: string }).name; + return null; + }) + .filter((id): id is string => typeof id === "string" && id.length > 0) .sort(); } diff --git a/src/app/[locale]/settings/providers/_components/forms/fetch-models-button.tsx b/src/app/[locale]/settings/providers/_components/forms/fetch-models-button.tsx index aaa4d8ca2..31bf752aa 100644 --- a/src/app/[locale]/settings/providers/_components/forms/fetch-models-button.tsx +++ b/src/app/[locale]/settings/providers/_components/forms/fetch-models-button.tsx @@ -65,12 +65,14 @@ export function FetchModelsButton({ if (!result.ok) { toast.error(result.error || t("fillKeyFirst")); setStatus("error"); + setTimeout(() => setStatus("idle"), 3000); return; } if (!result.data?.key) { toast.error(t("fillKeyFirst")); setStatus("error"); + setTimeout(() => setStatus("idle"), 3000); return; } @@ -80,6 +82,7 @@ export function FetchModelsButton({ if (!resolvedKey) { toast.error(t("fillKeyFirst")); setStatus("error"); + setTimeout(() => setStatus("idle"), 3000); return; } @@ -95,12 +98,15 @@ export function FetchModelsButton({ if (!response.ok) { toast.error(response.error || t("fetchFailed")); setStatus("error"); + setTimeout(() => setStatus("idle"), 3000); return; } if (!response.data?.models || response.data.models.length === 0) { toast.warning(t("noModelsFound")); - setStatus("error"); + setStatus("success"); + setLastFetchCount(0); + setTimeout(() => setStatus("idle"), 3000); return; } @@ -123,6 +129,7 @@ export function FetchModelsButton({ console.error("Fetch models failed:", error); toast.error(t("fetchFailed")); setStatus("error"); + setTimeout(() => setStatus("idle"), 3000); } }; diff --git a/src/app/[locale]/settings/providers/_components/model-multi-select.tsx b/src/app/[locale]/settings/providers/_components/model-multi-select.tsx index 2ec3af4f6..c1a742ee5 100644 --- a/src/app/[locale]/settings/providers/_components/model-multi-select.tsx +++ b/src/app/[locale]/settings/providers/_components/model-multi-select.tsx @@ -168,7 +168,7 @@ export function ModelMultiSelect({ const bSelected = selectedModels.includes(b); if (aSelected && !bSelected) return -1; if (!aSelected && bSelected) return 1; - return 0; + return a.localeCompare(b); }) .map((model) => ( Date: Sat, 29 Nov 2025 13:59:07 +0800 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20=E5=88=87=E6=8D=A2=20providerType=20?= =?UTF-8?q?=E6=97=B6=E6=B8=85=E7=A9=BA=E6=A8=A1=E5=9E=8B=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../settings/providers/_components/forms/provider-form.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form.tsx b/src/app/[locale]/settings/providers/_components/forms/provider-form.tsx index 8f3636be9..81a4edc75 100644 --- a/src/app/[locale]/settings/providers/_components/forms/provider-form.tsx +++ b/src/app/[locale]/settings/providers/_components/forms/provider-form.tsx @@ -610,7 +610,10 @@ export function ProviderForm({