Skip to content

Commit

Permalink
Add Timeout/Retries features
Browse files Browse the repository at this point in the history
  • Loading branch information
neuecc committed Mar 8, 2024
1 parent 04027e5 commit ac40019
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 56 deletions.
98 changes: 95 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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" }]
});
Expand All @@ -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
{
/// <summary>There was an issue with the format or content of your request.</summary>
InvalidRequestError = 400,
/// <summary>There's an issue with your API key.</summary>
AuthenticationError = 401,
/// <summary>Your API key does not have permission to use the specified resource.</summary>
PermissionError = 403,
/// <summary>The requested resource was not found.</summary>
NotFoundError = 404,
/// <summary>Your account has hit a rate limit.</summary>
RateLimitError = 429,
/// <summary>An unexpected error has occurred internal to Anthropic's systems.</summary>
ApiError = 500,
/// <summary>Anthropic's API is temporarily overloaded.</summary>
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
---
Expand Down
93 changes: 57 additions & 36 deletions sandbox/ConsoleApp1/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
});
29 changes: 17 additions & 12 deletions src/Claudia/Anthropic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ namespace Claudia;

public interface IMessages
{
Task<MessagesResponse> CreateAsync(MessageRequest request, CancellationToken cancellationToken = default);
Task<MessagesResponse> 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
Expand Down Expand Up @@ -45,7 +52,7 @@ public Anthropic(HttpClient httpClient)
this.httpClient = httpClient;
}

async Task<MessagesResponse> IMessages.CreateAsync(MessageRequest request, CancellationToken cancellationToken)
async Task<MessagesResponse> IMessages.CreateAsync(MessageRequest request, RequestOptions? overrideOptions, CancellationToken cancellationToken)
{
var bytes = JsonSerializer.SerializeToUtf8Bytes(request, DefaultJsonSerializerOptions);

Expand All @@ -55,16 +62,16 @@ async Task<MessagesResponse> 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<MessagesResponse>(DefaultJsonSerializerOptions, ct)).ConfigureAwait(false);
var result = await RequestWithCancelAsync(msg, cancellationToken, overrideOptions, static (x, ct) => x.Content.ReadFromJsonAsync<MessagesResponse>(DefaultJsonSerializerOptions, ct)).ConfigureAwait(false);
return result!;
default:
var shape = await RequestWithCancelAsync(msg, cancellationToken, static (x, ct) => x.Content.ReadFromJsonAsync<ErrorResponseShape>(DefaultJsonSerializerOptions, ct)).ConfigureAwait(false);
var shape = await RequestWithCancelAsync(msg, cancellationToken, overrideOptions, static (x, ct) => x.Content.ReadFromJsonAsync<ErrorResponseShape>(DefaultJsonSerializerOptions, ct)).ConfigureAwait(false);

var error = shape!.ErrorResponse;
var errorMsg = error.Message;
Expand All @@ -77,13 +84,14 @@ async Task<MessagesResponse> IMessages.CreateAsync(MessageRequest request, Cance
}
}

async Task<TResult> RequestWithCancelAsync<TResult, TState>(TState state, CancellationToken cancellationToken, Func<TState, CancellationToken, Task<TResult>> func)
async Task<TResult> RequestWithCancelAsync<TResult, TState>(TState state, CancellationToken cancellationToken, RequestOptions? overrideOptions, Func<TState, CancellationToken, Task<TResult>> 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
{
Expand Down Expand Up @@ -146,6 +154,3 @@ static class ApiEndpoints
public static readonly Uri Messages = new Uri("https://api.anthropic.com/v1/messages", UriKind.RelativeOrAbsolute);
}
}



10 changes: 5 additions & 5 deletions src/Claudia/ClaudiaException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}";
}
}

Expand Down

0 comments on commit ac40019

Please sign in to comment.