| | | 1 | | using System.Globalization; |
| | | 2 | | using System.Text.RegularExpressions; |
| | | 3 | | |
| | | 4 | | namespace NexusLabs.Needlr.Copilot; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Thrown when a Copilot web search request is rejected due to rate limiting. |
| | | 8 | | /// This can occur either via an HTTP 429 response after retry exhaustion or |
| | | 9 | | /// when the MCP tool returns a rate-limit error message as its content. |
| | | 10 | | /// </summary> |
| | | 11 | | /// <remarks> |
| | | 12 | | /// Callers should catch this exception to implement fallback behavior |
| | | 13 | | /// (e.g., trying an alternative search provider or waiting before retrying). |
| | | 14 | | /// <see cref="RetryAfter"/> provides a hint for how long to wait, when |
| | | 15 | | /// available from the response. |
| | | 16 | | /// </remarks> |
| | | 17 | | public sealed partial class CopilotRateLimitException : Exception |
| | | 18 | | { |
| | | 19 | | /// <summary> |
| | | 20 | | /// Creates a new <see cref="CopilotRateLimitException"/> with the given |
| | | 21 | | /// message, optional retry delay, and optional inner exception. |
| | | 22 | | /// </summary> |
| | | 23 | | /// <param name="message">A description of the rate-limit condition.</param> |
| | | 24 | | /// <param name="retryAfter"> |
| | | 25 | | /// How long the caller should wait before retrying, if known from the |
| | | 26 | | /// HTTP <c>Retry-After</c> header or the error message text. |
| | | 27 | | /// </param> |
| | | 28 | | /// <param name="innerException">The exception that caused this failure, if any.</param> |
| | | 29 | | public CopilotRateLimitException( |
| | | 30 | | string message, |
| | | 31 | | TimeSpan? retryAfter = null, |
| | | 32 | | Exception? innerException = null) |
| | 6 | 33 | | : base(message, innerException) |
| | | 34 | | { |
| | 6 | 35 | | RetryAfter = retryAfter; |
| | 6 | 36 | | } |
| | | 37 | | |
| | | 38 | | /// <summary> |
| | | 39 | | /// The suggested wait duration before retrying, parsed from the HTTP |
| | | 40 | | /// <c>Retry-After</c> header or the error message text. <c>null</c> when |
| | | 41 | | /// no retry hint was provided. |
| | | 42 | | /// </summary> |
| | 3 | 43 | | public TimeSpan? RetryAfter { get; } |
| | | 44 | | |
| | | 45 | | /// <summary> |
| | | 46 | | /// Attempts to parse a "Try again in N seconds" hint from the given text. |
| | | 47 | | /// </summary> |
| | | 48 | | internal static TimeSpan? ParseRetryAfterFromText(string text) |
| | | 49 | | { |
| | 5 | 50 | | var match = RetryAfterPattern().Match(text); |
| | 5 | 51 | | if (match.Success && |
| | 5 | 52 | | int.TryParse(match.Groups[1].Value, CultureInfo.InvariantCulture, out var seconds)) |
| | | 53 | | { |
| | 2 | 54 | | return TimeSpan.FromSeconds(seconds); |
| | | 55 | | } |
| | | 56 | | |
| | 3 | 57 | | return null; |
| | | 58 | | } |
| | | 59 | | |
| | | 60 | | [GeneratedRegex(@"Try again in (\d+) seconds?", RegexOptions.IgnoreCase)] |
| | | 61 | | private static partial Regex RetryAfterPattern(); |
| | | 62 | | } |