AI: OpenAI proxy via free Cloudflare Worker (Firebase-token auth) — replaces Blaze-only callable - #4
Open
notAidven wants to merge 2 commits into
Open
Conversation
…provider Add an OpenAI option that runs through a Firebase callable so the browser never holds the API key. Default behavior is unchanged (gemini, then rule-based); the proxy is strictly opt-in. functions/ (new 2nd-gen Cloud Functions, TypeScript): - `aiChat` callable: REQUIRES auth (rejects when request.auth is missing), reads the key from the v2 secret OPENAI_API_KEY (bound via `secrets`), calls OpenAI Chat Completions via the official `openai` package, supports a JSON-output mode (response_format json_object), and validates input. Errors return a clean message and never leak the key/request. - Adds the "functions" codebase to firebase.json (build predeploy). web/ client wiring: - New `openai-proxy` provider (providers/openai-proxy.ts) conforming to the existing LLMProvider interface; calls aiChat via httpsCallable. generateText (and thus aiClient.generateJSON) route through it; streamText falls back to a single non-streamed call. All failures soft-fail to null. - New firebaseFunctions.ts lazy Functions accessor (keeps firebase config untouched). Registered in providers/index.ts and selectable via VITE_LLM_PROVIDER=openai-proxy (opt-in; gemini stays the default). - Room 2 LLM opponents + AI coach/table-talk are provider-agnostic, so they use the proxy when selected while preserving the existing fallback chain (rule-based on AI off / proxy error). Room 1's rule-based coach untouched. SECURITY: the OPENAI_API_KEY secret is set and the function deployed SEPARATELY by the maintainer; no key is hardcoded, logged, or committed. Co-authored-by: Cursor <cursoragent@cursor.com>
… callable)
Cloud Functions require the Blaze plan; this project is on the free Spark plan.
Replace the aiChat Firebase callable with a free-tier Cloudflare Worker that keeps
the OpenAI key server-side, so AI works without enabling billing.
- worker/: TypeScript Worker (POST /chat) validates input, calls OpenAI Chat
Completions (default gpt-4o-mini, optional json_object), returns
{ text, model, finishReason }. OPENAI_API_KEY is a Worker secret (never committed).
- Auth gating: requires Authorization: Bearer <Firebase ID token>; verifies RS256
against Google's securetoken x509 certs with Web Crypto (zero runtime deps),
checks aud/iss/exp, caches certs per Cache-Control. 401 on missing/invalid.
- CORS: allows the hosting domains + http://localhost:5173/5174/5175; OPTIONS preflight.
- Client: providers/openai-proxy.ts now fetches the Worker URL (VITE_AI_PROXY_URL)
with the user's ID token instead of httpsCallable; same soft-fail-to-null fallback;
still opt-in via VITE_LLM_PROVIDER=openai-proxy (default gemini->rule-based unchanged).
- Remove functions/ and the firebase.json functions block; drop now-unused
web/src/lib/firebaseFunctions.ts. Hosting/Firestore/Auth config unchanged.
Deploy (free): wrangler login -> wrangler secret put OPENAI_API_KEY -> wrangler deploy
-> set VITE_AI_PROXY_URL -> rebuild web & redeploy hosting (see worker/README.md).
Co-authored-by: Cursor <cursoragent@cursor.com>
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.
Summary
Re-hosts the secure OpenAI proxy on a free-tier Cloudflare Worker instead of a Firebase Cloud Function, so the "Suited" app can use OpenAI without enabling the Blaze plan (this project is on the free Spark plan; Cloud Functions require Blaze). The OpenAI key stays server-side and the client keeps its soft-fail → rule-based fallback.
This replaces the previous approach in this PR (the
aiChatFirebase callable). It stacks on #3 (feature/casino-2rooms-preflop) because the AI layer (web/src/lib/ai/*) and Room 2 LLM opponents live on the casino stack, notmain.Default behavior is unchanged: provider selection still defaults to gemini (Firebase AI Logic) → rule-based. The proxy is strictly opt-in.
Worker design (
worker/)A small TypeScript Worker (
name = suited-ai-proxy) with zero runtime dependencies (onlywrangler/@cloudflare/workers-types/typescriptas devDeps).POST /chat— accepts{ model?, messages, temperature?, max_tokens?, json? }, validates it (roles/content, array size ≤ 50, content ≤ 24k chars, temperature 0–2, positive-intmax_tokenscapped at 4096), then calls OpenAI Chat Completions (https://api.openai.com/v1/chat/completions). Default model gpt-4o-mini;json: truesetsresponse_format: { type: 'json_object' }. Returns{ text, model, finishReason }.GET /(or/health) — unauthenticated liveness probe →{ "status": "ok" }(handy to confirm a deploy).OPENAI_API_KEY(env.OPENAI_API_KEY), set viawrangler secret put. It is never in any file, committed, logged, or returned. Errors are sanitized to clean messages.Auth gating — Firebase ID token verify (Web Crypto, no deps)
Every
/chatcall must sendAuthorization: Bearer <Firebase ID token>. The Worker verifies it itself with Web Crypto (chosen over a library to keep deps minimal and avoid needing a KV namespace on the free tier):https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.comand caches them per the response'sCache-Control: max-age(in-isolate).header.payload.aud === "brilliant-alpha-clone-54be9",iss === "https://securetoken.google.com/brilliant-alpha-clone-54be9",expnot expired (+ lightiat/auth_time/subchecks).Missing/invalid/expired token → 401.
CORS
Allows
https://brilliant-alpha-clone-54be9.web.app,https://brilliant-alpha-clone-54be9.firebaseapp.com, andhttp://localhost:5173/5174/5175; handlesOPTIONSpreflight and echoes the allowedOriginwith properAccess-Control-*headers (Vary: Origin).Client wiring (
web/)web/src/lib/ai/providers/openai-proxy.tsrewritten to call the Worker viafetch(POST JSON,Authorization: Bearer <token from auth.currentUser.getIdToken()>) instead ofhttpsCallable. The Worker URL comes fromVITE_AI_PROXY_URL(the client appends/chat; base URL or full.../chatboth work).generateText/generateJSON/streamTextmapping preserved (streaming = one non-streamed call emitted once).nullpreserved: unsetVITE_AI_PROXY_URL, no sign-in, network/timeout, or non-2xx →null, so Room 2's rule-based fallback (and the coach/table-talk) still work and nothing crashes.VITE_LLM_PROVIDER=openai-proxy; default (gemini → rule-based) unchanged and never auto-selected.web/.env.exampledocumentsVITE_AI_PROXY_URL+ the deploy/opt-in flow.Removed (no more Cloud Functions)
functions/directory (theaiChatcallable project).functionsblock fromfirebase.json(hosting / firestore / auth config untouched).web/src/lib/firebaseFunctions.ts(only the old provider imported it; confirmed unused after the rewrite).Verification
From
web/:npm run build— exit 0npx vitest run— 409 passed (5 files)node_modules/.bin/tsc -p tsconfig.app.json --noEmit— cleannode scripts/mvp-logic-check.mjs— all PASSnpx eslinton the changed provider — cleanFrom
worker/:npm install— exit 0 (only devDeps)npx tsc --noEmit— cleannpx wrangler deploy --dry-run --outdir dist— builds OK (~13 KiB, no deploy)Not exercised (needs the maintainer's key + accounts): real OpenAI completions and a live deployed Worker round-trip.
Deploy steps (maintainer — all free)
cd worker && npm installnpx wrangler login(free Cloudflare account)npx wrangler secret put OPENAI_API_KEY(paste thesk-...key)npx wrangler deploy→ note the Worker URL (https://suited-ai-proxy.<subdomain>.workers.dev)web/.env.local:VITE_AI_PROXY_URL=<that URL>andVITE_LLM_PROVIDER=openai-proxycd ../web && npm run build && firebase deploy --only hostingNotes
mainandVITE_LLM_PROVIDER=openai-proxy(+VITE_AI_PROXY_URL) are set. Until then, behavior is the unchanged gemini → rule-based default..env.localis committed (a gitignored dummy was used locally for AI-off builds).Made with Cursor