Skip to content

feat: manage provider custom headers via PATCH and ocx provider edit --headers - #961

Draft
Yuxin-Qiao wants to merge 2 commits into
lidge-jun:devfrom
Yuxin-Qiao:fix/provider-headers-management
Draft

feat: manage provider custom headers via PATCH and ocx provider edit --headers#961
Yuxin-Qiao wants to merge 2 commits into
lidge-jun:devfrom
Yuxin-Qiao:fix/provider-headers-management

Conversation

@Yuxin-Qiao

@Yuxin-Qiao Yuxin-Qiao commented Aug 3, 2026

Copy link
Copy Markdown

Summary

The provider management plane has no way to set or restore the headers field, so custom providers that require client-fingerprint headers (e.g. Agent Router via the openai-chat/anthropic adapters) fail with upstream 401 unauthorized client detected after the block is lost — the only recovery was hand-editing ~/.opencodex/config.json.

  • PATCH /api/providers?name=... now accepts a headers object. It is shallow-merged onto the provider's existing block (adding one fingerprint header never wipes the rest); null or {} clears the whole block. Validation reuses providerHeadersConfigError, so sensitive headers (Authorization etc.), invalid names, non-string values, and CRLF injection are rejected with 400.
  • ocx provider edit gains --headers <json> (and --headers - / --headers '{}' to clear). The value is parsed client-side and sent through the same PATCH route, so CLI and API validation stay identical.
  • POST /api/providers already round-trips headers for custom providers, so a re-save keeps them; the issue's third ask is covered by the existing path.
  • Docs updated in all locales (en/ko/ja/zh-cn/ru).

Example:

ocx provider edit AGR-OAI --headers '{"x-app":"cli","anthropic-version":"2023-06-01"}'

Tests

  • bun run typecheck — pass
  • bun test tests/cli-headless-parity.test.ts tests/cli-provider.test.ts tests/config.test.ts tests/repo-hygiene.test.ts — 168 pass
  • bun run privacy:scan — pass
  • New regression tests: PATCH merge/clear/validation cases in tests/management-provider-validation.test.ts; CLI --headers forwarding and malformed-JSON rejection in tests/cli-headless-parity.test.ts.

Environment note: tests/management-provider-validation.test.ts cannot fully run on this machine because local Clash DNS maps *.example.test to 198.18.x.x benchmark addresses, which the destination policy deliberately rejects. The same pre-existing tests fail identically on clean dev; CI's unresolvable .test hosts take the allowed path.

Fixes #959

Summary by CodeRabbit

  • New Features

    • Added support for managing custom provider request headers through ocx provider edit.
    • Headers can be merged, updated, or cleared using {}, -, or null.
    • Provider API updates now support merged headers configuration.
  • Bug Fixes

    • Invalid, malformed, sensitive, or non-string header values are rejected without changing existing settings.
  • Documentation

    • Updated CLI and API documentation across supported languages.

PATCH /api/providers now accepts a headers field, shallow-merged onto the existing block (null or {} clears it), and ocx provider edit gains --headers <json>. Custom providers such as Agent Router can restore required fingerprint headers through the management plane instead of hand-editing config.json. POST /api/providers already round-trips headers; validation reuses providerHeadersConfigError.
@github-actions github-actions Bot added the enhancement New feature or request label Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 745be455-4997-4928-8893-b771a3fc3109

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The provider edit CLI now accepts --headers <json>. The management API validates, merges, or clears provider headers. Tests cover valid updates, invalid values, persistence, and catalog refresh behavior. CLI and API documentation were updated in five locales.

Changes

Provider header management

Layer / File(s) Summary
CLI header parsing and request construction
src/cli/provider-runtime.ts, tests/cli-headless-parity.test.ts, docs-site/src/content/docs/*/reference/cli/providers-accounts.md
ocx provider edit parses --headers <json>, sends object values, clears headers with - or null, and rejects malformed or non-object JSON. Documentation covers merge and clear behavior.
PATCH header updates and validation
src/server/management/provider-routes.ts, tests/management-provider-validation.test.ts, docs-site/src/content/docs/*/reference/management-api.md
PATCH /api/providers validates header names and string values, shallowly merges valid objects, clears headers with null or {}, preserves state after rejection, and refreshes the catalog.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI as ocx provider edit
  participant Runtime as provider-runtime.ts
  participant API as PATCH /api/providers
  participant Route as provider-routes.ts
  participant State as Provider state and catalog

  CLI->>Runtime: Read --headers <json>
  Runtime->>Runtime: Parse or reject header value
  Runtime->>API: Send headers patch
  API->>Route: Validate and apply update
  Route->>State: Persist headers and refresh catalog
Loading

Possibly related PRs

  • lidge-jun/opencodex#595: Overlaps in provider CLI and PATCH handling, but adds apiKeyTransport rather than headers.

Suggested reviewers: luvs01, ingwannu, wibias

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes provider header management through both PATCH and the ocx provider edit --headers option.
Linked Issues check ✅ Passed The changes satisfy issue #959 by adding PATCH and CLI header management, validation, clearing, persistence support, documentation, and regression tests.
Out of Scope Changes check ✅ Passed All implementation, documentation, and test changes directly support provider header management described in issue #959.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2f2eb0f53d

ℹ️ 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".

const authMode = cleared(takeOption(args, "--auth-mode"));
const note = cleared(takeOption(args, "--note"));
const apiKeyTransport = cleared(takeOption(args, "--api-key-transport"));
const headers = takeOption(args, "--headers");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact header JSON left in CLI arguments

When a user uses the common --headers={...} spelling or repeats --headers, takeOption leaves the unconsumed token in args, and rejectArgs prints the complete JSON verbatim to stderr. Custom header values can contain tokens or account identifiers, so a syntax error can expose them in terminal or CI logs before server-side validation runs; parse this option with a secret-aware helper or explicitly redact its value from usage errors.

AGENTS.md reference: AGENTS.md:L189-L195

Useful? React with 👍 / 👎.

if (!isPlainRecord(headersValue)) return jsonResponse({ error: "headers must be an object" }, 400);
const headersError = providerHeadersConfigError(headersValue);
if (headersError) return jsonResponse({ error: headersError }, 400);
next.headers = { ...next.headers, ...headersValue } as Record<string, string>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replace header keys case-insensitively during merge

When an existing header is patched using different capitalization, such as replacing X-Custom with x-custom, this object spread retains both keys instead of replacing the HTTP header. Bun's Headers normalization then emits a combined value such as x-custom: v1, v2, so the PATCH succeeds while the upstream receives an invalid fingerprint; remove any existing case-insensitive match before adding the patched entry.

Useful? React with 👍 / 👎.

Comment on lines +294 to +295
if (headersValue === null || (isPlainRecord(headersValue) && Object.keys(headersValue).length === 0)) {
delete next.headers;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve registry static headers when clearing custom headers

When a registry provider carries required static metadata in headers, clearing custom headers also deletes that metadata. For example, ocx provider add opencode-free seeds x-opencode-client: desktop, but a later --headers - or --headers '{}' removes the entire block, so subsequent upstream requests lose the provider's client marker; clear only user-managed entries or rederive the registry's static headers before persisting.

AGENTS.md reference: src/AGENTS.md:L18-L18

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs-site/src/content/docs/ja/reference/management-api.md`:
- Line 169: Update the Markdown table row for PATCH /api/providers in the
management API reference by inserting the missing cell delimiter before the
“400” response description, restoring three properly separated cells while
preserving the existing Japanese documentation text.

In `@docs-site/src/content/docs/reference/cli/providers-accounts.md`:
- Line 19: Update the `edit <name>` provider-field description to state that
`--headers` applies only to custom/OpenAI-compatible upstream endpoints and
cannot set or override sensitive authentication headers; retain the existing
merge and clear behavior. Apply the same restriction wording in English at
docs-site/src/content/docs/reference/cli/providers-accounts.md:19-19, and
translate it for
docs-site/src/content/docs/ja/reference/cli/providers-accounts.md:18-18,
docs-site/src/content/docs/ko/reference/cli/providers-accounts.md:18-18, and
docs-site/src/content/docs/ru/reference/cli/providers-accounts.md:20-20.

In `@docs-site/src/content/docs/reference/management-api.md`:
- Line 186: Update the PATCH /api/providers endpoint row in
docs-site/src/content/docs/reference/management-api.md:186-186 to document
shallow merging of non-empty headers, clearing with null or {}, rejection of
sensitive authentication headers, invalid names, non-string values, and CRLF
with HTTP 400, and the distinction between configurable custom-adapter headers
and forwarded caller credentials. Translate and apply the same semantics in
docs-site/src/content/docs/ja/reference/management-api.md:169-169,
docs-site/src/content/docs/ko/reference/management-api.md:169-169,
docs-site/src/content/docs/ru/reference/management-api.md:188-188, and
docs-site/src/content/docs/zh-cn/reference/management-api.md:169-169.

In `@src/server/management/provider-routes.ts`:
- Around line 289-300: The headers PATCH merge in the provider route must be
case-insensitive. Update the merge logic in the headers handling block to remove
existing keys whose lowercase names match any incoming patch key before adding
the patch, so casing-only updates cannot preserve duplicates or conflicts. Keep
validation and null/empty clearing behavior unchanged, and add a regression test
covering an existing X-Foo header followed by a x-foo patch.
- Around line 289-304: Update handleProviderRoutes so PATCH mutations re-read
the latest provider and merge changes inside the configuration mutation lock
immediately before saving, rather than persisting the stale snapshot captured
before awaited validation; preserve existing validation and PATCH semantics. Add
a regression test in management provider validation tests that performs
concurrent PATCH requests for different headers and asserts both updates remain.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: aefa9390-56be-44f9-a7c0-aca0dc6a935d

📥 Commits

Reviewing files that changed from the base of the PR and between cb8fcf3 and 2f2eb0f.

📒 Files selected for processing (14)
  • docs-site/src/content/docs/ja/reference/cli/providers-accounts.md
  • docs-site/src/content/docs/ja/reference/management-api.md
  • docs-site/src/content/docs/ko/reference/cli/providers-accounts.md
  • docs-site/src/content/docs/ko/reference/management-api.md
  • docs-site/src/content/docs/reference/cli/providers-accounts.md
  • docs-site/src/content/docs/reference/management-api.md
  • docs-site/src/content/docs/ru/reference/cli/providers-accounts.md
  • docs-site/src/content/docs/ru/reference/management-api.md
  • docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md
  • docs-site/src/content/docs/zh-cn/reference/management-api.md
  • src/cli/provider-runtime.ts
  • src/server/management/provider-routes.ts
  • tests/cli-headless-parity.test.ts
  • tests/management-provider-validation.test.ts

| `GET /api/providers` |編集されたプロバイダー設定と検出状態をリストする | — |
| `POST /api/providers` |検証済みプロバイダーを 1 つ追加または置換し、必要に応じてそれをデフォルトにします。 400 無効または危険な宛先または構成。 409 名前空間の衝突 |
| `PATCH /api/providers?name=...` |許可されたプロバイダー フィールド、有効/デフォルト状態、または OpenAI アカウント モードを更新します。 400 無効なフィールドまたは遷移。 404 不明なプロバイダ |
| `PATCH /api/providers?name=...` |許可されたプロバイダー フィールド(マージされる `headers` ブロックを含む)、有効/デフォルト状態、または OpenAI アカウント モードを更新します。 400 無効なフィールドまたは遷移。 404 不明なプロバイダ |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Restore the missing Markdown table delimiter.

Line 169 lacks the | separator before 400. The error text joins the purpose cell, so the row has two cells instead of the required three.

-| `PATCH /api/providers?name=...` | 許可されたプロバイダー フィールド(マージされる `headers` ブロックを含む)、有効/デフォルト状態、または OpenAI アカウント モードを更新します。 400 無効なフィールドまたは遷移。 404 不明なプロバイダ |
+| `PATCH /api/providers?name=...` | 許可されたプロバイダー フィールド(マージされる `headers` ブロックを含む)、有効/デフォルト状態、または OpenAI アカウント モードを更新します。 | 400 無効なフィールドまたは遷移。 404 不明なプロバイダ |

As per path instructions, docs-site/** changes must preserve readable user-facing documentation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `PATCH /api/providers?name=...` |許可されたプロバイダー フィールド(マージされる `headers` ブロックを含む)、有効/デフォルト状態、または OpenAI アカウント モードを更新します。 400 無効なフィールドまたは遷移。 404 不明なプロバイダ |
| `PATCH /api/providers?name=...` |許可されたプロバイダー フィールド(マージされる `headers` ブロックを含む)、有効/デフォルト状態、または OpenAI アカウント モードを更新します。 | 400 無効なフィールドまたは遷移。 404 不明なプロバイダ |
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 169-169: Table column count
Expected: 3; Actual: 2; Too few cells, row will be missing data

(MD056, table-column-count)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-site/src/content/docs/ja/reference/management-api.md` at line 169,
Update the Markdown table row for PATCH /api/providers in the management API
reference by inserting the missing cell delimiter before the “400” response
description, restoring three properly separated cells while preserving the
existing Japanese documentation text.

Sources: Path instructions, Linters/SAST tools

| `list` | `--json` | List configured providers and the remaining registry entries. |
| `add <name>` | `--adapter <adapter>`, `--base-url <url>`, `--api-key <key>`, `--default-model <model>`, `--set-default`, `--force`, `--json`, `--sync` | Add a registry/custom provider. `--force` overwrites; `--sync` refreshes a running proxy in human-output mode. |
| `edit <name>` | provider field flags, `--json` | Edit validated live provider fields without replacing key pools. |
| `edit <name>` | provider field flags, `--headers <json>`, `--json` | Edit validated live provider fields without replacing key pools. `--headers` merges custom request headers; pass `{}` or `-` to clear them. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Document the header scope and authentication restriction.

The descriptions say that --headers merges custom request headers. They do not state that these headers apply only to custom/OpenAI-compatible upstream endpoints. They also do not state that sensitive authentication headers cannot be set or overridden.

  • docs-site/src/content/docs/reference/cli/providers-accounts.md#L19-L19: Add both restrictions to the English description.
  • docs-site/src/content/docs/ja/reference/cli/providers-accounts.md#L18-L18: Translate the same restrictions.
  • docs-site/src/content/docs/ko/reference/cli/providers-accounts.md#L18-L18: Translate the same restrictions.
  • docs-site/src/content/docs/ru/reference/cli/providers-accounts.md#L20-L20: Translate the same restrictions.

As per path instructions, custom-header documentation must distinguish custom upstream headers from forwarded credentials and must not imply that sensitive authentication headers are overridable.

📍 Affects 4 files
  • docs-site/src/content/docs/reference/cli/providers-accounts.md#L19-L19 (this comment)
  • docs-site/src/content/docs/ja/reference/cli/providers-accounts.md#L18-L18
  • docs-site/src/content/docs/ko/reference/cli/providers-accounts.md#L18-L18
  • docs-site/src/content/docs/ru/reference/cli/providers-accounts.md#L20-L20
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-site/src/content/docs/reference/cli/providers-accounts.md` at line 19,
Update the `edit <name>` provider-field description to state that `--headers`
applies only to custom/OpenAI-compatible upstream endpoints and cannot set or
override sensitive authentication headers; retain the existing merge and clear
behavior. Apply the same restriction wording in English at
docs-site/src/content/docs/reference/cli/providers-accounts.md:19-19, and
translate it for
docs-site/src/content/docs/ja/reference/cli/providers-accounts.md:18-18,
docs-site/src/content/docs/ko/reference/cli/providers-accounts.md:18-18, and
docs-site/src/content/docs/ru/reference/cli/providers-accounts.md:20-20.

Source: Path instructions

| `GET /api/providers` | List redacted provider configuration and discovery state | — |
| `POST /api/providers` | Add or replace one validated provider and optionally make it default | 400 invalid/dangerous destination or config; 409 namespace collision |
| `PATCH /api/providers?name=...` | Update allowed provider fields, enabled/default state, or OpenAI account mode | 400 invalid field or transition; 404 unknown provider |
| `PATCH /api/providers?name=...` | Update allowed provider fields (including a merged `headers` block), enabled/default state, or OpenAI account mode | 400 invalid field or transition; 404 unknown provider |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the provider headers contract complete in English and every locale.

The endpoint does more than merge headers. A non-empty object is shallow-merged; null or {} clears the block; sensitive authentication headers, invalid names, non-string values, and CRLF are rejected with HTTP 400. The documentation must also distinguish configurable custom adapter headers from forwarded caller credentials.

  • docs-site/src/content/docs/reference/management-api.md#L186-L186: update the English endpoint row with the complete clear, validation, and header-scope semantics.
  • docs-site/src/content/docs/ja/reference/management-api.md#L169-L169: translate the same semantics.
  • docs-site/src/content/docs/ko/reference/management-api.md#L169-L169: translate the same semantics.
  • docs-site/src/content/docs/ru/reference/management-api.md#L188-L188: translate the same semantics.
  • docs-site/src/content/docs/zh-cn/reference/management-api.md#L169-L169: translate the same semantics.
Suggested English row wording
-| `PATCH /api/providers?name=...` | Update allowed provider fields (including a merged `headers` block), enabled/default state, or OpenAI account mode | 400 invalid field or transition; 404 unknown provider |
+| `PATCH /api/providers?name=...` | Update allowed provider fields. A non-empty `headers` object is shallow-merged; `null` or `{}` clears it. These are custom adapter headers, not forwarded caller credentials. | 400 invalid field, header name/value, or transition; 404 unknown provider |

As per path instructions, docs-site/** documentation must stay in sync with actual CLI/API behavior and translated locale pages must not contradict the English source.

📍 Affects 5 files
  • docs-site/src/content/docs/reference/management-api.md#L186-L186 (this comment)
  • docs-site/src/content/docs/ja/reference/management-api.md#L169-L169
  • docs-site/src/content/docs/ko/reference/management-api.md#L169-L169
  • docs-site/src/content/docs/ru/reference/management-api.md#L188-L188
  • docs-site/src/content/docs/zh-cn/reference/management-api.md#L169-L169
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-site/src/content/docs/reference/management-api.md` at line 186, Update
the PATCH /api/providers endpoint row in
docs-site/src/content/docs/reference/management-api.md:186-186 to document
shallow merging of non-empty headers, clearing with null or {}, rejection of
sensitive authentication headers, invalid names, non-string values, and CRLF
with HTTP 400, and the distinction between configurable custom-adapter headers
and forwarded caller credentials. Translate and apply the same semantics in
docs-site/src/content/docs/ja/reference/management-api.md:169-169,
docs-site/src/content/docs/ko/reference/management-api.md:169-169,
docs-site/src/content/docs/ru/reference/management-api.md:188-188, and
docs-site/src/content/docs/zh-cn/reference/management-api.md:169-169.

Source: Path instructions

Comment on lines +289 to +300
// headers is the one object-valued field in the mask. PATCH semantics merge it
// shallowly into the existing block so a single fingerprint header can be added
// without wiping the rest; null or an empty object clears the whole block.
if (Object.hasOwn(rawBody, "headers")) {
const headersValue = rawBody.headers;
if (headersValue === null || (isPlainRecord(headersValue) && Object.keys(headersValue).length === 0)) {
delete next.headers;
} else {
if (!isPlainRecord(headersValue)) return jsonResponse({ error: "headers must be an object" }, 400);
const headersError = providerHeadersConfigError(headersValue);
if (headersError) return jsonResponse({ error: headersError }, 400);
next.headers = { ...next.headers, ...headersValue } as Record<string, string>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Expect: adapters either canonicalize header names or tests cover casing-only updates.
rg -n -C 8 -e 'new Headers' -e 'providerOutbound' -e 'build.*Request' -e 'headers' src tests || true

Repository: lidge-jun/opencodex

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== candidate adapter files =='
fd -t f -e ts -e tsx src | rg -i 'adapter|provider|route|request' | head -200

printf '%s\n' '== adapter contracts and header configuration =='
rg -n -C 5 'interface ProviderAdapter|type ProviderAdapter|buildRequest|runTurn|providerHeadersConfigError|headers:' src/server src/lib src/providers src/adapters 2>/dev/null | head -1200

printf '%s\n' '== header forwarding and normalization operations =='
rg -n -C 4 'new Headers|Object\.entries\(.*headers|Object\.keys\(.*headers|headers\s*:' src --glob '*.ts' | rg -i 'adapter|provider|buildRequest|fetch|headers' | head -1200

Repository: lidge-jun/opencodex

Length of output: 187


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '== tracked source files containing adapter/provider names =='
git ls-files | grep -E '(^|/)([^/]*(adapter|provider|route|request)[^/]*)\.(ts|tsx)$' | head -300

printf '%s\n' '== relevant symbols in tracked TypeScript files =='
git grep -n -E -C 5 'ProviderAdapter|buildRequest|runTurn|providerHeadersConfigError' -- 'src/**/*.ts' 'src/**/*.tsx' 2>/dev/null | head -1600

printf '%s\n' '== provider route and header-related symbols =='
git grep -n -E -C 8 'headers|isPlainRecord|providerHeadersConfigError' -- 'src/server/management/provider-routes.ts' 'tests/**/*.ts' 2>/dev/null | head -1600

Repository: lidge-jun/opencodex

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== provider header consumption in adapters =='
git grep -n -E -C 6 'provider\.headers|\.headers\b' -- 'src/adapters/*.ts' 'src/adapters/**/*.ts' | head -2400

printf '%s\n' '== request construction sites =='
git grep -n -E -C 10 'headers:|new Headers|fetch\(' -- 'src/adapters/*.ts' 'src/adapters/**/*.ts' | head -3000

printf '%s\n' '== exact provider header validator and config types =='
git grep -n -E -C 12 'function providerHeadersConfigError|providerHeadersConfigError|headers\??:.*Record|headers:.*Record' -- 'src/config.ts' 'src/types.ts' 'src/config/**/*.ts' 2>/dev/null | head -1400

printf '%s\n' '== existing management tests for header PATCH behavior =='
git grep -n -E -C 10 'headers|PATCH|patch' -- 'tests/management-provider-validation.test.ts' 'tests/management-*.test.ts' | head -1800

Repository: lidge-jun/opencodex

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== all provider.headers consumers =='
git grep -n 'provider\.headers' -- 'src' | sort

printf '%s\n' '== header validator implementation =='
git grep -n -E 'providerHeadersConfigError|function isPlainRecord|const.*HEADER|sensitive.*header' -- 'src/config.ts' 'src/config' 'src/lib' | head -300

printf '%s\n' '== provider header tests =='
git grep -n -E -C 8 'case|casing|headers|X-Foo|x-foo|duplicate' -- 'tests/management-provider-validation.test.ts' 'tests/provider-*.test.ts' | head -1600

printf '%s\n' '== relevant adapter request construction slices =='
sed -n '645,665p' src/adapters/openai-chat.ts
sed -n '970,1010p' src/adapters/openai-responses.ts
sed -n '810,840p' src/adapters/anthropic.ts
sed -n '320,345p' src/adapters/google.ts

Repository: lidge-jun/opencodex

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== validator implementation =='
sed -n '560,630p' src/config.ts

printf '%s\n' '== provider PATCH tests near the existing patch coverage =='
sed -n '710,760p' tests/management-provider-validation.test.ts
sed -n '840,900p' tests/management-provider-validation.test.ts

printf '%s\n' '== deterministic casing probe =='
python3 - <<'PY'
existing = {"X-Foo": "old", "X-Bar": "keep"}
patch = {"x-foo": "new"}
merged = {**existing, **patch}
print("merged_record:", merged)
print("case_insensitive_foo_keys:", [k for k in merged if k.lower() == "x-foo"])
print("duplicate_case_insensitive_name:", len([k for k in merged if k.lower() == "x-foo"]) > 1)
PY

node - <<'JS'
const headers = new Headers({ "X-Foo": "old", "x-foo": "new" });
console.log("Headers.get(x-foo):", headers.get("x-foo"));
console.log("Headers.entries:", [...headers.entries()]);
JS

printf '%s\n' '== static check for case-insensitive cleanup =='
python3 - <<'PY'
from pathlib import Path
paths = list(Path("src").rglob("*.ts"))
hits = []
for p in paths:
    text = p.read_text()
    if "provider.headers" in text:
        hits.append((str(p), "hasLowerCaseComparison" if "toLowerCase()" in text and "provider.headers" in text else "noLocalCaseNormalization"))
print(*hits, sep="\n")
PY

Repository: lidge-jun/opencodex

Length of output: 8934


Normalize header names before merging. src/server/management/provider-routes.ts:300 preserves both X-Foo and x-foo. providerHeadersConfigError does not reject case-only duplicates. Adapters such as src/adapters/openai-chat.ts:660, src/adapters/openai-responses.ts:981, src/adapters/anthropic.ts:835, and src/adapters/google.ts:330 apply Object.assign without case-insensitive cleanup. A casing-only PATCH can therefore send combined or conflicting values. Remove existing keys whose lowercase names match the patch before adding it, and add a regression test for X-Foo followed by x-foo.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/management/provider-routes.ts` around lines 289 - 300, The headers
PATCH merge in the provider route must be case-insensitive. Update the merge
logic in the headers handling block to remove existing keys whose lowercase
names match any incoming patch key before adding the patch, so casing-only
updates cannot preserve duplicates or conflicts. Keep validation and null/empty
clearing behavior unchanged, and add a regression test covering an existing
X-Foo header followed by a x-foo patch.

Source: Path instructions

Comment on lines +289 to +304
// headers is the one object-valued field in the mask. PATCH semantics merge it
// shallowly into the existing block so a single fingerprint header can be added
// without wiping the rest; null or an empty object clears the whole block.
if (Object.hasOwn(rawBody, "headers")) {
const headersValue = rawBody.headers;
if (headersValue === null || (isPlainRecord(headersValue) && Object.keys(headersValue).length === 0)) {
delete next.headers;
} else {
if (!isPlainRecord(headersValue)) return jsonResponse({ error: "headers must be an object" }, 400);
const headersError = providerHeadersConfigError(headersValue);
if (headersError) return jsonResponse({ error: headersError }, 400);
next.headers = { ...next.headers, ...headersValue } as Record<string, string>;
}
touched = true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Expect: a lock covers provider reads, validation, and persistence.
rg -n -C 10 -e 'handleProviderRoutes' -e 'saveConfigPreservingClaudeCode' -e 'CONFIG_MUTATION_LOCK' -e 'mutation.*lock' src tests || true

Repository: lidge-jun/opencodex

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- provider-routes outline ---'
ast-grep outline src/server/management/provider-routes.ts
printf '%s\n' '--- management dispatcher references ---'
rg -n -C 8 'handleProviderRoutes|provider-routes|handleManagementAPI|management.*lock|withConfigMutationLock|mutatePersistedConfig' src/server src tests -g '*.ts' -g '*.tsx' | head -n 500
printf '%s\n' '--- route section ---'
sed -n '220,340p' src/server/management/provider-routes.ts
printf '%s\n' '--- config lock/save sections ---'
sed -n '1580,1785p' src/config.ts
sed -n '1980,2075p' src/config.ts

Repository: lidge-jun/opencodex

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- provider PATCH setup and persistence ---'
sed -n '73,220p' src/server/management/provider-routes.ts
sed -n '330,475p' src/server/management/provider-routes.ts
printf '%s\n' '--- exact save implementation ---'
sed -n '2018,2075p' src/config.ts
printf '%s\n' '--- provider PATCH tests and concurrency coverage ---'
rg -n -C 6 'PATCH|headers|concurrent|Promise\.all|provider-routes|/api/providers' tests -g '*.ts' | head -n 700
printf '%s\n' '--- management context and dependency seams ---'
cat -n src/server/management/context.ts

Repository: lidge-jun/opencodex

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

route = Path("src/server/management/provider-routes.ts").read_text()
api = Path("src/server/management-api.ts").read_text()
config = Path("src/config.ts").read_text()

def line_of(text, needle):
    pos = text.index(needle)
    return text.count("\n", 0, pos) + 1

checks = {
    "PATCH route is async": "export async function handleProviderRoutes" in route,
    "PATCH snapshots provider before validation": route.index("const next: OcxProviderConfig") < route.index("await providerDestinationResolvedError", route.index("const next: OcxProviderConfig")),
    "PATCH saves after validation": route.index("await providerDestinationResolvedError", route.index("const next: OcxProviderConfig")) < route.index("save(config);", route.index("const next: OcxProviderConfig")),
    "dispatcher has no provider mutation lock": not bool(re.search(r"withConfigMutationLockSync|mutatePersistedConfig", api[api.index("export async function handleManagementAPI"):api.index("export async function handleManagementAPI") + 12000])),
    "save lock is synchronous": "export function withConfigMutationLockSync" in config and "never return a Promise from `fn`" in config,
    "save does not re-read providers": "Scope residual: only `claudeCode` is reconciled." in config and "const persistedConfig: OcxConfig = { ...config, port: persistedBinding.port }" in config,
}

for name, result in checks.items():
    print(f"{'PASS' if result else 'FAIL'}: {name}")

for label, text, needle in [
    ("next snapshot", route, "const next: OcxProviderConfig"),
    ("destination validation", route, "const resolvedError = await providerDestinationResolvedError(name, next)"),
    ("provider save", route, "config.providers[name] = stripRegistryOnlyStaticHeaders(name, next)"),
    ("dispatcher", api, "routed = (await handleConfigRoutes(ctx))"),
    ("save lock", config, "export function saveConfigPreservingClaudeCode"),
]:
    print(f"{label}: line {line_of(text, needle)}")
PY

printf '%s\n' '--- focused provider-management test files ---'
rg -l '(/api/providers|provider patch|provider.*PATCH|headers)' tests -g '*.ts' | sort
printf '%s\n' '--- concurrent provider test markers ---'
rg -n -i -C 2 'concurrent|Promise\.all|headers' tests -g '*provider*' -g '*management*' -g '*.ts' | rg -i 'provider|headers|Promise\.all|concurrent' | head -n 250

Repository: lidge-jun/opencodex

Length of output: 33833


Rebase concurrent provider PATCH updates under the config mutation lock.

handleProviderRoutes copies config.providers[name] at line 216, awaits destination validation at line 319, then saves the stale snapshot at line 347. The dispatcher does not hold a lock across this sequence, and saveConfigPreservingClaudeCode only locks the final synchronous write. Concurrent PATCH requests can therefore erase each other’s header updates.

Re-read and merge the current provider inside a lock-aware mutation before saving. Add a concurrent PATCH regression test in tests/management-provider-validation.test.ts that updates different headers and asserts both persist.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/management/provider-routes.ts` around lines 289 - 304, Update
handleProviderRoutes so PATCH mutations re-read the latest provider and merge
changes inside the configuration mutation lock immediately before saving, rather
than persisting the stale snapshot captured before awaited validation; preserve
existing validation and PATCH semantics. Add a regression test in management
provider validation tests that performs concurrent PATCH requests for different
headers and asserts both updates remain.

Source: Path instructions

@Wibias

Wibias commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Please put your Pull-Request on Ready for Review, once you are finished.

@lidge-jun

Copy link
Copy Markdown
Owner

Left open and out of the current bug stack (#951#973), because this is a feature rather than a defect: there is no way to set headers today, so nothing is behaving incorrectly — the capability is missing.

That is not a rejection. The motivating case is concrete (providers needing client-fingerprint headers, with hand-editing config.json as the only recovery), and both the PATCH surface and the CLI flag are the right shape for it. It just belongs on its own review track with its own docs and security consideration, since custom headers on an outbound provider request are a credential-adjacent surface — a header set through this path can carry auth material, and that deserves an explicit look rather than inheriting a bug stack's approval.

- redact --headers values from CLI usage errors so tokens never hit stderr (P1)
- merge header blocks case-insensitively so a casing-only PATCH replaces the
  existing key instead of leaving a combined duplicate for Headers (P2)
- restore registry static headers when clearing custom headers so transports
  like opencode-free keep their client marker (P2)
- re-apply the field mask under the config mutation lock immediately before
  saving so concurrent PATCHes merge instead of clobbering each other (major)
- regression tests for redaction, casing merge, static-header clear, and
  concurrent PATCH persistence
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants