SubtitleEdit/subtitleedit · error · HttpRequestException

OpenRouter STT request failed with status

Error message

OpenRouter STT request failed with status {statusCode} ({response.StatusCode}). Response: {errorContent}

What it means

OpenRouter STT endpoint returned non-2xx; the status code and response body are embedded in the message. Note the log line writes _settings.EndpointUrl RAW (unlike the OpenAI path which sanitizes), and the thrown message itself does not include the endpoint.

Solutions

  1. Read statusCode and errorContent in the message to see the upstream reason.
  2. Confirm _settings.Model is a valid OpenRouter model id that supports audio.
  3. Validate/rotate the API key.
  4. Retry with backoff on 429.

Example fix

// before: model id OpenRouter rejects
Model = "whisper-1";
// after: a model id OpenRouter actually routes for audio
Model = "openai/whisper-large-v3";
Defensive patterns

Strategy: try-catch

Validate before calling

if (string.IsNullOrWhiteSpace(_settings.Model) || !_settings.Model.Contains('/'))
    return Invalid("OpenRouter Model must be a provider/model id");

Try / catch

try { return await openRouter.TranscribeAsync(...); }
catch (HttpRequestException ex) when (ex.Message.Contains("OpenRouter STT request failed"))
{ /* parse statusCode from message; 401 -> key, 404 -> model/endpoint, 429 -> backoff */ }

Prevention

When it happens

Trigger: Non-success status from POST _settings.EndpointUrl: 400 bad model/format, 401 bad API key, 404 wrong endpoint, 429 rate limit, 5xx from OpenRouter or the routed upstream model.

Common situations: Wrong _settings.Model name; invalid/expired OpenRouter API key; endpoint URL misconfigured; rate limit on the account; the routed upstream provider returned an error OpenRouter forwards.

Related errors


AI-assisted analysis of SubtitleEdit/subtitleedit@17a9f07487 (2026-08-13). Data as JSON: /api/errors/029438e0251f1d51. Report an issue: GitHub.

Appendix: source

Thrown at src/ui/Features/Video/SpeechToText/OpenRouter/OpenRouterSttService.cs:119

        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _settings.ApiKey);
        }

        // OpenRouter uses these to attribute traffic; harmless if the server ignores them.
        request.Headers.TryAddWithoutValidation("HTTP-Referer", "https://www.nikse.dk/subtitleedit");
        request.Headers.TryAddWithoutValidation("X-Title", "Subtitle Edit");

        using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
        if (!response.IsSuccessStatusCode)
        {
            var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
            var statusCode = (int)response.StatusCode;
            _settings.Logger?.Invoke(
                $"OpenRouter STT failed: POST {_settings.EndpointUrl}{Environment.NewLine}" +
                $"Status: {statusCode} {response.StatusCode}{Environment.NewLine}" +
                $"RequestParams: model={_settings.Model}, language={language ?? _settings.Language}, format={format}, bytes={audioBytes.Length}{Environment.NewLine}" +
                $"ResponseBody: {errorContent}");
            throw new HttpRequestException(
                $"OpenRouter STT request failed with status {statusCode} ({response.StatusCode}). Response: {errorContent}");
        }

        var json = await response.Content.ReadAsStringAsync(cancellationToken);
        return ParseResponse(json);
    }

    /// <summary>
    /// Serialize the OpenRouter transcription request body. The audio is
    /// base64-encoded (raw, not a data URI) under <c>input_audio</c>, and
    /// <c>verbose_json</c> + <c>timestamp_granularities[]</c> ask for segment and
    /// word timings when the underlying model supports them.
    /// </summary>
    internal static string BuildRequestBody(OpenRouterSttSettings settings, byte[] audioBytes, string format, string? language)
    {
        using var stream = new MemoryStream();
        using (var writer = new Utf8JsonWriter(stream))
        {

View on GitHub (pinned to 17a9f07487)