mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## 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.ts` — `InstallOptions.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 - [x] Unit tests pass - [x] TypeScript typecheck passes - [x] 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 - [x] 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 - [x] I have performed a self-review - [x] This PR is ready for human review --------- Signed-off-by: Radhakrishnan P <gingeekrishna@gmail.com> Signed-off-by: Radhakrishnan Pachyappan <gingeekrishna@gmail.com>
70 lines
1.9 KiB
TypeScript
70 lines
1.9 KiB
TypeScript
import type { Plugin } from "@opencode-ai/plugin";
|
|
import { tool } from "@opencode-ai/plugin";
|
|
import { z } from "zod";
|
|
|
|
import { createHeadroomRetrieveTool, getDefaultProxyUrl } from "./retrieve.js";
|
|
import { installHeadroomTransport } from "./transport.js";
|
|
|
|
export interface HeadroomOpenCodePluginOptions {
|
|
proxyUrl?: string;
|
|
project?: string;
|
|
backend?: string;
|
|
debug?: boolean;
|
|
}
|
|
|
|
function normalizeProxyUrl(url: string): string {
|
|
return url.replace(/\/+$/, "");
|
|
}
|
|
|
|
function resolveProxyUrl(options?: HeadroomOpenCodePluginOptions): string {
|
|
return normalizeProxyUrl(
|
|
options?.proxyUrl ??
|
|
process.env.HEADROOM_PROXY_URL ??
|
|
process.env.HEADROOM_BASE_URL ??
|
|
getDefaultProxyUrl(),
|
|
);
|
|
}
|
|
|
|
export const HeadroomPlugin: Plugin = async (input, options = {}) => {
|
|
const pluginOptions = options as HeadroomOpenCodePluginOptions;
|
|
const proxyUrl = resolveProxyUrl(pluginOptions);
|
|
const project =
|
|
pluginOptions.project ??
|
|
(input.project as { id?: string } | undefined)?.id ??
|
|
input.directory;
|
|
const retrieveTool = createHeadroomRetrieveTool({ proxyBaseUrl: proxyUrl });
|
|
const uninstallTransport = installHeadroomTransport({
|
|
proxyUrl,
|
|
project,
|
|
debug: pluginOptions.debug,
|
|
});
|
|
|
|
return {
|
|
dispose: async () => {
|
|
uninstallTransport();
|
|
},
|
|
tool: {
|
|
headroom_retrieve: tool({
|
|
description: retrieveTool.description,
|
|
args: {
|
|
hash: z
|
|
.string()
|
|
.regex(/^[a-f0-9]{24}$/i, "Expected 24-character hex hash"),
|
|
},
|
|
async execute(args) {
|
|
return retrieveTool.execute(args);
|
|
},
|
|
}),
|
|
},
|
|
"shell.env": async (_input, output) => {
|
|
output.env.HEADROOM_ACTIVE = "1";
|
|
output.env.HEADROOM_PROXY_URL = proxyUrl;
|
|
output.env.HEADROOM_PROJECT = project;
|
|
if (pluginOptions.backend) {
|
|
output.env.HEADROOM_BACKEND = pluginOptions.backend;
|
|
}
|
|
},
|
|
};
|
|
};
|
|
|
|
export default HeadroomPlugin;
|