|
1 | 1 | using System.Collections.Concurrent; |
| 2 | +using System.Diagnostics; |
2 | 3 | using System.Net.Http.Headers; |
3 | 4 | using Grpc.Net.Client; |
| 5 | +using Microsoft.Extensions.Http; |
| 6 | +using Polly; |
| 7 | +using Polly.Extensions.Http; |
4 | 8 | using xAI.Protocol; |
5 | 9 |
|
6 | 10 | namespace xAI; |
@@ -47,12 +51,59 @@ public sealed class GrokClient(string apiKey, GrokClientOptions options) : IDisp |
47 | 51 |
|
48 | 52 | internal GrpcChannel Channel => channels.GetOrAdd((Endpoint, ApiKey), key => |
49 | 53 | { |
| 54 | + var inner = Options.ChannelOptions?.HttpHandler; |
| 55 | + if (inner == null) |
| 56 | + { |
| 57 | + // If no custom HttpHandler is provided, we create one with Polly retry |
| 58 | + // policies to handle transient errors, including gRPC-specific ones. |
| 59 | + var retryPolicy = HttpPolicyExtensions |
| 60 | + .HandleTransientHttpError() |
| 61 | + .Or<Grpc.Core.RpcException>(ex => |
| 62 | + ex.StatusCode is Grpc.Core.StatusCode.Unavailable or |
| 63 | + Grpc.Core.StatusCode.DeadlineExceeded or |
| 64 | + Grpc.Core.StatusCode.Internal && |
| 65 | + ex.Status.Detail?.Contains("504") == true || |
| 66 | + ex.Status.Detail?.Contains("INTERNAL_ERROR") == true) |
| 67 | + .WaitAndRetryAsync( |
| 68 | + retryCount: 3, |
| 69 | + sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)) |
| 70 | +#if DEBUG |
| 71 | + , onRetry: (outcome, delay, retryCount, ctx) => |
| 72 | + { |
| 73 | + Debug.WriteLine($"[xAI Streaming Retry #{retryCount}] {outcome.Exception?.Message} — waiting {delay.TotalSeconds}s"); |
| 74 | + } |
| 75 | +#endif |
| 76 | + ); |
| 77 | + |
| 78 | + inner = new PolicyHttpMessageHandler(retryPolicy) |
| 79 | + { |
| 80 | + InnerHandler = new SocketsHttpHandler |
| 81 | + { |
| 82 | + PooledConnectionLifetime = TimeSpan.FromMinutes(20), |
| 83 | + KeepAlivePingDelay = TimeSpan.FromSeconds(30), |
| 84 | + KeepAlivePingTimeout = TimeSpan.FromSeconds(10), |
| 85 | + KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always, // crucial for long streams |
| 86 | + EnableMultipleHttp2Connections = true, |
| 87 | + ConnectTimeout = TimeSpan.FromSeconds(60), |
| 88 | + MaxConnectionsPerServer = 10 |
| 89 | + } |
| 90 | + }; |
| 91 | + } |
| 92 | + |
50 | 93 | var handler = new AuthenticationHeaderHandler(ApiKey) |
51 | 94 | { |
52 | | - InnerHandler = Options.ChannelOptions?.HttpHandler ?? new HttpClientHandler() |
| 95 | + InnerHandler = inner |
| 96 | + }; |
| 97 | + |
| 98 | + // Provide some sensible defaults for gRPC channel options, while allowing users to |
| 99 | + // override them via GrokClientOptions.ChannelOptions if needed. |
| 100 | + var options = Options.ChannelOptions ?? new GrpcChannelOptions |
| 101 | + { |
| 102 | + DisposeHttpClient = true, |
| 103 | + MaxReceiveMessageSize = 128 * 1024 * 1024, // large enough for tool output |
| 104 | + MaxSendMessageSize = 16 * 1024 * 1024, |
53 | 105 | }; |
54 | 106 |
|
55 | | - var options = Options.ChannelOptions ?? new GrpcChannelOptions(); |
56 | 107 | options.HttpHandler = handler; |
57 | 108 |
|
58 | 109 | return GrpcChannel.ForAddress(Endpoint, options); |
|
0 commit comments