feat(onboarding): add a demo mode that runs without an API key - #57
Merged
Conversation
The first screen demanded an API key before anything would run, and users who wanted to try the tool before handing over a credential stopped there. Shipping a shared Gemini key was the obvious fix and does not work. The free-tier quota belongs to the project rather than the caller, so everyone holding a copy competes for one bucket while a single turn is up to 40 requests; a key printed in a terminal reaches screenshots and gets revoked; and there is no way to replace it once it is on thousands of disks. So the key stays on a proxy and the client holds a token instead. The token is worthless to Google — it authenticates to Woopcode's proxy, which applies the quota and forwards under the real key — which makes the demo revocable, rationable and switchable off without shipping a release. Mechanically this is one line: @google/genai takes a full-URL override, so a demo session is an ordinary Gemini client pointed somewhere else. The wire format, the streaming, the loop and the message type are untouched, and the proxy's two refusals were chosen to land on rules runtime/retry.ts already has — 403 for a spent allowance is fatal there, 429 for a busy proxy is retryable, so load shedding works and exhaustion does not cost three attempts to reach the same answer. The proxy runs a free-tier key, so demo traffic falls under terms that let Google train on it and have humans read it. The wizard says so and requires an explicit keypress, because the alternative is a user sending a private repository somewhere they were never told about. Moving to a paid-tier key later is a server-side change and needs no client release. Two failures this turned up in code that already existed: every path that stored a user's key spread over the previous entry, and normalizeConfig rebuilds provider entries field by field. Together they meant an upgrade out of demo mode would keep the proxy URL underneath a real Google key, and a demo entry would lose that URL on the next read while keeping the token. Both are covered by tests that were confirmed to fail with the fix reverted.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Woopcode's first screen demanded an API key.
onboarding/setupWizard.tsxwalked welcome → select provider → key URL → paste, and nothing ran until a key was stored. Users who wanted to try the tool before handing over a credential stopped there.The obvious fix — ship a free-tier Gemini key for people to copy — does not work, for three reasons that have nothing to do with cost:
So the key stays server-side and the client holds a token instead.
What
A demo session stores
{ type: "demo", apiKey: <token>, baseUrl, demoExpiresAt }. The token is worthless to Google — it authenticates to Woopcode's proxy, which applies the quota and forwards under the real key. That makes the demo revocable, rationable and switchable off without a client release.Mechanically it is one line.
@google/genaitakes a full-URL override, so a demo session is an ordinary Gemini client pointed elsewhere:The wire format, the streaming,
runtime/loop.tsand theMessagetype are untouched.baseUrlis honoured only by Google — forwarding it would point the Anthropic or OpenAI SDK at a proxy that answers neither.The proxy's two refusals were chosen to land on rules
runtime/retry.tsalready has, so that file needed no change:Retry-Afterconfig/demoAccount.tsowns the lifecycle so nothing else knows the endpoint: a random install id to ration by, session requests with a 10s deadline, expiry helpers, and the exhaustion-message mapping.WOOPCODE_DEMO_URLpoints it at a local proxy for development.The disclosure is mandatory and deliberate. The proxy runs a free-tier key, so demo traffic falls under the training and human-review terms above. The wizard states that plainly and requires an explicit
yrather than any keypress — a screen dismissed by whatever someone pressed next is not a notice anyone read. Moving to a paid-tier key later is a server-side change needing no client release.Two pre-existing defects this turned up
Both were found by reading the diff, not by the suite:
/login,providers login, the wizard). Upgrading out of demo mode would keeptype: "demo", the proxy URL and the old expiry underneath the new key — sending a real Google credential to Woopcode's proxy and expiring it on the demo's schedule. All three now build the entry fresh viaapiProviderEntry().normalizeConfigrebuilds provider entries field by field, so a field it does not name is dropped on the next read. The docstring claimed extra keys were preserved; that is true at the top level only. A demo entry would have kept its token and lost the URL it is only valid against.setProviderhas the same hazard at runtime and now assignsbaseUrlunconditionally, unlikemodel.Verified
Both regression tests were proven by reverting their fix and confirming the revert applied:
normalizeConfigturned 2 tests red, and revealed a cascade worth knowing about — a live demo session resolves asnull, because the expiry is dropped alongside the URL;httpOptionsline turned 3 of the 5 diversion tests red, correctly leaving the two negative ones green.The
baseUrltests run against a realBun.serveon a real port, because the failure being guarded is silent: a request that still goes to Google carries a demo token as if it were a Google key. Two of them initially made live calls to Google, Anthropic and OpenAI; those now stub the global fetch, which took the file from 1.72s to 114ms and removed a network dependency from CI.Not verified, and outside the suite's reach: every wizard screen, including the disclosure keypress, since none of it is rendered in a test; a real 403 or 429 from an actual proxy; and the exhaustion message end to end — the
DEMO_EXHAUSTED_MARKERcontract is tested as a pure function only.This is inert until the proxy exists. The default endpoint does not resolve, so no user can reach demo mode; the wizard reports the service as unavailable and falls through to provider selection. The server is a separate private repo and is not in this PR.