Skip to content

[Feature] Graceful rate-limit fallback UX with upstream subscription routing #741

@Astro-Han

Description

@Astro-Han

What task are you trying to do?

When users hit the OpenCode Zen free quota limit while using PawWork for daily work, they need to understand what happened, when it resets, and what they can do right now if they want to keep going.

Which area would this change affect?

App flow or product behavior

What do you do today?

Today, when Zen returns a rate-limit error (FreeUsageLimitError or similar), the raw error is forwarded to the UI. It often surfaces as misleading copy like "engine is currently overloaded" (#740 documents one instance). Users perceive this as the product being broken — they don't know it's a rate limit, when it resets, or whether there's a paid alternative.

The fact that PawWork users hit this limit is structural, not a bug:

So hitting the limit is a known design constraint, but the current failure path does not communicate that at all.

What would a good result look like?

When users hit the rate limit, they should see a clear "known-rule" state — not an error dialog — and have an obvious path to "I want more right now → subscribe to OpenCode Go".

Ideal experience:

  1. Rate-limit errors are classified and surfaced as a dedicated state, not forwarded as "model overloaded".
  2. The UI explains three things: what happened (free quota used up for today), when it resets (UTC reset time shown in the user's local timezone), and what to do if you want to keep going right now.
  3. The primary "keep going" path is a link to subscribe to OpenCode Go (PawWork is compatible with the full OpenCode account system). The secondary path is BYO-key / wait for reset.
  4. The rate-limit state is part of the product experience, not a technical error page.

What would count as done?

  • When Zen returns a FreeUsageLimitError / 429, the UI shows a dedicated "free quota used up" state instead of the raw error text.
  • The state card includes: a title that frames this as a free-tier limit (not a failure), the daily reset time displayed in the user's local timezone, a subscription link to the official OpenCode Go pricing page, and a link to the BYO-key setting.
  • Copy makes clear this is by design for the free tier, with no "something went wrong" framing.
  • Clicks on the subscription link are instrumented, so we can measure how many users click through from the rate-limit state to subscribe. This metric is the key deliverable — see "Extra context" below.
  • E2E coverage: mock Zen returning a rate-limit error and assert the user sees the dedicated state instead of an error dialog.

What should stay out of scope?

  • Don't build a PawWork-side subscription/billing system. The subscription path is an outbound link to OpenCode Go.
  • Don't switch the default provider to a self-funded OEM key (Groq / Zhipu / etc). That path was evaluated in [Task] Audit OpenCode Go free quota isolation per install #737 and found structurally worse: a shared org-level pool means one heavy user can drain quota for everyone, whereas Zen's per-IP fallback gives each user an independent bucket.
  • Don't spoof the User-Agent to bypass the limit (PR fix(opencode): align Zen User-Agent with upstream OpenCode client #738 closed).
  • Don't try to show "remaining quota" or a live countdown in v1. The upstream API does not expose that data; guessing it would mislead users.

Which audience does this matter to most?

Both

Extra context

Strategic intent — why instrumentation is not optional:

This feature is not only a UX fix. It is the foundation for a future conversation with the upstream maintainers (anomalyco).

PawWork currently routes anonymous traffic through Zen. Asking for a partner allowlist or whitelist directly has no negotiation leverage on its own. But if PawWork can demonstrate that it consistently converts users who hit the rate limit into OpenCode Go subscribers, the conversation shifts from "please give us free access" to "we drive paying customers to your service — please include PawWork in the regular compatible-client free tier".

That is a commercial-value framing, which is far more credible than a charity request. The click-through metric on the subscription link is the evidence base for that future conversation, so it must be in scope from v1.

Related issues and prior decisions:

Upstream rate-limit mechanics (for context):

The Zen fallback bucket is per-IP, UTC daily, decrementing per request. Source: packages/console/app/src/routes/zen/util/ipRateLimiter.ts in the upstream sst/opencode repository. This means each PawWork user gets their own independent bucket — they are not sharing a pool — but the bucket is narrow and agent fan-out (one user message can trigger 5–15 LLM calls) makes it easy to hit in a single conversation, especially from shared NAT exits.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High priorityenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions