feat(proxy): 添加本地代理请求覆盖功能,支持自定义请求头和请求体 (#4589)

* feat(proxy): 添加本地代理请求覆盖功能,支持自定义请求头和请求体

* fix(proxy): harden local request overrides validation

* feat(proxy): 添加受保护的本地代理请求头名称验证功能

* fix(i18n): 更新本地代理请求覆盖的错误提示信息格式

---------

Co-authored-by: jason.mei <jason.mei@ucloud.cn>
This commit is contained in:
Jianfeng Mei
2026-06-24 22:19:01 +08:00
committed by GitHub
parent edeee25fae
commit 6fd4e6f462
14 changed files with 855 additions and 10 deletions
@@ -48,6 +48,7 @@ import {
type FetchedModel,
} from "@/lib/api/model-fetch";
import { CustomUserAgentField } from "./CustomUserAgentField";
import { LocalProxyRequestOverridesField } from "./LocalProxyRequestOverridesField";
import type {
ProviderCategory,
ClaudeApiFormat,
@@ -145,6 +146,10 @@ interface ClaudeFormFieldsProps {
// Local proxy User-Agent override
customUserAgent: string;
onCustomUserAgentChange: (value: string) => void;
localProxyHeadersOverride: string;
onLocalProxyHeadersOverrideChange: (value: string) => void;
localProxyBodyOverride: string;
onLocalProxyBodyOverrideChange: (value: string) => void;
}
export function ClaudeFormFields({
@@ -201,8 +206,15 @@ export function ClaudeFormFields({
onFullUrlChange,
customUserAgent,
onCustomUserAgentChange,
localProxyHeadersOverride,
onLocalProxyHeadersOverrideChange,
localProxyBodyOverride,
onLocalProxyBodyOverrideChange,
}: ClaudeFormFieldsProps) {
const { t } = useTranslation();
const hasRequestOverrides = Boolean(
localProxyHeadersOverride.trim() || localProxyBodyOverride.trim(),
);
const hasAnyAdvancedValue = !!(
claudeModel ||
defaultHaikuModel ||
@@ -211,7 +223,8 @@ export function ClaudeFormFields({
defaultFableModel ||
apiFormat !== "anthropic" ||
apiKeyField !== "ANTHROPIC_AUTH_TOKEN" ||
customUserAgent
customUserAgent ||
hasRequestOverrides
);
const [advancedExpanded, setAdvancedExpanded] = useState(hasAnyAdvancedValue);
@@ -963,6 +976,15 @@ export function ClaudeFormFields({
value={customUserAgent}
onChange={onCustomUserAgentChange}
/>
<div className="border-t border-border-default pt-3">
<LocalProxyRequestOverridesField
headersJson={localProxyHeadersOverride}
bodyJson={localProxyBodyOverride}
onHeadersJsonChange={onLocalProxyHeadersOverrideChange}
onBodyJsonChange={onLocalProxyBodyOverrideChange}
/>
</div>
</CollapsibleContent>
</Collapsible>
)}
@@ -26,6 +26,7 @@ import {
type FetchedModel,
} from "@/lib/api/model-fetch";
import { CustomUserAgentField } from "./CustomUserAgentField";
import { LocalProxyRequestOverridesField } from "./LocalProxyRequestOverridesField";
import { cn } from "@/lib/utils";
import type {
CodexApiFormat,
@@ -78,6 +79,10 @@ interface CodexFormFieldsProps {
// Local proxy User-Agent override
customUserAgent: string;
onCustomUserAgentChange: (value: string) => void;
localProxyHeadersOverride: string;
onLocalProxyHeadersOverrideChange: (value: string) => void;
localProxyBodyOverride: string;
onLocalProxyBodyOverrideChange: (value: string) => void;
}
type CodexCatalogRow = CodexCatalogModel & { rowId: string };
@@ -136,6 +141,10 @@ export function CodexFormFields({
speedTestEndpoints,
customUserAgent,
onCustomUserAgentChange,
localProxyHeadersOverride,
onLocalProxyHeadersOverrideChange,
localProxyBodyOverride,
onLocalProxyBodyOverrideChange,
}: CodexFormFieldsProps) {
const { t } = useTranslation();
@@ -150,7 +159,11 @@ export function CodexFormFields({
const supportsEffort = codexChatReasoning.supportsEffort === true;
// needsLocalRouting 非默认值说明预设/用户动过路由配置,需要让模型映射保持可见
const hasAnyAdvancedValue = !!customUserAgent || needsLocalRouting;
const hasRequestOverrides = Boolean(
localProxyHeadersOverride.trim() || localProxyBodyOverride.trim(),
);
const hasAnyAdvancedValue =
!!customUserAgent || hasRequestOverrides || needsLocalRouting;
const [advancedExpanded, setAdvancedExpanded] = useState(hasAnyAdvancedValue);
// 预设/编辑加载填充高级值后自动展开(仅从折叠→展开,不会自动折叠)
@@ -480,6 +493,7 @@ export function CodexFormFields({
<div
className={cn(
"space-y-3",
(shouldShowSpeedTest ||
(needsLocalRouting && canEditReasoning)) &&
"border-t border-border-default pt-3",
@@ -490,6 +504,14 @@ export function CodexFormFields({
value={customUserAgent}
onChange={onCustomUserAgentChange}
/>
<div className="border-t border-border-default pt-3">
<LocalProxyRequestOverridesField
headersJson={localProxyHeadersOverride}
bodyJson={localProxyBodyOverride}
onHeadersJsonChange={onLocalProxyHeadersOverrideChange}
onBodyJsonChange={onLocalProxyBodyOverrideChange}
/>
</div>
</div>
{/* 模型映射 —— 仅在本地路由 + 可编辑时显示;上方恒有 UA 字段,分隔线无需条件 */}
@@ -0,0 +1,91 @@
import { useTranslation } from "react-i18next";
import { FormLabel } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import {
parseBodyOverrideJson,
parseHeaderOverrideJson,
} from "@/lib/requestOverrides";
interface LocalProxyRequestOverridesFieldProps {
headersJson: string;
bodyJson: string;
onHeadersJsonChange: (value: string) => void;
onBodyJsonChange: (value: string) => void;
}
export function LocalProxyRequestOverridesField({
headersJson,
bodyJson,
onHeadersJsonChange,
onBodyJsonChange,
}: LocalProxyRequestOverridesFieldProps) {
const { t } = useTranslation();
const headerError = parseHeaderOverrideJson(headersJson).error;
const bodyError = parseBodyOverrideJson(bodyJson).error;
return (
<div className="space-y-3">
<div className="space-y-1">
<FormLabel>
{t("providerForm.localProxyRequestOverrides", {
defaultValue: "本地代理请求覆盖",
})}
</FormLabel>
<p className="text-xs text-muted-foreground">
{t("providerForm.localProxyRequestOverridesHint", {
defaultValue:
"仅在本地路由/代理接管后生效,应用于协议转换后的上游请求。",
})}
</p>
</div>
<div className="grid gap-3 md:grid-cols-2">
<div className="space-y-2">
<FormLabel className="text-xs text-muted-foreground">
{t("providerForm.localProxyHeaderOverrides", {
defaultValue: "Header 覆盖",
})}
</FormLabel>
<Textarea
value={headersJson}
onChange={(event) => onHeadersJsonChange(event.target.value)}
placeholder={'{\n "X-Provider": "cc-switch"\n}'}
className="min-h-[132px] resize-y font-mono text-xs"
aria-invalid={Boolean(headerError)}
/>
{headerError && (
<p className="text-xs text-destructive">
{t("providerForm.localProxyHeaderOverridesInvalidDetail", {
error: headerError,
defaultValue: "Header 覆盖格式错误:{{error}}",
})}
</p>
)}
</div>
<div className="space-y-2">
<FormLabel className="text-xs text-muted-foreground">
{t("providerForm.localProxyBodyOverrides", {
defaultValue: "Body 覆盖",
})}
</FormLabel>
<Textarea
value={bodyJson}
onChange={(event) => onBodyJsonChange(event.target.value)}
placeholder={'{\n "temperature": 0.2\n}'}
className="min-h-[132px] resize-y font-mono text-xs"
aria-invalid={Boolean(bodyError)}
/>
{bodyError && (
<p className="text-xs text-destructive">
{t("providerForm.localProxyBodyOverridesInvalidDetail", {
error: bodyError,
defaultValue: "Body 覆盖格式错误:{{error}}",
})}
</p>
)}
</div>
</div>
</div>
);
}
@@ -8,6 +8,10 @@ import { Button } from "@/components/ui/button";
import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider";
import {
buildLocalProxyRequestOverrides,
formatRequestOverrideObject,
} from "@/lib/requestOverrides";
import { providersApi, settingsApi, type AppId } from "@/lib/api";
import { useDarkMode } from "@/hooks/useDarkMode";
import type {
@@ -211,6 +215,10 @@ const normalizeCodexChatReasoningForSave = (
};
};
type LocalProxyRequestOverridesBuildResult = ReturnType<
typeof buildLocalProxyRequestOverrides
>;
export interface ProviderFormProps {
appId: AppId;
providerId?: string;
@@ -358,6 +366,16 @@ function ProviderFormFull({
});
setCodexChatReasoning(initialData?.meta?.codexChatReasoning ?? {});
setCustomUserAgent(initialData?.meta?.customUserAgent ?? "");
setLocalProxyHeadersOverride(
formatRequestOverrideObject(
initialData?.meta?.localProxyRequestOverrides?.headers,
),
);
setLocalProxyBodyOverride(
formatRequestOverrideObject(
initialData?.meta?.localProxyRequestOverrides?.body,
),
);
}, [appId, initialData, supportsFullUrl]);
const defaultValues: ProviderFormData = useMemo(
@@ -414,6 +432,10 @@ function ProviderFormFull({
const [softIssues, setSoftIssues] = useState<string[] | null>(null);
const [pendingFormValues, setPendingFormValues] =
useState<ProviderFormData | null>(null);
const [
pendingLocalProxyRequestOverridesResult,
setPendingLocalProxyRequestOverridesResult,
] = useState<LocalProxyRequestOverridesBuildResult | null>(null);
// 确认框走的提交路径绕过了 react-hook-form 的 isSubmitting,单独追踪
const [isConfirmSubmitting, setIsConfirmSubmitting] = useState(false);
@@ -517,6 +539,18 @@ function ProviderFormFull({
const [customUserAgent, setCustomUserAgent] = useState<string>(
() => initialData?.meta?.customUserAgent ?? "",
);
const [localProxyHeadersOverride, setLocalProxyHeadersOverride] =
useState<string>(() =>
formatRequestOverrideObject(
initialData?.meta?.localProxyRequestOverrides?.headers,
),
);
const [localProxyBodyOverride, setLocalProxyBodyOverride] = useState<string>(
() =>
formatRequestOverrideObject(
initialData?.meta?.localProxyRequestOverrides?.body,
),
);
const {
codexAuth,
@@ -931,7 +965,26 @@ function ProviderFormFull({
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
const shouldApplyLocalProxyRequestOverrides =
(appId === "claude" || appId === "codex") && category !== "official";
const handleSubmit = async (values: ProviderFormData) => {
const overridesResult = shouldApplyLocalProxyRequestOverrides
? buildLocalProxyRequestOverrides(
localProxyHeadersOverride,
localProxyBodyOverride,
)
: {};
if (overridesResult.error) {
toast.error(
t("providerForm.localProxyRequestOverridesInvalid", {
defaultValue: `本地代理请求覆盖格式错误:${overridesResult.error}`,
error: overridesResult.error,
}),
);
return;
}
// 软性问题(业务约束,用户可选择仍要保存)
const issues: string[] = [];
@@ -1169,13 +1222,27 @@ function ProviderFormFull({
// 弹确认框让用户决定是否仍要保存
setSoftIssues(issues);
setPendingFormValues(values);
setPendingLocalProxyRequestOverridesResult(overridesResult);
return;
}
await performSubmit(values);
await performSubmit(values, overridesResult);
};
const performSubmit = async (values: ProviderFormData) => {
const performSubmit = async (
values: ProviderFormData,
overridesResult: LocalProxyRequestOverridesBuildResult,
) => {
if (overridesResult.error) {
toast.error(
t("providerForm.localProxyRequestOverridesInvalid", {
defaultValue: `本地代理请求覆盖格式错误:${overridesResult.error}`,
error: overridesResult.error,
}),
);
return;
}
// OAuth / 其它身份识别(与 handleSubmit 保持一致)
const isCopilotProvider =
templatePreset?.providerType === "github_copilot" ||
@@ -1398,6 +1465,9 @@ function ProviderFormFull({
(appId === "claude" || appId === "codex") && category !== "official"
? customUserAgent.trim() || undefined
: undefined,
localProxyRequestOverrides: shouldApplyLocalProxyRequestOverrides
? overridesResult.overrides
: undefined,
testConfig: testConfig.enabled ? testConfig : undefined,
costMultiplier: pricingConfig.enabled
? pricingConfig.costMultiplier
@@ -2025,6 +2095,10 @@ function ProviderFormFull({
onFullUrlChange={setLocalIsFullUrl}
customUserAgent={customUserAgent}
onCustomUserAgentChange={setCustomUserAgent}
localProxyHeadersOverride={localProxyHeadersOverride}
onLocalProxyHeadersOverrideChange={setLocalProxyHeadersOverride}
localProxyBodyOverride={localProxyBodyOverride}
onLocalProxyBodyOverrideChange={setLocalProxyBodyOverride}
/>
)}
@@ -2059,6 +2133,10 @@ function ProviderFormFull({
speedTestEndpoints={speedTestEndpoints}
customUserAgent={customUserAgent}
onCustomUserAgentChange={setCustomUserAgent}
localProxyHeadersOverride={localProxyHeadersOverride}
onLocalProxyHeadersOverrideChange={setLocalProxyHeadersOverride}
localProxyBodyOverride={localProxyBodyOverride}
onLocalProxyBodyOverrideChange={setLocalProxyBodyOverride}
/>
)}
@@ -2383,15 +2461,19 @@ function ProviderFormFull({
onConfirm={async () => {
if (isConfirmSubmitting) return;
const values = pendingFormValues;
if (!values) {
const overridesResult = pendingLocalProxyRequestOverridesResult;
if (!values || !overridesResult) {
setSoftIssues(null);
setPendingFormValues(null);
setPendingLocalProxyRequestOverridesResult(null);
return;
}
setIsConfirmSubmitting(true);
try {
await performSubmit(values);
await performSubmit(values, overridesResult);
setSoftIssues(null);
setPendingFormValues(null);
setPendingLocalProxyRequestOverridesResult(null);
} catch (error) {
console.error("[ProviderForm] soft-confirm submit failed:", error);
// 保留确认框和 pending values,让用户可以重试或取消
@@ -2403,6 +2485,7 @@ function ProviderFormFull({
if (isConfirmSubmitting) return;
setSoftIssues(null);
setPendingFormValues(null);
setPendingLocalProxyRequestOverridesResult(null);
}}
/>
</>
+7
View File
@@ -1067,6 +1067,13 @@
"customUserAgentHint": "Only takes effect when local routing/proxy takeover is enabled; replaces the User-Agent in requests forwarded to the provider API.",
"customUserAgentInvalid": "User-Agent must not contain control characters (e.g. line breaks); otherwise it will be ignored.",
"customUserAgentPresets": "Presets",
"localProxyRequestOverrides": "Local proxy request overrides",
"localProxyRequestOverridesHint": "Only takes effect after local routing/proxy takeover; applied to the transformed upstream request.",
"localProxyRequestOverridesInvalid": "Local proxy request overrides format error: {{error}}",
"localProxyHeaderOverrides": "Header overrides",
"localProxyHeaderOverridesInvalidDetail": "Header overrides format error: {{error}}",
"localProxyBodyOverrides": "Body overrides",
"localProxyBodyOverridesInvalidDetail": "Body overrides format error: {{error}}",
"fullUrlLabel": "Full URL",
"fullUrlEnabled": "Full URL Mode",
"fullUrlDisabled": "Mark as Full URL",
+7
View File
@@ -1067,6 +1067,13 @@
"customUserAgentHint": "ローカルルーティング/プロキシ引き継ぎが有効な場合にのみ適用され、プロバイダー API へ転送するリクエストの User-Agent を置き換えます。",
"customUserAgentInvalid": "User-Agent に制御文字(改行など)を含めることはできません。含まれている場合は無視されます。",
"customUserAgentPresets": "プリセット",
"localProxyRequestOverrides": "ローカルプロキシのリクエスト上書き",
"localProxyRequestOverridesHint": "ローカルルーティング/プロキシ引き継ぎ後にのみ有効で、変換後の上流リクエストへ適用されます。",
"localProxyRequestOverridesInvalid": "ローカルプロキシのリクエスト上書き形式エラー: {{error}}",
"localProxyHeaderOverrides": "Header 上書き",
"localProxyHeaderOverridesInvalidDetail": "Header 上書きの形式エラー: {{error}}",
"localProxyBodyOverrides": "Body 上書き",
"localProxyBodyOverridesInvalidDetail": "Body 上書きの形式エラー: {{error}}",
"fullUrlLabel": "フル URL",
"fullUrlEnabled": "フル URL モード",
"fullUrlDisabled": "フル URL として設定",
+7
View File
@@ -1039,6 +1039,13 @@
"customUserAgentHint": "僅在開啟本地路由/代理接管後生效,會取代轉發至供應商 API 請求中的 User-Agent。",
"customUserAgentInvalid": "User-Agent 不能包含控制字元(如換行字元),否則將被忽略。",
"customUserAgentPresets": "預設",
"localProxyRequestOverrides": "本地代理請求覆蓋",
"localProxyRequestOverridesHint": "僅在本地路由/代理接管後生效,套用於協定轉換後的上游請求。",
"localProxyRequestOverridesInvalid": "本地代理請求覆蓋格式錯誤:{{error}}",
"localProxyHeaderOverrides": "Header 覆蓋",
"localProxyHeaderOverridesInvalidDetail": "Header 覆蓋格式錯誤:{{error}}",
"localProxyBodyOverrides": "Body 覆蓋",
"localProxyBodyOverridesInvalidDetail": "Body 覆蓋格式錯誤:{{error}}",
"fullUrlLabel": "完整 URL",
"fullUrlEnabled": "完整 URL 模式",
"fullUrlDisabled": "標記為完整 URL",
+7
View File
@@ -1067,6 +1067,13 @@
"customUserAgentHint": "仅在开启本地路由/代理接管后生效,会替换转发到供应商 API 请求中的 User-Agent。",
"customUserAgentInvalid": "User-Agent 不能包含控制字符(如换行符),否则将被忽略。",
"customUserAgentPresets": "预设",
"localProxyRequestOverrides": "本地代理请求覆盖",
"localProxyRequestOverridesHint": "仅在本地路由/代理接管后生效,应用于协议转换后的上游请求。",
"localProxyRequestOverridesInvalid": "本地代理请求覆盖格式错误:{{error}}",
"localProxyHeaderOverrides": "Header 覆盖",
"localProxyHeaderOverridesInvalidDetail": "Header 覆盖格式错误:{{error}}",
"localProxyBodyOverrides": "Body 覆盖",
"localProxyBodyOverridesInvalidDetail": "Body 覆盖格式错误:{{error}}",
"fullUrlLabel": "完整 URL",
"fullUrlEnabled": "完整 URL 模式",
"fullUrlDisabled": "标记为完整 URL",
+182
View File
@@ -0,0 +1,182 @@
import type { LocalProxyRequestOverrides } from "@/types";
export interface RequestOverrideJsonResult {
value?: Record<string, unknown>;
error?: string;
}
export interface HeaderOverrideValidationResult {
headers?: Record<string, string>;
error?: string;
}
// RFC 9110 HTTP field-name token. Keep this aligned with Rust's
// http::HeaderName parser for user-facing validation.
export function isValidHttpHeaderName(name: string): boolean {
return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name);
}
export const PROTECTED_LOCAL_PROXY_HEADER_NAMES = new Set([
"host",
"content-length",
"transfer-encoding",
"connection",
"proxy-authorization",
"proxy-authenticate",
"te",
"trailer",
"upgrade",
"accept-encoding",
"content-type",
"authorization",
"x-api-key",
"x-goog-api-key",
"chatgpt-account-id",
"session_id",
"x-client-request-id",
"x-codex-window-id",
"x-forwarded-host",
"x-forwarded-port",
"x-forwarded-proto",
"forwarded",
"cf-connecting-ip",
"cf-ipcountry",
"cf-ray",
"cf-visitor",
"true-client-ip",
"fastly-client-ip",
"x-azure-clientip",
"x-azure-fdid",
"x-azure-ref",
"akamai-origin-hop",
"x-akamai-config-log-detail",
"x-request-id",
"x-correlation-id",
"x-trace-id",
"x-amzn-trace-id",
"x-b3-traceid",
"x-b3-spanid",
"x-b3-parentspanid",
"x-b3-sampled",
"traceparent",
"tracestate",
]);
export function isProtectedLocalProxyHeaderName(name: string): boolean {
return PROTECTED_LOCAL_PROXY_HEADER_NAMES.has(name.toLowerCase());
}
// Keep this aligned with Rust's http::HeaderValue runtime guard: only control
// characters other than tab are rejected.
export function isValidHttpHeaderValue(value: string): boolean {
// eslint-disable-next-line no-control-regex
return !/[\x00-\x08\x0a-\x1f\x7f]/.test(value);
}
export function isPlainObject(
value: unknown,
): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function parseRequestOverrideJson(
raw: string,
): RequestOverrideJsonResult {
const trimmed = raw.trim();
if (!trimmed) return {};
try {
const parsed = JSON.parse(trimmed);
if (!isPlainObject(parsed)) {
return { error: "JSON must be an object" };
}
return { value: parsed };
} catch (error) {
return {
error: error instanceof Error ? error.message : "Invalid JSON",
};
}
}
export function parseBodyOverrideJson(raw: string): RequestOverrideJsonResult {
const parsed = parseRequestOverrideJson(raw);
if (parsed.error || !parsed.value) {
return parsed;
}
if (Object.prototype.hasOwnProperty.call(parsed.value, "stream")) {
return { error: 'Body override must not include protocol field "stream"' };
}
return parsed;
}
export function parseHeaderOverrideJson(
raw: string,
): HeaderOverrideValidationResult {
const parsed = parseRequestOverrideJson(raw);
if (parsed.error || !parsed.value) {
return parsed.error ? { error: parsed.error } : {};
}
const headers: Record<string, string> = {};
for (const [name, value] of Object.entries(parsed.value)) {
const headerName = name.trim();
if (!headerName) {
return { error: "Header name must not be empty" };
}
if (!isValidHttpHeaderName(headerName)) {
return { error: `Header "${name}" name is not a valid HTTP token` };
}
if (typeof value !== "string") {
return { error: `Header "${name}" value must be a string` };
}
if (!isValidHttpHeaderValue(value)) {
return { error: `Header "${name}" value contains control characters` };
}
const normalizedHeaderName = headerName.toLowerCase();
if (Object.prototype.hasOwnProperty.call(headers, normalizedHeaderName)) {
return {
error: `Header "${name}" duplicates another header after case normalization`,
};
}
if (isProtectedLocalProxyHeaderName(normalizedHeaderName)) {
return {
error: `Header "${name}" is managed by the local proxy and cannot be overridden`,
};
}
headers[normalizedHeaderName] = value;
}
return { headers };
}
export function formatRequestOverrideObject(
value: Record<string, unknown> | undefined,
): string {
if (!value || Object.keys(value).length === 0) return "";
return JSON.stringify(value, null, 2);
}
export function buildLocalProxyRequestOverrides(
headersJson: string,
bodyJson: string,
): { overrides?: LocalProxyRequestOverrides; error?: string } {
const headerResult = parseHeaderOverrideJson(headersJson);
if (headerResult.error) {
return { error: headerResult.error };
}
const bodyResult = parseBodyOverrideJson(bodyJson);
if (bodyResult.error) {
return { error: bodyResult.error };
}
const overrides: LocalProxyRequestOverrides = {};
if (headerResult.headers && Object.keys(headerResult.headers).length > 0) {
overrides.headers = headerResult.headers;
}
if (bodyResult.value && Object.keys(bodyResult.value).length > 0) {
overrides.body = bodyResult.value;
}
return Object.keys(overrides).length > 0 ? { overrides } : {};
}
+7
View File
@@ -171,6 +171,11 @@ export interface CodexChatReasoning {
outputFormat?: CodexChatReasoningOutputFormat;
}
export interface LocalProxyRequestOverrides {
headers?: Record<string, string>;
body?: Record<string, unknown>;
}
// 供应商元数据(字段名与后端一致,保持 snake_case
export interface ProviderMeta {
// 自定义端点:以 URL 为键,值为端点信息
@@ -219,6 +224,8 @@ export interface ProviderMeta {
codexChatReasoning?: CodexChatReasoning;
// Custom User-Agent for local proxy routing. Only applied by the local proxy.
customUserAgent?: string;
// Local proxy request overrides. Only applied by the local proxy after route transforms.
localProxyRequestOverrides?: LocalProxyRequestOverrides;
// 供应商类型(用于识别 Copilot 等特殊供应商)
providerType?: string;
// GitHub Copilot 关联账号 ID(旧字段,保留兼容读取)