forked from admin/VolcanicAgenticCore
feat(telegram): retry transient speech-to-text failures up to 3 attempts
This commit is contained in:
@@ -31,7 +31,7 @@ public sealed class OpenAiSpeechToTextTests
|
||||
private static (OpenAiSpeechToText Stt, StubHandler Handler) Create(HttpStatusCode status, string body, string? language = null)
|
||||
{
|
||||
var handler = new StubHandler(status, body);
|
||||
var options = new SpeechOptions { ApiKey = Key, Language = language, BaseUrl = "https://stt.example/v1/" };
|
||||
var options = new SpeechOptions { ApiKey = Key, Language = language, BaseUrl = "https://stt.example/v1/", RetryDelay = TimeSpan.Zero };
|
||||
return (new OpenAiSpeechToText(new HttpClient(handler), options), handler);
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ public sealed class OpenAiSpeechToTextTests
|
||||
public async Task A_client_timeout_is_a_speech_failure_not_a_cancellation()
|
||||
{
|
||||
var http = new HttpClient(new HangingHandler()) { Timeout = TimeSpan.FromMilliseconds(50) };
|
||||
var stt = new OpenAiSpeechToText(http, new SpeechOptions { ApiKey = Key });
|
||||
var stt = new OpenAiSpeechToText(http, new SpeechOptions { ApiKey = Key, RetryDelay = TimeSpan.Zero });
|
||||
var ex = await Assert.ThrowsAsync<SpeechToTextException>(() => stt.TranscribeAsync(new byte[] { 1 }, "voice.ogg", "audio/ogg", CancellationToken.None));
|
||||
Assert.Equal("OpenAI transcription failed: the request timed out.", ex.Message);
|
||||
}
|
||||
@@ -114,8 +114,84 @@ public sealed class OpenAiSpeechToTextTests
|
||||
[Fact]
|
||||
public async Task The_callers_cancellation_still_cancels()
|
||||
{
|
||||
var stt = new OpenAiSpeechToText(new HttpClient(new HangingHandler()), new SpeechOptions { ApiKey = Key });
|
||||
var stt = new OpenAiSpeechToText(new HttpClient(new HangingHandler()), new SpeechOptions { ApiKey = Key, RetryDelay = TimeSpan.Zero });
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => stt.TranscribeAsync(new byte[] { 1 }, "voice.ogg", "audio/ogg", cts.Token));
|
||||
}
|
||||
|
||||
/// <summary>Answers each attempt from the script in turn: a status with a body, or an exception to throw.</summary>
|
||||
private sealed class ScriptedHandler(params object[] script) : HttpMessageHandler
|
||||
{
|
||||
public int Calls { get; private set; }
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var step = script[Math.Min(Calls++, script.Length - 1)];
|
||||
if (step is Exception ex) throw ex;
|
||||
var (status, body) = ((HttpStatusCode, string))step;
|
||||
return Task.FromResult(new HttpResponseMessage(status) { Content = new StringContent(body, Encoding.UTF8, "application/json") });
|
||||
}
|
||||
}
|
||||
|
||||
private static OpenAiSpeechToText Create(ScriptedHandler handler) =>
|
||||
new(new HttpClient(handler), new SpeechOptions { ApiKey = Key, RetryDelay = TimeSpan.Zero });
|
||||
|
||||
[Theory]
|
||||
[InlineData(HttpStatusCode.InternalServerError)]
|
||||
[InlineData(HttpStatusCode.BadGateway)]
|
||||
[InlineData(HttpStatusCode.ServiceUnavailable)]
|
||||
[InlineData(HttpStatusCode.TooManyRequests)]
|
||||
[InlineData(HttpStatusCode.RequestTimeout)]
|
||||
public async Task A_transient_status_is_retried_until_the_third_attempt_succeeds(HttpStatusCode status)
|
||||
{
|
||||
var handler = new ScriptedHandler((status, "busy"), (status, "busy"), (HttpStatusCode.OK, "{\"text\":\"hello\"}"));
|
||||
Assert.Equal("hello", await Create(handler).TranscribeAsync(new byte[] { 1 }, "voice.ogg", "audio/ogg", CancellationToken.None));
|
||||
Assert.Equal(3, handler.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Three_transient_failures_give_up_with_the_last_error()
|
||||
{
|
||||
var handler = new ScriptedHandler((HttpStatusCode.ServiceUnavailable, "down"));
|
||||
var ex = await Assert.ThrowsAsync<SpeechToTextException>(() => Create(handler).TranscribeAsync(new byte[] { 1 }, "voice.ogg", "audio/ogg", CancellationToken.None));
|
||||
Assert.Equal("OpenAI transcription failed: HTTP 503 down", ex.Message);
|
||||
Assert.Equal(3, handler.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_network_error_is_retried()
|
||||
{
|
||||
var handler = new ScriptedHandler(new HttpRequestException("connection reset"), (HttpStatusCode.OK, "{\"text\":\"hi\"}"));
|
||||
Assert.Equal("hi", await Create(handler).TranscribeAsync(new byte[] { 1 }, "voice.ogg", "audio/ogg", CancellationToken.None));
|
||||
Assert.Equal(2, handler.Calls);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(HttpStatusCode.BadRequest)]
|
||||
[InlineData(HttpStatusCode.Unauthorized)]
|
||||
[InlineData(HttpStatusCode.RequestEntityTooLarge)]
|
||||
public async Task A_client_error_is_not_retried(HttpStatusCode status)
|
||||
{
|
||||
var handler = new ScriptedHandler((status, "nope"), (HttpStatusCode.OK, "{\"text\":\"hi\"}"));
|
||||
await Assert.ThrowsAsync<SpeechToTextException>(() => Create(handler).TranscribeAsync(new byte[] { 1 }, "voice.ogg", "audio/ogg", CancellationToken.None));
|
||||
Assert.Equal(1, handler.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_bad_body_is_not_retried()
|
||||
{
|
||||
var handler = new ScriptedHandler((HttpStatusCode.OK, "<html>gateway</html>"), (HttpStatusCode.OK, "{\"text\":\"hi\"}"));
|
||||
await Assert.ThrowsAsync<SpeechToTextException>(() => Create(handler).TranscribeAsync(new byte[] { 1 }, "voice.ogg", "audio/ogg", CancellationToken.None));
|
||||
Assert.Equal(1, handler.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_callers_cancellation_stops_the_wait_between_attempts()
|
||||
{
|
||||
var handler = new ScriptedHandler((HttpStatusCode.ServiceUnavailable, "down"));
|
||||
var stt = new OpenAiSpeechToText(new HttpClient(handler), new SpeechOptions { ApiKey = Key, RetryDelay = TimeSpan.FromMinutes(5) });
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => stt.TranscribeAsync(new byte[] { 1 }, "voice.ogg", "audio/ogg", cts.Token));
|
||||
Assert.Equal(1, handler.Calls);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user