📨 feat: Custom Headers on Built-in Provider Endpoints#13742
Conversation
Add a `headers` config option to the built-in `openAI`, `anthropic`, and
`google` endpoints (incl. Anthropic/Google Vertex), mirroring the custom
endpoint header mechanism. Values support the same placeholder resolution
(env vars, `{{LIBRECHAT_USER_*}}`, `{{LIBRECHAT_BODY_CONVERSATIONID}}`) and
are resolved at request time so dynamic values like conversationId resolve
against the live request — without losing provider-native request shaping.
Closes #13082. Covers #13713: forwarding conversationId to a reverse proxy
is now `X-Conversation-Id: '{{LIBRECHAT_BODY_CONVERSATIONID}}'` — an unknown
header is ignored by the native Anthropic API, so no 400 and no metadata
gating needed.
- Schema: `headers` on `baseEndpointSchema` (openAI/google/anthropic/all).
- New `mergeHeaders`/`resolveConfigHeaders` utils centralize the per-provider
header locations (`configuration.defaultHeaders`, Anthropic
`clientOptions.defaultHeaders`, Google `customHeaders`); provider-managed
headers (auth, `anthropic-beta`) always win on collision.
- Each initializer threads configured headers (endpoint over `all`) into the
right place; request-time resolution runs across all locations in the main
and title flows.
|
@codex review |
Adding `headers` (a Record) to `baseEndpointSchema` makes `DeepPartial<TCustomConfig>` widen its value type to `string | undefined`, which is not assignable to the concrete `TEndpoint['headers']: Record<string, string>` at the `loadedEndpoints.all` assignment. Cast at the assignment site, mirroring the existing `anthropicConfig as TAnthropicEndpoint` cast in the same function.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a1600abebf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * (OpenAI `configuration.defaultHeaders`, Google `customHeaders`). The | ||
| * Anthropic `clientOptions` carrier is stripped from title requests via | ||
| * `omitTitleOptions`, so its headers do not apply to title generation. | ||
| */ | ||
| if (clientOptions?.configuration?.defaultHeaders != null) { | ||
| clientOptions.configuration.defaultHeaders = resolveHeaders({ | ||
| headers: clientOptions.configuration.defaultHeaders, | ||
| user: createSafeUser(this.options.req?.user), | ||
| body: { | ||
| messageId: this.responseMessageId, | ||
| conversationId: this.conversationId, | ||
| parentMessageId: this.parentMessageId, | ||
| }, | ||
| }); | ||
| } | ||
| resolveConfigHeaders({ |
There was a problem hiding this comment.
Preserve Anthropic headers on title requests
When the title provider is Anthropic and endpoints.anthropic.headers or endpoints.all.headers is used for a gateway/reverse proxy, this call runs after omitTitleOptions has already removed the Anthropic clientOptions carrier, so resolveConfigHeaders never sees clientOptions.defaultHeaders. Normal agent calls include the configured headers, but title generation omits them and can fail or be routed without the required metadata; keep the default headers for title requests or move them into a carrier that is not stripped.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 998a945. Title flow now captures clientOptions.defaultHeaders before the omitTitleOptions strip and restores just that carrier afterward (api/server/controllers/agents/client.js), so Anthropic gateway metadata reaches title requests while thinking/streaming options stay stripped. Added a test in client.test.js ("preserves Anthropic custom headers on title requests despite omitTitleOptions").
| const merged: Record<string, string> = { ...(base ?? {}), ...(override ?? {}) }; | ||
|
|
||
| const baseBeta = base?.['anthropic-beta']; | ||
| const overrideBeta = override?.['anthropic-beta']; |
There was a problem hiding this comment.
Normalize header names before merging
HTTP header names are case-insensitive, but this merge only detects exact key matches and only unions the exact lowercase anthropic-beta key. If an operator configures a case variant such as authorization or Anthropic-Beta while the provider code adds Authorization/anthropic-beta, the outbound object contains both names; clients commonly collapse those into one header value, which can invalidate auth or drop/garble the beta header instead of making provider-managed headers win.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 998a945. mergeHeaders now matches header names case-insensitively: an override key replaces any case variant from the base (keeping the override's casing) instead of emitting both, and anthropic-beta is unioned case-insensitively. Covered by two new cases in headers.spec.ts.
| const headers = mergeHeaders(allConfig?.headers, openAIConfig?.headers); | ||
| if (headers) { | ||
| clientOptions.headers = headers; |
There was a problem hiding this comment.
Skip headers for user-provided OpenAI URLs
When OPENAI_REVERSE_PROXY is user_provided, baseURL is controlled by the user, but this branch still attaches admin-configured headers that can contain ${...} gateway secrets or user/OpenID placeholders resolved later by resolveConfigHeaders. In that configuration a user can point the built-in OpenAI endpoint at their own URL and receive those headers; avoid forwarding configured headers when userProvidesURL is true.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 998a945. initializeOpenAI now withholds configured headers when userProvidesURL is true (OPENAI_REVERSE_PROXY=user_provided), so ${SECRET}/token placeholders never reach a user-controlled base URL — mirroring the existing fetchModels/custom-endpoint guard. Test: "withholds configured headers when the user supplies the base URL".
| clientOptions.azure = | ||
| userProvidesKey && userValues?.apiKey ? JSON.parse(userValues.apiKey) : getAzureCredentials(); | ||
| apiKey = clientOptions.azure ? clientOptions.azure.azureOpenAIApiKey : undefined; | ||
| } else { |
There was a problem hiding this comment.
Apply global headers to Azure OpenAI too
Because the header merge lives only in this non-Azure branch, requests through azureOpenAI never receive endpoints.all.headers even though the new schema/docs present all.headers as the global default and Azure uses the same OpenAI configuration.defaultHeaders carrier. In deployments where the Azure endpoint is routed through a gateway that requires the global metadata/auth header, OpenAI/Google/Anthropic calls work but Azure calls are sent without it; merge the global headers into the Azure path while keeping Azure's managed api-key/version headers authoritative.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 998a945. endpoints.all.headers now applies to both Azure paths (mapped-config and env-based) via the shared OpenAI configuration.defaultHeaders carrier; the Azure-managed api-key/version headers are still applied last in getOpenAIConfig so they stay authoritative. Test: "applies endpoints.all headers to the env-based Azure path".
Address Codex P2 findings on the custom-headers feature:
- Anthropic title requests: `omitTitleOptions` strips the `clientOptions`
carrier, which dropped its `defaultHeaders`. Preserve just the header carrier
so gateway/reverse-proxy metadata still reaches title generation.
- mergeHeaders: match header names case-insensitively so an override (e.g. a
provider-managed `Authorization`/`anthropic-beta`) replaces/uniones a
case-variant from the base instead of emitting two names a client may collapse.
- OpenAI: withhold admin-configured headers when the user supplies the base URL
(`user_provided`), since values may carry `${SECRET}`/token placeholders that
must not reach a user-controlled endpoint — mirrors the custom-endpoint guard.
- Azure: honor global `endpoints.all` headers (same OpenAI carrier) while keeping
Azure-managed `api-key`/version headers authoritative.
Adds tests for each.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 998a945aad
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * `api-key`/version headers added in `getOpenAIConfig`) stay authoritative. */ | ||
| clientOptions.headers = resolveHeaders({ | ||
| headers: { ...headers, ...(clientOptions.headers ?? {}) }, | ||
| headers: { ...globalHeaders, ...headers, ...(clientOptions.headers ?? {}) }, |
There was a problem hiding this comment.
Keep Azure global headers unresolved until request time
In the Azure model-group path, the newly added globalHeaders are passed through resolveHeaders during initialization, but the same configuration.defaultHeaders are resolved again later by resolveConfigHeaders in the agent/title request flow. If an endpoints.all.headers value contains a user placeholder and the user's name/email contains a ${SOME_ENV} pattern, the first pass substitutes the user-controlled value and the second pass expands it as an environment variable, violating the env-before-user invariant and forwarding that env value to the Azure/gateway request. Keep these new global headers unresolved here and let request-time resolution run once.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ceecdd6. The global endpoints.all headers are no longer passed through resolveHeaders at init in the Azure model-group path — they're merged in unresolved and resolved exactly once at request time by resolveConfigHeaders, so an already-substituted user value can't be re-expanded as an env var. Test: openai initialize spec now asserts the global header reaches getOpenAIConfig unresolved ({{LIBRECHAT_USER_ID}}).
| if (googleConfig.customHeaders != null) { | ||
| googleConfig.customHeaders = resolve(googleConfig.customHeaders as Record<string, string>); |
There was a problem hiding this comment.
Do not template-resolve Google auth headers
When GOOGLE_AUTH_HEADER=true, getGoogleConfig builds customHeaders.Authorization from the API key, and this new resolver now treats that provider-managed header as an admin-authored template. With GOOGLE_KEY=user_provided, a user can save a key containing ${OPENAI_API_KEY} (or another non-blocklisted env name), and this line expands it before the outbound Google/gateway request, letting user-controlled key material reference server environment values. Only the configured metadata headers should be resolved, or auth headers should be excluded from placeholder expansion.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ceecdd6. resolveConfigHeaders now skips the provider-managed Authorization (case-insensitive) when resolving Google customHeaders, so a user-provided key containing ${ENV} is never env-expanded; admin metadata headers in the same map still resolve. Scoped to the Google customHeaders carrier only — custom/OpenAI configuration.defaultHeaders still resolve Authorization (custom endpoints rely on it). Test added in headers.spec.ts.
| streamRate: z.number().optional(), | ||
| baseURL: z.string().optional(), | ||
| /** | ||
| * Custom request headers forwarded to the provider on every request. Values |
There was a problem hiding this comment.
Thread built-in headers into model fetches
This makes built-in endpoint headers a valid config and describes them as forwarded on every provider request, but the /api/models path still loads OpenAI/Anthropic defaults via loadDefaultModels → getOpenAIModels/fetchAnthropicModels without passing appConfig.endpoints.*.headers. When a built-in provider is behind a gateway that requires one of these headers for routing/auth, chat calls include it but model-list fetches omit it and fall back to stale defaults or fail to expose gateway models, so the header config needs to be threaded through model loading too.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ceecdd6. loadDefaultModels now threads the configured headers (endpoint over all) plus the user object into getOpenAIModels/getAnthropicModels → fetchModels. Also fixed fetchModels to merge custom headers for Anthropic rather than overwriting them with just x-api-key/anthropic-version (managed headers still win). Body placeholders can't resolve without conversation context, same as the custom-endpoint model-fetch path. Tests added in models.spec.ts.
…und 2)
Address Codex P2 findings:
- Azure: keep global `endpoints.all` headers unresolved at init and let
request-time `resolveConfigHeaders` resolve them once, avoiding a
second-order env expansion of already-substituted user values.
- Google: `resolveConfigHeaders` no longer template-resolves the
provider-managed `Authorization` header (built from a possibly user-provided
key), so a user key like `${ENV}` can't leak server environment values.
- Model fetches: thread configured headers (endpoint over `all`) + user object
through `getOpenAIModels`/`getAnthropicModels` → `fetchModels`, so a
gateway-fronted built-in provider receives the header on `/models` too. Fixed
`fetchModels` to merge custom headers for Anthropic instead of overwriting
them (managed `x-api-key`/version still win).
Adds/updates tests for each.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ceecdd6ab5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for (const [key, value] of Object.entries(headers)) { | ||
| (key.toLowerCase() === 'authorization' ? managed : templated)[key] = value; | ||
| } |
There was a problem hiding this comment.
Resolve admin-supplied Google Authorization headers
When a Google/Vertex endpoint is configured to authenticate to a gateway with headers.Authorization: 'Bearer {{LIBRECHAT_OPENID_ID_TOKEN}}' (or another placeholder) and GOOGLE_AUTH_HEADER is not enabled, this branch classifies that admin-supplied header as provider-managed and skips resolveHeaders, so the gateway receives the literal placeholder instead of the user token. Only the SDK-generated Authorization: Bearer ${apiKey} needs this protection; custom Authorization headers still need the same placeholder resolution as other configured headers.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 828169a. Reworked the approach: native Google headers are now resolved once at init in initializeGoogle (before the GOOGLE_AUTH_HEADER key-derived header is built), and resolveConfigHeaders no longer touches Google customHeaders. So an admin-supplied Authorization: Bearer {{LIBRECHAT_OPENID_ID_TOKEN}} resolves again (the previous round's name-based skip was too broad — thanks for catching that), while the SDK auth header still never passes through env expansion.
| ...(model_parameters ?? {}), | ||
| user: req.user?.id, | ||
| }, | ||
| ...(headers && { headers }), |
There was a problem hiding this comment.
Resolve native headers for memory runs
When a memory agent uses the built-in Anthropic endpoint with configured headers containing placeholders, the headers are added here intentionally unresolved, but the memory extraction path never calls resolveConfigHeaders and only resolves OpenAI-style configuration.defaultHeaders in packages/api/src/agents/memory.ts. The normal chat/title paths are covered, but memory requests will forward literals like {{LIBRECHAT_USER_EMAIL}} or ${GATEWAY_KEY}, so gateways that require these resolved metadata/auth headers reject memory extraction.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 828169a. processMemory now calls resolveConfigHeaders (with { conversationId, messageId }) instead of the OpenAI-only configuration.defaultHeaders resolution, so native Anthropic clientOptions.defaultHeaders resolve for memory runs too. Native Google headers are covered by the init-time resolution above. Test added in memory.spec.ts via the updated mock.
| if (anthropicDefaultHeaders != null && clientOptions.clientOptions == null) { | ||
| clientOptions.clientOptions = { defaultHeaders: anthropicDefaultHeaders }; |
There was a problem hiding this comment.
Preserve the Vertex clientOptions object for titles
For Anthropic Vertex title generation with templated headers, this recreates clientOptions after omitTitleOptions removes the original, but the preserved createClient closure from getLLMConfig still closes over the original requestOptions.clientOptions. resolveConfigHeaders then replaces the headers on this new object, while the Vertex client is created with the old unresolved headers, so title calls through a gateway can receive {{LIBRECHAT_BODY_CONVERSATIONID}} literally. Keep the original clientOptions object (or update the object captured by createClient) when preserving the headers.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 828169a. The title flow now restores the original clientOptions object reference (not a fresh { defaultHeaders }) after omitTitleOptions, so the Vertex createClient closure and resolveConfigHeaders operate on the same object — the client is built with the resolved headers. Good catch on the closure aliasing.
| resolveConfigHeaders({ | ||
| llmConfig, | ||
| user: createSafeUser(user), | ||
| body: requestBody, | ||
| }); |
There was a problem hiding this comment.
Avoid resolving the same header map twice
When the same initialized built-in-provider agent object is reused as both a root agent and a subagent target (or by multiple parents), buildAgentInput can run more than once on the same nested header objects. Because resolveConfigHeaders mutates those objects in place, the second call runs env expansion after user/body substitution from the first call; a user-controlled value such as a name containing ${GATEWAY_SECRET} can then be expanded against process.env, violating the env-before-user invariant documented in resolveHeaders.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 828169a. resolveConfigHeaders is now idempotent — it tracks resolved header maps in a WeakSet (keyed by object identity, per-request fresh) and skips re-resolution, so a second buildAgentInput pass over a reused agent's nested header objects can't re-run env expansion over already-substituted user/body values. Test added in headers.spec.ts.
GitNexus: ❌ deploy failedThe deploy failed — the previous index (if any) continues to be served. |
…ound 3) Address Codex P2 findings, including two regressions from the prior round: - Google auth (findings 6 & 8): move native Google header resolution to init (`initializeGoogle`), resolving admin templates BEFORE the key-derived auth header is built. resolveConfigHeaders no longer touches Google `customHeaders`, so admin `Authorization` templates resolve again (fixes the round-2 regression) while the SDK auth header (possibly a user-provided key) is never env-expanded. - Memory runs: memory extraction now calls `resolveConfigHeaders`, so native Anthropic (and OpenAI) headers resolve for memory requests too. - Vertex titles: restore the ORIGINAL `clientOptions` object reference (not a copy) when preserving headers across `omitTitleOptions`, so the Vertex `createClient` closure and the resolved headers stay on the same object. - Reuse: `resolveConfigHeaders` is now idempotent (resolve-once per header map), preventing a second pass from env-expanding values already substituted with user/body data when an agent object flows through buildAgentInput twice. Adds/updates tests for each.
|
@codex review |
|
Codex Review: Didn't find any major issues. Nice work! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
GitNexus: ❌ deploy failedThe deploy failed — the previous index (if any) continues to be served. |
) * 📨 feat: Custom Headers on Built-in Provider Endpoints Add a `headers` config option to the built-in `openAI`, `anthropic`, and `google` endpoints (incl. Anthropic/Google Vertex), mirroring the custom endpoint header mechanism. Values support the same placeholder resolution (env vars, `{{LIBRECHAT_USER_*}}`, `{{LIBRECHAT_BODY_CONVERSATIONID}}`) and are resolved at request time so dynamic values like conversationId resolve against the live request — without losing provider-native request shaping. Closes danny-avila#13082. Covers danny-avila#13713: forwarding conversationId to a reverse proxy is now `X-Conversation-Id: '{{LIBRECHAT_BODY_CONVERSATIONID}}'` — an unknown header is ignored by the native Anthropic API, so no 400 and no metadata gating needed. - Schema: `headers` on `baseEndpointSchema` (openAI/google/anthropic/all). - New `mergeHeaders`/`resolveConfigHeaders` utils centralize the per-provider header locations (`configuration.defaultHeaders`, Anthropic `clientOptions.defaultHeaders`, Google `customHeaders`); provider-managed headers (auth, `anthropic-beta`) always win on collision. - Each initializer threads configured headers (endpoint over `all`) into the right place; request-time resolution runs across all locations in the main and title flows. * 🩹 fix: Cast endpoints.all to TEndpoint for headers DeepPartial widening Adding `headers` (a Record) to `baseEndpointSchema` makes `DeepPartial<TCustomConfig>` widen its value type to `string | undefined`, which is not assignable to the concrete `TEndpoint['headers']: Record<string, string>` at the `loadedEndpoints.all` assignment. Cast at the assignment site, mirroring the existing `anthropicConfig as TAnthropicEndpoint` cast in the same function. * 🛡️ fix: Harden built-in endpoint custom headers (Codex review) Address Codex P2 findings on the custom-headers feature: - Anthropic title requests: `omitTitleOptions` strips the `clientOptions` carrier, which dropped its `defaultHeaders`. Preserve just the header carrier so gateway/reverse-proxy metadata still reaches title generation. - mergeHeaders: match header names case-insensitively so an override (e.g. a provider-managed `Authorization`/`anthropic-beta`) replaces/uniones a case-variant from the base instead of emitting two names a client may collapse. - OpenAI: withhold admin-configured headers when the user supplies the base URL (`user_provided`), since values may carry `${SECRET}`/token placeholders that must not reach a user-controlled endpoint — mirrors the custom-endpoint guard. - Azure: honor global `endpoints.all` headers (same OpenAI carrier) while keeping Azure-managed `api-key`/version headers authoritative. Adds tests for each. * 🔐 fix: Resolve-once + provider-managed header safety (Codex review round 2) Address Codex P2 findings: - Azure: keep global `endpoints.all` headers unresolved at init and let request-time `resolveConfigHeaders` resolve them once, avoiding a second-order env expansion of already-substituted user values. - Google: `resolveConfigHeaders` no longer template-resolves the provider-managed `Authorization` header (built from a possibly user-provided key), so a user key like `${ENV}` can't leak server environment values. - Model fetches: thread configured headers (endpoint over `all`) + user object through `getOpenAIModels`/`getAnthropicModels` → `fetchModels`, so a gateway-fronted built-in provider receives the header on `/models` too. Fixed `fetchModels` to merge custom headers for Anthropic instead of overwriting them (managed `x-api-key`/version still win). Adds/updates tests for each. * 🧯 fix: Header provenance, memory/title coverage, idempotency (Codex round 3) Address Codex P2 findings, including two regressions from the prior round: - Google auth (findings 6 & 8): move native Google header resolution to init (`initializeGoogle`), resolving admin templates BEFORE the key-derived auth header is built. resolveConfigHeaders no longer touches Google `customHeaders`, so admin `Authorization` templates resolve again (fixes the round-2 regression) while the SDK auth header (possibly a user-provided key) is never env-expanded. - Memory runs: memory extraction now calls `resolveConfigHeaders`, so native Anthropic (and OpenAI) headers resolve for memory requests too. - Vertex titles: restore the ORIGINAL `clientOptions` object reference (not a copy) when preserving headers across `omitTitleOptions`, so the Vertex `createClient` closure and the resolved headers stay on the same object. - Reuse: `resolveConfigHeaders` is now idempotent (resolve-once per header map), preventing a second pass from env-expanding values already substituted with user/body data when an agent object flows through buildAgentInput twice. Adds/updates tests for each.
) * 📨 feat: Custom Headers on Built-in Provider Endpoints Add a `headers` config option to the built-in `openAI`, `anthropic`, and `google` endpoints (incl. Anthropic/Google Vertex), mirroring the custom endpoint header mechanism. Values support the same placeholder resolution (env vars, `{{LIBRECHAT_USER_*}}`, `{{LIBRECHAT_BODY_CONVERSATIONID}}`) and are resolved at request time so dynamic values like conversationId resolve against the live request — without losing provider-native request shaping. Closes danny-avila#13082. Covers danny-avila#13713: forwarding conversationId to a reverse proxy is now `X-Conversation-Id: '{{LIBRECHAT_BODY_CONVERSATIONID}}'` — an unknown header is ignored by the native Anthropic API, so no 400 and no metadata gating needed. - Schema: `headers` on `baseEndpointSchema` (openAI/google/anthropic/all). - New `mergeHeaders`/`resolveConfigHeaders` utils centralize the per-provider header locations (`configuration.defaultHeaders`, Anthropic `clientOptions.defaultHeaders`, Google `customHeaders`); provider-managed headers (auth, `anthropic-beta`) always win on collision. - Each initializer threads configured headers (endpoint over `all`) into the right place; request-time resolution runs across all locations in the main and title flows. * 🩹 fix: Cast endpoints.all to TEndpoint for headers DeepPartial widening Adding `headers` (a Record) to `baseEndpointSchema` makes `DeepPartial<TCustomConfig>` widen its value type to `string | undefined`, which is not assignable to the concrete `TEndpoint['headers']: Record<string, string>` at the `loadedEndpoints.all` assignment. Cast at the assignment site, mirroring the existing `anthropicConfig as TAnthropicEndpoint` cast in the same function. * 🛡️ fix: Harden built-in endpoint custom headers (Codex review) Address Codex P2 findings on the custom-headers feature: - Anthropic title requests: `omitTitleOptions` strips the `clientOptions` carrier, which dropped its `defaultHeaders`. Preserve just the header carrier so gateway/reverse-proxy metadata still reaches title generation. - mergeHeaders: match header names case-insensitively so an override (e.g. a provider-managed `Authorization`/`anthropic-beta`) replaces/uniones a case-variant from the base instead of emitting two names a client may collapse. - OpenAI: withhold admin-configured headers when the user supplies the base URL (`user_provided`), since values may carry `${SECRET}`/token placeholders that must not reach a user-controlled endpoint — mirrors the custom-endpoint guard. - Azure: honor global `endpoints.all` headers (same OpenAI carrier) while keeping Azure-managed `api-key`/version headers authoritative. Adds tests for each. * 🔐 fix: Resolve-once + provider-managed header safety (Codex review round 2) Address Codex P2 findings: - Azure: keep global `endpoints.all` headers unresolved at init and let request-time `resolveConfigHeaders` resolve them once, avoiding a second-order env expansion of already-substituted user values. - Google: `resolveConfigHeaders` no longer template-resolves the provider-managed `Authorization` header (built from a possibly user-provided key), so a user key like `${ENV}` can't leak server environment values. - Model fetches: thread configured headers (endpoint over `all`) + user object through `getOpenAIModels`/`getAnthropicModels` → `fetchModels`, so a gateway-fronted built-in provider receives the header on `/models` too. Fixed `fetchModels` to merge custom headers for Anthropic instead of overwriting them (managed `x-api-key`/version still win). Adds/updates tests for each. * 🧯 fix: Header provenance, memory/title coverage, idempotency (Codex round 3) Address Codex P2 findings, including two regressions from the prior round: - Google auth (findings 6 & 8): move native Google header resolution to init (`initializeGoogle`), resolving admin templates BEFORE the key-derived auth header is built. resolveConfigHeaders no longer touches Google `customHeaders`, so admin `Authorization` templates resolve again (fixes the round-2 regression) while the SDK auth header (possibly a user-provided key) is never env-expanded. - Memory runs: memory extraction now calls `resolveConfigHeaders`, so native Anthropic (and OpenAI) headers resolve for memory requests too. - Vertex titles: restore the ORIGINAL `clientOptions` object reference (not a copy) when preserving headers across `omitTitleOptions`, so the Vertex `createClient` closure and the resolved headers stay on the same object. - Reuse: `resolveConfigHeaders` is now idempotent (resolve-once per header map), preventing a second pass from env-expanding values already substituted with user/body data when an agent object flows through buildAgentInput twice. Adds/updates tests for each.
Summary
I added a
headersconfig option to the built-inopenAI,anthropic, andgoogleendpoints (including Anthropic and Google Vertex), mirroring the dynamic-header mechanism that custom endpoints already have. This lets operators route native providers through an AI gateway / reverse proxy and attach per-user/per-conversation metadata headers — without giving up provider-native request shaping (Anthropicsystem/VertexrawPredict, Google SDK behavior, streaming, provider-specific params).Resolves #13082 and covers #13713. It also paves the way for the maintainer plan: the existing
{{LIBRECHAT_BODY_CONVERSATIONID}}convention now works on the built-in Anthropic endpoint, so forwarding the conversation id to a reverse proxy is simply a header — and because native provider APIs ignore unknown headers, there is no 400 and no reverse-proxy gating to reason about.Example:
headerstobaseEndpointSchemasoopenAI,google,anthropic, andallaccept it (values keep their placeholders until request time).mergeHeadersandresolveConfigHeadersutilities that centralize the three provider-specific header carriers — OpenAI/Azure/customconfiguration.defaultHeaders, native AnthropicclientOptions.defaultHeaders, and native GooglecustomHeaders— and resolve env/user/body placeholders across all of them.endpoints.all) into each initializer (anthropic,openai,google), keeping provider-managed headers authoritative:anthropic-betais comma-unioned, and auth/version headers always win on collision so a metadata header can never override them.resolveConfigHeaders, so native providers resolve request-time placeholders (e.g.conversationId) too.getOpenAIConfigto reuse the sharedmergeHeadershelper, removing the duplicate local merge function.openAI,google, andanthropicinlibrechat.example.yaml.Change Type
Testing
Default behavior is unchanged for anyone not configuring
headers. To verify the feature, point a provider at a gateway/reverse-proxy (e.g.ANTHROPIC_REVERSE_PROXY), add aheadersblock under that endpoint inlibrechat.yaml, send a message, and confirm the resolved headers (with{{LIBRECHAT_BODY_CONVERSATIONID}}/{{LIBRECHAT_USER_EMAIL}}substituted) arrive at the proxy while native request formatting is intact.Test Configuration:
Automated coverage added/updated and passing:
packages/api/src/utils/headers.spec.ts—mergeHeadersprecedence +anthropic-betaunion;resolveConfigHeadersacross all three carriers, including env-var and conversationId placeholders.packages/api/src/endpoints/anthropic/{llm,initialize}.spec.ts— native Anthropic formatting preserved while a custom metadata header is attached;anthropic-betanot clobbered;endpoints.alllayering; placeholders left unresolved at init.packages/api/src/endpoints/google/llm.spec.ts— headers merged intocustomHeaders, providerAuthorizationwins.packages/api/src/endpoints/openai/initialize.spec.ts— endpoint headers (merged overall) forwarded togetOpenAIConfig.tsc --noEmit, ESLint, and the import-sort check pass for all changed files; the affectedpackages/api(endpoints,utils,agents,app) andapiagents-client suites are green.Checklist