@nofendian17/opencode-relay-plugin 2.0.0 → 2.0.3

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 (2) hide show
  1. package/index.ts +61 -51
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import { Plugin } from "@opencode/plugin"
2
2
  import { appendFile } from "node:fs/promises"
3
+ import { tmpdir } from "node:os"
4
+ import { join } from "node:path"
3
5
 
4
6
  interface RelayOptions {
5
7
  url?: string | string[]
6
8
  debug?: boolean
7
9
  }
8
10
 
9
- const DEBUG_LOG = "/tmp/opencode-relay.log"
11
+ const DEBUG_LOG = join(tmpdir(), "opencode-relay.log")
10
12
 
11
13
  const ZEN_PREFIX = "https://opencode.ai/zen/"
12
14
 
@@ -59,50 +61,78 @@ function withDuplex(init: RequestInit, bodyBuf: ArrayBuffer | undefined): Reques
59
61
  return bodyBuf ? ({ ...init, duplex: "half" } as RequestInit) : init
60
62
  }
61
63
 
62
- // Returns true if the signal was aborted and the caller should return early.
63
- async function failoverToDirect(
64
- event: { response: Response },
64
+ interface RelayPoolResult {
65
+ response?: Response
66
+ lastError?: unknown
67
+ aborted: boolean
68
+ }
69
+
70
+ /**
71
+ * Try each relay in the pool starting from `start`, up to `count` attempts.
72
+ * Shared by both failover paths (session hooks + global fetch patch).
73
+ */
74
+ async function tryRelayPool(
65
75
  relays: readonly string[],
66
76
  start: number,
67
- orig: { origin: string; path: string },
77
+ count: number,
68
78
  method: string,
69
- headers: Headers,
79
+ baseHeaders: Headers,
80
+ orig: { origin: string; path: string },
70
81
  bodyBuf: ArrayBuffer | undefined,
71
82
  signal: AbortSignal | undefined,
72
83
  directFetch: typeof fetch,
73
84
  log: Logger,
74
- emit: Logger,
75
- ): Promise<boolean> {
76
- let lastStatus = event.response.status
77
- // Try the other relays in the pool (the first was already tried in http.request).
78
- for (let i = 1; i < relays.length; i++) {
79
- if (signal?.aborted) return true
85
+ ): Promise<RelayPoolResult> {
86
+ let lastError: unknown
87
+ for (let i = 0; i < count; i++) {
88
+ if (signal?.aborted) return { aborted: true }
80
89
  const relay = relays[(start + i) % relays.length]
81
- const h = new Headers(headers)
82
- h.set("x-relay-target", orig.origin)
83
- h.set("x-relay-path", orig.path)
84
- log(`retry ${method} ${orig.origin} via next relay`, { relay, prevStatus: lastStatus })
90
+ const headers = new Headers(baseHeaders)
91
+ headers.set("x-relay-target", orig.origin)
92
+ headers.set("x-relay-path", orig.path)
93
+ log(`retry ${method} ${orig.origin} via relay`, { relay, attempt: i + 1 })
85
94
  try {
86
- const res = await directFetch(relay, withDuplex({ method, headers: h, body: bodyBuf, signal }, bodyBuf))
87
- if (!isRelayFailureStatus(res.status)) {
88
- event.response = res
89
- return false
90
- }
91
- lastStatus = res.status
95
+ const res = await directFetch(relay, withDuplex({ method, headers, body: bodyBuf, signal }, bodyBuf))
96
+ if (!isRelayFailureStatus(res.status)) return { response: res, aborted: false }
97
+ lastError = new Error(`relay responded ${res.status}`)
92
98
  try {
93
99
  await res.body?.cancel()
94
100
  } catch {
95
101
  // ignore
96
102
  }
97
103
  } catch (e) {
98
- if (signal?.aborted) return true
99
- log("relay retry network error", { relay, error: String(e) })
100
- continue
104
+ if (signal?.aborted) return { aborted: true }
105
+ lastError = e
106
+ log("relay network error", { relay, error: String(e) })
101
107
  }
102
108
  }
109
+ return { lastError, aborted: false }
110
+ }
111
+
112
+ // Returns true if the signal was aborted and the caller should return early.
113
+ async function failoverToDirect(
114
+ event: { response: Response },
115
+ relays: readonly string[],
116
+ start: number,
117
+ orig: { origin: string; path: string },
118
+ method: string,
119
+ headers: Headers,
120
+ bodyBuf: ArrayBuffer | undefined,
121
+ signal: AbortSignal | undefined,
122
+ directFetch: typeof fetch,
123
+ log: Logger,
124
+ emit: Logger,
125
+ ): Promise<boolean> {
126
+ // Try the other relays in the pool (the first was already tried in http.request).
127
+ const result = await tryRelayPool(relays, start + 1, relays.length - 1, method, headers, orig, bodyBuf, signal, directFetch, log)
128
+ if (result.aborted) return true
129
+ if (result.response) {
130
+ event.response = result.response
131
+ return false
132
+ }
103
133
 
104
134
  // All relays failed -> direct fallback (v1 parity).
105
- emit("relay fallback to direct", { target: orig.origin, path: orig.path, status: lastStatus })
135
+ emit("relay fallback to direct", { target: orig.origin, path: orig.path, status: event.response.status })
106
136
  if (signal?.aborted) return true
107
137
  try {
108
138
  event.response = await directFetch(orig.origin + orig.path, withDuplex({ method, headers, body: bodyBuf, signal }, bodyBuf))
@@ -154,35 +184,15 @@ function createRelayFetch(
154
184
  }
155
185
  }
156
186
 
157
- const start = pickIndex()
158
- let lastError: unknown
159
- for (let attempt = 0; attempt < relays.length; attempt++) {
160
- const relay = relays[(start + attempt) % relays.length]
161
- const headers = new Headers(originalHeaders)
162
- headers.set("x-relay-target", url.origin)
163
- headers.set("x-relay-path", relayPath(url))
164
- if (debug) log(`${method} ${url.origin}${url.pathname}`, { relay, attempt: attempt + 1 })
165
-
166
- try {
167
- const res = await direct(relay, withDuplex({ method, headers, body: bodyBuf, signal }, bodyBuf))
168
- if (!isRelayFailureStatus(res.status)) return res
169
- lastError = new Error(`relay responded ${res.status}`)
170
- // Consume/cancel relay error body so the socket can be reused.
171
- try {
172
- await res.body?.cancel()
173
- } catch {
174
- // ignore
175
- }
176
- } catch (e) {
177
- if (signal.aborted) throw e
178
- lastError = e
179
- }
180
- }
187
+ const orig = { origin: url.origin, path: relayPath(url) }
188
+ const result = await tryRelayPool(relays, pickIndex(), relays.length, method, originalHeaders, orig, bodyBuf, signal, direct, log)
189
+ if (result.aborted) throw new Error("The operation was aborted")
190
+ if (result.response) return result.response
181
191
 
182
192
  emit("relay fallback to direct", {
183
193
  target: url.origin,
184
194
  path: url.pathname,
185
- error: String(lastError),
195
+ error: String(result.lastError),
186
196
  })
187
197
  return direct(url.href, withDuplex({ method, headers: originalHeaders, body: bodyBuf, signal }, bodyBuf))
188
198
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nofendian17/opencode-relay-plugin",
3
- "version": "2.0.0",
3
+ "version": "2.0.3",
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",