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)
This commit is contained in:
YoVinchen
2026-02-05 00:52:10 +08:00
parent 4b18c232f4
commit 7f6ce72a88
10 changed files with 555 additions and 46 deletions
+80 -8
View File
@@ -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();
+61
View File
@@ -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() {
+4 -1
View File
@@ -195,7 +195,10 @@ function App() {
switchProvider,
deleteProvider,
saveUsageScript,
} = useProviderActions(activeApp);
} = useProviderActions(activeApp, {
isProxyRunning,
isTakeoverActive: isCurrentAppTakeoverActive,
});
// 监听来自托盘菜单的切换事件
useEffect(() => {
@@ -169,6 +169,8 @@ export function ClaudeFormFields({
: t("providerForm.apiHint")
}
onManageClick={() => onEndpointModalToggle(true)}
appType="claude"
apiFormat={apiFormat}
/>
)}
@@ -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 && (
<div className="space-y-2">
<FormLabel htmlFor="codexApiFormat">
{t("providerForm.apiFormat", { defaultValue: "API 格式" })}
</FormLabel>
<Select value={apiFormat} onValueChange={onApiFormatChange}>
<SelectTrigger id="codexApiFormat" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="responses">
{t("providerForm.codexApiFormatResponses", {
defaultValue: "OpenAI Responses (默认)",
})}
</SelectItem>
<SelectItem value="chat">
{t("providerForm.codexApiFormatChat", {
defaultValue: "OpenAI Chat Completions",
})}
</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{t("providerForm.codexApiFormatHint", {
defaultValue: "选择供应商支持的 API 格式",
})}
</p>
</div>
)}
{/* Codex Base URL 输入框 */}
{shouldShowSpeedTest && (
<EndpointField
@@ -92,8 +151,10 @@ export function CodexFormFields({
value={codexBaseUrl}
onChange={onBaseUrlChange}
placeholder={t("providerForm.codexApiEndpointPlaceholder")}
hint={t("providerForm.codexApiHint")}
hint={apiHint}
onManageClick={() => onEndpointModalToggle(true)}
appType="codex"
apiFormat={apiFormat}
/>
)}
@@ -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: 直连固定 /responsesbase 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 (
<div className="space-y-2">
<div className="flex items-center justify-between">
@@ -55,6 +161,71 @@ export function EndpointField({
placeholder={placeholder}
autoComplete="off"
/>
{/* 请求地址预览 */}
{showUrlPreview && directUrlPreview && (
<div className="p-2 bg-muted/50 border border-border rounded-md space-y-2">
{/* 直连模式请求地址 */}
<div>
<p className="text-xs text-muted-foreground mb-0.5 flex items-center gap-1">
<Unplug className="h-3 w-3" />
{t("providerForm.directRequestUrl", {
defaultValue: "直连请求地址:",
})}
</p>
<p className="text-xs font-mono text-foreground break-all pl-4">
{directUrlPreview}
</p>
<p className="text-xs text-muted-foreground/70 mt-0.5 pl-4">
{t("providerForm.directRequestUrlDesc", {
defaultValue: "不开启代理时,客户端直接请求此地址",
})}
</p>
</div>
{/* 代理模式请求地址 */}
{proxyUrlPreview && (
<div className="pt-1.5 border-t border-border/50">
<p className="text-xs text-muted-foreground mb-0.5 flex items-center gap-1">
<Server className="h-3 w-3" />
{t("providerForm.proxyRequestUrl", {
defaultValue: "代理请求地址:",
})}
</p>
<p className="text-xs font-mono text-foreground break-all pl-4">
{proxyUrlPreview}
</p>
<p className="text-xs text-muted-foreground/70 mt-0.5 pl-4">
{t("providerForm.proxyRequestUrlDesc", {
defaultValue:
"开启代理后,代理服务会将请求转发到此地址(支持格式转换)",
})}
</p>
</div>
)}
</div>
)}
{/* 全链接警告 */}
{urlEndsWithApiPath && (
<div className="flex items-start gap-2 p-2 bg-orange-50 dark:bg-orange-950/30 border border-orange-200 dark:border-orange-800 rounded-md">
<AlertTriangle className="h-4 w-4 text-orange-500 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="text-xs text-orange-600 dark:text-orange-400 font-medium">
{t("providerForm.fullUrlWarningTitle", {
defaultValue: "检测到完整 API 路径",
})}
</p>
<p className="text-xs text-orange-600/80 dark:text-orange-400/80 mt-0.5">
{t("providerForm.fullUrlWarning", {
defaultValue:
"填写了包含 API 路径的完整地址,此配置仅在代理模式下生效。直连模式下请只填写基础地址。",
})}
</p>
</div>
</div>
)}
{hint ? (
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
<p className="text-xs text-amber-600 dark:text-amber-400">{hint}</p>
+135 -32
View File
@@ -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,
],
);
// 删除供应商
+13 -1
View File
@@ -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}}",
+13 -1
View File
@@ -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}} を入力してください",
+13 -1
View File
@@ -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}}",