feat: add provider user agent override

This commit is contained in:
RoromoriYuzu
2026-04-26 02:40:32 +08:00
committed by Jason
parent e8b07cb2a5
commit ff706e9e96
6 changed files with 109 additions and 1 deletions
+3
View File
@@ -460,6 +460,9 @@ pub struct ProviderMeta {
/// Codex Responses -> Chat Completions reasoning capability metadata.
#[serde(rename = "codexChatReasoning", skip_serializing_if = "Option::is_none")]
pub codex_chat_reasoning: Option<CodexChatReasoningConfig>,
/// Custom User-Agent for local proxy routing.
#[serde(rename = "customUserAgent", skip_serializing_if = "Option::is_none")]
pub custom_user_agent: Option<String>,
/// 累加模式应用中,该 provider 是否已写入 live config。
/// `None` 表示旧数据/未知状态,`Some(false)` 表示明确仅存在于数据库中。
#[serde(rename = "liveConfigManaged", skip_serializing_if = "Option::is_none")]
+29
View File
@@ -1537,6 +1537,15 @@ impl RequestForwarder {
Vec::new()
};
let custom_user_agent = provider
.meta
.as_ref()
.and_then(|meta| meta.custom_user_agent.as_deref())
.map(str::trim)
.filter(|ua| !ua.is_empty())
.filter(|_| !is_copilot)
.and_then(|ua| http::HeaderValue::from_str(ua).ok());
// --- Copilot 优化器:动态 header 注入 ---
if let Some((ref classification, ref det_request_id, ref interaction_id)) =
copilot_optimization
@@ -1631,6 +1640,7 @@ impl RequestForwarder {
let mut ordered_headers = http::HeaderMap::new();
let mut saw_auth = false;
let mut saw_accept_encoding = false;
let mut saw_user_agent = false;
let mut saw_anthropic_beta = false;
let mut saw_anthropic_version = false;
@@ -1711,6 +1721,19 @@ impl RequestForwarder {
continue;
}
// --- user-agent: provider-level override for local proxy routing ---
if !is_copilot && key_str.eq_ignore_ascii_case("user-agent") {
if !saw_user_agent {
saw_user_agent = true;
if let Some(ref ua) = custom_user_agent {
ordered_headers.append(http::header::USER_AGENT, ua.clone());
} else {
ordered_headers.append(key.clone(), value.clone());
}
}
continue;
}
// --- anthropic-beta — 用重建值替换(确保含 claude-code 标记) ---
if key_str.eq_ignore_ascii_case("anthropic-beta") {
if !saw_anthropic_beta {
@@ -1760,6 +1783,12 @@ impl RequestForwarder {
);
}
if !saw_user_agent {
if let Some(ref ua) = custom_user_agent {
ordered_headers.append(http::header::USER_AGENT, ua.clone());
}
}
// 如果原始请求中没有 anthropic-beta 且有值需要添加,追加
if !saw_anthropic_beta {
if let Some(ref beta_val) = anthropic_beta_value {
@@ -138,6 +138,10 @@ interface ClaudeFormFieldsProps {
// Full URL mode
isFullUrl: boolean;
onFullUrlChange: (value: boolean) => void;
// Local proxy User-Agent override
customUserAgent: string;
onCustomUserAgentChange: (value: string) => void;
}
export function ClaudeFormFields({
@@ -190,6 +194,8 @@ export function ClaudeFormFields({
onApiKeyFieldChange,
isFullUrl,
onFullUrlChange,
customUserAgent,
onCustomUserAgentChange,
}: ClaudeFormFieldsProps) {
const { t } = useTranslation();
const hasAnyAdvancedValue = !!(
@@ -665,7 +671,30 @@ export function ClaudeFormFields({
/>
)}
{/* 高级选项(API 格式 + 认证字段 + 模型映射) */}
{category !== "official" && (
<div className="space-y-2">
<FormLabel htmlFor="claude-custom-user-agent">
{t("providerForm.customUserAgent", {
defaultValue: "自定义 User-Agent",
})}
</FormLabel>
<Input
id="claude-custom-user-agent"
type="text"
value={customUserAgent}
onChange={(e) => onCustomUserAgentChange(e.target.value)}
placeholder="Mozilla/5.0 ..."
autoComplete="off"
/>
<p className="text-xs text-muted-foreground">
{t("providerForm.customUserAgentHint", {
defaultValue:
"仅在开启本地路由/代理接管后生效,会替换转发到供应商 API 请求中的 User-Agent。",
})}
</p>
</div>
)}
{shouldShowModelSelector && (
<Collapsible open={advancedExpanded} onOpenChange={setAdvancedExpanded}>
<CollapsibleTrigger asChild>
@@ -72,6 +72,10 @@ interface CodexFormFieldsProps {
// Speed Test Endpoints
speedTestEndpoints: EndpointCandidate[];
// Local proxy User-Agent override
customUserAgent: string;
onCustomUserAgentChange: (value: string) => void;
}
type CodexCatalogRow = CodexCatalogModel & { rowId: string };
@@ -128,6 +132,8 @@ export function CodexFormFields({
catalogModels = [],
onCatalogModelsChange,
speedTestEndpoints,
customUserAgent,
onCustomUserAgentChange,
}: CodexFormFieldsProps) {
const { t } = useTranslation();
@@ -582,6 +588,33 @@ export function CodexFormFields({
onCustomEndpointsChange={onCustomEndpointsChange}
/>
)}
{category !== "official" && (
<div className="space-y-2">
<label
htmlFor="codex-custom-user-agent"
className="block text-sm font-medium text-foreground"
>
{t("providerForm.customUserAgent", {
defaultValue: "自定义 User-Agent",
})}
</label>
<Input
id="codex-custom-user-agent"
type="text"
value={customUserAgent}
onChange={(e) => onCustomUserAgentChange(e.target.value)}
placeholder="Mozilla/5.0 ..."
autoComplete="off"
/>
<p className="text-xs text-muted-foreground">
{t("providerForm.customUserAgentHint", {
defaultValue:
"仅在开启本地路由/代理接管后生效,会替换转发到供应商 API 请求中的 User-Agent。",
})}
</p>
</div>
)}
</>
);
}
@@ -355,6 +355,7 @@ function ProviderFormFull({
),
});
setCodexChatReasoning(initialData?.meta?.codexChatReasoning ?? {});
setCustomUserAgent(initialData?.meta?.customUserAgent ?? "");
}, [appId, initialData, supportsFullUrl]);
const defaultValues: ProviderFormData = useMemo(
@@ -509,6 +510,9 @@ function ProviderFormFull({
useState<CodexChatReasoning>(
() => initialData?.meta?.codexChatReasoning ?? {},
);
const [customUserAgent, setCustomUserAgent] = useState<string>(
() => initialData?.meta?.customUserAgent ?? "",
);
const {
codexAuth,
@@ -1386,6 +1390,10 @@ function ProviderFormFull({
localCodexApiFormat === "openai_chat"
? normalizeCodexChatReasoningForSave(codexChatReasoning)
: undefined,
customUserAgent:
appId === "claude" || appId === "codex"
? customUserAgent.trim() || undefined
: undefined,
testConfig: testConfig.enabled ? testConfig : undefined,
costMultiplier: pricingConfig.enabled
? pricingConfig.costMultiplier
@@ -2009,6 +2017,8 @@ function ProviderFormFull({
onApiKeyFieldChange={handleApiKeyFieldChange}
isFullUrl={localIsFullUrl}
onFullUrlChange={setLocalIsFullUrl}
customUserAgent={customUserAgent}
onCustomUserAgentChange={setCustomUserAgent}
/>
)}
@@ -2041,6 +2051,8 @@ function ProviderFormFull({
catalogModels={codexCatalogModels}
onCatalogModelsChange={setCodexCatalogModels}
speedTestEndpoints={speedTestEndpoints}
customUserAgent={customUserAgent}
onCustomUserAgentChange={setCustomUserAgent}
/>
)}
+2
View File
@@ -219,6 +219,8 @@ export interface ProviderMeta {
codexFastMode?: boolean;
// Codex Responses -> Chat Completions reasoning capability metadata
codexChatReasoning?: CodexChatReasoning;
// Custom User-Agent for local proxy routing. Only applied by the local proxy.
customUserAgent?: string;
// 供应商类型(用于识别 Copilot 等特殊供应商)
providerType?: string;
// GitHub Copilot 关联账号 ID(旧字段,保留兼容读取)