{"record":{"id":"a163354b1ea4c16a","repo":"SubtitleEdit/subtitleedit","slug":"openrouter-transcription-timed-out-after-setting","errorCode":null,"errorMessage":"OpenRouter transcription timed out after {_settings.TimeoutSeconds} seconds.","messagePattern":"OpenRouter transcription timed out after (.+?) seconds\\.","errorType":"exception","errorClass":"TimeoutException","httpStatus":null,"severity":"error","filePath":"src/ui/Features/Video/SpeechToText/OpenRouter/OpenRouterSttService.cs","lineNumber":83,"sourceCode":"        string? language,\n        CancellationToken cancellationToken)\n    {\n        using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n        if (_settings.TimeoutSeconds > 0)\n        {\n            timeoutCts.CancelAfter(TimeSpan.FromSeconds(_settings.TimeoutSeconds));\n        }\n        var ct = timeoutCts.Token;\n\n        try\n        {\n            return await TranscribeCoreAsync(audioBytes, format, language, ct);\n        }\n        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)\n        {\n            // Our own timeout fired, not a user cancel — surface it as an error\n            // so the caller doesn't mistake it for cancellation.\n            throw new TimeoutException($\"OpenRouter transcription timed out after {_settings.TimeoutSeconds} seconds.\");\n        }\n    }\n\n    private async Task<OpenAiCompatibleSttResponse> TranscribeCoreAsync(\n        byte[] audioBytes,\n        string format,\n        string? language,\n        CancellationToken cancellationToken)\n    {\n        var body = BuildRequestBody(_settings, audioBytes, format, language);\n        using var content = new StringContent(body, Encoding.UTF8, \"application/json\");\n        using var request = new HttpRequestMessage(HttpMethod.Post, _settings.EndpointUrl)\n        {\n            Content = content,\n        };\n\n        if (!string.IsNullOrEmpty(_settings.ApiKey))\n        {","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/SubtitleEdit/subtitleedit/blob/17a9f0748781032255db3526b7215d2fb891e3af/src/ui/Features/Video/SpeechToText/OpenRouter/OpenRouterSttService.cs#L65-L101","documentation":"Same timeout pattern as the OpenAI-compatible engine but for OpenRouter: the internal timeoutCTS (CancelAfter TimeoutSeconds) fires before TranscribeCoreAsync returns, and it is not a user cancel. Wrapped as TimeoutException so it is not mistaken for caller cancellation.","triggerScenarios":"OpenRouter TranscribeCoreAsync (POST _settings.EndpointUrl with base64 audio body) exceeds _settings.TimeoutSeconds while the caller's token is still active.","commonSituations":"OpenRouter routing to a slow underlying model; large base64 body inflating upload time; TimeoutSeconds too low for the chosen model; network stall.","solutions":["Increase _settings.TimeoutSeconds for OpenRouter.","Pick a faster underlying model in OpenRouter settings.","Verify network path to OpenRouter.","Shorten the audio clip per request."],"exampleFix":"// before\n_settings.TimeoutSeconds = 60;\n// after: base64-encoded body + remote inference needs more headroom\n_settings.TimeoutSeconds = 240;","handlingStrategy":"retry","validationCode":"if (_settings.TimeoutSeconds < 120) _settings.TimeoutSeconds = 120; // base64 body + remote infer","typeGuard":null,"tryCatchPattern":"try { return await openRouter.TranscribeAsync(...); }\ncatch (TimeoutException ex) when (ex.Message.Contains(\"OpenRouter transcription timed out\"))\n{ /* raise TimeoutSeconds or pick a faster model, then retry */ }","preventionTips":["Account for base64 inflation of the audio body when sizing the timeout.","Pick a faster underlying OpenRouter model for long clips.","Retry on timeout only after confirming the caller did not cancel."],"tags":["openrouter","timeout","network","speech-to-text"],"backgroundTag":null,"analyzedSha":"17a9f0748781032255db3526b7215d2fb891e3af","analyzedAt":"2026-08-13T18:11:43.374Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}