From 7f6ce72a881d5919f314082826854df26a221b6c Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Thu, 5 Feb 2026 00:52:10 +0800 Subject: [PATCH] feat(proxy): add smart URL path detection for full API endpoints - Detect when base URL already contains API path suffix (/v1/messages, /v1/chat/completions, etc.) - Show direct and proxy request URL previews in endpoint field - Block provider switch when proxy is required but not active - Add API format selector for Codex providers (Responses/Chat) --- src-tauri/src/proxy/providers/claude.rs | 88 ++++++++- src-tauri/src/proxy/providers/codex.rs | 61 ++++++ src/App.tsx | 5 +- .../providers/forms/ClaudeFormFields.tsx | 2 + .../providers/forms/CodexFormFields.tsx | 63 ++++++- .../providers/forms/shared/EndpointField.tsx | 173 +++++++++++++++++- src/hooks/useProviderActions.ts | 167 +++++++++++++---- src/i18n/locales/en.json | 14 +- src/i18n/locales/ja.json | 14 +- src/i18n/locales/zh.json | 14 +- 10 files changed, 555 insertions(+), 46 deletions(-) diff --git a/src-tauri/src/proxy/providers/claude.rs b/src-tauri/src/proxy/providers/claude.rs index ac8b2c8e4..8fe3ee307 100644 --- a/src-tauri/src/proxy/providers/claude.rs +++ b/src-tauri/src/proxy/providers/claude.rs @@ -252,11 +252,28 @@ impl ProviderAdapter for ClaudeAdapter { // 现在 OpenRouter 已推出 Claude Code 兼容接口,因此默认直接透传 endpoint。 // 如需回退旧逻辑,可在 forwarder 中根据 needs_transform 改写 endpoint。 - let mut base = format!( - "{}/{}", - base_url.trim_end_matches('/'), - endpoint.trim_start_matches('/') - ); + let base_trimmed = base_url.trim_end_matches('/'); + let endpoint_trimmed = endpoint.trim_start_matches('/'); + + // 检测 base_url 是否已经以 API 路径结尾(用户填写了完整路径) + // 支持的 API 路径模式:/v1/messages, /messages, /v1/chat/completions, /chat/completions + let api_path_patterns = [ + "/v1/messages", + "/messages", + "/v1/chat/completions", + "/chat/completions", + ]; + + let base_ends_with_api_path = api_path_patterns + .iter() + .any(|pattern| base_trimmed.to_lowercase().ends_with(pattern)); + + // 如果 base_url 已经以 API 路径结尾,直接使用 base_url,不再追加 endpoint + let mut base = if base_ends_with_api_path { + base_trimmed.to_string() + } else { + format!("{base_trimmed}/{endpoint_trimmed}") + }; // 去除重复的 /v1/v1(可能由 base_url 与 endpoint 都带版本导致) while base.contains("/v1/v1") { @@ -266,9 +283,11 @@ impl ProviderAdapter for ClaudeAdapter { // 为 Claude 相关端点添加 ?beta=true 参数 // 这是某些上游服务(如 DuckCoding)验证请求来源的关键参数 // 注:openai_chat 模式下会转发到 /v1/chat/completions,此处也需要保持一致 - if (endpoint.contains("/v1/messages") || endpoint.contains("/v1/chat/completions")) - && !endpoint.contains('?') - { + // 检查最终 URL 是否包含需要 beta 参数的路径,且没有查询参数 + let needs_beta = (base.contains("/v1/messages") || base.contains("/v1/chat/completions")) + && !base.contains('?'); + + if needs_beta { format!("{base}?beta=true") } else { base @@ -513,6 +532,59 @@ mod tests { assert_eq!(url, "https://api.anthropic.com/v1/messages?foo=bar"); } + #[test] + fn test_build_url_full_path_messages() { + let adapter = ClaudeAdapter::new(); + // base_url 已包含完整路径 /v1/messages,不再追加 + let url = adapter.build_url("https://example.com/api/v1/messages", "/v1/messages"); + assert_eq!(url, "https://example.com/api/v1/messages?beta=true"); + } + + #[test] + fn test_build_url_full_path_chat_completions() { + let adapter = ClaudeAdapter::new(); + // base_url 已包含完整路径 /v1/chat/completions,不再追加 + let url = adapter.build_url( + "https://opencode.ai/zen/v1/chat/completions", + "/v1/chat/completions", + ); + assert_eq!(url, "https://opencode.ai/zen/v1/chat/completions?beta=true"); + } + + #[test] + fn test_build_url_full_path_short_suffix() { + let adapter = ClaudeAdapter::new(); + // base_url 以 /messages 结尾(无 /v1 前缀) + let url = adapter.build_url("https://example.com/api/messages", "/v1/messages"); + assert_eq!(url, "https://example.com/api/messages"); + + // base_url 以 /chat/completions 结尾(无 /v1 前缀) + let url2 = adapter.build_url( + "https://example.com/api/chat/completions", + "/v1/chat/completions", + ); + assert_eq!(url2, "https://example.com/api/chat/completions"); + } + + #[test] + fn test_build_url_v1_base_dedup() { + let adapter = ClaudeAdapter::new(); + // base_url 以 /v1 结尾,endpoint 也以 /v1 开头,应该去重 + // 场景:https://integrate.api.nvidia.com/v1 + /v1/chat/completions + let url = adapter.build_url( + "https://integrate.api.nvidia.com/v1", + "/v1/chat/completions", + ); + assert_eq!( + url, + "https://integrate.api.nvidia.com/v1/chat/completions?beta=true" + ); + + // 另一个场景:/v1 + /v1/messages + let url2 = adapter.build_url("https://api.example.com/v1", "/v1/messages"); + assert_eq!(url2, "https://api.example.com/v1/messages?beta=true"); + } + #[test] fn test_needs_transform() { let adapter = ClaudeAdapter::new(); diff --git a/src-tauri/src/proxy/providers/codex.rs b/src-tauri/src/proxy/providers/codex.rs index 02c24bcc2..5e16afb5f 100644 --- a/src-tauri/src/proxy/providers/codex.rs +++ b/src-tauri/src/proxy/providers/codex.rs @@ -141,6 +141,24 @@ impl ProviderAdapter for CodexAdapter { let base_trimmed = base_url.trim_end_matches('/'); let endpoint_trimmed = endpoint.trim_start_matches('/'); + // 检测 base_url 是否已经以 API 路径结尾(用户填写了完整路径) + // 支持的 API 路径模式:/v1/responses, /responses, /v1/chat/completions, /chat/completions + let api_path_patterns = [ + "/v1/responses", + "/responses", + "/v1/chat/completions", + "/chat/completions", + ]; + + let base_ends_with_api_path = api_path_patterns + .iter() + .any(|pattern| base_trimmed.to_lowercase().ends_with(pattern)); + + // 如果 base_url 已经以 API 路径结尾,直接使用 base_url,不再追加 endpoint + if base_ends_with_api_path { + return base_trimmed.to_string(); + } + // OpenAI/Codex 的 base_url 可能是: // - 纯 origin: https://api.openai.com (需要自动补 /v1) // - 已含 /v1: https://api.openai.com/v1 (直接拼接) @@ -268,6 +286,49 @@ mod tests { assert_eq!(url, "https://www.packyapi.com/v1/responses"); } + #[test] + fn test_build_url_full_path_responses() { + let adapter = CodexAdapter::new(); + // base_url 已包含完整路径 /v1/responses,不再追加 + let url = adapter.build_url("https://example.com/v1/responses", "/responses"); + assert_eq!(url, "https://example.com/v1/responses"); + } + + #[test] + fn test_build_url_full_path_chat_completions() { + let adapter = CodexAdapter::new(); + // base_url 已包含完整路径 /v1/chat/completions,不再追加 + let url = adapter.build_url( + "https://example.com/v1/chat/completions", + "/chat/completions", + ); + assert_eq!(url, "https://example.com/v1/chat/completions"); + } + + #[test] + fn test_build_url_full_path_short_suffix() { + let adapter = CodexAdapter::new(); + // base_url 以 /responses 结尾(无 /v1 前缀) + let url = adapter.build_url("https://example.com/api/responses", "/responses"); + assert_eq!(url, "https://example.com/api/responses"); + } + + #[test] + fn test_build_url_v1_base_dedup() { + let adapter = CodexAdapter::new(); + // base_url 以 /v1 结尾,endpoint 也以 /v1 开头,应该去重 + // 场景:https://integrate.api.nvidia.com/v1 + /v1/responses + let url = adapter.build_url("https://integrate.api.nvidia.com/v1", "/v1/responses"); + assert_eq!(url, "https://integrate.api.nvidia.com/v1/responses"); + + // 另一个场景:/v1 + /v1/chat/completions + let url2 = adapter.build_url( + "https://integrate.api.nvidia.com/v1", + "/v1/chat/completions", + ); + assert_eq!(url2, "https://integrate.api.nvidia.com/v1/chat/completions"); + } + // 官方客户端检测测试 #[test] fn test_is_official_client_vscode() { diff --git a/src/App.tsx b/src/App.tsx index de9c6881c..222821aeb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -195,7 +195,10 @@ function App() { switchProvider, deleteProvider, saveUsageScript, - } = useProviderActions(activeApp); + } = useProviderActions(activeApp, { + isProxyRunning, + isTakeoverActive: isCurrentAppTakeoverActive, + }); // 监听来自托盘菜单的切换事件 useEffect(() => { diff --git a/src/components/providers/forms/ClaudeFormFields.tsx b/src/components/providers/forms/ClaudeFormFields.tsx index 699f5aa01..afdc6f715 100644 --- a/src/components/providers/forms/ClaudeFormFields.tsx +++ b/src/components/providers/forms/ClaudeFormFields.tsx @@ -169,6 +169,8 @@ export function ClaudeFormFields({ : t("providerForm.apiHint") } onManageClick={() => onEndpointModalToggle(true)} + appType="claude" + apiFormat={apiFormat} /> )} diff --git a/src/components/providers/forms/CodexFormFields.tsx b/src/components/providers/forms/CodexFormFields.tsx index c527b991f..38abb4f05 100644 --- a/src/components/providers/forms/CodexFormFields.tsx +++ b/src/components/providers/forms/CodexFormFields.tsx @@ -2,11 +2,22 @@ import { useTranslation } from "react-i18next"; import EndpointSpeedTest from "./EndpointSpeedTest"; import { ApiKeySection, EndpointField } from "./shared"; import type { ProviderCategory } from "@/types"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { FormLabel } from "@/components/ui/form"; interface EndpointCandidate { url: string; } +// Codex API 格式类型 +export type CodexApiFormat = "responses" | "chat"; + interface CodexFormFieldsProps { providerId?: string; // API Key @@ -35,6 +46,11 @@ interface CodexFormFieldsProps { // Speed Test Endpoints speedTestEndpoints: EndpointCandidate[]; + + // API Format (for Codex providers) + apiFormat?: CodexApiFormat; + onApiFormatChange?: (format: CodexApiFormat) => void; + shouldShowApiFormatSelector?: boolean; } export function CodexFormFields({ @@ -58,9 +74,21 @@ export function CodexFormFields({ modelName = "", onModelNameChange, speedTestEndpoints, + apiFormat = "responses", + onApiFormatChange, + shouldShowApiFormatSelector = false, }: CodexFormFieldsProps) { const { t } = useTranslation(); + // 根据 API 格式选择提示文本 + const apiHint = + apiFormat === "chat" + ? t("providerForm.codexApiHintChat", { + defaultValue: + "💡 填写兼容 OpenAI Chat Completions 格式的服务端点地址", + }) + : t("providerForm.codexApiHint"); + return ( <> {/* Codex API Key 输入框 */} @@ -84,6 +112,37 @@ export function CodexFormFields({ }} /> + {/* API 格式选择器 */} + {shouldShowApiFormatSelector && onApiFormatChange && ( +
+ + {t("providerForm.apiFormat", { defaultValue: "API 格式" })} + + +

+ {t("providerForm.codexApiFormatHint", { + defaultValue: "选择供应商支持的 API 格式", + })} +

+
+ )} + {/* Codex Base URL 输入框 */} {shouldShowSpeedTest && ( onEndpointModalToggle(true)} + appType="codex" + apiFormat={apiFormat} /> )} diff --git a/src/components/providers/forms/shared/EndpointField.tsx b/src/components/providers/forms/shared/EndpointField.tsx index ccdea7d9a..69ce225c2 100644 --- a/src/components/providers/forms/shared/EndpointField.tsx +++ b/src/components/providers/forms/shared/EndpointField.tsx @@ -1,7 +1,50 @@ +import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { FormLabel } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { Zap } from "lucide-react"; +import { Zap, AlertTriangle, Server, Unplug } from "lucide-react"; +import type { ClaudeApiFormat } from "@/types"; + +// 检测 URL 是否以 API 路径结尾的模式(只检测结尾,不包含 /v1 因为它是正常的 base URL 后缀) +const API_PATH_SUFFIX_PATTERNS = [ + // Claude 完整路径 + "/v1/messages", + "/messages", + // OpenAI Chat Completions 完整路径 + "/v1/chat/completions", + "/chat/completions", + // Codex Responses 完整路径 + "/v1/responses", + "/responses", + // Gemini 完整路径 + "/v1beta/models", +]; + +// 从 URL 中提取路径部分(移除查询参数和尾部斜杠) +function extractUrlPath(url: string): string { + try { + // 移除查询参数 + const pathPart = url.split("?")[0]; + // 移除尾部斜杠并转为小写 + return pathPart.replace(/\/+$/, "").toLowerCase(); + } catch { + return url.toLowerCase(); + } +} + +// 构建 URL 并去重 /v1/v1 +function buildUrl(base: string, suffix: string): string { + const trimmedBase = base.trim().replace(/\/+$/, ""); + let url = `${trimmedBase}${suffix}`; + // 去重 /v1/v1 模式 + while (url.includes("/v1/v1")) { + url = url.replace("/v1/v1", "/v1"); + } + return url; +} + +type AppType = "claude" | "codex" | "gemini"; +type CodexApiFormat = "responses" | "chat"; interface EndpointFieldProps { id: string; @@ -13,6 +56,11 @@ interface EndpointFieldProps { showManageButton?: boolean; onManageClick?: () => void; manageButtonLabel?: string; + // 新增:应用类型和 API 格式 + appType?: AppType; + apiFormat?: ClaudeApiFormat | CodexApiFormat; + // 是否显示请求地址预览 + showUrlPreview?: boolean; } export function EndpointField({ @@ -25,6 +73,9 @@ export function EndpointField({ showManageButton = true, onManageClick, manageButtonLabel, + appType, + apiFormat, + showUrlPreview = true, }: EndpointFieldProps) { const { t } = useTranslation(); @@ -32,6 +83,61 @@ export function EndpointField({ defaultValue: "管理和测速", }); + // 根据 appType 和 apiFormat 计算直连和代理后缀 + const suffixes = useMemo(() => { + if (!appType) return null; + + if (appType === "claude") { + // Claude: 直连固定 /v1/messages,代理根据 apiFormat 决定 + return { + direct: "/v1/messages", + proxy: + apiFormat === "openai_chat" ? "/v1/chat/completions" : "/v1/messages", + }; + } + + if (appType === "codex") { + // Codex: 直连固定 /responses(base URL 已含 /v1),代理根据 apiFormat 决定 + return { + direct: "/responses", + proxy: apiFormat === "chat" ? "/chat/completions" : "/responses", + }; + } + + if (appType === "gemini") { + // Gemini: 两种模式相同 + return { + direct: "/v1beta/models", + proxy: "/v1beta/models", + }; + } + + return null; + }, [appType, apiFormat]); + + // 检测 URL 是否已以 API 路径结尾(全链接) + const urlEndsWithApiPath = useMemo(() => { + if (!value) return false; + const urlPath = extractUrlPath(value); + return API_PATH_SUFFIX_PATTERNS.some((pattern) => + urlPath.endsWith(pattern.toLowerCase()), + ); + }, [value]); + + // 构建直连模式请求 URL + const directUrlPreview = useMemo(() => { + if (!value || !suffixes) return null; + if (urlEndsWithApiPath) return value; + return buildUrl(value, suffixes.direct); + }, [value, suffixes, urlEndsWithApiPath]); + + // 构建代理模式请求 URL + const proxyUrlPreview = useMemo(() => { + if (!value || !suffixes) return null; + if (urlEndsWithApiPath) return value; + return buildUrl(value, suffixes.proxy); + }, [value, suffixes, urlEndsWithApiPath]); + return (
@@ -55,6 +161,71 @@ export function EndpointField({ placeholder={placeholder} autoComplete="off" /> + + {/* 请求地址预览 */} + {showUrlPreview && directUrlPreview && ( +
+ {/* 直连模式请求地址 */} +
+

+ + {t("providerForm.directRequestUrl", { + defaultValue: "直连请求地址:", + })} +

+

+ {directUrlPreview} +

+

+ {t("providerForm.directRequestUrlDesc", { + defaultValue: "不开启代理时,客户端直接请求此地址", + })} +

+
+ + {/* 代理模式请求地址 */} + {proxyUrlPreview && ( +
+

+ + {t("providerForm.proxyRequestUrl", { + defaultValue: "代理请求地址:", + })} +

+

+ {proxyUrlPreview} +

+

+ {t("providerForm.proxyRequestUrlDesc", { + defaultValue: + "开启代理后,代理服务会将请求转发到此地址(支持格式转换)", + })} +

+
+ )} +
+ )} + + {/* 全链接警告 */} + {urlEndsWithApiPath && ( +
+ +
+

+ {t("providerForm.fullUrlWarningTitle", { + defaultValue: "检测到完整 API 路径", + })} +

+

+ {t("providerForm.fullUrlWarning", { + defaultValue: + "填写了包含 API 路径的完整地址,此配置仅在代理模式下生效。直连模式下请只填写基础地址。", + })} +

+
+
+ )} + {hint ? (

{hint}

diff --git a/src/hooks/useProviderActions.ts b/src/hooks/useProviderActions.ts index ede55f8a2..c49c6ba65 100644 --- a/src/hooks/useProviderActions.ts +++ b/src/hooks/useProviderActions.ts @@ -12,11 +12,96 @@ import { } from "@/lib/query"; import { extractErrorMessage } from "@/utils/errorUtils"; +// API 路径后缀模式,用于检测是否是全链接 +const API_PATH_SUFFIX_PATTERNS = [ + "/v1/messages", + "/messages", + "/v1/chat/completions", + "/chat/completions", + "/v1/responses", + "/responses", + "/v1beta/models", +]; + +/** + * 从 Codex TOML 配置字符串中提取 base_url + */ +function extractCodexBaseUrlFromToml(tomlConfig: string | null): string | null { + if (!tomlConfig || typeof tomlConfig !== "string") return null; + // 匹配 base_url = "xxx" 或 base_url = 'xxx' + const match = tomlConfig.match(/base_url\s*=\s*(['"])([^'"]+)\1/); + return match?.[2]?.trim() || null; +} + +/** + * 从 provider 配置中提取 base URL + */ +function extractBaseUrl(provider: Provider, appId: AppId): string | null { + try { + const config = provider.settingsConfig; + if (!config) return null; + + if (appId === "claude") { + // Claude: env.ANTHROPIC_BASE_URL + const envUrl = config?.env?.ANTHROPIC_BASE_URL; + return typeof envUrl === "string" ? envUrl.trim() : null; + } + + if (appId === "codex") { + // Codex: base_url 存储在 config 字段中(TOML 字符串) + const tomlConfig = config?.config; + if (typeof tomlConfig === "string") { + return extractCodexBaseUrlFromToml(tomlConfig); + } + // 回退:尝试直接获取 base_url 字段 + const baseUrl = config?.base_url; + return typeof baseUrl === "string" ? baseUrl.trim() : null; + } + + if (appId === "gemini") { + // Gemini: env.GOOGLE_GEMINI_BASE_URL 或 GEMINI_API_BASE + const envUrl = config?.env?.GOOGLE_GEMINI_BASE_URL; + if (typeof envUrl === "string") return envUrl.trim(); + const baseUrl = config?.GEMINI_API_BASE || config?.base_url; + return typeof baseUrl === "string" ? baseUrl.trim() : null; + } + + return null; + } catch { + return null; + } +} + +/** + * 检测 URL 是否以 API 路径后缀结尾(全链接) + */ +function isFullApiUrl(url: string | null): boolean { + if (!url) return false; + + // 移除查询参数和尾部斜杠 + const pathPart = url.split("?")[0].replace(/\/+$/, "").toLowerCase(); + + return API_PATH_SUFFIX_PATTERNS.some((pattern) => + pathPart.endsWith(pattern.toLowerCase()), + ); +} + +interface UseProviderActionsOptions { + /** 代理服务是否正在运行 */ + isProxyRunning?: boolean; + /** 当前应用的代理接管是否激活 */ + isTakeoverActive?: boolean; +} + /** * Hook for managing provider actions (add, update, delete, switch) * Extracts business logic from App.tsx */ -export function useProviderActions(activeApp: AppId) { +export function useProviderActions( + activeApp: AppId, + options: UseProviderActionsOptions = {}, +) { + const { isProxyRunning = false, isTakeoverActive = false } = options; const { t } = useTranslation(); const queryClient = useQueryClient(); @@ -81,46 +166,64 @@ export function useProviderActions(activeApp: AppId) { // 切换供应商 const switchProvider = useCallback( async (provider: Provider) => { + // 检测是否是全链接配置(URL 以 API 路径结尾) + const baseUrl = extractBaseUrl(provider, activeApp); + const hasFullApiUrl = isFullApiUrl(baseUrl); + + // 检测是否是 Claude OpenAI Chat 格式(需要代理进行格式转换) + const isClaudeOpenAIChatFormat = + activeApp === "claude" && + provider.category !== "official" && + provider.meta?.apiFormat === "openai_chat"; + + // 如果需要代理但代理未激活,阻止切换并提示 + const needsProxy = hasFullApiUrl || isClaudeOpenAIChatFormat; + if (needsProxy && !(isProxyRunning && isTakeoverActive)) { + const message = isClaudeOpenAIChatFormat + ? t("notifications.openAIChatFormatRequiresProxy", { + defaultValue: + "此供应商使用 OpenAI Chat 格式,需要开启代理服务进行格式转换才能正常使用。请先开启代理并接管当前应用。", + }) + : t("notifications.fullUrlRequiresProxy", { + defaultValue: + "此供应商配置了完整 API 路径,需要开启代理服务才能正常使用。请先开启代理并接管当前应用。", + }); + + toast.warning(message, { + duration: 6000, + closeButton: true, + }); + return; // 阻止切换 + } + try { await switchProviderMutation.mutateAsync(provider.id); await syncClaudePlugin(provider); - // 根据供应商类型显示不同的成功提示 - if ( - activeApp === "claude" && - provider.category !== "official" && - provider.meta?.apiFormat === "openai_chat" - ) { - // OpenAI Chat 格式供应商:显示代理提示 - toast.info( - t("notifications.openAIChatFormatHint", { - defaultValue: - "此供应商使用 OpenAI Chat 格式,需要开启代理服务才能正常使用", - }), - { - duration: 5000, - closeButton: true, - }, - ); - } else { - // 普通供应商:显示切换成功 - // OpenCode: show "added to config" message instead of "switched" - const messageKey = - activeApp === "opencode" - ? "notifications.addToConfigSuccess" - : "notifications.switchSuccess"; - const defaultMessage = - activeApp === "opencode" ? "已添加到配置" : "切换成功!"; + // 普通供应商:显示切换成功 + // OpenCode: show "added to config" message instead of "switched" + const messageKey = + activeApp === "opencode" + ? "notifications.addToConfigSuccess" + : "notifications.switchSuccess"; + const defaultMessage = + activeApp === "opencode" ? "已添加到配置" : "切换成功!"; - toast.success(t(messageKey, { defaultValue: defaultMessage }), { - closeButton: true, - }); - } + toast.success(t(messageKey, { defaultValue: defaultMessage }), { + closeButton: true, + }); } catch { // 错误提示由 mutation 处理 } }, - [switchProviderMutation, syncClaudePlugin, activeApp, t], + [ + switchProviderMutation, + syncClaudePlugin, + activeApp, + t, + isProxyRunning, + isTakeoverActive, + ], ); // 删除供应商 diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 2fd797aba..2088ecb2a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -156,7 +156,9 @@ "deleteFailed": "Failed to delete provider: {{error}}", "settingsSaved": "Settings saved", "settingsSaveFailed": "Failed to save settings: {{error}}", - "openAIChatFormatHint": "This provider uses OpenAI Chat format and requires the proxy service to be enabled" + "openAIChatFormatHint": "This provider uses OpenAI Chat format and requires the proxy service to be enabled", + "openAIChatFormatRequiresProxy": "This provider uses OpenAI Chat format and requires the proxy service for format conversion. Please enable the proxy and takeover the current app first.", + "fullUrlRequiresProxy": "This provider is configured with a full API path and requires the proxy service to work properly. Please enable the proxy and takeover the current app first." }, "confirm": { "deleteProvider": "Delete Provider", @@ -489,6 +491,16 @@ "apiHint": "💡 Fill in Claude API compatible service endpoint, avoid trailing slash", "apiHintOAI": "💡 Fill in OpenAI Chat Completions compatible service endpoint, avoid trailing slash", "codexApiHint": "💡 Fill in service endpoint compatible with OpenAI Response format", + "codexApiHintChat": "💡 Fill in service endpoint compatible with OpenAI Chat Completions format", + "codexApiFormatResponses": "OpenAI Responses (Default)", + "codexApiFormatChat": "OpenAI Chat Completions", + "codexApiFormatHint": "Select the API format supported by the provider", + "directRequestUrl": "Direct request URL:", + "directRequestUrlDesc": "When proxy is disabled, the client requests this URL directly", + "proxyRequestUrl": "Proxy request URL:", + "proxyRequestUrlDesc": "When proxy is enabled, requests are forwarded to this URL (with format conversion)", + "fullUrlWarningTitle": "Full API path detected", + "fullUrlWarning": "The URL contains a full API path. This configuration only works in proxy mode. For direct connection mode, please enter only the base URL.", "fillSupplierName": "Please fill in provider name", "fillConfigContent": "Please fill in configuration content", "fillParameter": "Please fill in {{label}}", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 371496c54..ee4b854ac 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -156,7 +156,9 @@ "deleteFailed": "プロバイダーの削除に失敗しました: {{error}}", "settingsSaved": "設定を保存しました", "settingsSaveFailed": "設定の保存に失敗しました: {{error}}", - "openAIChatFormatHint": "このプロバイダーは OpenAI Chat フォーマットを使用しており、プロキシサービスの有効化が必要です" + "openAIChatFormatHint": "このプロバイダーは OpenAI Chat フォーマットを使用しており、プロキシサービスの有効化が必要です", + "openAIChatFormatRequiresProxy": "このプロバイダーは OpenAI Chat フォーマットを使用しており、フォーマット変換のためにプロキシサービスが必要です。先にプロキシを有効にして、現在のアプリをテイクオーバーしてください。", + "fullUrlRequiresProxy": "このプロバイダーは完全な API パスで構成されており、プロキシサービスが必要です。先にプロキシを有効にして、現在のアプリをテイクオーバーしてください。" }, "confirm": { "deleteProvider": "プロバイダーを削除", @@ -489,6 +491,16 @@ "apiHint": "💡 Claude API 互換サービスのエンドポイントを入力してください。末尾にスラッシュを付けないでください", "apiHintOAI": "💡 OpenAI Chat Completions 互換サービスのエンドポイントを入力してください。末尾にスラッシュを付けないでください", "codexApiHint": "💡 OpenAI Response 互換のサービスエンドポイントを入力してください", + "codexApiHintChat": "💡 OpenAI Chat Completions 互換のサービスエンドポイントを入力してください", + "codexApiFormatResponses": "OpenAI Responses(デフォルト)", + "codexApiFormatChat": "OpenAI Chat Completions", + "codexApiFormatHint": "プロバイダーがサポートする API フォーマットを選択", + "directRequestUrl": "直接接続リクエスト URL:", + "directRequestUrlDesc": "プロキシ無効時、クライアントはこの URL に直接リクエストします", + "proxyRequestUrl": "プロキシリクエスト URL:", + "proxyRequestUrlDesc": "プロキシ有効時、リクエストはこの URL に転送されます(フォーマット変換対応)", + "fullUrlWarningTitle": "完全な API パスを検出", + "fullUrlWarning": "URL には完全な API パスが含まれています。この設定はプロキシモードでのみ有効です。直接接続モードではベース URL のみを入力してください。", "fillSupplierName": "プロバイダー名を入力してください", "fillConfigContent": "設定内容を入力してください", "fillParameter": "{{label}} を入力してください", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index fa7a1c88f..8205a169b 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -156,7 +156,9 @@ "deleteFailed": "删除供应商失败:{{error}}", "settingsSaved": "设置已保存", "settingsSaveFailed": "保存设置失败:{{error}}", - "openAIChatFormatHint": "此供应商使用 OpenAI Chat 格式,需要开启代理服务才能正常使用" + "openAIChatFormatHint": "此供应商使用 OpenAI Chat 格式,需要开启代理服务才能正常使用", + "openAIChatFormatRequiresProxy": "此供应商使用 OpenAI Chat 格式,需要开启代理服务进行格式转换才能正常使用。请先开启代理并接管当前应用。", + "fullUrlRequiresProxy": "此供应商配置了完整 API 路径,需要开启代理服务才能正常使用。请先开启代理并接管当前应用。" }, "confirm": { "deleteProvider": "删除供应商", @@ -489,6 +491,16 @@ "apiHint": "💡 填写兼容 Claude API 的服务端点地址,不要以斜杠结尾", "apiHintOAI": "💡 填写兼容 OpenAI Chat Completions 的服务端点地址,不要以斜杠结尾", "codexApiHint": "💡 填写兼容 OpenAI Response 格式的服务端点地址", + "codexApiHintChat": "💡 填写兼容 OpenAI Chat Completions 格式的服务端点地址", + "codexApiFormatResponses": "OpenAI Responses (默认)", + "codexApiFormatChat": "OpenAI Chat Completions", + "codexApiFormatHint": "选择供应商支持的 API 格式", + "directRequestUrl": "直连请求地址:", + "directRequestUrlDesc": "不开启代理时,客户端直接请求此地址", + "proxyRequestUrl": "代理请求地址:", + "proxyRequestUrlDesc": "开启代理后,代理服务会将请求转发到此地址(支持格式转换)", + "fullUrlWarningTitle": "检测到完整 API 路径", + "fullUrlWarning": "填写了包含 API 路径的完整地址,此配置仅在代理模式下生效。直连模式下请只填写基础地址。", "fillSupplierName": "请填写供应商名称", "fillConfigContent": "请填写配置内容", "fillParameter": "请填写 {{label}}",