feat(ui): add format transform configuration in provider form

- Add FormatTransformConfig type definition
- Add format transform section in ProviderAdvancedConfig component
- Support source/target format selection (Anthropic/OpenAI)
- Add transform streaming toggle option
This commit is contained in:
YoVinchen
2026-01-25 19:58:28 +08:00
Unverified
parent be246f8596
commit 4ee45d2bc3
3 changed files with 175 additions and 1 deletions
@@ -8,19 +8,33 @@ import {
Eye,
EyeOff,
X,
ArrowLeftRight,
} from "lucide-react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import type { ProviderTestConfig, ProviderProxyConfig } from "@/types";
import type {
ProviderTestConfig,
ProviderProxyConfig,
FormatTransformConfig,
} from "@/types";
interface ProviderAdvancedConfigProps {
testConfig: ProviderTestConfig;
proxyConfig: ProviderProxyConfig;
formatTransform?: FormatTransformConfig;
onTestConfigChange: (config: ProviderTestConfig) => void;
onProxyConfigChange: (config: ProviderProxyConfig) => void;
onFormatTransformChange?: (config: FormatTransformConfig) => void;
}
/** 从 ProviderProxyConfig 构建完整 URL */
@@ -71,14 +85,19 @@ function parseProxyUrl(url: string): Partial<ProviderProxyConfig> {
export function ProviderAdvancedConfig({
testConfig,
proxyConfig,
formatTransform,
onTestConfigChange,
onProxyConfigChange,
onFormatTransformChange,
}: ProviderAdvancedConfigProps) {
const { t } = useTranslation();
const [isTestConfigOpen, setIsTestConfigOpen] = useState(testConfig.enabled);
const [isProxyConfigOpen, setIsProxyConfigOpen] = useState(
proxyConfig.enabled,
);
const [isFormatTransformOpen, setIsFormatTransformOpen] = useState(
formatTransform?.enabled ?? false,
);
const [showPassword, setShowPassword] = useState(false);
// 代理 URL 输入状态(仅在初始化时从 proxyConfig 构建)
@@ -97,6 +116,11 @@ export function ProviderAdvancedConfig({
setIsProxyConfigOpen(proxyConfig.enabled);
}, [proxyConfig.enabled]);
// 同步外部 formatTransform.enabled 变化到展开状态
useEffect(() => {
setIsFormatTransformOpen(formatTransform?.enabled ?? false);
}, [formatTransform?.enabled]);
// 仅在外部 proxyConfig 变化且非用户输入时同步(如:重置表单、加载数据)
useEffect(() => {
if (!isUserTyping) {
@@ -450,6 +474,135 @@ export function ProviderAdvancedConfig({
</div>
</div>
</div>
{/* 格式转换配置 */}
{onFormatTransformChange && (
<div className="rounded-lg border border-border/50 bg-muted/20">
<button
type="button"
className="flex w-full items-center justify-between p-4 hover:bg-muted/30 transition-colors"
onClick={() => setIsFormatTransformOpen(!isFormatTransformOpen)}
>
<div className="flex items-center gap-3">
<ArrowLeftRight className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">
{t("providerAdvanced.formatTransform")}
</span>
</div>
<div className="flex items-center gap-3">
<div
className="flex items-center gap-2"
onClick={(e) => e.stopPropagation()}
>
<Label
htmlFor="format-transform-enabled"
className="text-sm text-muted-foreground"
>
{t("providerAdvanced.enableFormatTransform")}
</Label>
<Switch
id="format-transform-enabled"
checked={formatTransform?.enabled ?? false}
onCheckedChange={(checked) => {
onFormatTransformChange({
...(formatTransform ?? { enabled: false }),
enabled: checked,
});
if (checked) setIsFormatTransformOpen(true);
}}
/>
</div>
{isFormatTransformOpen ? (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronRight className="h-4 w-4 text-muted-foreground" />
)}
</div>
</button>
<div
className={cn(
"overflow-hidden transition-all duration-200",
isFormatTransformOpen
? "max-h-[500px] opacity-100"
: "max-h-0 opacity-0",
)}
>
<div className="border-t border-border/50 p-4 space-y-4">
<p className="text-sm text-muted-foreground">
{t("providerAdvanced.formatTransformDesc")}
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="source-format">
{t("providerAdvanced.sourceFormat")}
</Label>
<Select
value={formatTransform?.sourceFormat ?? "anthropic"}
onValueChange={(value) =>
onFormatTransformChange({
...(formatTransform ?? { enabled: false }),
sourceFormat: value as "anthropic" | "openai",
})
}
disabled={!formatTransform?.enabled}
>
<SelectTrigger id="source-format">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="anthropic">
Anthropic (Claude)
</SelectItem>
<SelectItem value="openai">OpenAI</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="target-format">
{t("providerAdvanced.targetFormat")}
</Label>
<Select
value={formatTransform?.targetFormat ?? "openai"}
onValueChange={(value) =>
onFormatTransformChange({
...(formatTransform ?? { enabled: false }),
targetFormat: value as "anthropic" | "openai",
})
}
disabled={!formatTransform?.enabled}
>
<SelectTrigger id="target-format">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="anthropic">
Anthropic (Claude)
</SelectItem>
<SelectItem value="openai">OpenAI</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex items-center gap-2">
<Switch
id="transform-streaming"
checked={formatTransform?.transformStreaming ?? true}
onCheckedChange={(checked) =>
onFormatTransformChange({
...(formatTransform ?? { enabled: false }),
transformStreaming: checked,
})
}
disabled={!formatTransform?.enabled}
/>
<Label htmlFor="transform-streaming" className="text-sm">
{t("providerAdvanced.transformStreaming")}
</Label>
</div>
</div>
</div>
</div>
)}
</div>
);
}
@@ -13,6 +13,7 @@ import type {
ProviderMeta,
ProviderTestConfig,
ProviderProxyConfig,
FormatTransformConfig,
} from "@/types";
import {
providerPresets,
@@ -168,6 +169,9 @@ export function ProviderForm({
const [proxyConfig, setProxyConfig] = useState<ProviderProxyConfig>(
() => initialData?.meta?.proxyConfig ?? { enabled: false },
);
const [formatTransform, setFormatTransform] = useState<FormatTransformConfig>(
() => initialData?.meta?.formatTransform ?? { enabled: false },
);
// 使用 category hook
const { category } = useProviderCategory({
@@ -940,6 +944,7 @@ export function ProviderForm({
// 添加高级配置
testConfig: testConfig.enabled ? testConfig : undefined,
proxyConfig: proxyConfig.enabled ? proxyConfig : undefined,
formatTransform: formatTransform.enabled ? formatTransform : undefined,
};
onSubmit(payload);
@@ -1464,8 +1469,10 @@ export function ProviderForm({
<ProviderAdvancedConfig
testConfig={testConfig}
proxyConfig={proxyConfig}
formatTransform={formatTransform}
onTestConfigChange={setTestConfig}
onProxyConfigChange={setProxyConfig}
onFormatTransformChange={setFormatTransform}
/>
{showButtons && (
+14
View File
@@ -119,6 +119,18 @@ export interface ProviderProxyConfig {
proxyPassword?: string;
}
// 格式转换配置(用于 OpenRouter 等需要 API 格式转换的供应商)
export interface FormatTransformConfig {
// 是否启用格式转换
enabled: boolean;
// 源格式:anthropic, openai, gemini
sourceFormat?: "anthropic" | "openai" | "gemini";
// 目标格式:anthropic, openai, gemini
targetFormat?: "anthropic" | "openai" | "gemini";
// 是否转换流式响应(默认 true)
transformStreaming?: boolean;
}
// 供应商元数据(字段名与后端一致,保持 snake_case
export interface ProviderMeta {
// 自定义端点:以 URL 为键,值为端点信息
@@ -135,6 +147,8 @@ export interface ProviderMeta {
testConfig?: ProviderTestConfig;
// 供应商单独的代理配置
proxyConfig?: ProviderProxyConfig;
// 格式转换配置(用于 OpenRouter 等需要 API 格式转换的供应商)
formatTransform?: FormatTransformConfig;
}
// Skill 同步方式