Skip to content

fix(opencode): send x-headroom-project header on all proxied requests - #2868

Merged
chopratejas merged 2 commits into
headroomlabs-ai:mainfrom
gingeekrishna:fix/2847-opencode-project-header
Aug 18, 2026
Merged

fix(opencode): send x-headroom-project header on all proxied requests#2868
chopratejas merged 2 commits into
headroomlabs-ai:mainfrom
gingeekrishna:fix/2847-opencode-project-header

Conversation

@gingeekrishna

@gingeekrishna gingeekrishna commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Description

The OpenCode transport plugin set HEADROOM_PROJECT as a shell env var for child processes but never forwarded it as x-headroom-project on the actual proxied HTTP requests. The proxy's classify_project only attributes traffic via x-headroom-project header or /p/<name> URL prefix — without the header, every OpenCode request was unattributed and the Per-Project Savings dashboard showed 0 project(s) permanently.

Fixes #2847.

Root cause

installHeadroomTransport was called with only { proxyUrl, debug }. The project value was computed and used only in the shell.env hook (for subprocess env injection), never threaded through to mergeFetchHeaders or headersForNodeRequest.

Changes Made

  1. Add project?: string to InstallOptions and TransportState.
  2. Resolve the project value once at plugin init (pluginOptions.project → input.project.id → input.directory) and pass it to installHeadroomTransport.
  3. Both header-building seams now set x-headroom-project when a project is present:
    • mergeFetchHeaders (wrapped fetch path)
    • headersForNodeRequest (wrapped http.request / https.request path)
  4. Reuse the resolved project in the shell.env hook (removes the duplicate resolution that was there before).

Changes

  • plugins/opencode/src/transport.tsInstallOptions.project, TransportState.project; mergeFetchHeaders, headersForNodeRequest, routedNodeOptions, withRoutedFetchInput, installHeadroomTransport updated
  • plugins/opencode/src/plugin.ts — resolve project once, pass it to transport; reuse in shell.env
  • plugins/opencode/src/transport.test.ts — 3 new tests: project header on fetch, project header on https.request, no header when project unset
  • headroom/providers/opencode/_dist/entry.opencode.js — rebuilt with npm run build:standalone to match source

Testing

  • Unit tests pass
  • TypeScript typecheck passes
  • New regression tests added

Test Output

cd plugins/opencode && npm test
# 17 passed (14 existing + 3 new)

TypeScript build also passes: npm run typecheck (no errors).

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature
  • Breaking change
  • Documentation update
  • Performance improvement
  • Code refactoring

Real Behavior Proof

  • Environment: OpenCode transport plugin test environment on the current PR head.
  • Exact command / steps: ran the plugin test suite and TypeScript typecheck after rebuilding the standalone bundle.
  • Observed result: all 17 tests passed, including project-header coverage for fetch and Node HTTPS paths plus the unset-project control; typechecking passed.
  • Not tested: a live OpenCode session against a deployed Headroom proxy.

Review Readiness

  • I have performed a self-review
  • This PR is ready for human review

Copilot AI lite review requested due to automatic review settings August 8, 2026 19:48

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

PR governance

This PR does not yet satisfy the required template fields:

  • Missing required section Runtime Rollout Safety.

Please update the PR body, or move the PR back to draft while it is still in progress.

@JerrettDavis JerrettDavis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed both routed transport seams and the plugin-level project resolution. The header is applied only after shouldRoute, so it is not added to unrelated direct requests, and the generated standalone bundle matches the source change. Verified the latest head locally: all 17 OpenCode plugin tests pass.

@github-actions github-actions Bot added status: ready for review Pull request body is complete and the author marked it ready for human review and removed status: needs author action Pull request body or readiness checklist still needs author updates labels Aug 12, 2026

@chopratejas chopratejas left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved, mergeable, and no failing checks — but CI on this branch only recorded a handful of check runs, well short of the ~35 a full run produces on current main. That usually means the run predates the current workflow set or was path-gated, so "no failures" here isn't the same claim as "green".

I've held it out of today's merge batch for that reason alone, not on content. Please rebase on main (ten PRs landed today) and push so a complete run reports, and I'll merge on the strength of that.

The OpenCode plugin set HEADROOM_PROJECT as a shell env var for child
processes but never forwarded it as an HTTP header on proxied requests.
The proxy's project-attribution logic reads x-headroom-project (or a
/p/<name> URL prefix) to attribute traffic to a project — without the
header, every OpenCode request was unattributed and the Per-Project
Savings dashboard showed zero forever.

Thread the project value (resolved once at plugin init from
pluginOptions.project → input.project.id → input.directory) through
installHeadroomTransport. Both header-building seams now set
x-headroom-project when a project is present:
  - mergeFetchHeaders (wrapped fetch path)
  - headersForNodeRequest (wrapped http.request / https.request path)

Closes headroomlabs-ai#2847

Signed-off-by: Radhakrishnan P <gingeekrishna@gmail.com>
@gingeekrishna
gingeekrishna force-pushed the fix/2847-opencode-project-header branch from b0ac69c to 6df0748 Compare August 17, 2026 04:05
@gingeekrishna

Copy link
Copy Markdown
Contributor Author

Rebased on current main (143 commits) — full CI run should now report.

@github-actions github-actions Bot added status: ci failing Required or reported CI checks are failing status: needs author action Pull request body or readiness checklist still needs author updates and removed status: ready for review Pull request body is complete and the author marked it ready for human review labels Aug 17, 2026
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Regenerated by running `npm run build:standalone` in plugins/opencode.

Signed-off-by: Radhakrishnan Pachyappan <gingeekrishna@gmail.com>
@github-actions github-actions Bot removed the status: ci failing Required or reported CI checks are failing label Aug 17, 2026
@chopratejas
chopratejas merged commit eeb038b into headroomlabs-ai:main Aug 18, 2026
37 checks passed
chopratejas pushed a commit that referenced this pull request Aug 20, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.36.0](v0.35.0...v0.36.0)
(2026-08-20)


### Features

* add deterministic runtime rollout controls
([#1490](#1490))
([3077ac8](3077ac8))
* **proxy:** let extensions report cost savings and their own latency
([#3051](#3051))
([f9807fd](f9807fd))
* **proxy:** unify savings attribution across stats, perf, metrics, and
dashboard
([1b0b0b8](1b0b0b8)),
closes [#2976](#2976)
* **wrap/claude:** make the --1m fallback model configurable via
HEADROOM_1M_MODEL
([#2983](#2983))
([2a84725](2a84725))


### Bug Fixes

* **anthropic:** honor the [1m] 1M-context tier, and price it correctly
([#3073](#3073))
([6d2254d](6d2254d))
* **ccr:** make --no-ccr disable server-side response handling too
([#3101](#3101))
([131b119](131b119)),
closes [#3082](#3082)
* **ccr:** make StreamingCCRHandler work on OpenAI streams
([#3069](#3069))
([7ef736f](7ef736f))
* **ccr:** only buffer a stream when a marker is actually redeemable
([#3092](#3092))
([c502087](c502087))
* **ccr:** re-inject headroom_retrieve when history references it on the
sessionless path
([942af56](942af56))
* **ccr:** relay a successful upstream turn when post-processing fails
([#3094](#3094))
([0ec73fa](0ec73fa))
* **ccr:** send Accept: application/json on a buffered stream:false turn
([#3102](#3102))
([139c7cb](139c7cb)),
closes [#3078](#3078)
* **ccr:** verify a scanned marker's hash before advertising it
([#2908](#2908))
([41dab2d](41dab2d))
* **ci:** prevent native detector from hanging test shards
([#2996](#2996))
([a708c05](a708c05))
* **ci:** scope the release credential and stop persisting it to disk
([#3062](#3062))
([ac8646a](ac8646a))
* **ci:** unjam release and Docker publishing
([#2958](#2958))
([e269afb](e269afb))
* **claude:** reject conflicting auth before proxy startup
([#2993](#2993))
([2d88e31](2d88e31))
* **cli/install:** resolve the deployment profile instead of dead-ending
on default
([#2832](#2832))
([8252619](8252619))
* **cli:** stop the macOS malloc re-exec replacing an embedder's process
([#3064](#3064))
([96c25f5](96c25f5))
* **copilot:** route VS Code inline completions to Copilot, not OpenAI
([#3077](#3077))
([204e751](204e751))
* **copilot:** send VS Code inline completions to the host that serves
them ([#3112](#3112))
([b77d612](b77d612))
* **deps:** bump datasets past PYSEC-2026-3716
([#3136](#3136))
([df6ff6b](df6ff6b))
* **deps:** clear the two Rust advisories and make cargo audit blocking
([#3121](#3121))
([93c474e](93c474e))
* **deps:** raise the GitPython floor to 3.1.58 to clear 9 open
advisories
([#3120](#3120))
([8156d4d](8156d4d))
* **docker:** publish compose ports on loopback only
([#3061](#3061))
([481e0b8](481e0b8))
* **docker:** ship Bedrock auth and current registry
([#2982](#2982))
([eafdf11](eafdf11))
* **doctor:** surface that Claude Desktop agent sessions bypass the
proxy ([#2987](#2987))
([be5b26d](be5b26d))
* **install:** consolidate Windows fallback and cleanup safety
([#2980](#2980))
([ddd2a25](ddd2a25))
* **install:** honor HEADROOM_PORT in install apply and deploy
([#3085](#3085))
([58f28dc](58f28dc))
* **install:** stop the PowerShell installer leaking temp dirs into the
real user PATH
([#2985](#2985))
([ddd9f76](ddd9f76))
* **learn:** include stdout in CLI failure messages, not just stderr
([#3080](#3080))
([c5563d3](c5563d3))
* **mcp:** restore SDK v1 compatibility cap
([#2978](#2978))
([6077e5a](6077e5a))
* **memory:** sanitize entity_refs to prevent dict-shaped entries
crashing search
([#2951](#2951))
([2d1e96b](2d1e96b))
* **onnx:** enforce Rust API-24 runtime compatibility
([#2979](#2979))
([a3fe5cb](a3fe5cb))
* **openclaw-plugin:** circuit breaker + per-request timeout for proxy
resilience
([#639](#639))
([6576ef6](6576ef6))
* **opencode:** send x-headroom-project header on all proxied requests
([#2868](#2868))
([eeb038b](eeb038b))
* **policy:** price net-cost mutations with the 1h cache-write tier
([#2780](#2780))
([ef7e07e](ef7e07e))
* **providers:** don't crash on a non-object HEADROOM_MODEL_LIMITS /
models.json
([#3089](#3089))
([3ed8f76](3ed8f76))
* **proxy/anthropic:** don't buffer a CCR stream when passthrough
discards the stream flip
([#2953](#2953))
([f1c34d3](f1c34d3))
* **proxy/anthropic:** don't replay recorded prefix over live history
([#3026](#3026))
([#3052](#3052))
([c16be9b](c16be9b))
* **proxy/anthropic:** repair headroom_retrieve history references the
tools array cannot support
([#2876](#2876))
([7de3573](7de3573))
* **proxy/anthropic:** stop answering a non-streaming turn with an event
stream
([#3142](#3142))
([0e26fb8](0e26fb8))
* **proxy/cache:** strip cache_control from messages in the semantic
cache key
([#3086](#3086))
([2cae0f8](2cae0f8))
* **proxy/gemini:** guard CCR continuation usage against present-null
counts
([#3035](#3035))
([a01897c](a01897c))
* **proxy/openai:** propagate provider usage on the Responses
WS-&gt;HTTP fallback
([#2988](#2988))
([536c949](536c949))
* **proxy:** adapt 200 SSE upstream replies on buffered /v1/responses
instead of 502
([#2622](#2622))
([d76fce0](d76fce0))
* **proxy:** align signed-thinking wire accounting
([#3015](#3015))
([b3f4436](b3f4436))
* **proxy:** complete stateless Responses and buffered CCR lifecycle
([#2997](#2997))
([8a1d38b](8a1d38b))
* **proxy:** guard feedback endpoints and add CSRF checks to loopback
writes
([#3060](#3060))
([a6ab359](a6ab359))
* **proxy:** keep prefixed core tools resident
([#3046](#3046))
([2f4d001](2f4d001))
* **proxy:** preserve Codex WebSocket model attribution
([#3029](#3029))
([a06a51e](a06a51e))
* **proxy:** relocate stray system-role messages to the top-level system
param ([#765](#765))
([#1357](#1357))
([9fde127](9fde127))
* **proxy:** restore the buffered-CCR heartbeat behind a grace window
([#3091](#3091))
([a29d201](a29d201))
* **proxy:** scope the signed-thinking lock to blocks that actually
changed
([#3124](#3124))
([17522fb](17522fb))
* **proxy:** stop a lone surrogate turning a thinking body into a 500
([#3134](#3134))
([284ff31](284ff31))
* **proxy:** stop cached responses replaying the producing turn's wire
framing
([#3024](#3024))
([9d37059](9d37059))
* **proxy:** stop operator secrets following a client-chosen upstream
([#3122](#3122))
([05f5ef4](05f5ef4))
* **proxy:** tune macOS libmalloc and trim allocator pages so long-lived
RSS stays bounded
([#2879](#2879))
([6d87825](6d87825))
* **reporting:** show net vs gross savings, real skip thresholds, and
the effective profile
([#3123](#3123))
([250ede2](250ede2))
* tool_search_tool_regex deferred and falsely resolved on
direct-Anthropic path
([#2971](#2971))
([8ea87e7](8ea87e7))
* **vscode:** persist compatible Claude modes and route Copilot CAPI
([#2986](#2986))
([1aa701a](1aa701a))
* **wrap:** set xAI upstream for grok-build proxy
([#2772](#2772))
([c831081](c831081))
* **wrap:** stop the Serena pre-index stalling the launch path for 300s
([#2945](#2945))
([6147883](6147883))
* **wrap:** verify proxy deps before mutating Codex config
([#1628](#1628))
([b7f342c](b7f342c))


### Performance Improvements

* **perf:** skip rotated logs outside the requested window
([#3081](#3081))
([6c9f41e](6c9f41e))


### Dependencies

* bump axum from 0.7.9 to 0.8.9
([#2966](#2966))
([5731be7](5731be7))
* bump criterion from 0.5.1 to 0.8.2
([#2965](#2965))
([b30f339](b30f339))
* bump ruff from 0.15.22 to 0.16.2 in the pip-minor-patch group across 1
directory
([#2962](#2962))
([ff17961](ff17961))
* bump sha2 from 0.10.9 to 0.11.0
([#2288](#2288))
([322425c](322425c))
* bump the cargo-minor-patch group across 1 directory with 4 updates
([#2964](#2964))
([888a9f4](888a9f4))
* bump tokio-tungstenite from 0.24.0 to 0.30.0
([#2967](#2967))
([bbe9013](bbe9013))
* update mcp requirement from &lt;2.0.0,&gt;=1.28.1 to
&gt;=1.28.1,&lt;3.0.0
([#2963](#2963))
([d6fb536](d6fb536))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: needs author action Pull request body or readiness checklist still needs author updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] OpenCode plugin never sends x-headroom-project — Per-Project Savings dashboard can't attribute OpenCode traffic

5 participants