@nofendian17/opencode-relay-plugin 2.0.3 → 2.0.5

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.
Files changed (3) hide show
  1. package/README.md +29 -10
  2. package/index.ts +14 -10
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,10 +1,16 @@
1
1
  # @nofendian17/opencode-relay-plugin (v2)
2
2
 
3
- Route OpenCode **v2** Zen traffic (`https://opencode.ai/zen/*`) through your
4
- relay endpoint(s), ported from the v1 `relay.js`
5
- (`@opencode-ai/plugin` → `@opencode/plugin`).
3
+ Route OpenCode **v2** opencode.ai traffic through your relay endpoint(s),
4
+ ported from the v1 `relay.js` (`@opencode-ai/plugin` → `@opencode/plugin`).
6
5
 
7
- Only Zen requests are relayed; other providers already point at direct
6
+ ## Scope
7
+
8
+ Matched by **origin** (`https://opencode.ai`), not by path prefix. This covers
9
+ `/zen/*`, `/inference/*`, and any future path on the same origin. (Before
10
+ v2.0.4 the match was prefix-based on `/zen/*`, so `/inference/*` traffic
11
+ silently went direct — same rate-limit symptom, no relay in the log.)
12
+
13
+ Only opencode.ai requests are relayed; other providers already point at direct
8
14
  gateways and are left untouched (no rewrite, no forced retries).
9
15
 
10
16
  V1 patched `provider.options.fetch` + `globalThis.fetch`. V2 providers no longer expose `fetch`,
@@ -16,7 +22,7 @@ so this plugin uses the documented v2 interception points:
16
22
  - `ctx.session.hook("retry")` — relay network errors never reach `http.response`, so force a
17
23
  retry (next attempt picks the next relay); after the pool is exhausted the next attempt goes direct
18
24
  - `ctx.session.hook("experimental.ws.handshake")` — no-op (relay is HTTP-only, kept direct)
19
- - `globalThis.fetch` patch — only for out-of-session `https://opencode.ai/zen/*` traffic (v1 parity)
25
+ - `globalThis.fetch` patch — only for out-of-session `https://opencode.ai/*` traffic (v1 parity)
20
26
  - `ctx.provider.transform` — re-enable the built-in `opencode` (Zen) provider if disabled
21
27
 
22
28
  Reference: https://opencode.ai/v2/docs/plugins/ + migration guide
@@ -59,17 +65,21 @@ Local file (auto-discovered):
59
65
  | Option | Env fallback | Description |
60
66
  | --- | --- | --- |
61
67
  | `url: string \| string[]` | `RELAY_URL` (comma-separated) | Relay endpoint(s), `http(s)` only. Invalid entries are skipped with a warning log; if none remain valid the plugin disables itself. |
62
- | `debug: boolean` | `RELAY_DEBUG=1` | Verbose per-request logging. When on, log lines are also appended to `/tmp/opencode-relay.log` (plugin `console.log` is not forwarded to opencode logs). |
68
+ | `debug: boolean` | `RELAY_DEBUG=1` | Verbose per-request logging. When on, log lines are also appended to `/tmp/opencode-relay.log` (plugin `console.log` is not forwarded to opencode logs). Override the path with `RELAY_DEBUG_LOG` (used by tests for isolation). |
63
69
 
64
70
  ## Relay protocol (unchanged from v1)
65
71
 
66
72
  - `<relay-url>` with original method/body/headers plus:
67
- - `x-relay-target: https://api.anthropic.com` (origin)
68
- - `x-relay-path: /v1/messages?foo=bar` (path + query)
73
+ - `x-relay-target: https://opencode.ai` (origin)
74
+ - `x-relay-path: /inference/v1/chat/completions` (path + query)
69
75
  - Relay returns the provider response verbatim (status/body/headers).
70
76
  - `429` or `>= 500` → try next relay → fall back to direct.
71
77
  - Network error → try next relay → fall back to direct.
72
78
 
79
+ Retry accounting: attempts `1..N` (pool size) each try the next relay in
80
+ round-robin order; attempt `N+1` goes direct once (per-session flag, consumed
81
+ by the next `http.request`), then the pool is used again.
82
+
73
83
  ## Security
74
84
 
75
85
  The relay terminates TLS and sees everything it forwards: request bodies and
@@ -79,10 +89,19 @@ access logs as secret-bearing.
79
89
 
80
90
  ## Verify
81
91
 
82
- 1. `RELAY_DEBUG=1` + set `url` to a local echo server, run a prompt, confirm
92
+ 1. Set `debug: true` + `url` to a local echo server, run a prompt, confirm
83
93
  `x-relay-target` / `x-relay-path` headers arrive.
84
- 2. Stop the relay → request still succeeds via direct fallback (check `[relay] fallback to direct` log).
94
+ 2. Stop the relay → request still succeeds via direct fallback (check
95
+ `[relay] relay fallback to direct` in `/tmp/opencode-relay.log`).
85
96
  3. Unset `url`/`RELAY_URL` → `[relay] disabled` log, traffic goes direct.
97
+ 4. Confirm scope: a request to `https://opencode.ai/inference/...` must show
98
+ `via relay` in the log. If the log only shows
99
+ `direct fallback (all relays failed previously)` with an
100
+ `https://opencode.ai/...` URL and zero `via relay` lines, the request never
101
+ entered the pool — check the origin match in `index.ts`.
102
+
103
+ Note: the debug file is the shared `/tmp/opencode-relay.log`; concurrent
104
+ opencode processes append to the same file.
86
105
 
87
106
  ## V1 vs V2 notes
88
107
 
package/index.ts CHANGED
@@ -10,7 +10,7 @@ interface RelayOptions {
10
10
 
11
11
  const DEBUG_LOG = join(tmpdir(), "opencode-relay.log")
12
12
 
13
- const ZEN_PREFIX = "https://opencode.ai/zen/"
13
+ const OPENCODE_ORIGIN = "https://opencode.ai"
14
14
 
15
15
  type Logger = (message: string, extra?: Record<string, unknown>) => void
16
16
 
@@ -149,7 +149,7 @@ type RelayFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Resp
149
149
  * `x-relay-target` / `x-relay-path` (original method preserved),
150
150
  * round-robin across the pool, fail over on 429/5xx + network errors,
151
151
  * fall back to direct.
152
- * Used for out-of-session traffic (global fetch patch for Zen).
152
+ * Used for out-of-session traffic (global fetch patch for opencode.ai).
153
153
  */
154
154
  function createRelayFetch(
155
155
  relays: readonly string[],
@@ -219,8 +219,11 @@ export default Plugin.define({
219
219
  const debug = parsed.debug
220
220
  const prefix = "[relay]"
221
221
  // console output from v2 plugins is not forwarded to opencode logs,
222
- // so when debug is on, mirror everything to a file in /tmp as well.
222
+ // so when debug is on, mirror everything to a file as well.
223
+ // RELAY_DEBUG_LOG overrides the path (tests use an isolated file so a
224
+ // live opencode process appending to the default never interferes).
223
225
  // Async fire-and-forget: logging never breaks requests.
226
+ const debugLogFile = process.env.RELAY_DEBUG_LOG ?? DEBUG_LOG
224
227
  let warnedLogFile = false
225
228
  const fileLog: Logger | undefined = debug
226
229
  ? (message, extra) => {
@@ -228,10 +231,10 @@ export default Plugin.define({
228
231
  `${new Date().toISOString()} ${prefix} ${message}` +
229
232
  (extra ? ` ${JSON.stringify(extra)}` : "") +
230
233
  "\n"
231
- appendFile(DEBUG_LOG, line).catch((e: unknown) => {
234
+ appendFile(debugLogFile, line).catch((e: unknown) => {
232
235
  if (!warnedLogFile) {
233
236
  warnedLogFile = true
234
- console.log(prefix, "debug log write failed", DEBUG_LOG, String(e))
237
+ console.log(prefix, "debug log write failed", debugLogFile, String(e))
235
238
  }
236
239
  })
237
240
  }
@@ -280,15 +283,15 @@ export default Plugin.define({
280
283
  const relayOrigins = buildRelayOrigins(relays)
281
284
  const relayFetch = createRelayFetch(relays, pickIndex, directFetch, debug, log, emit)
282
285
 
283
- // --- Global fetch patch for out-of-session Zen traffic (v1 parity) ---
286
+ // --- Global fetch patch for out-of-session opencode.ai traffic (v1 parity) ---
284
287
  // Session model traffic is covered by hooks below; this covers anything
285
- // else that calls fetch("https://opencode.ai/zen/...") directly.
288
+ // else that calls fetch("https://opencode.ai/...") directly.
286
289
  if (!g[PATCH_MARKER]) {
287
290
  g[PATCH_MARKER] = originalForPatch
288
291
  globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
289
292
  const urlStr =
290
293
  typeof input === "string" ? input : input instanceof URL ? input.href : input.url
291
- if (typeof urlStr === "string" && urlStr.startsWith(ZEN_PREFIX)) {
294
+ if (typeof urlStr === "string" && urlStr.startsWith(OPENCODE_ORIGIN)) {
292
295
  try {
293
296
  if (!relayOrigins.has(new URL(urlStr).origin)) return relayFetch(input, init)
294
297
  } catch {
@@ -347,9 +350,10 @@ export default Plugin.define({
347
350
  // Avoid relaying the relay itself (loop).
348
351
  if (relayOrigins.has(url.origin)) return
349
352
 
350
- // Scope: only the Zen endpoint goes through the relay. Other
353
+ // Scope: only opencode.ai endpoints go through the relay (zen,
354
+ // inference, and any future path on the same origin). Other
351
355
  // providers already point at direct gateways and are left untouched.
352
- if (!url.href.startsWith(ZEN_PREFIX)) return
356
+ if (url.origin !== OPENCODE_ORIGIN) return
353
357
 
354
358
  // Buffer body (streams are one-shot).
355
359
  let bodyBuf: ArrayBuffer | undefined
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nofendian17/opencode-relay-plugin",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Route OpenCode v2 Zen (opencode.ai) traffic through relay endpoint(s) with round-robin, failover, and direct fallback. Other providers are left untouched.",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -26,7 +26,7 @@
26
26
  "@opencode/plugin": "^2"
27
27
  },
28
28
  "devDependencies": {
29
- "@opencode/plugin": "^2.0.24",
29
+ "@opencode/plugin": "^2.0.28",
30
30
  "@types/node": "^26.6.4",
31
31
  "typescript": "^5.9.3"
32
32
  }