@@ -985,6 +985,11 @@ export async function sendViaRelay(options: {
985985}
986986
987987export const WORKER_SCRIPT = `
988+ function getPlanConfig(env) {
989+ const paid = (env.RELAY_PLAN || '').toLowerCase() === 'paid'
990+ return { paid, allowWebSocket: paid, logRequests: paid }
991+ }
992+
988993async function hashBody(body) {
989994 const bytes = new TextEncoder().encode(body)
990995 const digest = await crypto.subtle.digest('SHA-256', bytes)
@@ -1032,7 +1037,7 @@ async function resolveBody(env, payload) {
10321037 return { error: 'unknown mode', status: 400 }
10331038}
10341039
1035- async function prepareUpstream(env, payload) {
1040+ async function prepareUpstream(env, payload, config ) {
10361041 if ((payload.protocol !== 1 && payload.protocol !== 2) || payload.type !== 'request' || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {
10371042 return { error: 'invalid payload', status: 400 }
10381043 }
@@ -1045,14 +1050,16 @@ async function prepareUpstream(env, payload) {
10451050 }
10461051
10471052 const stateWrite = writeState(env, payload.affinity, { body, hash: payload.next_hash, revision: payload.revision }).catch(() => {})
1048- console.log(JSON.stringify({
1049- relay: 'opencode-anthropic-auth',
1050- transport: 'relay',
1051- mode: payload.mode,
1052- revision: payload.revision,
1053- affinity: String(payload.affinity).slice(0, 12),
1054- bodyBytes: body.length,
1055- }))
1053+ if (config.logRequests) {
1054+ console.log(JSON.stringify({
1055+ relay: 'opencode-anthropic-auth',
1056+ transport: 'http',
1057+ mode: payload.mode,
1058+ revision: payload.revision,
1059+ affinity: String(payload.affinity).slice(0, 12),
1060+ bodyBytes: body.length,
1061+ }))
1062+ }
10561063
10571064 return { body, stateWrite }
10581065}
@@ -1105,16 +1112,8 @@ async function prepareWebSocketUpstream(env, state, payload) {
11051112
11061113 const nextState = { body, hash: payload.next_hash, revision: payload.revision }
11071114 const checkpoint = checkpointWebSocketState(env, payload, body, nextState)
1108- const logAccepted = () => console.log(JSON.stringify({
1109- relay: 'opencode-anthropic-auth',
1110- transport: 'websocket',
1111- mode: payload.mode,
1112- revision: payload.revision,
1113- affinity: String(payload.affinity).slice(0, 12),
1114- bodyBytes: body.length,
1115- }))
1116-
1117- return { body, state: nextState, checkpoint, logAccepted }
1115+
1116+ return { body, state: nextState, checkpoint }
11181117}
11191118
11201119function headersToObject(headers) {
@@ -1123,18 +1122,51 @@ function headersToObject(headers) {
11231122 return result
11241123}
11251124
1126- async function handleRelayPayload(env, payload) {
1127- const prepared = await prepareUpstream(env, payload)
1125+ const SKIP_ERROR_LOG_STATUSES = new Set([429, 403])
1126+
1127+ async function logUpstreamError(env, ctx, upstream, meta) {
1128+ if (!upstream.status || upstream.status < 400 || SKIP_ERROR_LOG_STATUSES.has(upstream.status)) return
1129+ try {
1130+ const body = await upstream.clone().text()
1131+ const key = 'error:' + Date.now() + ':' + (meta.id || meta.affinity || 'unknown')
1132+ const entry = JSON.stringify({
1133+ ts: new Date().toISOString(),
1134+ status: upstream.status,
1135+ statusText: upstream.statusText,
1136+ transport: meta.transport,
1137+ mode: meta.mode,
1138+ affinity: meta.affinity,
1139+ id: meta.id,
1140+ bodyBytes: meta.bodyBytes,
1141+ responseBody: body.slice(0, 50000),
1142+ responseHeaders: headersToObject(upstream.headers),
1143+ })
1144+ const kvWrite = env.RELAY_STATE.put(key, entry, { expirationTtl: 604800 }).catch(() => {})
1145+ if (ctx?.waitUntil) ctx.waitUntil(kvWrite)
1146+ else void kvWrite
1147+ console.error(JSON.stringify({
1148+ relay: 'opencode-anthropic-auth',
1149+ event: 'upstream_error',
1150+ status: upstream.status,
1151+ transport: meta.transport,
1152+ affinity: String(meta.affinity || '').slice(0, 12),
1153+ responsePreview: body.slice(0, 500),
1154+ }))
1155+ } catch {}
1156+ }
1157+
1158+ async function handleRelayPayload(env, payload, config) {
1159+ const prepared = await prepareUpstream(env, payload, config)
11281160 if (prepared.error) return prepared
11291161 const upstream = await fetch(payload.upstream.url, {
11301162 method: payload.upstream.method || 'POST',
11311163 headers: payload.upstream.headers,
11321164 body: prepared.body,
11331165 })
1134- return { upstream, stateWrite: prepared.stateWrite }
1166+ return { upstream, stateWrite: prepared.stateWrite, bodyBytes: prepared.body.length }
11351167}
11361168
1137- async function handleWebSocket(socket, env, ctx, payload, getState, setState) {
1169+ async function handleWebSocket(socket, env, ctx, payload, getState, setState, config ) {
11381170 const heartbeat = setInterval(() => {
11391171 try {
11401172 socket.send(JSON.stringify({ protocol: 2, type: 'keepalive' }))
@@ -1151,7 +1183,16 @@ async function handleWebSocket(socket, env, ctx, payload, getState, setState) {
11511183
11521184 setState(result.state)
11531185 socket.send(JSON.stringify({ protocol: 2, type: 'accepted', id: payload.id, hash: result.state.hash, revision: result.state.revision }))
1154- ctx?.waitUntil?.(deferWorkerTask(result.logAccepted))
1186+ if (config.logRequests) {
1187+ console.log(JSON.stringify({
1188+ relay: 'opencode-anthropic-auth',
1189+ transport: 'websocket',
1190+ mode: payload.mode,
1191+ revision: payload.revision,
1192+ affinity: String(payload.affinity).slice(0, 12),
1193+ bodyBytes: result.body.length,
1194+ }))
1195+ }
11551196
11561197 const upstreamPromise = fetch(payload.upstream.url, {
11571198 method: payload.upstream.method || 'POST',
@@ -1160,6 +1201,19 @@ async function handleWebSocket(socket, env, ctx, payload, getState, setState) {
11601201 })
11611202 ctx?.waitUntil?.(result.checkpoint)
11621203 const upstream = await upstreamPromise
1204+ // Log non-429/403 errors to KV for debugging
1205+ if (upstream.status >= 400 && !SKIP_ERROR_LOG_STATUSES.has(upstream.status)) {
1206+ const errorClone = upstream.clone()
1207+ const errorLog = logUpstreamError(env, ctx, errorClone, {
1208+ transport: 'websocket',
1209+ mode: payload.mode,
1210+ affinity: payload.affinity,
1211+ id: payload.id,
1212+ bodyBytes: result.body.length,
1213+ })
1214+ if (ctx?.waitUntil) ctx.waitUntil(errorLog)
1215+ else void errorLog
1216+ }
11631217 socket.send(JSON.stringify({
11641218 protocol: 2,
11651219 type: 'response_start',
@@ -1187,7 +1241,12 @@ async function handleWebSocket(socket, env, ctx, payload, getState, setState) {
11871241
11881242export default {
11891243 async fetch(request, env, ctx) {
1244+ const config = getPlanConfig(env)
1245+
11901246 if (request.headers.get('Upgrade') === 'websocket') {
1247+ if (!config.allowWebSocket) {
1248+ return new Response('WebSocket transport requires Workers Paid plan. Use HTTP transport or upgrade your plan.', { status: 403 })
1249+ }
11911250 const url = new URL(request.url)
11921251 const token = url.searchParams.get('token')
11931252 const affinity = url.searchParams.get('affinity')
@@ -1233,15 +1292,19 @@ export default {
12331292 }
12341293 payload.affinity = affinity
12351294 busy = true
1236- const run = handleWebSocket(server, env, ctx, payload, () => state, (nextState) => { state = nextState }).finally(() => { busy = false })
1295+ const run = handleWebSocket(server, env, ctx, payload, () => state, (nextState) => { state = nextState }, config ).finally(() => { busy = false })
12371296 ctx?.waitUntil?.(run)
12381297 if (!ctx?.waitUntil) void run
12391298 })
12401299 return new Response(null, { status: 101, webSocket: client })
12411300 }
12421301
12431302 if (request.method === 'GET') {
1244- return Response.json({ status: 'ok', transports: ['http', 'websocket'] })
1303+ return Response.json({
1304+ status: 'ok',
1305+ plan: config.paid ? 'paid' : 'free',
1306+ transports: config.allowWebSocket ? ['http', 'websocket'] : ['http'],
1307+ })
12451308 }
12461309 if (request.method !== 'POST') return new Response('method not allowed', { status: 405 })
12471310 if (request.headers.get('x-relay-token') !== env.RELAY_TOKEN) {
@@ -1250,12 +1313,18 @@ export default {
12501313
12511314 try {
12521315 const payload = await request.json()
1253- const result = await handleRelayPayload(env, payload)
1316+ const result = await handleRelayPayload(env, payload, config )
12541317 if (result.error) return Response.json({ error: result.error }, { status: result.status })
12551318
12561319 if (result.stateWrite) ctx.waitUntil(result.stateWrite)
12571320
12581321 const upstream = result.upstream
1322+ await logUpstreamError(env, ctx, upstream, {
1323+ transport: 'http',
1324+ mode: payload.mode,
1325+ affinity: payload.affinity,
1326+ bodyBytes: result.bodyBytes,
1327+ })
12591328 return new Response(upstream.body, {
12601329 status: upstream.status,
12611330 statusText: upstream.statusText,
0 commit comments