feat(web): a Keyframe client mints one avatar session on Luke's key

Merged
CH
5d ago
+326-0

Plan item 2. web: Keyframe client (video calls with a Keyframe avatar).

apps/web/server/voice/keyframe.ts is the Keyframe service (Context.Service):

  • createSession(): Effect<AvatarGrant, KeyframeError> POSTs https://api.keyframelabs.com/v1/sessions with Authorization: Bearer <key> and {persona_id}, and decodes {server_url, participant_token, agent_identity} with a Schema (extra keys dropped) into AvatarGrant {serverUrl, participantToken, agentIdentity}.
  • KeyframeError {failure, status?}, with failure from KEYFRAME_FAILURE: NO_CAPACITY (503 or 429), PLAN_REFUSED (403), UPSTREAM (any other status; also a success the schema refuses, with its status, and no answer at all, with no status).
  • Errors carry the status only, never Keyframe's body, as openai.ts does. The key is held as Redacted and unwrapped only into the bearer header. Nothing logs the grant or the key.
  • Keyframe.layer({apiKey: Redacted, personaId, baseUrl?, requestTimeoutMs?}): Layer<Keyframe, never, HttpClient>. It goes through accountCall. One 15 s deadline covers the whole session, body read included, and a timeout is UPSTREAM with no status.

Consumer: nothing calls this yet. Plan item #7 (web: avatar frames and records) reads KEYFRAME_API_KEY and LUKE_KEYFRAME_PERSONA_ID in function.ts, provides this layer, and calls createSession on avatar.start.

A LiveAvatar client (#2445, reverted in #2455) used to sit in the same place. This is a different vendor and one call.

Tests

apps/web/tests/voice-keyframe.test.ts runs hermetically through fakeHttpClientLayer at the HttpClient boundary:

  • the request carries the bearer key, the method, the path and the persona, and the grant decodes;
  • 503 and 429 map to NO_CAPACITY, 403 to PLAN_REFUSED, and 401 and 500 to UPSTREAM, each with the status;
  • a malformed success is UPSTREAM with its status;
  • a transport failure is UPSTREAM with no status;
  • a 200 whose body never ends is UPSTREAM with no status at the deadline (TestClock);
  • every error is checked to carry none of the upstream body, the key or the token.

./scripts/check.sh exits 0. This is server-only, with no Mac or UI surface, so it needs no verify.sh evidence.

šŸ¤– Generated with Claude Code


Open workspace in Conductor


Devin Review


Summary by cubic

Adds the Keyframe client in apps/web/server/voice/keyframe.ts, which POSTs to Keyframe Labs' /v1/sessions with the persona and API key to mint one avatar session, returning the LiveKit server, participant token, and agent identity. Nothing calls it yet.

Refusals are classified by status only: 429/503 become NO_CAPACITY, 403 PLAN_REFUSED, and everything else — including unreadable successes and transport failures — UPSTREAM. Errors never include Keyframe's response body, the API key stays Redacted, and a single 15 s deadline covers the whole call including the body read, timing out as UPSTREAM with no status. Hermetic tests in apps/web/tests/voice-keyframe.test.ts cover the request shape, status mapping, and that no error leaks the body, key, or token.