Repository navigation
[Bug]: No management-plane support for provider headers — custom providers needing client fingerprints (Agent Router) 401 after headers are lost #959
Description
Client or integration
Direct HTTP/API client (Codex App / Claude Code also affected via the proxy)
Area
Proxy and routing · Provider adapter
Summary
The provider management API and CLI have no way to set or restore the headers field on a provider. PATCH /api/providers accepts only a fixed allowlist of fields (disabled, adapter, baseUrl, defaultModel, authMode, apiKeyTransport, note, allowPrivateNetwork, liveModels, codexAccountMode, setDefault) and ocx provider edit exposes the same set as flags — there is no headers option. If a custom provider's headers block is lost (e.g. the provider entry is re-saved via POST /api/providers without round-tripping it, or a config migration drops it), the only recovery is hand-editing ~/.opencodex/config.json and restarting the proxy.
Concrete failure: the Agent Router gateway (agentrouter.org) rejects requests that carry a valid API key but not the Claude Code CLI client fingerprint (User-Agent, x-app, X-Stainless-*, anthropic-version, anthropic-beta). The openai-chat adapter sends Authorization: Bearer <key> and then merges provider.headers; without the fingerprint headers the gateway returns 401 unauthorized client detected. In this install the headers block on the AGR-OAI provider (present in a 2026-07-29 config backup) was missing from the 2026-08-02 config, and every routed request failed with that 401 until the block was restored by hand.
Expected: PATCH /api/providers and ocx provider edit should accept headers (merge or replace), and any provider save path should round-trip unknown provider fields so custom headers survive.
Reproduction
- Install
@bitkyc08/opencodex2.10.0 and start the proxy (ocx start). - Add a custom provider that requires fingerprint headers, e.g. Agent Router, in
~/.opencodex/config.json:{ "providers": { "AGR-OAI": { "adapter": "openai-chat", "baseUrl": "https://agentrouter.org/v1", "authMode": "key", "apiKey": "sk-<redacted>", "headers": { "User-Agent": "claude-cli/2.1.219 (external, sdk-cli)", "anthropic-version": "2023-06-01", "anthropic-beta": "claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24", "x-app": "cli", "X-Stainless-Retry-Count": "0", "X-Stainless-Package-Version": "0.94.0" } } } } - Try to manage the headers through the management plane:
Or via API:
ocx provider edit AGR-OAI --enabled on # works, headers preserved ocx provider edit AGR-OAI --headers '{"x-app":"cli"}' # no such flag; exits with usage errorPATCH /api/providers?name=AGR-OAI { "headers": { "x-app": "cli" } } → 400 {"error":"no recognized fields to update"} - Simulate headers being lost:
POST /api/providerswith a provider payload that omitsheaders(the field is optional and not validated as required), then send a request with an image-free message:POST /v1/responses { "model": "AGR-OAI/gpt-5.6-sol", "input": [...] } → 401 {"error":{"message":"unauthorized client detected, contact support for assistance at https://discord.gg/aYq5B4RW3"},"message":"UNAUTHENTICATED","success":false,"type":"unauthorized_client_error"} - Observe: only a hand edit of
~/.opencodex/config.json(restoringheaders) plus a restart fixes it; the management plane cannot.
Version
2.10.0 (npm @bitkyc08/opencodex); Codex runtime 0.146.0
Operating system
Windows 11 (current), America/Fortaleza timezone
Provider and model
agentrouter.org via custom provider AGR-OAI (openai-chat adapter), model gpt-5.6-sol; same issue applies to any provider needing custom headers (e.g. AGR anthropic adapter uses the same header block)
Logs or error output
Provider error 401: {"error":{"message":"unauthorized client detected, contact support for assistance at https://discord.gg/aYq5B4RW3"},"message":"UNAUTHENTICATED","success":false,"type":"unauthorized_client_error"}
Verified live: GET https://agentrouter.org/v1/models with Authorization: Bearer <key> and no fingerprint headers → 401; with the claude-cli headers added → 200 and the model list is returned.
Redacted configuration
{
"providers": {
"AGR-OAI": {
"adapter": "openai-chat",
"baseUrl": "https://agentrouter.org/v1",
"authMode": "key",
"apiKey": "sk-<redacted>",
"headers": {
"User-Agent": "claude-cli/2.1.219 (external, sdk-cli)",
"anthropic-version": "2023-06-01",
"anthropic-beta": "claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24",
"anthropic-dangerous-direct-browser-access": "true",
"x-app": "cli",
"X-Stainless-Retry-Count": "0",
"X-Stainless-Timeout": "600",
"X-Stainless-Lang": "js",
"X-Stainless-Package-Version": "0.94.0",
"X-Stainless-OS": "MacOS",
"X-Stainless-Arch": "arm64",
"X-Stainless-Runtime": "node",
"X-Stainless-Runtime-Version": "v26.3.0"
}
}
}
}Code references
PATCH /api/providersfield allowlist:src/server/management/provider-routes.ts— onlydisabled,adapter,baseUrl,defaultModel,authMode,apiKeyTransport,note,allowPrivateNetwork,liveModels(plus the exclusivecodexAccountMode/setDefaultpaths); unknown fields →400 "no recognized fields to update".- CLI flags mirror that allowlist:
src/cli/provider-runtime.ts(ocx provider edit ...has no--headers). - The adapter applies
provider.headersafter the defaultAuthorizationheader:src/adapters/openai-chat.tsandsrc/adapters/anthropic.ts. providerHeadersConfigErrorinsrc/config.tsalready validates theheadersshape, so the schema supports it — only the management-plane mutation paths are missing it.
Suggested fix
- Accept
headers(object) inPATCH /api/providers, merging or replacing per field semantics, validated byproviderHeadersConfigError. - Add a
--headers <json>option toocx provider edit(or a dedicated management route). - Ensure
POST /api/providerssave paths (GUI/CLI) round-trip unknown provider fields likeheadersso a provider re-save does not silently drop them.
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
github-actions commentedon Aug 3, 2026
Automated translation bookkeeping — detected language: unknown.
Ingwannu commentedon Aug 4, 2026
Maintainer triage: the core management-plane gap is confirmed. The config schema and adapters support validated custom
headers, but currentPATCH /api/providersandocx provider editexpose no way to set, merge, or clear them.Draft PR #961 is the active implementation, so I will not duplicate the contributor work. Its current merge/clear semantics and sensitive-header validation are directionally correct, but the draft is not review-ready yet: a casing-only update can preserve conflicting keys such as
X-Fooandx-foo, and concurrent PATCH requests can still persist stale provider snapshots and erase each other. The documentation also needs to state the validation and clear behavior consistently.One scope clarification:
ocx provider edit ... --enabledalready uses PATCH and preserves existing headers.POST /api/providersis an overwrite operation, so omission replaces the submitted provider shape; the independently confirmed bug is that the supported management surfaces cannot safely edit or restore the header block.Leaving this open and linked to #961 until those blockers are addressed and the focused management/CLI tests are green.
Merge pull request #1033 from lidge-jun/codex/lane-11-provider-headers
feat(providers): expose hasHeaders on GET /api/providers — presence o…
lidge-jun commentedon Aug 5, 2026
Fixed on
devin #1033 (merged as51c4be686).Custom provider headers can now be managed through
PATCH /api/providers/:nameandocx provider edit --headers <json>, with merge semantics and{}/-to clear. Header names are matched case-insensitively and static headers are restored correctly.Two things are deliberately constrained. The validator rejects standard credential header names (
Authorization,X-Api-Key,Cookie, …) and points atapiKey/authModeinstead, andGET /api/providersexposes onlyhasHeaders— presence, never names or values — matching the existing presence-only DTO on/api/config.Worth reading before you use it:
--headersis documented in all five locales as non-secret request metadata. The validator cannot recognize an arbitrary name likeX-My-Token, and the JSON is a command-line argument, so a secret placed there lands in shell history and the process list, and persists inconfig.jsonin cleartext unlike API keys.Thanks to @Yuxin-Qiao — #961 is the implementation that shipped, carried with authorship intact.
Not in a release yet. This is on
dev, andmainis atv2.10.0. It ships in the next release.feat(providers): expose hasHeaders on GET /api/providers — presence o…
Merge pull request lidge-jun#1033 from lidge-jun/codex/lane-11-provid…