Skip to content

feat(providers): config-driven User-Agent and extra header overrides - #1872

Open
alecuba16 wants to merge 2 commits into
1jehuang:masterfrom
alecuba16:feat/provider-ua-headers
Open

alecuba16 wants to merge 2 commits into
1jehuang:masterfrom
alecuba16:feat/provider-ua-headers

Conversation

@alecuba16

@alecuba16 alecuba16 commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Lets providers send a custom User-Agent and extra headers from config. Useful for gateways that route or auth on header values instead of query params. Applies to the openrouter-compatible runtime and the anthropic runtime, with catalog and wire tests covering the new fields.

Closes #1520.

@greptile-apps

greptile-apps Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 3/5

[Medium impact] The changes affect outbound requests in the OpenAI-compatible and Anthropic provider workflows.

The PR should not merge while a built-in catalog refresh can send a named profile’s private headers to another service.

Findings

  1. P1 Security Private headers reach another service ▶
  2. P2 Discovery loses profile headers ▶
  3. P2 Catalog auth test stops running ▶
  4. P2 Models retain previous account ▶
  5. P2 Old route models remain visible ▶
Fix with agent prompt
### Issue 1
crates/jcode-provider-openrouter-runtime/src/lib.rs:704-708
The background catalog refresh uses a built-in provider’s endpoint but borrows headers from any named profile with the same ID. If `GROQ_API_KEY` is configured and `[providers.groq]` points to a corporate gateway, a missing or stale built-in catalog causes the gateway’s private headers to be sent to `https://api.groq.com/openai/v1/models`. This exposes gateway credentials or tenant information to a different service, even though the named profile specifies another endpoint.

Keep the endpoint and headers tied to the same profile. Built-in refreshes should not borrow named-profile headers. Fix this before merging.

**How this was verified:** A locally intercepted request preserved the Groq destination and carried both dummy corporate headers at the PR head, while the same request at the base carried neither.

### Issue 2
crates/jcode-provider-anthropic-runtime/src/catalog.rs:undefined-116
`refresh_model_catalog_for_route` reloads current settings instead of using the provider’s saved `direct_transport.headers`. Named Anthropic profiles clear `JCODE_ANTHROPIC_HEADERS` immediately after constructing the provider, so discovery falls back to global headers while chat retains the profile overrides. The same mismatch exists in `fetch_live_model_ids_for_doctor`.

For a named profile using the official Anthropic endpoint, captured requests confirmed that chat sends the profile’s overridden `x-api-key` and succeeds, while both catalog paths send the underlying key and receive 401 responses. A valid underlying key for another account could instead produce an account-inconsistent catalog. This is a non-blocking discovery inconsistency; use the provider’s saved headers in both direct catalog paths.

### Issue 3
crates/jcode-provider-openrouter-runtime/src/openrouter_tests.rs:undefined-3546
- **Bug**
  - Removing the catalog_auth module declaration disconnects catalog_auth_tests.rs from the compiled test module graph. The base lists and passes catalog_request_headers_follow_provider_auth_rules; the rebuilt head lists and runs zero matching tests, and no replacement inclusion was found. This is a non-blocking loss of regression coverage for catalog request authentication rules, not evidence of an actual product authentication failure.
- **Cause**
  - The full PR diff removes #[path = "catalog_auth_tests.rs"] mod catalog_auth; from the end of openrouter_tests.rs. The test definition remains in catalog_auth_tests.rs:4 without a replacement module inclusion.
- **Fix**
  - Restore the catalog_auth module declaration under the existing test module, or include the test file elsewhere with the appropriate scope, then verify that enumeration lists the test and filtered execution runs it.

### Issue 4
crates/jcode-provider-anthropic-runtime/src/catalog.rs:116-119
Changing `[provider.headers].x-api-key` changes the credential used for direct Anthropic requests, but the catalog cache scope still hashes the underlying key. After changing the override and opening a new provider session, a fresh catalog from the previous account suppresses discovery. If the accounts expose different models, the picker therefore keeps showing the previous account’s models while chat authenticates as the new account. A forced refresh corrects the list, but still writes it under the same scope. This is a non-blocking discovery inconsistency; include the effective credential in the cache identity used by both catalog refresh and readers.

### Issue 5
crates/jcode-provider-openrouter-runtime/src/catalog_fetch.rs:undefined-54
Changing a gateway’s routing or account header leaves its old model list valid. These overrides reach discovery requests, but the saved catalog distinguishes only the endpoint and profile. If a user changes `x-route` or `Authorization` and opens a new session at the same endpoint, `fetch_models` returns the previous route’s cached models. A cache younger than 15 minutes also skips background discovery.

This is a non-blocking discovery inconsistency: the picker can offer models the new route cannot use. Include a fingerprint of the effective headers in the saved catalog identity and check it before reuse.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds configurable user_agent and extra headers for OpenRouter-compatible and direct Anthropic requests.

  • Provider settings let you set shared headers and override them per profile.
  • OpenAI-compatible requests use the configured headers across their request paths.
  • Anthropic chat and catalog requests use configured header overrides.
  • Repository budget files track the expanded code and test files.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  G["Global provider settings"] --> M["Merge headers by name"]
  P["Named profile settings"] --> M
  M --> H["Parse HeaderMap"]
  H --> O["OpenRouter-compatible runtime"]
  H --> A["Direct Anthropic runtime"]
  O --> C["Chat and discovery requests"]
  A --> D["Direct chat requests"]
Loading

Reviews (18) · Last reviewed commit: "feat(providers): config-driven User-Agen..." · Reviewed by Greptile

@alecuba16
alecuba16 force-pushed the feat/provider-ua-headers branch 2 times, most recently from 0869b46 to b5f16be Compare October 19, 2026 19:43
@alecuba16
alecuba16 force-pushed the feat/provider-ua-headers branch from b5f16be to 43e1b98 Compare October 20, 2026 05:24
Comment thread scripts/test_size_budget.json Resolved
@alecuba16
alecuba16 force-pushed the feat/provider-ua-headers branch 9 times, most recently from ec6e115 to 474303e Compare October 23, 2026 06:35
@alecuba16
alecuba16 force-pushed the feat/provider-ua-headers branch 4 times, most recently from 7898d74 to 2bc80a2 Compare November 1, 2026 00:04
@alecuba16

Copy link
Copy Markdown
Contributor Author

Rebased. The branch now sits on #1906's head (0ba439f, master + the red-master repairs), since plain master fails the same gates this PR needs green: its own latest push run 41177474980 has Quality Guardrails and all three Build & Test jobs red, and the branch bases off that. Diff vs master is unchanged in substance: config-driven user_agent + headers for the default provider and named profiles, plus the tests. Conflict with master's new browser_handoff config test resolved by keeping both. Also folded in: two field_reassign_with_default clippy fixes in the branch's own tests (provider_catalog_tests, openrouter_tests) that the current clippy flags, and the ratchet baselines refreshed for the branch's own growth only (panic 175->182 via ollama_context, swallowed-error 3963->3966, code/test-size for openrouter lib+tests). Local: clippy -D warnings clean on all touched crates, fmt clean, budgets green, openrouter + provider_catalog suites pass. One provider_catalog test fails only under ambient JCODE_PROVIDER_PROFILE_ACTIVE/JCODE_NAMED_PROVIDER_PROFILE dev env, passes with them unset; that isolation is #1882's scope, not this change.

jcode_base::provider::fetch_anthropic_model_catalog(&self.direct_api_key()?).await?
// Same header-override parity as the doctor path: chat honors
// `[provider.headers]`, so the direct catalog refresh must too.
let headers = super::configured_direct_headers();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Discovery loses profile headers

refresh_model_catalog_for_route reloads current settings instead of using the provider’s saved direct_transport.headers. Named Anthropic profiles clear JCODE_ANTHROPIC_HEADERS immediately after constructing the provider, so discovery falls back to global headers while chat retains the profile overrides. The same mismatch exists in fetch_live_model_ids_for_doctor.

For a named profile using the official Anthropic endpoint, captured requests confirmed that chat sends the profile’s overridden x-api-key and succeeds, while both catalog paths send the underlying key and receive 401 responses. A valid underlying key for another account could instead produce an account-inconsistent catalog. This is a non-blocking discovery inconsistency; use the provider’s saved headers in both direct catalog paths.

Knowledge Base Used: Provider selection and runtime adapters

Artifacts

Executed Rust test for named-profile header retention

  • The authored test constructs an official-endpoint named profile, clears its environment bridge, and executes chat and both catalog paths, asserting the observed mismatch and saved-header control.

Executed local request-capture runner

  • The authored runner provides a local TLS fixture, runs the same test before and after a temporary saved-header change, captures responses, and restores tracked source files.

Request captures with the supplied HEAD implementation

  • The HEAD run captures successful chat and unauthorized refresh and doctor-facing catalog requests, proving that only chat retains the named-profile key override.

Request captures with the temporary saved-header control

  • The control run captures HTTP/1.1 200 OK for all three requests and records the temporary source diff, showing that using saved headers removes the mismatch.

View artifacts

TREX Ran code and verified through TREX

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/jcode-provider-anthropic-runtime/src/catalog.rs
Line: 116

Comment:
**Discovery loses profile headers**

`refresh_model_catalog_for_route` reloads current settings instead of using the provider’s saved `direct_transport.headers`. Named Anthropic profiles clear `JCODE_ANTHROPIC_HEADERS` immediately after constructing the provider, so discovery falls back to global headers while chat retains the profile overrides. The same mismatch exists in `fetch_live_model_ids_for_doctor`.

For a named profile using the official Anthropic endpoint, captured requests confirmed that chat sends the profile’s overridden `x-api-key` and succeeds, while both catalog paths send the underlying key and receive 401 responses. A valid underlying key for another account could instead produce an account-inconsistent catalog. This is a non-blocking discovery inconsistency; use the provider’s saved headers in both direct catalog paths.

**Knowledge Base Used:** [Provider selection and runtime adapters](https://app.greptile.com/-/switch-tenant?tenant=solo-systems&next=%2Fcustom-context%2Fknowledge-base%2F1jehuang%2Fjcode%2F-%2Fdocs%2Fprovider-selection-and-runtime-adapters.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@alecuba16
alecuba16 force-pushed the feat/provider-ua-headers branch from 2bc80a2 to a601f1d Compare November 2, 2026 12:50
@@ -3539,6 +3544,3 @@ fn venice_disables_venice_system_prompt_by_default_but_user_can_override() {
);
assert!(OpenRouterProvider::with_profile_default_extra_body(Some("cerebras"), None).is_none());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Catalog auth test stops running

  • Bug
    • Removing the catalog_auth module declaration disconnects catalog_auth_tests.rs from the compiled test module graph. The base lists and passes catalog_request_headers_follow_provider_auth_rules; the rebuilt head lists and runs zero matching tests, and no replacement inclusion was found. This is a non-blocking loss of regression coverage for catalog request authentication rules, not evidence of an actual product authentication failure.
  • Cause
    • The full PR diff removes #[path = "catalog_auth_tests.rs"] mod catalog_auth; from the end of openrouter_tests.rs. The test definition remains in catalog_auth_tests.rs:4 without a replacement module inclusion.
  • Fix
    • Restore the catalog_auth module declaration under the existing test module, or include the test file elsewhere with the appropriate scope, then verify that enumeration lists the test and filtered execution runs it.

TREX Ran code and verified through TREX

@alecuba16
alecuba16 force-pushed the feat/provider-ua-headers branch from a601f1d to b4f0c4f Compare November 2, 2026 19:01
Comment on lines +116 to +119
let headers = super::configured_direct_headers();
jcode_base::provider::fetch_anthropic_model_catalog_with_headers(
&self.direct_api_key()?,
headers.ok().as_ref(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Models retain previous account

Changing [provider.headers].x-api-key changes the credential used for direct Anthropic requests, but the catalog cache scope still hashes the underlying key. After changing the override and opening a new provider session, a fresh catalog from the previous account suppresses discovery. If the accounts expose different models, the picker therefore keeps showing the previous account’s models while chat authenticates as the new account. A forced refresh corrects the list, but still writes it under the same scope. This is a non-blocking discovery inconsistency; include the effective credential in the cache identity used by both catalog refresh and readers.

Knowledge Base Used:

Artifacts

Evidence from the check

  • The executed test exercises real provider scope, cache admission, picker results, and HTTP requests against isolated account-specific responses, demonstrating the credential-to-cache mismatch.

Evidence from the check

  • The executed runner reconstructs the PR parent and HEAD, injects the identical test and local endpoint seam, captures both command outputs, and restores tracked source bytes.

Command output from the check

  • The pre-change test captured catalog and chat requests receiving HTTP/1.1 200 OK while all retained account A authentication, establishing the baseline.

Command output from the check

  • The HEAD test captured successful account B chat, suppressed normal catalog refresh, and successful forced account B discovery, confirming that the unchanged cache identity causes stale account A models.

View artifacts

TREX Ran code and verified through TREX

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/jcode-provider-anthropic-runtime/src/catalog.rs
Line: 116-119

Comment:
**Models retain previous account**

Changing `[provider.headers].x-api-key` changes the credential used for direct Anthropic requests, but the catalog cache scope still hashes the underlying key. After changing the override and opening a new provider session, a fresh catalog from the previous account suppresses discovery. If the accounts expose different models, the picker therefore keeps showing the previous account’s models while chat authenticates as the new account. A forced refresh corrects the list, but still writes it under the same scope. This is a non-blocking discovery inconsistency; include the effective credential in the cache identity used by both catalog refresh and readers.

**Knowledge Base Used:**
- [Model provider integration](https://app.greptile.com/-/switch-tenant?tenant=solo-systems&next=%2Fcustom-context%2Fknowledge-base%2F1jehuang%2Fjcode%2F-%2Fdocs%2Fprovider-integration.md)
- [Provider selection and runtime adapters](https://app.greptile.com/-/switch-tenant?tenant=solo-systems&next=%2Fcustom-context%2Fknowledge-base%2F1jehuang%2Fjcode%2F-%2Fdocs%2Fprovider-selection-and-runtime-adapters.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@alecuba16
alecuba16 force-pushed the feat/provider-ua-headers branch 2 times, most recently from c48b937 to 00748ff Compare November 3, 2026 01:27
Comment on lines +704 to +708
resolved_profile_http_header_overrides(
&jcode_base::config::config().provider,
jcode_base::config::config().providers.get(&profile_id),
&profile_id,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security Private headers reach another service

The background catalog refresh uses a built-in provider’s endpoint but borrows headers from any named profile with the same ID. If GROQ_API_KEY is configured and [providers.groq] points to a corporate gateway, a missing or stale built-in catalog causes the gateway’s private headers to be sent to https://api.groq.com/openai/v1/models. This exposes gateway credentials or tenant information to a different service, even though the named profile specifies another endpoint.

Keep the endpoint and headers tied to the same profile. Built-in refreshes should not borrow named-profile headers. Fix this before merging.

How this was verified: A locally intercepted request preserved the Groq destination and carried both dummy corporate headers at the PR head, while the same request at the base carried neither.

Knowledge Base Used: Provider selection and runtime adapters

Artifacts

Executed Rust test for the mismatched profile and endpoint

  • The executed test verifies built-in eligibility and the differing gateway URL, calls the real refresh function, and confirms the intercepted response was consumed.

Executed before and after runner with safe local TLS interception

  • The runner executes both full-PR revisions with dummy credentials and a non-forwarding interceptor, demonstrating the header change without contacting public providers.

Before-change request without corporate headers

  • The passing base-revision run captures the canonical Groq models request and full local HTTP 200 OK response, showing that corporate headers were absent.

After-change request carrying corporate headers to Groq

  • The passing head-revision run captures both dummy corporate headers on the canonical Groq models request and its full local HTTP 200 OK response, confirming the destination mismatch.

Production caller trace and tracked-file restoration check

  • Captured commands show startup hook registration, scheduler eligibility checks, the changed header lookup, and a clean tracked diff, establishing realistic reachability separately from the direct runtime test.

Executed artifact upload script

  • The script uploads the authored sources and observed evidence through assigned pre-signed slots and records their exact authenticated artifact references.

Successful upload receipts for sources and evidence

  • The captured upload command exited 0 and recorded HTTP 200 OK receipts with artifact IDs and URLs, confirming that the evidence was uploaded rather than merely saved locally.

Evidence from the check

  • The executed test verifies built-in eligibility and the differing gateway URL, calls the real refresh function, and confirms the intercepted response was consumed.

Evidence from the check

  • The runner executes both full-PR revisions with dummy credentials and a non-forwarding interceptor, demonstrating the header change without contacting public providers.

Command output from the check

  • The passing base-revision run captures the canonical Groq models request and full local HTTP 200 OK response, showing that corporate headers were absent.

Command output from the check

  • The passing head-revision run captures both dummy corporate headers on the canonical Groq models request and its full local HTTP 200 OK response, confirming the destination mismatch.

Command output from the check

  • Captured commands show startup hook registration, scheduler eligibility checks, the changed header lookup, and a clean tracked diff, establishing realistic reachability separately from the direct runtime test.

View artifacts

TREX Ran code and verified through TREX

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/jcode-provider-openrouter-runtime/src/lib.rs
Line: 704-708

Comment:
**Private headers reach another service**

The background catalog refresh uses a built-in provider’s endpoint but borrows headers from any named profile with the same ID. If `GROQ_API_KEY` is configured and `[providers.groq]` points to a corporate gateway, a missing or stale built-in catalog causes the gateway’s private headers to be sent to `https://api.groq.com/openai/v1/models`. This exposes gateway credentials or tenant information to a different service, even though the named profile specifies another endpoint.

Keep the endpoint and headers tied to the same profile. Built-in refreshes should not borrow named-profile headers. Fix this before merging.

**How this was verified:** A locally intercepted request preserved the Groq destination and carried both dummy corporate headers at the PR head, while the same request at the base carried neither.

**Knowledge Base Used:** [Provider selection and runtime adapters](https://app.greptile.com/-/switch-tenant?tenant=solo-systems&next=%2Fcustom-context%2Fknowledge-base%2F1jehuang%2Fjcode%2F-%2Fdocs%2Fprovider-selection-and-runtime-adapters.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@alecuba16
alecuba16 force-pushed the feat/provider-ua-headers branch from 00748ff to 585e981 Compare November 3, 2026 08:42
@alecuba16
alecuba16 force-pushed the feat/provider-ua-headers branch 2 times, most recently from 85ae421 to a7c1db3 Compare November 3, 2026 18:57
@alecuba16
alecuba16 force-pushed the feat/provider-ua-headers branch from a7c1db3 to 5d0bb94 Compare November 4, 2026 07:37
.await
// Config-driven overrides apply last so they win over any same-named
// header jcode set earlier on the request.
.headers(http_header_overrides.clone())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Old route models remain visible

Changing a gateway’s routing or account header leaves its old model list valid. These overrides reach discovery requests, but the saved catalog distinguishes only the endpoint and profile. If a user changes x-route or Authorization and opens a new session at the same endpoint, fetch_models returns the previous route’s cached models. A cache younger than 15 minutes also skips background discovery.

This is a non-blocking discovery inconsistency: the picker can offer models the new route cannot use. Include a fingerprint of the effective headers in the saved catalog identity and check it before reuse.

Knowledge Base Used: Model provider integration

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/jcode-provider-openrouter-runtime/src/catalog_fetch.rs
Line: 54

Comment:
**Old route models remain visible**

Changing a gateway’s routing or account header leaves its old model list valid. These overrides reach discovery requests, but the saved catalog distinguishes only the endpoint and profile. If a user changes `x-route` or `Authorization` and opens a new session at the same endpoint, `fetch_models` returns the previous route’s cached models. A cache younger than 15 minutes also skips background discovery.

This is a non-blocking discovery inconsistency: the picker can offer models the new route cannot use. Include a fingerprint of the effective headers in the saved catalog identity and check it before reuse.

**Knowledge Base Used:** [Model provider integration](https://app.greptile.com/-/switch-tenant?tenant=solo-systems&next=%2Fcustom-context%2Fknowledge-base%2F1jehuang%2Fjcode%2F-%2Fdocs%2Fprovider-integration.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@alecuba16
alecuba16 force-pushed the feat/provider-ua-headers branch from 5d0bb94 to 6ed61d7 Compare November 4, 2026 09:39

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Configurable User-Agent and global HTTP header overrides for providers

1 participant