Problem
The cloud transcription adapter (crates/transcribe/src/openai_compat.rs::OpenAICompatibleTranscriber::transcribe) checks the CancellationToken only at two boundaries:
- Before/during ffmpeg remux (via
AudioPipeline::remux_for_upload)
- After the HTTP call returns (the use-case can drop the result)
The actual upload + HTTP request itself is not cancellable — once the request is in-flight, calling cancel.cancel() has no effect until the response arrives. For a 50 MB audio file on a slow connection, that can mean tens of seconds of unresponsive UI after the user clicks Cancel.
Why it's not in v1
async-openai's client wraps reqwest::Client but doesn't expose a per-request abort handle. The plumbing options are:
- Wrap each call in
tokio::select! against cancel.cancelled() — drops the future, the underlying connection gets reset (or pooled), works but is awkward to test
- Wrap with
tokio::time::timeout — bounds the worst case but doesn't honor user-driven cancel
- Fork
async-openai to expose a cancel hook — high-friction
- Switch to a hand-rolled
reqwest multipart upload — loses the typed builder ergonomics
For v1 we accepted the "click Cancel; UI updates after current request finishes" UX. The user-facing impact is bounded because:
- Average Groq turnaround is ~3s for 1-hour video (audio-only post-remux)
- The use-case (T5) will not start a new transcribe after cancel, so subsequent items in the queue honor cancel immediately
- Cancel during remux (the slowest local step for big files) IS honored
Suggested approach
Wrap the client.audio().transcription().create_verbose_json(request) await in tokio::select! against the cancel token. On cancel-fire, drop the future and return TranscriptionError::Cancelled. Add a wiremock test with a ResponseTemplate::new(200).set_delay(Duration::from_secs(10)) and assert cancel after 100ms aborts within < 200ms.
Definition of done
- HTTP call honors cancel within ~50ms of
cancel.cancel() being called
- New wiremock test verifies the cancel-during-HTTP path
- No regression to the existing happy-path / error-mapping coverage
Problem
The cloud transcription adapter (
crates/transcribe/src/openai_compat.rs::OpenAICompatibleTranscriber::transcribe) checks theCancellationTokenonly at two boundaries:AudioPipeline::remux_for_upload)The actual upload + HTTP request itself is not cancellable — once the request is in-flight, calling
cancel.cancel()has no effect until the response arrives. For a 50 MB audio file on a slow connection, that can mean tens of seconds of unresponsive UI after the user clicks Cancel.Why it's not in v1
async-openai's client wrapsreqwest::Clientbut doesn't expose a per-request abort handle. The plumbing options are:tokio::select!againstcancel.cancelled()— drops the future, the underlying connection gets reset (or pooled), works but is awkward to testtokio::time::timeout— bounds the worst case but doesn't honor user-driven cancelasync-openaito expose a cancel hook — high-frictionreqwestmultipart upload — loses the typed builder ergonomicsFor v1 we accepted the "click Cancel; UI updates after current request finishes" UX. The user-facing impact is bounded because:
Suggested approach
Wrap the
client.audio().transcription().create_verbose_json(request)await intokio::select!against the cancel token. On cancel-fire, drop the future and returnTranscriptionError::Cancelled. Add a wiremock test with aResponseTemplate::new(200).set_delay(Duration::from_secs(10))and assert cancel after 100ms aborts within < 200ms.Definition of done
cancel.cancel()being called