Skip to content

feat: run OpenAI Realtime drift probes live in CI via OPENAI_API_KEY fallback - #324

Merged
jpr5 merged 6 commits into
mainfrom
feat/realtime-live-drift-reuse-openai-key
Jul 23, 2026
Merged

feat: run OpenAI Realtime drift probes live in CI via OPENAI_API_KEY fallback#324
jpr5 merged 6 commits into
mainfrom
feat/realtime-live-drift-reuse-openai-key

Conversation

@jpr5

@jpr5 jpr5 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

What

Un-skips the 3 OpenAI Realtime WS protocol/event-shape probes in
src/__tests__/drift/ws-realtime.drift.ts so they run live in daily CI:

  1. WS text event sequence and shapes match (GA)
  2. WS tool call event sequence matches (GA)
  3. GA and Beta event sequences are consistent after normalization

Previously these gated on it.skipIf(!process.env.OPENAI_REALTIME_KEY). No
OPENAI_REALTIME_KEY secret exists in CI, so they always skipped — only
the model-listing canary (which reads OPENAI_API_KEY) actually ran. DRIFT.md
called Realtime "Verified", but that was only ever true with a manually
supplied local key.

The change (reuse a key, don't add one)

OpenAI Realtime authenticates with the standard project OpenAI key — scope
is project-level; there is no separate "realtime key" type. So the correct fix
is to resolve the credential as:

OPENAI_REALTIME_KEY ?? OPENAI_API_KEY
  • Keeps OPENAI_REALTIME_KEY as an optional preferred override (for the
    rare case where realtime access lives on a different project).
  • Falls back to the standard OPENAI_API_KEY, which is already a repo secret
    and already threaded into the drift job env
    in both test-drift.yml and
    fix-drift.yml (used by every other OpenAI leg). No workflow change needed.

Net effect: this removes a secret dependency rather than adding one, and
flips the 3 probes from SKIP → RUN in CI.

Cost-minimal scoping

The probes were already the cheapest possible realtime interaction and this PR
keeps them that way: text modality only (modalities: ["text"]), the cheapest
realtime model (gpt-4o-mini-realtime-preview), one-word prompts ("Say hello",
"Weather in Paris"), no audio streaming, session closed at response.done.
It's a cheap daily protocol canary, not a full realtime exercise.

Red-green (structural, local)

The live probes need a real OpenAI key (unavailable in this dev env), so the
live validation is deferred to CI (see risk note below). The structural
red-green exercises the real failure surface — the drift triangulation +
Surface:-marked report used by the realtime probes
(compareSSESequences + formatDriftReport(..., "openai-realtime")),
network-free:

RED — drop a real event type from the mock leg:

API DRIFT DETECTED: OpenAI Realtime WS (GA text events)
  Surface: openai-realtime

  1. [critical] LLMOCK DRIFT — real API emits event type "response.output_text.done" but mock does not
     Path:    SSE:response.output_text.done
     Real:    response.output_text.done
     Mock:    <absent>
 FAIL  src/__tests__/drift/ ... > WS text event sequence and shapes match (GA)

GREEN — shapes aligned:

 Test Files  1 passed (1)
      Tests  1 passed (1)

Gate-flip verified network-free: with OPENAI_API_KEY set and
OPENAI_REALTIME_KEY unset, old skipIf(!OPENAI_REALTIME_KEY)skips;
new skipIf(!OPENAI_REALTIME_CREDENTIAL)runs; explicit override still
wins when present.

What CI confirms or refutes (the one risk)

This PR's premise is that this project's OPENAI_API_KEY has Realtime model
access. CI is the live validation. If the now-running realtime probes fail
with an auth/scope error, it means this project's key lacks Realtime access
— in which case the fallback is wrong and we genuinely need a separate
OPENAI_REALTIME_KEY (acquisition covered in the runbook). Any other
diff — event names/shapes — is real protocol drift the canary is meant to catch.

Notes

  • Tests-inclusive ES2022 typecheck: 0 new errors in the touched file
    (src/__tests__ is excluded from every tsconfig; ~113 pre-existing errors in
    other test files are untouched).
  • No version bump (drift tests aren't in the published package).
  • Draft: do not merge until CI confirms the realtime probes pass live.

🤖 Generated with Claude Code

@pkg-pr-new

pkg-pr-new Bot commented Jul 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@copilotkit/aimock@324

commit: ab8db68

@jpr5

jpr5 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

CI live result — the probes now RUN, and they surface a real problem

drift-live-pr failed, which is the informative outcome this PR was set up
to produce. The gate-flip works: the 3 realtime WS probes ran live in CI
(pre-change they skipped). The collector reported exactly "3 test failure(s)"
— the 3 un-skipped probes — all failing identically:

Error: waitUntil timeout after 30000ms. Collected 1 messages: [error]
  at Timeout._onTimeout (src/__tests__/drift/ws-providers.ts:204:23)

What this means

The realtime socket connected (the client received one message), but that
message's type was error and no session.created ever arrived, so
openaiRealtimeWS timed out at step 1. The server's first and only reply being
an error event means the session was rejected.

Auth-scope vs stale model id — leaning stale model

The connect-then-error-event pattern argues against a hard auth rejection (a
key with no realtime access typically 403s the WS upgrade, so the socket would
never open). The socket opened and OpenAI returned a realtime error event,
which is what it sends for an invalid/retired model id or bad session config
on an otherwise-authenticated connection. Corroborating: this file's header says
"Updated for GA protocol — uses gpt-realtime-2", but ws-providers.ts still
hardcodes the retired gpt-4o-mini-realtime-preview in both the connect URL
and the session config. So the most likely culprit is a stale model id, not
missing realtime scope — meaning the OPENAI_API_KEY fallback premise is
probably fine and no new secret is needed; the model id needs updating to a
current realtime model. (Definitive disambiguation needs the error event's
code/message, which the collector doesn't print — it quarantines the raw
timeout as unparseable, exit 5.)

Secondary finding

Because the probe throws a raw connection timeout rather than producing a
drift report, the collector quarantines it (exit 5) and drift-live-pr
hard-fails at the "Head drift report" step instead of routing through the delta
gate. These probes never ran before, so this fragility (a realtime handshake
failure ⇒ unparseable/quarantine) is newly exposed by un-skipping them.

Recommendation (out of this PR's scope)

Hold this draft. The un-skip is correct and working; landing it needs a
follow-up that (a) updates the realtime model id in ws-providers.ts to a
current GA model and (b) makes a realtime handshake failure emit a parseable
drift/auth marker instead of an exit-5 quarantine, so CI routes it cleanly.

jpr5 added a commit that referenced this pull request Jul 23, 2026
…seline) (#330)

## What

Gates OFF the fal LIVE queue-lifecycle drift leg so it no longer runs in
CI / the shared base drift collector, while KEEPING all of fal's static
drift coverage.

The live probe (`falQueueLifecycleCanary`, driven by the live block in
`src/__tests__/drift/fal-queue.drift.ts`, merged in #327) fails against
real fal: `flux/schnell` returns `status:"IN_PROGRESS"` immediately (not
the assumed `IN_QUEUE`) → AssertionError → collector exit-5 quarantine →
`drift-live-pr` **base** step (which runs main's code) reds on EVERY PR,
blocking #324 and #325 from merging.

A proper fix for the probe is being built in parallel
(`fix/fal-probe-in-progress`). This PR is the fast, reversible descope
so coverage PRs can land now.

## The change (one hunk)

Added an explicit opt-in env gate to the live `describe` — it now skips
unless BOTH are set:

```ts
describe.skipIf(!FAL_KEY || !process.env.FAL_LIVE_QUEUE)("fal.ai queue lifecycle (live, cost-safe)", ...)
```

`FAL_LIVE_QUEUE` is not set in CI, so the live leg is skipped
everywhere. Static fal tests (`fal.drift.ts` + the mock-vs-exemplar
tests in `fal-queue.drift.ts`) are untouched. Re-enable by removing the
`FAL_LIVE_QUEUE` gate once the probe is fixed.

## Red / green (local)

**RED** (unmodified main, `FAL_KEY` set) — live leg runs and fails:
```
× fal.ai queue lifecycle (live, cost-safe) > real submit + status + cancel ...
  → INFRA_ERROR: fal queue submit: API returned 401 ...
Tests  1 failed | 4 passed (5)
```

**GREEN** (this PR, `FAL_KEY` set, no `FAL_LIVE_QUEUE`) — live leg
skips, no AssertionError:
```
✓ src/__tests__/drift/fal-queue.drift.ts (5 tests | 1 skipped) 22ms
Tests  4 passed | 1 skipped (5)
```

**Re-enable path verified** (`FAL_KEY` + `FAL_LIVE_QUEUE` both set) —
live leg runs again.

Full `pnpm run test:drift` (CI-style, no `FAL_KEY`): **93 passed | 65
skipped**. Typecheck, prettier, eslint, build all clean.

## Expected CI note (fix-forward)

This PR's own `drift-live-pr` will be RED at the **base** step because
base runs main's still-broken fal probe — that red is exactly the poison
this PR removes, and it cannot go green pre-merge (base runs old main).
All other checks are expected green. Merging fix-forward heals main so
subsequent PRs' base steps no longer run the fal live leg.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
jpr5 added 6 commits July 22, 2026 18:59
Gate the 3 realtime WS protocol probes on OPENAI_REALTIME_KEY ?? OPENAI_API_KEY
so they run live in CI via the existing key instead of skipping. No new secret.
OpenAI retired the Realtime Beta API — a live Beta handshake now returns
{"code":"beta_api_shape_disabled","message":"The Realtime Beta API is no
longer supported. Please use /v1/realtime for the GA API."}. The drift leg's
Beta branch therefore hit a dead endpoint, surfaced an unparseable error, and
quarantined the whole leg (exit 5), turning drift-live-pr red at HEAD.

Remove the Beta branch from openaiRealtimeWS (GA-only: session.type:"realtime"
+ output_modalities) and delete the now-dead GA<->Beta consistency probe plus
its normalization constants. GA is the only live surface and the only surface
aimock mocks.
@jpr5
jpr5 force-pushed the feat/realtime-live-drift-reuse-openai-key branch from 04c8890 to ab8db68 Compare July 23, 2026 01:59
@jpr5
jpr5 marked this pull request as ready for review July 23, 2026 02:01
@jpr5
jpr5 merged commit 23e1550 into main Jul 23, 2026
27 checks passed
@jpr5
jpr5 deleted the feat/realtime-live-drift-reuse-openai-key branch July 23, 2026 02:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant