Skip to content

feat(relay): plan-based gating, KV error logging, and request logging#33

Open
iceteaSA wants to merge 1 commit into
cortexkit:mainfrom
iceteaSA:feat/relay-worker-improvements
Open

feat(relay): plan-based gating, KV error logging, and request logging#33
iceteaSA wants to merge 1 commit into
cortexkit:mainfrom
iceteaSA:feat/relay-worker-improvements

Conversation

@iceteaSA
Copy link
Copy Markdown
Contributor

@iceteaSA iceteaSA commented May 21, 2026

Worker-script improvements — all within the WORKER_SCRIPT constant; no client-side relay code modified.

  • Plan-based gating: WebSocket transport requires the paid plan (RELAY_PLAN=paid); free-plan upgrade attempts get a 403 before any handshake. The HTTP relay path is unaffected by plan tier.
  • KV error logging: non-429/403 upstream errors are logged to RELAY_STATE KV with a 7-day TTL (response preview + headers). The HTTP path defers the write via ctx.waitUntil (matching the WebSocket path) so reading the error body never adds latency to forwarding, and the KV key carries a crypto.randomUUID() suffix so two errors for the same id/affinity in the same millisecond can't overwrite each other.
  • Request logging: HTTP and WebSocket requests are logged on the paid plan only.
  • GET health endpoint: returns status, plan (paid/free), and allowed transports (HTTP on free; HTTP + WebSocket on paid).
  • Tests: Miniflare coverage for the free-plan WebSocket 403 gate and the health-endpoint shape on both free and paid plans.

Summary by cubic

Adds plan-based gating to the relay worker and improves observability with KV-backed upstream error logging, paid-only request logs, and a health endpoint. All runtime changes are scoped to WORKER_SCRIPT; no client relay code touched.

  • New Features
    • WebSocket transport requires paid plan (RELAY_PLAN=paid); returns 403 on free before upgrade.
    • Non-429/403 upstream errors (HTTP/WebSocket) are logged to RELAY_STATE KV with a 7-day TTL (includes response preview and headers); logging runs async via ctx.waitUntil.
    • Request logging is enabled only on the paid plan.
    • GET health endpoint returns status, plan (paid/free), and allowed transports (HTTP on free; HTTP + WebSocket on paid).

Written for commit e3cc138. Summary will update on new commits.

Review in cubic

Greptile Summary

This PR adds plan-based gating (WebSocket requires RELAY_PLAN=paid), async KV error logging with collision-resistant keys, paid-only request logging, and a richer GET health endpoint to the relay worker script. Tests cover the new free-plan 403 gate and health-endpoint shape on both plans.

  • Plan gating: getPlanConfig drives allowWebSocket and logRequests; free-plan WebSocket upgrades get a 403 before the handshake and before token validation (intentional fast rejection).
  • KV error logging: logUpstreamError reads the upstream error body asynchronously, writes to RELAY_STATE KV with a 7-day TTL and a crypto.randomUUID() suffix to prevent key collisions; the WebSocket path correctly guards upstream.clone() behind a status >= 400 check, but the HTTP path still calls upstream.clone() unconditionally (flagged in a prior review), leaving one tee branch unread on every 2xx SSE response.
  • Tests: startWorker now accepts a plan parameter; two new tests cover the free-plan gate and both plan shapes on the health endpoint.

Confidence Score: 4/5

The change is safe to merge for all error-response paths, but the unconditional upstream.clone() on the HTTP path (first flagged in a prior review, still unresolved) creates an unread tee branch on every successful Anthropic SSE response and risks stalling token delivery to the client once the internal buffer fills.

All new code paths (plan gating, KV error writes, health endpoint, tests) are implemented correctly. The WebSocket error-logging path guards clone() behind a status check. The HTTP path still clones the response unconditionally before checking the status, so every 2xx SSE stream has a tee whose second branch is never consumed — backpressure from that unread branch can pause upstream reads and stall streaming responses mid-flight.

packages/core/src/relay.ts — specifically the unconditional upstream.clone() at the HTTP relay path (lines 1337–1345); the WebSocket path at lines 1218–1228 handles this correctly and is the reference pattern.

Important Files Changed

Filename Overview
packages/core/src/relay.ts Adds plan-based gating, KV error logging via logUpstreamError, and paid-only request logging to the worker script. The WebSocket path correctly guards clone() behind a status check; the HTTP path still calls upstream.clone() unconditionally on every request, leaving the tee branch unread on 2xx SSE streams (flagged in a previous review, unresolved).
packages/opencode/src/tests/relay-worker-miniflare.test.ts Parameterises startWorker with a plan argument (default 'paid') and adds two new tests: free-plan WebSocket 403 gate and health-endpoint shape on both plans. Test structure and URL construction are correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Incoming Request] --> B{Upgrade: websocket?}
    B -- Yes --> C{config.allowWebSocket?}
    C -- No / free plan --> D[403 WebSocket requires paid plan]
    C -- Yes / paid plan --> E[WebSocket Handshake + handleWebSocket]
    E --> F[prepareWebSocketUpstream]
    F --> G[fetch upstream]
    G --> H{status >= 400 and not 429/403?}
    H -- Yes --> I[upstream.clone → logUpstreamError via ctx.waitUntil]
    H -- No --> J[stream response chunks to client]
    I --> J
    B -- No --> K{GET?}
    K -- Yes --> L[Return status / plan / transports]
    K -- No --> M{POST + valid token?}
    M -- No --> N[401 / 405]
    M -- Yes --> O[handleRelayPayload → fetch upstream]
    O --> P[upstream.clone unconditionally]
    P --> Q[logUpstreamError via ctx.waitUntil]
    Q --> R[Return new Response upstream.body]
Loading

Comments Outside Diff (1)

  1. packages/core/src/relay.ts, line 1332-1348 (link)

    P1 Unconditional clone tees every streaming response body

    upstream.clone() is called before checking whether the status is an error, so every successful Anthropic SSE response is teed. On the 2xx path logUpstreamError returns immediately without reading errorClone.body, leaving one half of the tee unread. In Cloudflare Workers, an unread ReadableStream branch causes backpressure: once the internal tee buffer fills up, the upstream pull is paused and tokens stop flowing to the client mid-stream. The WebSocket path (line 1216) correctly guards the clone behind if (upstream.status >= 400 && !SKIP_ERROR_LOG_STATUSES.has(upstream.status)) — the HTTP path should do the same.

Reviews (4): Last reviewed commit: "feat(relay): add plan-based gating, KV e..." | Re-trigger Greptile

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread packages/core/src/relay.ts
@iceteaSA iceteaSA force-pushed the feat/relay-worker-improvements branch from 5e33a83 to 693b3db Compare May 22, 2026 17:08
@iceteaSA iceteaSA force-pushed the feat/relay-worker-improvements branch 5 times, most recently from f381ba8 to 373871d Compare June 3, 2026 18:17
Comment thread packages/core/src/relay.ts Outdated
Comment thread packages/opencode/src/tests/relay-worker-miniflare.test.ts Outdated
Comment thread packages/core/src/relay.ts Outdated
@iceteaSA iceteaSA force-pushed the feat/relay-worker-improvements branch 2 times, most recently from 1a57f74 to d23b2a8 Compare June 3, 2026 18:54
…ging

Worker script improvements:
- Plan-based gating: WebSocket transport requires paid plan (RELAY_PLAN=paid)
- KV error logging: non-429/403 upstream errors logged to KV with 7-day TTL
- Request logging: HTTP and WebSocket requests logged on paid plan
- GET health endpoint returns plan info and available transports
@iceteaSA iceteaSA force-pushed the feat/relay-worker-improvements branch from d23b2a8 to e3cc138 Compare June 3, 2026 19:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant