fix(opencode): send x-headroom-project header on all proxied requests - #2868
Conversation
PR governanceThis PR does not yet satisfy the required template fields:
Please update the PR body, or move the PR back to draft while it is still in progress. |
6d53685 to
b0ac69c
Compare
JerrettDavis
left a comment
There was a problem hiding this comment.
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.
chopratejas
left a comment
There was a problem hiding this comment.
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>
b0ac69c to
6df0748
Compare
|
Rebased on current |
|
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>
🤖 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->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 <2.0.0,>=1.28.1 to >=1.28.1,<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>
Description
The OpenCode transport plugin set
HEADROOM_PROJECTas a shell env var for child processes but never forwarded it asx-headroom-projecton the actual proxied HTTP requests. The proxy'sclassify_projectonly attributes traffic viax-headroom-projectheader or/p/<name>URL prefix — without the header, every OpenCode request was unattributed and the Per-Project Savings dashboard showed0 project(s)permanently.Fixes #2847.
Root cause
installHeadroomTransportwas called with only{ proxyUrl, debug }. Theprojectvalue was computed and used only in theshell.envhook (for subprocess env injection), never threaded through tomergeFetchHeadersorheadersForNodeRequest.Changes Made
project?: stringtoInstallOptionsandTransportState.pluginOptions.project → input.project.id → input.directory) and pass it toinstallHeadroomTransport.x-headroom-projectwhen a project is present:mergeFetchHeaders(wrappedfetchpath)headersForNodeRequest(wrappedhttp.request/https.requestpath)projectin theshell.envhook (removes the duplicate resolution that was there before).Changes
plugins/opencode/src/transport.ts—InstallOptions.project,TransportState.project;mergeFetchHeaders,headersForNodeRequest,routedNodeOptions,withRoutedFetchInput,installHeadroomTransportupdatedplugins/opencode/src/plugin.ts— resolveprojectonce, pass it to transport; reuse inshell.envplugins/opencode/src/transport.test.ts— 3 new tests: project header on fetch, project header on https.request, no header when project unsetheadroom/providers/opencode/_dist/entry.opencode.js— rebuilt withnpm run build:standaloneto match sourceTesting
Test Output
TypeScript build also passes:
npm run typecheck(no errors).Type of Change
Real Behavior Proof
Review Readiness