feat: support transcription replay and API key validation - #345
Merged
Conversation
commit: |
jpr5
force-pushed
the
fix/live-transcribe-support-pr
branch
from
July 30, 2026 01:54
f2ffc7e to
23aca19
Compare
jpr5
added a commit
that referenced
this pull request
Aug 3, 2026
…ilies (wrong modality)
OpenAI shipped two transcription families on 2026-07-28. `gpt-transcribe`
serves /v1/audio/transcriptions plus realtime transcription sessions;
`gpt-live-transcribe` is realtime/WebSocket transcription only. Neither
responds on /v1/chat/completions, so neither is text-generation drift.
Two independent listing canaries were both red on them:
- models.drift.ts (via includeFamilies ∪ excludeFamilies) reported
`Unclassified model family "gpt-transcribe"` / `"gpt-live-transcribe"`.
- ws-realtime.drift.ts (via knownVoiceModelFamilies, whose isVoiceModelId
regex matches /transcribe/) reported
UNKNOWN_REALTIME_MODELS=gpt-transcribe,gpt-live-transcribe.
Add both families to excludeFamilies.openai's transcribe cluster (alongside
gpt-4o-transcribe / whisper-1 / gpt-realtime-*) and to
knownVoiceModelFamilies' transcription group. The two sets are deliberately
disjoint surfaces, so both need the entry. Re-pin the excludeFamilies.openai
DATA_FROZEN membership hash accordingly — the sorted-membership delta is
exactly these two additions and nothing else.
Record the decision on the two drift-proposals/ notes PR #343 carries. The
decision is EXCLUDE, written in prose rather than via drift-sync's automated
`Decision: include` marker: that path calls
addFamilyLiteralInSource(..., "includeFamilies", ...) exclusively, so using it
would have put these in includeFamilies and mismarked them as families aimock
mocks on the chat surface.
Does not touch src/ws-realtime.ts's runtime serve-gate — that is PR #345.
jpr5
force-pushed
the
fix/live-transcribe-support-pr
branch
from
August 3, 2026 16:35
23aca19 to
89d4e5c
Compare
jpr5
added a commit
that referenced
this pull request
Aug 3, 2026
…ilies (wrong modality)
OpenAI shipped two transcription families on 2026-07-28. `gpt-transcribe`
serves /v1/audio/transcriptions plus realtime transcription sessions;
`gpt-live-transcribe` is realtime/WebSocket transcription only. Neither
responds on /v1/chat/completions, so neither is text-generation drift.
Two independent listing canaries were both red on them:
- models.drift.ts (via includeFamilies ∪ excludeFamilies) reported
`Unclassified model family "gpt-transcribe"` / `"gpt-live-transcribe"`.
- ws-realtime.drift.ts (via knownVoiceModelFamilies, whose isVoiceModelId
regex matches /transcribe/) reported
UNKNOWN_REALTIME_MODELS=gpt-transcribe,gpt-live-transcribe.
Add both families to excludeFamilies.openai's transcribe cluster (alongside
gpt-4o-transcribe / whisper-1 / gpt-realtime-*) and to
knownVoiceModelFamilies' transcription group. The two sets are deliberately
disjoint surfaces, so both need the entry. Re-pin the excludeFamilies.openai
DATA_FROZEN membership hash accordingly — the sorted-membership delta is
exactly these two additions and nothing else.
Record the decision on the two drift-proposals/ notes PR #343 carries. The
decision is EXCLUDE, written in prose rather than via drift-sync's automated
`Decision: include` marker: that path calls
addFamilyLiteralInSource(..., "includeFamilies", ...) exclusively, so using it
would have put these in includeFamilies and mismarked them as families aimock
mocks on the chat surface.
Does not touch src/ws-realtime.ts's runtime serve-gate — that is PR #345.
jpr5
force-pushed
the
fix/live-transcribe-support-pr
branch
from
August 4, 2026 18:51
447d08c to
0e74e57
Compare
…ation
A realtime conversation session that merely sets input_audio_transcription
(the canonical {model:"whisper-1"} setup) was treated as a live
transcription session, so input_audio_buffer.commit took the transcription
path: it appended a phantom [audio] turn and consumed a fixture match before
the response-shape check. With two sequenced fixtures the following
response.create then replayed the NEXT turn's content -- the client got a
well-formed but wrong response, silently. Under strict mode the socket was
closed with 1008 instead.
Discriminate live transcription sessions by session.type, which is what
?intent=transcription and transcription_session.update both set, matching the
OpenAI realtime API. A conversation session configuring input transcription
now behaves exactly as it did before.
The live gpt-transcribe&stream=true response ends with the [DONE] sentinel after transcript.text.done; aimock stopped at the typed done event, so a client that loops until the sentinel never saw the stream terminate. multimedia.test.ts pinned the sentinel-less body with an exact compare, so the defect was locked in by a test asserting the wrong contract -- corrected here, along with the SSE frame count and the replay expectation in multimedia-record.test.ts. The recorder's proxy passthrough still relays upstream bytes verbatim, so only replay gains the sentinel.
Four wire-fidelity defects, each contradicted by a live api.openai.com capture and each silent in a different way: - session.update carrying a differing model was rejected with no session.updated. Live GA returns a normal session.updated and ignores the field (probe sent model:"gpt-realtime" on a "gpt-realtime-mini" connection and got the original model back with the rest applied). Clients that echo the session object they received hung awaiting session.updated. - A transcription session was serialized as object:"realtime.session" with an invented session-level model plus conversation-only fields. Live sends object:"realtime.transcription_session", no model, audio.input only, and include:null. The conversation-session shape is untouched. - Transcription usage was fabricated as type:"tokens". Live reports type:"duration" with seconds. The synthesized default now carries the right shape with a zero magnitude; a fixture-supplied usage still wins. - The fixture match count was burned before the response-shape check. Because router.ts exempts realtime* requests from the shape gate, a generic chat fixture matches a realtime-transcription lookup, so a rejected response still advanced a sequenced conversation by a turn. The count is now taken only once the response is usable; a deliberate error fixture still counts. The two tests that pinned the model rejection and the transcription-session model are corrected to assert the live contract.
Brings in #343, #349, #351, #352, #353 and #342 so the branch reflects what 1.38.0 will actually ship. CHANGELOG.md was the only conflict. #342's encrypted-reasoning bullet arrived under [Unreleased], but #342 changed src/responses.ts and src/ws-responses.ts with no version bump, so that code ships inside 1.38.0. The bullet is folded verbatim into [1.38.0] ### Added (ahead of the OpenRouter bullets, preserving main's relative order) and [Unreleased] is left empty, so the published release notes describe every source change in the tarball. No version value changed.
Transcription usage is model-dependent on the live API, verified against
api.openai.com with one 5s clip on both the realtime WS
(conversation.item.input_audio_transcription.completed) and HTTP
(POST /v1/audio/transcriptions) surfaces:
whisper-1 -> {"type":"duration","seconds":5}
gpt-transcribe -> {"type":"duration","seconds":5}
gpt-live-transcribe -> {"type":"duration","seconds":5}
gpt-4o-transcribe -> {"type":"tokens","total_tokens":66,...}
gpt-4o-mini-transcribe -> {"type":"tokens","total_tokens":66,...}
The synthesized default was model-blind duration, which regressed the two
gpt-4o-transcribe families from a correct tokens discriminator to duration.
A consumer branching on usage.type === "tokens" then read undefined token
counts -- a silent failure.
The admission predicate is now the union of two family predicates, so the
split and the admitted set cannot disagree and a newly admitted model has to
declare its family. Tests pin BOTH families; either family alone passes under
a model-blind default.
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.
Final Timing Contract Coverage
Commit
b5ce7dfis test coverage only; it does not change replay behavior. The fake-clock writer test now proves no frame arrives one millisecond before each scheduled 40/20 ms boundary, each expected frame arrives at its boundary in order, and the stream stays open until the final frame. The route test spies on the real writer boundary and asserts the fixture forwards its exactrecordedTimingsobject andreplaySpeed: 2.Focused timing tests passed 20/20 repeated runs. Full pre-push validation passed: format, lint, TypeScript, 4,794 tests (3 skipped), build, and export checks.
Timing Determinism Correction
Commit
4b6cedbfixes a completion-gate flake in the replay-speed integration test without changing replay behavior. The real HTTP test had a 200 ms ceiling even though its actual 8-frame schedule is 180 ms before HTTP, event-loop, and timer-resolution overhead: role + 6 content + finish frames receive[40, 20, 20, 20, 20, 20, 20, 20]ms at 2x speed. The upper bound is replaced by a fake-timer writer assertion of that exact schedule; the HTTP test remains to prove the route wiresreplaySpeedinto the writer.Local Red-Green Proof
Red, clean
c45bc66, actual target test repeated 30 times:Green,
4b6cedb, same actual target test repeated 30 times:The deterministic writer contract and real route both pass. Full suite: 166 files, 4,794 passed, 3 skipped; format, lint, TypeScript check, build, and export checks pass.
Nested GA Output Correction
Commit
c45bc66accepts and round-trips nested GAsession.audio.output.voiceand its complete PCMformatobject without changing Beta or legacy session translation. The recorder integration now explicitly proves fixture persistence and replay of terminallanguagesandusagemetadata after the client closes.Direct Local Red-Green Proof
A standalone real local recorder and WebSocket probe ran against clean
ead88d4before the change, then against this commit.Red (
ead88d4):Green (
c45bc66):Focused real integration coverage: 168 tests passed. Full pre-push suite: 4,793 passed, 3 skipped.
Post-Rebase Corrections
ead88d4closes the rebase-review gaps without changing the release version: typed transcription terminals are now treated as complete on client close; Beta session events retain rootturn_detection; GA retains the documented PCMrate.Direct Local Red-Green Proof
The same standalone probe used a real upstream SSE recorder proxy, client abort, persisted-fixture local replay, and live WebSocket connections.
Red (
89d4e5c):Green (
ead88d4):The red JSON omits Beta keys because JavaScript omits
undefinedobject properties. The green output verifies default null, nested GA update, and legacy flat update independently.Release / Rebase Status
Rebased the transcription implementation and its contract coverage onto current
main(9ce3107) without changing the intended runtime behavior. The PR now releases@copilotkit/aimock1.38.0and aligns its publish surfaces: root package, dated changelog entry, HelmappVersion, pytest default pin plus pytest package0.5.1, and Claude plugin metadata/range.Current branch commits:
3cf3956 feat: support OpenAI transcription replaycfbbe0a test: cover transcription replay contracts89d4e5c chore: release v1.38.0The retained proof below was captured against the original behavior branch; the rebased branch passed the focused 400-test transcription suite, full 4,746-test npm suite, format, lint, explicit TypeScript check, build, exports, frozen-lockfile validation, pytest 17/17, and pytest package build.
Summary
Aligns OpenAI transcription HTTP, Realtime, recorder, and replay behavior with the current nested GA wire contract.
intent=transcriptionsockets ignore regular realtime query models.audio.input.formatandaudio.input.transcription; Beta keeps its flat shim.transcript.text.donewithout a[DONE]sentinel.Baseline vs Candidate Local Proof
The exact probe ran against a clean detached
8d425a4worktree and the isolated candidate worktree. It used real local HTTP, WebSocket, recorder proxy, persisted fixture, and replay paths.Red: clean
8d425a4Green: candidate
6ae6ec4Validation
Fourth-Pass Red-Green
Red: terminal reported four stale GA expectations (legacy audio transcription field and missing ).
Green:
RUN v3.2.4 /private/tmp/aimock-live-transcribe-pr.0mGErr
✓ src/tests/ws-realtime.test.ts (83 tests) 7614ms
Test Files 1 passed (1)
Tests 83 passed (83)
Start at 14:39:20
Duration 8.13s (transform 235ms, setup 0ms, collect 300ms, tests 7.61s, environment 0ms, prepare 44ms) passed 83/83. Direct clean-baseline vs candidate VAD probe changed into nested , progressive live deltas, and journal interruption before completion. Full format, lint, typecheck, 145-file test suite, and build passed.
Fourth-pass proof: red terminal ws-realtime test had four stale GA audio expectation failures. Green exact file: 83 of 83 passed. Clean baseline VAD emitted an error; candidate returned nested audio.input.turn_detection server_vad, progressive deltas, and truncateAfterChunks journal interruption. Full format, lint, typecheck, 145-file tests, and build passed.
Fifth-pass red-green: baseline emitted GA session type conversation, top-level turn_detection, and accepted model mutation. Candidate emits realtime for normal GA sessions, keeps VAD under audio.input, rejects established model mutation, and preserves explicit transcription/translation session types. Red conformance test at ws-api-conformance line 618 is now green (66/66); full format, lint, tsc, tests, and build passed.
Sixth-Pass Red-Green
Red: clean baseline 2871866 accepted a real WebSocket mixed model/type update and persisted
gpt-4o-transcribe transcription must-not-mutate.Green: candidate returned
error invalid_session_config; its next valid update reportedgpt-realtime-mini realtime safe, proving rejection occurs before any session mutation. Live-transcription interruption now uses socket destruction and journals the interruption.Validation: focused ws-realtime suite 83/83 passed. Full format check, lint, typecheck, test suite, and build passed locally.
P2 Coverage Hardening
Commit f2ffc7e adds tests only; it does not change runtime behavior. The clean 71452a7 baseline already demonstrated the documented intent=transcription strict no-match and ErrorResponse paths, versioned model matching, abrupt interrupted socket termination with journal details, and scheduled inter-frame delivery.
The new direct tests cover those established paths, HTTP stream=true JSON errors, and JSON/SSE recorder-to-local-replay preservation of languages and usage. Applied as a test-only diff to detached 71452a7, the focused suite passed 132/132; the candidate focused suite also passed 132/132. Full format, lint, typecheck, test, and build passed on the candidate.
API-key validation and proxy isolation
This PR also adds opt-in, server-wide inbound API-key validation for the pending
1.38.0Node release, plus theaimock-pytest0.5.2helper support. No major-version change is included.Authorization: Bearer,X-API-Key,X-Goog-API-Key,Api-Key, andxi-api-keybefore fixture, proxy, control, and WebSocket handling./health,/ready, and/metricspublic; validates auth-enabled bareOPTIONSrequests and WebSocket upgrades before mount routing.502, without calling upstream.api_keyand--aimock-api-keysupport to the pytest helper, injected through the child environment rather than command-line arguments.Local API-key red-green proof
Observed against the real server before the implementation:
The same raw HTTP/WebSocket probe after the implementation:
Value checks
200to401; the configured key remains200with the recorded fixture.101 Switching Protocolsto literal401; the configured key still upgrades with101.502and the fake upstream receives zero requests.PYTHONPATH=src AIMOCK_CLI_PATH=../../dist/cli.js pytest -q -p aimock_pytest.pluginpassed all 18 tests.Validation
CR repair: request-scoped API-key egress
This follow-up removes the process-global record-config auth marker. The egress policy is now tied to the authenticated incoming request, so servers that reuse a
RecordConfigcannot affect one another. It also acceptsbearerandkeyschemes case-insensitively, removesAuthorizationcase-insensitively before OpenRouter off-origin unsigned-URL fetches, and applies the same no-implicit-provider-key policy to AG-UI, fal, and video egress.The release remains Node
1.38.0and pytest0.5.2; no major or additional root-version bump is included. PyPI publishing is now gated on the pinned npm release existing first, enforcing npm1.38.0before pytest0.5.2.Local red-green proof
Clean
f3a21fbbaseline, exercised against live local server sockets and a local proxy upstream:The identical raw HTTP/WebSocket/proxy probes after the repair:
Coverage added
bearer/keyWebSocket upgrade coverage.RecordConfigisolation regression.Authorizationis absent.502with zero upstream requests when no provider key exists; OpenRouter video path is covered by the foreign-host capture.AIMOCK_API_KEYSoverride integration.--aimock-api-keyand the plugin fixture.Final validation
Pytest CI repair
The release test subprocess now disables automatic plugin discovery before it
explicitly loads
aimock_pytest.plugin. This prevents installed pytest entrypoint discovery from registering the same plugin a second time.
Local installed-package red-green proof (Python 3.12):
The repeated final gate is green: format, lint, TypeScript, full Node suite
(167 files, 4,806 passed, 3 skipped), build, exports, and source-tree pytest
integration (19 passed).
Publish-workflow security repair
actions/setup-nodev7 enabled package-manager caching by default in the PyPIpublish workflow. Caching is unnecessary for this small
npm viewreleaseordering check, so the workflow now explicitly sets
package-manager-cache: false.Shipper repair: release ordering and credential journals
Commit
0e74e57removes the independent PyPI workflow. PyPI publication nowruns as a
Releasejob after the npm publish job succeeds (or after thealready-published npm version is confirmed), then verifies the pinned npm
version before a wheel can be built. Unit CI builds before the CLI suite, so
the CLI auth integration is no longer skipped. The pytest plugin clears an
inherited
AIMOCK_API_KEYSvalue unless--aimock-api-keyexplicitly setsone. Journal header redaction now calls the same accepted-inbound-credential
registry as authentication, including Google and ElevenLabs API-key headers.
Local red-green proof from
29645e1:Final validation repeated:
pnpm format:check,pnpm lint,pnpm exec tsc --noEmit,pnpm test,pnpm build,pnpm test:exports,pytest
20 passed,pnpm install --frozen-lockfile --ignore-scripts,actionlint,zizmor, andgit diff --checkall passed.