From ac40019417340f79e0776e762ec161cd39d180f5 Mon Sep 17 00:00:00 2001 From: neuecc Date: Fri, 8 Mar 2024 17:14:06 +0900 Subject: [PATCH] Add Timeout/Retries features --- README.md | 98 ++++++++++++++++++++++++++++++++- sandbox/ConsoleApp1/Program.cs | 93 +++++++++++++++++++------------ src/Claudia/Anthropic.cs | 29 ++++++---- src/Claudia/ClaudiaException.cs | 10 ++-- 4 files changed, 174 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index f2b73a5..5df5e6f 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,12 @@ using Claudia; var anthropic = new Anthropic { - ApiKey = "my_api_key" + ApiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") // This is the default and can be omitted }; var message = await anthropic.Messages.CreateAsync(new() { - Model = "claude-3-opus-20240229", + Model = "claude-3-opus-20240229", // you can use Claudia.Models.Claude3Opus string constant MaxTokens = 1024, Messages = [new() { Role = "user", Content = "Hello, Claude" }] }); @@ -39,7 +39,99 @@ Coming Soon. Handling errors --- -If the API call fails, a `ClaudiaException` will be thrown. You can check the `ErrorCode`, `Type`, and `Message` from the `ClaudiaException`. +When the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of `ClaudiaException` will be thrown: + +```csharp +try +{ + var msg = await anthropic.Messages.CreateAsync(new() + { + Model = Models.Claude3Opus, + MaxTokens = 1024, + Messages = [new() { Role = "user", Content = "Hello, Claude" }] + }); +} +catch (ClaudiaException ex) +{ + Console.WriteLine((int)ex.Status); // 400(ErrorCode.InvalidRequestError) + Console.WriteLine(ex.Name); // invalid_request_error + Console.WriteLine(ex.Message); // Field required. Input:... +} +``` + +Error codes are as followed: + +```csharp +public enum ErrorCode +{ + /// There was an issue with the format or content of your request. + InvalidRequestError = 400, + /// There's an issue with your API key. + AuthenticationError = 401, + /// Your API key does not have permission to use the specified resource. + PermissionError = 403, + /// The requested resource was not found. + NotFoundError = 404, + /// Your account has hit a rate limit. + RateLimitError = 429, + /// An unexpected error has occurred internal to Anthropic's systems. + ApiError = 500, + /// Anthropic's API is temporarily overloaded. + OverloadedError = 529 +} +``` + +Retries +--- +Certain errors will be automatically retried 2 times by default, with a short exponential backoff. Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors will all be retried by default. + +You can use the `MaxRetries` option to configure or disable this: + +```csharp +// Configure the default for all requests: +var anthropic = new Anthropic +{ + MaxRetries = 0, // default is 2 +}; + +// Or, configure per-request: +await anthropic.Messages.CreateAsync(new() +{ + MaxTokens = 1024, + Messages = [new() { Role = "user", Content = "Hello, Claude" }], + Model = "claude-3-opus-20240229" +}, new() +{ + MaxRetries = 5 +}); +``` + +Timeouts +--- +Requests time out after 10 minutes by default. You can configure this with a `Timeout` option: + +```csharp +// Configure the default for all requests: +var anthropic = new Anthropic +{ + Timeout = TimeSpan.FromSeconds(20) // 20 seconds (default is 10 minutes) +}; + +// Override per-request: +await anthropic.Messages.CreateAsync(new() +{ + MaxTokens = 1024, + Messages = [new() { Role = "user", Content = "Hello, Claude" }], + Model = "claude-3-opus-20240229" +}, new() +{ + Timeout = TimeSpan.FromSeconds(5) +}); +``` + +On timeout, an `TimeoutException` is thrown. + +Note that requests which time out will be [retried twice by default](#retries). License --- diff --git a/sandbox/ConsoleApp1/Program.cs b/sandbox/ConsoleApp1/Program.cs index 7eb20a6..dd741ac 100644 --- a/sandbox/ConsoleApp1/Program.cs +++ b/sandbox/ConsoleApp1/Program.cs @@ -4,18 +4,6 @@ -//import Anthropic from '@anthropic-ai/sdk'; - -//const anthropic = new Anthropic({ -// apiKey: 'my_api_key', // defaults to process.env["ANTHROPIC_API_KEY"] -//}); - -//const msg = await anthropic.messages.create({ -// model: "claude-3-opus-20240229", -//max_tokens: 1024, -// messages: [{ role: "user", content: "Hello, Claude" }], -//}); -//console.log(msg); //var anthropic = new Anthropic @@ -24,35 +12,68 @@ // // Timeout = TimeSpan.FromMilliseconds(1) //}; -//var msg = await anthropic.Messages.CreateAsync(new() +////var msg = await anthropic.Messages.CreateAsync(new() +////{ +//// Model = Models.Claude3Opus, +//// MaxTokens = 1024, +//// Messages = [new() { Role = "user", Content = "Hello, Claude" }] +////}); + +////Console.WriteLine(msg); + + +//// error + +//try //{ -// Model = "claude-3-opus-20240229", -// MaxTokens = 1024, -// Messages = [new() { Role = "user", Content = "Hello, Claude" }] -//}); +// var msg = await anthropic.Messages.CreateAsync(new() +// { +// Model = Models.Claude3Opus, +// MaxTokens = 1024, +// Messages = [new() { Role = "user", Content = "Hello, Claude" }] +// }); +//} +//catch (ClaudiaException ex) +//{ +// Console.WriteLine((int)ex.Status); // 400(ErrorCode.InvalidRequestError) +// Console.WriteLine(ex.Name); // invalid_request_error +// Console.WriteLine(ex.Message); // Field required. Input:... +//} + +// retry -//Console.WriteLine(msg); +// Configure the default for all requests: +//var anthropic = new Anthropic +//{ +// MaxRetries = 0, // default is 2 +//}; +//// Or, configure per-request: +//await anthropic.Messages.CreateAsync(new() +//{ +// MaxTokens = 1024, +// Messages = [new() { Role = "user", Content = "Hello, Claude" }], +// Model = "claude-3-opus-20240229" +//}, new() +//{ +// MaxRetries = 5 +//}); -// Console.WriteLine(TimeSpan.FromMilliseconds(Anthropic.CalculateDefaultRetryTimeoutMillis(Random.Shared, 0, 4))); +// timeout +// Configure the default for all requests: +var anthropic = new Anthropic +{ + Timeout = TimeSpan.FromSeconds(20) // 20 seconds (default is 10 minutes) +}; -var MaxRetries = 0; -var retriesRemaining = MaxRetries; -RETRY: -try +// Override per-request: +await anthropic.Messages.CreateAsync(new() { - throw new Exception(); -} -catch + MaxTokens = 1024, + Messages = [new() { Role = "user", Content = "Hello, Claude" }], + Model = "claude-3-opus-20240229" +}, new() { - if (retriesRemaining > 0) - { - //var sleep = CalculateDefaultRetryTimeoutMillis(random, retriesRemaining, MaxRetries); - //await Task.Delay(TimeSpan.FromMilliseconds(sleep), cancellationToken).ConfigureAwait(false); - retriesRemaining--; - Console.WriteLine("RETRY"); - goto RETRY; - } - throw; -} + Timeout = TimeSpan.FromSeconds(5) +}); \ No newline at end of file diff --git a/src/Claudia/Anthropic.cs b/src/Claudia/Anthropic.cs index 028c4cb..f6ca1a6 100644 --- a/src/Claudia/Anthropic.cs +++ b/src/Claudia/Anthropic.cs @@ -8,7 +8,14 @@ namespace Claudia; public interface IMessages { - Task CreateAsync(MessageRequest request, CancellationToken cancellationToken = default); + Task CreateAsync(MessageRequest request, RequestOptions? overrideOptions = null, CancellationToken cancellationToken = default); +} + +public class RequestOptions +{ + public TimeSpan? Timeout { get; set; } + + public int? MaxRetries { get; set; } } public class Anthropic : IMessages, IDisposable @@ -45,7 +52,7 @@ public Anthropic(HttpClient httpClient) this.httpClient = httpClient; } - async Task IMessages.CreateAsync(MessageRequest request, CancellationToken cancellationToken) + async Task IMessages.CreateAsync(MessageRequest request, RequestOptions? overrideOptions, CancellationToken cancellationToken) { var bytes = JsonSerializer.SerializeToUtf8Bytes(request, DefaultJsonSerializerOptions); @@ -55,16 +62,16 @@ async Task IMessages.CreateAsync(MessageRequest request, Cance message.Headers.Add("Accept", "application/json"); message.Content = new ByteArrayContent(bytes); - var msg = await RequestWithCancelAsync((httpClient, message), cancellationToken, static (x, ct) => x.httpClient.SendAsync(x.message, ct)).ConfigureAwait(false); + var msg = await RequestWithCancelAsync((httpClient, message), cancellationToken, overrideOptions, static (x, ct) => x.httpClient.SendAsync(x.message, ct)).ConfigureAwait(false); var statusCode = (int)msg.StatusCode; switch (statusCode) { case 200: - var result = await RequestWithCancelAsync(msg, cancellationToken, static (x, ct) => x.Content.ReadFromJsonAsync(DefaultJsonSerializerOptions, ct)).ConfigureAwait(false); + var result = await RequestWithCancelAsync(msg, cancellationToken, overrideOptions, static (x, ct) => x.Content.ReadFromJsonAsync(DefaultJsonSerializerOptions, ct)).ConfigureAwait(false); return result!; default: - var shape = await RequestWithCancelAsync(msg, cancellationToken, static (x, ct) => x.Content.ReadFromJsonAsync(DefaultJsonSerializerOptions, ct)).ConfigureAwait(false); + var shape = await RequestWithCancelAsync(msg, cancellationToken, overrideOptions, static (x, ct) => x.Content.ReadFromJsonAsync(DefaultJsonSerializerOptions, ct)).ConfigureAwait(false); var error = shape!.ErrorResponse; var errorMsg = error.Message; @@ -77,13 +84,14 @@ async Task IMessages.CreateAsync(MessageRequest request, Cance } } - async Task RequestWithCancelAsync(TState state, CancellationToken cancellationToken, Func> func) + async Task RequestWithCancelAsync(TState state, CancellationToken cancellationToken, RequestOptions? overrideOptions, Func> func) { - var retriesRemaining = MaxRetries; + var retriesRemaining = overrideOptions?.MaxRetries ?? MaxRetries; + var timeout = overrideOptions?.Timeout ?? Timeout; RETRY: - using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) // check for timeout + using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) { - cts.CancelAfter(Timeout); + cts.CancelAfter(timeout); try { @@ -146,6 +154,3 @@ static class ApiEndpoints public static readonly Uri Messages = new Uri("https://api.anthropic.com/v1/messages", UriKind.RelativeOrAbsolute); } } - - - diff --git a/src/Claudia/ClaudiaException.cs b/src/Claudia/ClaudiaException.cs index af4b988..8343685 100644 --- a/src/Claudia/ClaudiaException.cs +++ b/src/Claudia/ClaudiaException.cs @@ -6,19 +6,19 @@ namespace Claudia; public class ClaudiaException : Exception { - public ErrorCode ErrorCode { get; } - public string Type { get; } + public ErrorCode Status { get; } + public string Name { get; } public ClaudiaException(ErrorCode errorCode, string type, string message) : base(message) { - this.ErrorCode = errorCode; - this.Type = type; + this.Status = errorCode; + this.Name = type; } public override string ToString() { - return $"{Type}: {Message}"; + return $"{Name}: {Message}"; } }