diff --git a/CHANGELOG.md b/CHANGELOG.md
index eed5dc9080..f4161cd2cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
## [Unreleased]
+### ✨ New Features
+
+- **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests.
+
---
## [3.5.0] — 2026-04-03
diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts
index fd261cec74..f3cb636079 100644
--- a/open-sse/executors/base.ts
+++ b/open-sse/executors/base.ts
@@ -70,11 +70,40 @@ export function mergeUpstreamExtraHeaders(
if (!extra) return;
for (const [k, v] of Object.entries(extra)) {
if (typeof k === "string" && k.length > 0 && typeof v === "string") {
+ if (k.toLowerCase() === "user-agent") {
+ setUserAgentHeader(headers, v);
+ continue;
+ }
headers[k] = v;
}
}
}
+export function getCustomUserAgent(providerSpecificData?: JsonRecord | null): string | null {
+ const customUserAgent =
+ typeof providerSpecificData?.customUserAgent === "string"
+ ? providerSpecificData.customUserAgent.trim()
+ : "";
+ return customUserAgent || null;
+}
+
+export function setUserAgentHeader(headers: Record, userAgent: string): void {
+ headers["User-Agent"] = userAgent;
+ if ("user-agent" in headers) {
+ headers["user-agent"] = userAgent;
+ }
+}
+
+export function applyConfiguredUserAgent(
+ headers: Record,
+ providerSpecificData?: JsonRecord | null
+): void {
+ const customUserAgent = getCustomUserAgent(providerSpecificData);
+ if (customUserAgent) {
+ setUserAgentHeader(headers, customUserAgent);
+ }
+}
+
export function mergeAbortSignals(primary: AbortSignal, secondary: AbortSignal): AbortSignal {
const controller = new AbortController();
@@ -156,9 +185,7 @@ export class BaseExecutor {
const envKey = `${providerId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_USER_AGENT`;
const envUA = process.env[envKey]?.trim();
if (envUA) {
- // Override both common casing variants
- headers["User-Agent"] = envUA;
- if (headers["user-agent"]) headers["user-agent"] = envUA;
+ setUserAgentHeader(headers, envUA);
}
}
@@ -238,6 +265,7 @@ export class BaseExecutor {
for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
const url = this.buildUrl(model, stream, urlIndex, credentials);
const headers = this.buildHeaders(credentials, stream);
+ applyConfiguredUserAgent(headers, credentials?.providerSpecificData);
// Append 1M context beta header when [1m] suffix was used
// Only supported for specific Claude models per Anthropic docs
diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts
index cfb903bcc6..61faa0ae43 100644
--- a/open-sse/handlers/chatCore.ts
+++ b/open-sse/handlers/chatCore.ts
@@ -632,18 +632,36 @@ export async function handleChatCore({
// Primary path: merge client model id + alias target so config on either key applies; resolved
// id wins on same header name. T5 family fallback uses only (nextModel, resolveModelAlias(next))
// so A-model headers are not sent to B — see buildUpstreamHeadersForExecute.
+ const connectionCustomUserAgent =
+ credentials?.providerSpecificData &&
+ typeof credentials.providerSpecificData === "object" &&
+ typeof credentials.providerSpecificData.customUserAgent === "string"
+ ? credentials.providerSpecificData.customUserAgent.trim()
+ : "";
+
const buildUpstreamHeadersForExecute = (modelToCall: string): Record => {
- if (modelToCall === effectiveModel) {
- return {
- ...getModelUpstreamExtraHeaders(provider || "", model || "", sourceFormat),
- ...getModelUpstreamExtraHeaders(provider || "", resolvedModel || "", sourceFormat),
- };
+ const upstreamHeaders =
+ modelToCall === effectiveModel
+ ? {
+ ...getModelUpstreamExtraHeaders(provider || "", model || "", sourceFormat),
+ ...getModelUpstreamExtraHeaders(provider || "", resolvedModel || "", sourceFormat),
+ }
+ : (() => {
+ const r = resolveModelAlias(modelToCall);
+ return {
+ ...getModelUpstreamExtraHeaders(provider || "", modelToCall || "", sourceFormat),
+ ...getModelUpstreamExtraHeaders(provider || "", r || "", sourceFormat),
+ };
+ })();
+
+ if (connectionCustomUserAgent) {
+ upstreamHeaders["User-Agent"] = connectionCustomUserAgent;
+ if ("user-agent" in upstreamHeaders) {
+ upstreamHeaders["user-agent"] = connectionCustomUserAgent;
+ }
}
- const r = resolveModelAlias(modelToCall);
- return {
- ...getModelUpstreamExtraHeaders(provider || "", modelToCall || "", sourceFormat),
- ...getModelUpstreamExtraHeaders(provider || "", r || "", sourceFormat),
- };
+
+ return upstreamHeaders;
};
// Default to false unless client explicitly sets stream: true (OpenAI spec compliant)
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx
index c0d6f5d6e5..3d2e20fb22 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx
+++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx
@@ -4523,11 +4523,13 @@ function AddApiKeyModal({
region: isVertex ? defaultRegion : "",
apiRegion: "international",
validationModelId: "",
+ customUserAgent: "",
});
const [validating, setValidating] = useState(false);
const [validationResult, setValidationResult] = useState(null);
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState(null);
+ const [showAdvanced, setShowAdvanced] = useState(false);
const handleValidate = async () => {
setValidating(true);
@@ -4540,6 +4542,7 @@ function AddApiKeyModal({
provider,
apiKey: formData.apiKey,
validationModelId: formData.validationModelId || undefined,
+ customUserAgent: formData.customUserAgent.trim() || undefined,
}),
});
const data = await res.json();
@@ -4578,6 +4581,7 @@ function AddApiKeyModal({
provider,
apiKey: formData.apiKey,
validationModelId: formData.validationModelId || undefined,
+ customUserAgent: formData.customUserAgent.trim() || undefined,
}),
});
const data = await res.json();
@@ -4594,29 +4598,27 @@ function AddApiKeyModal({
return;
}
+ const providerSpecificData: Record = {};
+ if (formData.customUserAgent.trim()) {
+ providerSpecificData.customUserAgent = formData.customUserAgent.trim();
+ }
+ if (isBailian) {
+ providerSpecificData.baseUrl = validatedBailianBaseUrl;
+ } else if (isVertex) {
+ providerSpecificData.region = formData.region;
+ } else if (isGlm) {
+ providerSpecificData.apiRegion = formData.apiRegion;
+ }
+
const payload = {
name: formData.name,
apiKey: formData.apiKey,
priority: formData.priority,
testStatus: "active",
- providerSpecificData: undefined,
+ providerSpecificData:
+ Object.keys(providerSpecificData).length > 0 ? providerSpecificData : undefined,
};
- // Include baseUrl in providerSpecificData for bailian-coding-plan
- if (isBailian) {
- payload.providerSpecificData = {
- baseUrl: validatedBailianBaseUrl,
- };
- } else if (isVertex) {
- payload.providerSpecificData = {
- region: formData.region,
- };
- } else if (isGlm) {
- payload.providerSpecificData = {
- apiRegion: formData.apiRegion,
- };
- }
-
const error = await onSave(payload);
if (error) {
setSaveError(typeof error === "string" ? error : t("failedSaveConnection"));
@@ -4694,6 +4696,35 @@ function AddApiKeyModal({
})}
)}
+
+ {showAdvanced && (
+
+ setFormData({ ...formData, customUserAgent: e.target.value })}
+ placeholder="my-app/1.0"
+ hint="Optional override sent upstream as the User-Agent header for this connection"
+ />
+
+ setFormData({ ...formData, customUserAgent: e.target.value })}
+ placeholder="my-app/1.0"
+ hint="Optional override sent upstream as the User-Agent header for this connection"
+ />
+