@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.
- package/index.ts +61 -51
- 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 = "
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
77
|
+
count: number,
|
|
68
78
|
method: string,
|
|
69
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
let
|
|
77
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
log(`retry ${method} ${orig.origin} via
|
|
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
|
|
87
|
-
if (!isRelayFailureStatus(res.status)) {
|
|
88
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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:
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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.
|
|
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",
|