feat(private-api): add ai-transcribe routes with multipart passthrough - #62
Conversation
Proxies the dictation endpoints added in ePoints #45: POST /private-api/ai-transcribe-price -> ai-transcribe-price?us=<caller> POST /private-api/ai-transcribe -> ai-transcribe (multipart) This is the first private-api route that carries a file, so the JSON path could not be reused. BaseApiRequest always sets a StringContent body with an application/json content type, while multipart needs the boundary that MultipartFormDataContent generates for itself. Added BaseMultipartRequest and ApiMultipartRequest alongside the existing pair, sharing the response handling via a factored-out ReadUpstreamResponse rather than duplicating it. The auth code arrives as a form field rather than a JSON property, so the handler lifts it into the body shape ValidateCode already takes instead of growing a second auth path. `us` is taken from the validated code and never from the request, matching AiAssist: upstream burns Points from whoever `us` names, so accepting it from the client would let a caller spend someone else's balance. Timeout is 120s, matching ai-assist and ai-image-generate -- transcription is a vendor round trip on top of the upload. Kestrel already allows 50MB bodies, comfortably above the 12MB cap upstream enforces. 8 tests over the multipart builder, covering the `us` provenance, omitting an empty idempotency_key rather than sending a blank one that fails upstream's validator, and the generated boundary.
Greptile SummaryAdds authenticated AI transcription pricing and multipart audio proxy routes.
Confidence Score: 5/5The PR appears safe to merge. The previously reported malformed media-type failure is fixed by non-throwing parsing that drops invalid labels, and no blocking failure remains.
|
| Filename | Overview |
|---|---|
| dotnet/EcencyApi/Handlers/PrivateApi.Misc.cs | Adds authenticated transcription handlers and safely drops malformed audio media types instead of throwing. |
| dotnet/EcencyApi/Infrastructure/ApiClient.cs | Factors required upstream authentication headers and adds a multipart request entry point. |
| dotnet/EcencyApi/Infrastructure/Upstream.cs | Adds multipart passthrough with boundary preservation and shared upstream response decoding. |
| dotnet/EcencyApi/Handlers/Routes.cs | Registers the two new private transcription routes. |
| dotnet/EcencyApi.Tests/AiTranscribeContentTests.cs | Covers authenticated username provenance, multipart fields, generated boundaries, filename fallback, and malformed media types. |
Sequence Diagram
sequenceDiagram
participant Client
participant Vision as Vision API
participant Auth as Code validation
participant EPoints as ePoints API
Client->>Vision: POST /private-api/ai-transcribe (multipart)
Vision->>Vision: Parse form and audio
Vision->>Auth: Validate code field
Auth-->>Vision: Authenticated username
Vision->>Vision: Build multipart with username as us
Vision->>EPoints: POST ai-transcribe
EPoints-->>Vision: Status and JSON/text response
Vision-->>Client: Forward response
Reviews (2): Last reviewed commit: "fix(transcribe): tolerate a malformed au..." | Re-trigger Greptile
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ea6d697875
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| app.MapPost("/private-api/ai-transcribe-price", PrivateApi.AiTranscribePrice); | ||
| app.MapPost("/private-api/ai-transcribe", PrivateApi.AiTranscribe); |
There was a problem hiding this comment.
Register the new routes as parity divergences
Adding these routes causes the legacy-Node parity run to report six deterministic failures: dotnet/parity/driver.py's load_routes and build_catalog automatically generate ::min, ::pop, and ::badcode JSON probes for each new POST route, while the reference image has neither route and returns 404 instead of the candidate's 400/401. Because none of those case IDs are in KNOWN_DIVERGENCES, every parity comparison now fails before it can identify unintended regressions; add exclusions for the intentional additive routes or otherwise teach the harness how to classify them.
Useful? React with 👍 / 👎.
| fileContent.Headers.ContentType = | ||
| System.Net.Http.Headers.MediaTypeHeaderValue.Parse(contentType); |
There was a problem hiding this comment.
Handle malformed audio Content-Type without returning 500
When an authenticated multipart upload contains a syntactically malformed per-file Content-Type, IFormFile.ContentType can carry that raw value here and MediaTypeHeaderValue.Parse throws FormatException. This occurs after the form-reading catch and before Upstream.Pipe, so the global middleware turns a client-controlled part header into a 500 and the transcription is never forwarded; use TryParse and omit an invalid media type, as is already done when the header is missing.
Useful? React with 👍 / 👎.
|
Warning Review limit reached
Next review available in: 49 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThe PR adds authenticated AI transcription pricing and transcription routes. It builds multipart requests with validated fields and audio metadata. It adds authenticated multipart upstream transport, timeout handling, shared response parsing, and content construction tests. ChangesAI transcription API
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant Routes
participant PrivateApi
participant ApiClient
participant Upstream
Caller->>Routes: POST ai-transcribe
Routes->>PrivateApi: Invoke AiTranscribe
PrivateApi->>PrivateApi: Validate form code and audio upload
PrivateApi->>PrivateApi: BuildTranscribeContent
PrivateApi->>ApiClient: ApiMultipartRequest with 120-second timeout
ApiClient->>Upstream: BaseMultipartRequest
Upstream-->>ApiClient: Parsed upstream response
ApiClient-->>PrivateApi: Transcription response
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
dotnet/EcencyApi.Tests/AiTranscribeContentTests.cs (1)
105-113: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a test for a malformed audio Content-Type.
TolerantOfAMissingAudioContentTypeonly covers anullcontent type. Add a case with a malformed, non-empty content type (for example"not a media type") to cover theMediaTypeHeaderValue.Parseissue flagged inPrivateApi.Misc.csBuildTranscribeContent.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@dotnet/EcencyApi.Tests/AiTranscribeContentTests.cs` around lines 105 - 113, Add a test case in AiTranscribeContentTests alongside TolerantOfAMissingAudioContentType that builds audio content with a malformed non-empty Content-Type such as "not a media type", renders it, and asserts the fake audio bytes remain present. This should exercise BuildTranscribeContent and confirm malformed media types are handled like missing content types.dotnet/EcencyApi/Infrastructure/ApiClient.cs (1)
94-121: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract duplicated auth-header construction.
ApiMultipartRequestrepeats theMakeApiAuthcall, null check, error log, and header-list build already present inApiRequest(Lines 72-91). Extract a shared helper so a future change to auth-header construction does not need to be applied in two places.♻️ Proposed refactor
+ private static List<KeyValuePair<string, string>> RequireApiAuthHeaders() + { + var apiAuth = MakeApiAuth(); + if (apiAuth == null) + { + Console.Error.WriteLine("Api auth couldn't be create!"); + throw new ApiAuthException(); + } + + return new List<KeyValuePair<string, string>>(apiAuth); + } + public static Task<UpstreamResponse> ApiRequest( string endpoint, HttpMethod method, IEnumerable<KeyValuePair<string, string>>? extraHeaders = null, JsonNode? payload = null, IEnumerable<KeyValuePair<string, string?>>? query = null, int timeoutMs = Upstream.DefaultTimeoutMs) { - var apiAuth = MakeApiAuth(); - if (apiAuth == null) - { - Console.Error.WriteLine("Api auth couldn't be create!"); - throw new ApiAuthException(); - } - var url = $"{Config.PrivateApiAddr}/{endpoint}"; - - var headers = new List<KeyValuePair<string, string>>(); - foreach (var kv in apiAuth) - { - headers.Add(kv); - } + var headers = RequireApiAuthHeaders(); if (extraHeaders != null) { headers.AddRange(extraHeaders); } return Upstream.BaseApiRequest(url, method, headers, payload, query, timeoutMs); } public static Task<UpstreamResponse> ApiMultipartRequest( string endpoint, MultipartFormDataContent content, int timeoutMs = Upstream.DefaultTimeoutMs) { - var apiAuth = MakeApiAuth(); - if (apiAuth == null) - { - Console.Error.WriteLine("Api auth couldn't be create!"); - throw new ApiAuthException(); - } - var url = $"{Config.PrivateApiAddr}/{endpoint}"; - - var headers = new List<KeyValuePair<string, string>>(); - foreach (var kv in apiAuth) - { - headers.Add(kv); - } + var headers = RequireApiAuthHeaders(); return Upstream.BaseMultipartRequest(url, content, headers, timeoutMs); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@dotnet/EcencyApi/Infrastructure/ApiClient.cs` around lines 94 - 121, The duplicated authentication setup in ApiMultipartRequest should be extracted into a shared helper used by both ApiRequest and ApiMultipartRequest. Move MakeApiAuth invocation, null handling, error logging, and header-list construction into the helper, then have both request methods reuse it while preserving the existing ApiAuthException behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@dotnet/EcencyApi/Handlers/PrivateApi.Misc.cs`:
- Around line 716-722: Update the audio content setup in the multipart handler
to use MediaTypeHeaderValue.TryParse for the client-supplied contentType before
assigning fileContent.Headers.ContentType. Set the header only when parsing
succeeds, while preserving the existing behavior for empty values and file
attachment naming.
---
Nitpick comments:
In `@dotnet/EcencyApi.Tests/AiTranscribeContentTests.cs`:
- Around line 105-113: Add a test case in AiTranscribeContentTests alongside
TolerantOfAMissingAudioContentType that builds audio content with a malformed
non-empty Content-Type such as "not a media type", renders it, and asserts the
fake audio bytes remain present. This should exercise BuildTranscribeContent and
confirm malformed media types are handled like missing content types.
In `@dotnet/EcencyApi/Infrastructure/ApiClient.cs`:
- Around line 94-121: The duplicated authentication setup in ApiMultipartRequest
should be extracted into a shared helper used by both ApiRequest and
ApiMultipartRequest. Move MakeApiAuth invocation, null handling, error logging,
and header-list construction into the helper, then have both request methods
reuse it while preserving the existing ApiAuthException behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 7972cdd8-ef4e-4b24-b951-668682296209
📒 Files selected for processing (5)
dotnet/EcencyApi.Tests/AiTranscribeContentTests.csdotnet/EcencyApi/Handlers/PrivateApi.Misc.csdotnet/EcencyApi/Handlers/Routes.csdotnet/EcencyApi/Infrastructure/ApiClient.csdotnet/EcencyApi/Infrastructure/Upstream.cs
…y cases Three review findings. A malformed audio Content-Type returned 500. The value is client-controlled and MediaTypeHeaderValue.Parse throws FormatException, which escapes the handler's form-reading catch and becomes a 500 from the global exception middleware. TryParse instead, and drop an unparseable label exactly like an absent one -- the upload is fine and upstream identifies the audio from its contents. Six malformed values are now covered by tests; all six were verified to genuinely throw under Parse, so the cases exercise the path they claim to. The two new POST routes broke the parity harness. The catalog generates ::min, ::pop and ::badcode for every POST route, so these added six cases where the reference build answers 404 (no such route) and this one answers 400/401. Left unlisted they would have been permanent deterministic failures, which is worse than useless because it hides real regressions. Added entries in the style of the existing post-tips GET twin, and verified every generated id is covered rather than assuming the key format was right. Extracted the duplicated PRIVATE_API_AUTH header construction shared by ApiRequest and ApiMultipartRequest into RequiredAuthHeaders.
|
All three findings confirmed and fixed. Greptile, Codex and CodeRabbit all ran; the malformed Content-Type was flagged by all three, the parity gap by Codex, the duplication by CodeRabbit. Malformed audio Content-Type returned 500Correct, and the 500 is the whole problem — it is a client-controlled value producing a server error. Now Six malformed values are covered by tests. I checked each actually throws under The last row is a test too — the permissive path must not have quietly become a no-op for valid types. New routes broke the parity harnessAlso correct, and worth more than the six cases: permanent deterministic failures are worse than useless because they train you to ignore the harness and hide real regressions. Added entries in the style of the existing Rather than assume my key format was right, I ran Duplicated auth-header constructionExtracted into 95 tests passing, up from 88. |
Proxies the dictation endpoints added in ePoints #45 (merged and deployed).
Second of four PRs: ePoints (done) → vapi → vision-next/SDK + web → mobile.
Why this needed new infrastructure
This is the first private-api route that carries a file.
BaseApiRequestalways sets aStringContentbody with anapplication/jsoncontent type, and multipart needs the boundary thatMultipartFormDataContentgenerates for itself — so the JSON path could not be reused with a different content type bolted on.Added
Upstream.BaseMultipartRequestandApiClient.ApiMultipartRequestalongside the existing pair. Response handling was factored out intoReadUpstreamResponseand is shared, so the two paths cannot drift on status/JSON/raw-text semantics.BaseMultipartRequestdeliberately ignores any caller-suppliedContent-Typeheader. Overriding it drops the generated boundary and makes the body unparseable upstream, which surfaces only as a confusing 400.Auth
The code arrives as a form field rather than a JSON property. Rather than grow a second auth path, the handler lifts it into the body shape
ValidateCodealready takes.uscomes from the validated code, never from the request. Upstream burns Points from whoeverusnames, so accepting it from the client would let a caller spend someone else's balance. This matchesAiAssist, and there is a test pinning it.Details
ai-assistandai-image-generate— transcription is a vendor round trip on top of the upload.Program.cs:27), comfortably above the 12MB cap ePoints enforces, so an oversized upload gets ePoints' structuredaudio_too_largerather than a Kestrel-level failure.idempotency_keyis omitted rather than forwarded blank: upstream validates it against[A-Za-z0-9_-]{8,64}, so a blank one is a 400 while an absent one is simply an un-deduplicated request.Tests
8 new, 88 passing overall. They cover the
usprovenance, the empty-key omission, filename fallback, a missing audio content type (expo-audio and MediaRecorder do not always label the part), and that the generated boundary survives.Note on merging
Merge to
mainis a production deploy here. ePoints is already live with these endpoints, so the routes will work immediately on merge — but nothing calls them until the SDK ships in the next PR.Summary by CodeRabbit
New Features
Bug Fixes