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 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 withShimEnv(env, proxyUrl) {
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, proxyUrl) {
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.proxyUrl));
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.proxyUrl));
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.proxyUrl));
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.proxyUrl));
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 shouldRoute(url2, proxy) {
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-UD4AB5X6.js.map
12966
+ //# sourceMappingURL=chunk-6I5ODRCW.js.map