feat(web): hosted voice and attention endpoints behind account sign-in

Merged
DA
2mo ago
+2617-71

What

The server half of the hosted free tier: a signed-in desktop no longer needs its own OPENAI_API_KEY to get voice and attention review. Two new Vercel endpoints run on the deployment's key, and BYOK stays exactly as it is — the desktop wiring lands in the next PR.

  • POST /api/voice/mint — mints one ephemeral Realtime client secret and returns it with callsUrl still pointing at OpenAI, so audio never transits this deployment; only the mint does. The session document is built server-side from the same realtimeClientSecretRequest the desktop mints with; the caller's whole say is a voice and a pace, each validated against the sets the build ships.
  • POST /api/attention/review — reviews one bounded AttentionUpdate and returns the parsed AttentionDecision. Not a proxy: the instructions, decision schema, store: false, and output-token cap come from a construction now shared with the desktop evaluator, and the request body is validated field-by-field (attentionPromptUpdateFromWire) against the same bounds the local roster holds — value sets refuse rather than repair, and an overlong developer ask refuses the whole update.

Auth, quota, and the kill switch

  • Both endpoints resolve the desktop's OAuth bearer token through the auth service's own oauth2UserInfo, in process — expiry, revocation, and scope stay the OAuth provider's answers, never a second implementation.
  • A new hosted_usage table (migration included) meters each user per UTC day: 50 voice calls, 500 attention reviews. The increment is one atomic upsert taken before the upstream call, so racing requests cannot slip past the ceiling; 429 answers carry {used, limit, remaining, resetsAt} so the panel can eventually say what ran out. The ceilings are product knobs; the OpenAI project budget behind the key is the backstop.
  • Without OPENAI_API_KEY in the environment both endpoints answer 503 — the same unset-key kill switch as the feedback endpoint. Upstream failures answer 502 with the status only; no upstream body, no key, and no request material is ever logged or forwarded.

Core refactor

The desktop evaluator's OpenAI Responses construction moved to sidecar-core (attention-openai.ts), and attentionUpdateInput now takes AttentionPromptUpdate — exactly the fields the prompt reads, so a hosted review sends no session identifiers or clocks. The desktop evaluator consumes the shared construction; its behavior is unchanged.

Environment (deployment)

OPENAI_API_KEY (new, enables the tier), optional LUKE_REALTIME_MODEL / LUKE_ATTENTION_MODEL overrides — the same names the desktop honours.

Testing

  • ./scripts/check.sh passes (exit 0): repository checks, typecheck, tests, builds.
  • New tests: core wire-contract and shared-construction tests (8), plus web tests for bearer resolution, quota arithmetic and racing-ceiling behavior, and both handlers' gate order, upstream failure paths, and key-never-leaks assertions (17).
  • Portable-only change: no macOS or UI surface touched, so no verify.sh / visual evidence run.

🤖 Generated with Claude Code


Open workspace in Conductor

Automated visual evidence

Download the deterministic macOS evidence · workflow run

  • Commit: 3361ee54a0f603fd3b0d6f607df25e08732d3ef7
  • Scenario: smoke
  • Physical-notch check: not performed by CI