headroom-opencode 0.37.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/{chunk-UD4AB5X6.js → chunk-6I5ODRCW.js} +50 -19
- package/dist/chunk-6I5ODRCW.js.map +1 -0
- package/dist/{entry.opencode-BdYgRYb4.d.ts → entry.opencode-CbfMY5Ox.d.ts} +1 -0
- package/dist/entry.opencode.d.ts +1 -1
- package/dist/entry.opencode.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/package.json +4 -4
- package/dist/chunk-UD4AB5X6.js.map +0 -1
package/README.md
CHANGED
|
@@ -53,6 +53,22 @@ export default async function plugin(input) {
|
|
|
53
53
|
- exposes the `headroom_retrieve` tool.
|
|
54
54
|
- publishes `HEADROOM_PROXY_URL` in the plugin output env.
|
|
55
55
|
- defaults to `http://127.0.0.1:8787` when no proxy URL is supplied.
|
|
56
|
+
- skips routing for hosts listed in `excludeHosts` (or `HEADROOM_OPENCODE_EXCLUDE_HOSTS`); those requests go straight upstream with no compression or telemetry.
|
|
57
|
+
|
|
58
|
+
To keep a provider off the proxy, list its host. An entry matches the host and all of its subdomains, and `.example.com` / `*.example.com` are accepted as spellings of `example.com`:
|
|
59
|
+
|
|
60
|
+
```jsonc
|
|
61
|
+
// opencode.json
|
|
62
|
+
{
|
|
63
|
+
"plugin": [["/path/to/headroom/providers/opencode/_dist/entry.opencode.js", { "excludeHosts": ["opencode.ai"] }]]
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
or, for `headroom wrap opencode` and spawned Node child processes, set the environment variable before launching:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
HEADROOM_OPENCODE_EXCLUDE_HOSTS="opencode.ai,.corp.internal" headroom wrap opencode
|
|
71
|
+
```
|
|
56
72
|
|
|
57
73
|
## Retrieve Tool
|
|
58
74
|
|
|
@@ -100,6 +116,7 @@ The provider config exposes these as `headroom/<model>` and defaults to `headroo
|
|
|
100
116
|
| Variable | Used by | Description |
|
|
101
117
|
|---|---|---|
|
|
102
118
|
| `HEADROOM_PROXY_URL` | Native plugin | Proxy URL used by `HeadroomPlugin` |
|
|
119
|
+
| `HEADROOM_OPENCODE_EXCLUDE_HOSTS` | Native plugin | Comma-separated hosts (and their subdomains) that bypass the proxy; the `excludeHosts` plugin option takes precedence |
|
|
103
120
|
| `OPENCODE_CONFIG_CONTENT` | OpenCode wrapper | Generated OpenCode provider, model, and MCP config |
|
|
104
121
|
|
|
105
122
|
## License
|
|
@@ -79,6 +79,7 @@ var BASE_URL_HEADER = "x-headroom-base-url";
|
|
|
79
79
|
var ORIGINAL_PATH_HEADER = "x-headroom-original-path";
|
|
80
80
|
var PROJECT_HEADER = "x-headroom-project";
|
|
81
81
|
var PROXY_ENV = "HEADROOM_OPENCODE_TRANSPORT_PROXY_URL";
|
|
82
|
+
var EXCLUDE_HOSTS_ENV = "HEADROOM_OPENCODE_EXCLUDE_HOSTS";
|
|
82
83
|
var STATE_KEY = /* @__PURE__ */ Symbol.for("headroom.opencode.transport");
|
|
83
84
|
function getState() {
|
|
84
85
|
return globalThis[STATE_KEY];
|
|
@@ -100,17 +101,26 @@ function withNodeImportOption(existing, shim) {
|
|
|
100
101
|
}
|
|
101
102
|
return parts.join(" ");
|
|
102
103
|
}
|
|
103
|
-
function
|
|
104
|
+
function withExcludeHostsEnv(env, excludeHosts) {
|
|
105
|
+
if (excludeHosts.length > 0) {
|
|
106
|
+
env[EXCLUDE_HOSTS_ENV] = excludeHosts.join(",");
|
|
107
|
+
} else {
|
|
108
|
+
delete env[EXCLUDE_HOSTS_ENV];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function withShimEnv(env, proxyUrl, excludeHosts) {
|
|
104
112
|
const nextEnv = { ...env ?? process.env };
|
|
105
113
|
nextEnv[PROXY_ENV] = proxyUrl;
|
|
114
|
+
withExcludeHostsEnv(nextEnv, excludeHosts);
|
|
106
115
|
const shim = shimImportSpecifier();
|
|
107
116
|
if (shim) {
|
|
108
117
|
nextEnv.NODE_OPTIONS = withNodeImportOption(nextEnv.NODE_OPTIONS, shim);
|
|
109
118
|
}
|
|
110
119
|
return nextEnv;
|
|
111
120
|
}
|
|
112
|
-
function installProcessEnv(proxyUrl) {
|
|
121
|
+
function installProcessEnv(proxyUrl, excludeHosts) {
|
|
113
122
|
process.env[PROXY_ENV] = proxyUrl;
|
|
123
|
+
withExcludeHostsEnv(process.env, excludeHosts);
|
|
114
124
|
const shim = shimImportSpecifier();
|
|
115
125
|
if (shim) {
|
|
116
126
|
process.env.NODE_OPTIONS = withNodeImportOption(process.env.NODE_OPTIONS, shim);
|
|
@@ -119,11 +129,11 @@ function installProcessEnv(proxyUrl) {
|
|
|
119
129
|
function isOptions(value) {
|
|
120
130
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value) && !(value instanceof URL);
|
|
121
131
|
}
|
|
122
|
-
function injectOptionsEnv(args, optionIndex,
|
|
132
|
+
function injectOptionsEnv(args, optionIndex, state) {
|
|
123
133
|
const nextArgs = [...args];
|
|
124
134
|
const callback = typeof nextArgs.at(-1) === "function" ? nextArgs.pop() : void 0;
|
|
125
135
|
const existing = isOptions(nextArgs[optionIndex]) ? { ...nextArgs[optionIndex] } : {};
|
|
126
|
-
existing.env = withShimEnv(existing.env, proxyUrl);
|
|
136
|
+
existing.env = withShimEnv(existing.env, state.proxyUrl, state.excludeHosts);
|
|
127
137
|
if (isOptions(nextArgs[optionIndex])) {
|
|
128
138
|
nextArgs[optionIndex] = existing;
|
|
129
139
|
} else {
|
|
@@ -141,7 +151,7 @@ function wrapSpawn(originalSpawn) {
|
|
|
141
151
|
return Reflect.apply(originalSpawn, this, args);
|
|
142
152
|
}
|
|
143
153
|
const optionIndex = Array.isArray(args[1]) ? 2 : 1;
|
|
144
|
-
return Reflect.apply(originalSpawn, this, injectOptionsEnv(args, optionIndex, state
|
|
154
|
+
return Reflect.apply(originalSpawn, this, injectOptionsEnv(args, optionIndex, state));
|
|
145
155
|
};
|
|
146
156
|
}
|
|
147
157
|
function wrapExec(originalExec) {
|
|
@@ -150,7 +160,7 @@ function wrapExec(originalExec) {
|
|
|
150
160
|
if (!state) {
|
|
151
161
|
return Reflect.apply(originalExec, this, args);
|
|
152
162
|
}
|
|
153
|
-
return Reflect.apply(originalExec, this, injectOptionsEnv(args, 1, state
|
|
163
|
+
return Reflect.apply(originalExec, this, injectOptionsEnv(args, 1, state));
|
|
154
164
|
};
|
|
155
165
|
}
|
|
156
166
|
function wrapExecFile(originalExecFile) {
|
|
@@ -160,7 +170,7 @@ function wrapExecFile(originalExecFile) {
|
|
|
160
170
|
return Reflect.apply(originalExecFile, this, args);
|
|
161
171
|
}
|
|
162
172
|
const optionIndex = Array.isArray(args[1]) ? 2 : 1;
|
|
163
|
-
return Reflect.apply(originalExecFile, this, injectOptionsEnv(args, optionIndex, state
|
|
173
|
+
return Reflect.apply(originalExecFile, this, injectOptionsEnv(args, optionIndex, state));
|
|
164
174
|
};
|
|
165
175
|
}
|
|
166
176
|
function wrapFork(originalFork) {
|
|
@@ -170,7 +180,7 @@ function wrapFork(originalFork) {
|
|
|
170
180
|
return Reflect.apply(originalFork, this, args);
|
|
171
181
|
}
|
|
172
182
|
const optionIndex = Array.isArray(args[1]) ? 2 : 1;
|
|
173
|
-
return Reflect.apply(originalFork, this, injectOptionsEnv(args, optionIndex, state
|
|
183
|
+
return Reflect.apply(originalFork, this, injectOptionsEnv(args, optionIndex, state));
|
|
174
184
|
};
|
|
175
185
|
}
|
|
176
186
|
function normalizeProxyUrl(proxyUrl) {
|
|
@@ -180,7 +190,21 @@ function isLoopback(hostname3) {
|
|
|
180
190
|
const normalized = hostname3.toLowerCase().replace(/^\[|\]$/g, "");
|
|
181
191
|
return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
|
|
182
192
|
}
|
|
183
|
-
function
|
|
193
|
+
function normalizeExcludeHosts(entries) {
|
|
194
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
195
|
+
for (const entry of typeof entries === "string" ? entries.split(",") : entries) {
|
|
196
|
+
const host = String(entry).trim().toLowerCase().replace(/^(\*\.|\.)/, "");
|
|
197
|
+
if (host) {
|
|
198
|
+
hosts.add(host);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return [...hosts];
|
|
202
|
+
}
|
|
203
|
+
function isExcludedHost(hostname3, excludeHosts) {
|
|
204
|
+
const normalized = hostname3.toLowerCase().replace(/^\[|\]$/g, "");
|
|
205
|
+
return excludeHosts.some((host) => normalized === host || normalized.endsWith(`.${host}`));
|
|
206
|
+
}
|
|
207
|
+
function shouldRoute(url2, proxy, excludeHosts) {
|
|
184
208
|
if (url2.protocol !== "http:" && url2.protocol !== "https:") {
|
|
185
209
|
return false;
|
|
186
210
|
}
|
|
@@ -190,6 +214,9 @@ function shouldRoute(url2, proxy) {
|
|
|
190
214
|
if (url2.origin === proxy.origin) {
|
|
191
215
|
return false;
|
|
192
216
|
}
|
|
217
|
+
if (isExcludedHost(url2.hostname, excludeHosts)) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
193
220
|
return true;
|
|
194
221
|
}
|
|
195
222
|
function routedUrl(upstream, proxy) {
|
|
@@ -243,9 +270,9 @@ function mergeFetchHeaders(input, init, upstream, originalPath = void 0, project
|
|
|
243
270
|
}
|
|
244
271
|
return headers;
|
|
245
272
|
}
|
|
246
|
-
function withRoutedFetchInput(input, init, proxy, project) {
|
|
273
|
+
function withRoutedFetchInput(input, init, proxy, project, excludeHosts) {
|
|
247
274
|
const upstream = requestUrl(input);
|
|
248
|
-
if (!shouldRoute(upstream, proxy)) {
|
|
275
|
+
if (!shouldRoute(upstream, proxy, excludeHosts)) {
|
|
249
276
|
return [input, init];
|
|
250
277
|
}
|
|
251
278
|
const { url: nextUrl, originalPath } = routedUrlForOpenCode(upstream, proxy);
|
|
@@ -313,8 +340,8 @@ function headersForNodeRequest(options, upstream, originalPath, project) {
|
|
|
313
340
|
});
|
|
314
341
|
return result;
|
|
315
342
|
}
|
|
316
|
-
function routedNodeOptions(parts, proxy, project) {
|
|
317
|
-
if (!parts.url || !shouldRoute(parts.url, proxy)) {
|
|
343
|
+
function routedNodeOptions(parts, proxy, project, excludeHosts) {
|
|
344
|
+
if (!parts.url || !shouldRoute(parts.url, proxy, excludeHosts)) {
|
|
318
345
|
return void 0;
|
|
319
346
|
}
|
|
320
347
|
const { url: nextUrl, originalPath } = routedUrlForOpenCode(parts.url, proxy);
|
|
@@ -355,7 +382,7 @@ function wrapRequest(originalHttpRequest, originalHttpsRequest, originalRequest)
|
|
|
355
382
|
}
|
|
356
383
|
const proxy = normalizeProxyUrl(state.proxyUrl);
|
|
357
384
|
const parts = splitNodeArgs(args);
|
|
358
|
-
const nextOptions = routedNodeOptions(parts, proxy, state.project);
|
|
385
|
+
const nextOptions = routedNodeOptions(parts, proxy, state.project, state.excludeHosts);
|
|
359
386
|
if (!nextOptions) {
|
|
360
387
|
return Reflect.apply(originalRequest, this, args);
|
|
361
388
|
}
|
|
@@ -377,7 +404,7 @@ function wrapHttp2Connect(originalConnect) {
|
|
|
377
404
|
if (state) {
|
|
378
405
|
const proxy = normalizeProxyUrl(state.proxyUrl);
|
|
379
406
|
const upstream = authority instanceof URL ? authority : new URL(String(authority));
|
|
380
|
-
if (shouldRoute(upstream, proxy)) {
|
|
407
|
+
if (shouldRoute(upstream, proxy, state.excludeHosts)) {
|
|
381
408
|
throw new Error(
|
|
382
409
|
`Headroom OpenCode wrap blocked direct HTTP/2 connection to ${upstream.origin}. Use fetch, http, or https so traffic can be routed through Headroom.`
|
|
383
410
|
);
|
|
@@ -387,19 +414,22 @@ function wrapHttp2Connect(originalConnect) {
|
|
|
387
414
|
};
|
|
388
415
|
}
|
|
389
416
|
function installHeadroomTransport(options) {
|
|
417
|
+
const excludeHosts = normalizeExcludeHosts(options.excludeHosts ?? process.env[EXCLUDE_HOSTS_ENV] ?? "");
|
|
390
418
|
const existing = getState();
|
|
391
419
|
if (existing) {
|
|
392
420
|
existing.refs += 1;
|
|
393
421
|
existing.proxyUrl = options.proxyUrl;
|
|
394
422
|
existing.project = options.project;
|
|
423
|
+
existing.excludeHosts = excludeHosts;
|
|
395
424
|
existing.debug = Boolean(options.debug);
|
|
396
|
-
installProcessEnv(options.proxyUrl);
|
|
425
|
+
installProcessEnv(options.proxyUrl, excludeHosts);
|
|
397
426
|
return () => uninstallHeadroomTransport();
|
|
398
427
|
}
|
|
399
428
|
const state = {
|
|
400
429
|
refs: 1,
|
|
401
430
|
proxyUrl: options.proxyUrl,
|
|
402
431
|
project: options.project,
|
|
432
|
+
excludeHosts,
|
|
403
433
|
debug: Boolean(options.debug),
|
|
404
434
|
originalFetch: globalThis.fetch,
|
|
405
435
|
originalHttpRequest: http.request,
|
|
@@ -413,14 +443,14 @@ function installHeadroomTransport(options) {
|
|
|
413
443
|
originalChildFork: childProcess.fork
|
|
414
444
|
};
|
|
415
445
|
setState(state);
|
|
416
|
-
installProcessEnv(options.proxyUrl);
|
|
446
|
+
installProcessEnv(options.proxyUrl, excludeHosts);
|
|
417
447
|
globalThis.fetch = async (...args) => {
|
|
418
448
|
const current = getState();
|
|
419
449
|
if (!current) {
|
|
420
450
|
return state.originalFetch(...args);
|
|
421
451
|
}
|
|
422
452
|
const proxy = normalizeProxyUrl(current.proxyUrl);
|
|
423
|
-
const [nextInput, nextInit] = withRoutedFetchInput(args[0], args[1], proxy, current.project);
|
|
453
|
+
const [nextInput, nextInit] = withRoutedFetchInput(args[0], args[1], proxy, current.project, current.excludeHosts);
|
|
424
454
|
return state.originalFetch(nextInput, nextInit);
|
|
425
455
|
};
|
|
426
456
|
http.request = wrapRequest(state.originalHttpRequest, state.originalHttpsRequest, state.originalHttpRequest);
|
|
@@ -12894,6 +12924,7 @@ var HeadroomPlugin = async (input, options = {}) => {
|
|
|
12894
12924
|
const uninstallTransport = installHeadroomTransport({
|
|
12895
12925
|
proxyUrl,
|
|
12896
12926
|
project,
|
|
12927
|
+
excludeHosts: pluginOptions.excludeHosts,
|
|
12897
12928
|
debug: pluginOptions.debug
|
|
12898
12929
|
});
|
|
12899
12930
|
return {
|
|
@@ -12932,4 +12963,4 @@ export {
|
|
|
12932
12963
|
HeadroomPlugin,
|
|
12933
12964
|
plugin_default
|
|
12934
12965
|
};
|
|
12935
|
-
//# sourceMappingURL=chunk-
|
|
12966
|
+
//# sourceMappingURL=chunk-6I5ODRCW.js.map
|