24 public class AIProvider
26 private readonly AIProviderType _providerType;
35 private const int RATE_LIMIT_ATTEMPTS = 3;
36 private const int RATE_LIMIT_DEFAULT_WAIT = 5;
37 private const int RATE_LIMIT_MAX_WAIT = 60;
41 _providerType = providerType;
42 _restClient = restClient ??
throw new ArgumentNullException(nameof(restClient));
43 _credentials = credentials;
47 if (_definition ==
null)
48 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"No provider definition found for [{providerType}]. Add it to AIProvidersConfig.yaml.",
null, GPALObjectType.AIProvider);
51 public string Call(
string prompt)
55 if (
string.IsNullOrEmpty(prompt))
61 string endpoint = GetEndpoint();
62 string apiBase = GetApiBase();
64 string accessToken =
null;
65 if (_credentials !=
null)
67 _credentials.FetchAccessToken(out accessToken);
68 if (
string.IsNullOrEmpty(accessToken))
69 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Unable to login using credentials. No access token granted.",
this, GPALObjectType.AIProvider);
72 var body = BuildRequestBody(prompt);
74 string response = Send(apiBase, endpoint, accessToken, body);
78 while (429 == _restClient.StatusCode && attempt < RATE_LIMIT_ATTEMPTS)
80 int wait = RateLimitWait();
82 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"[{_providerType}] is rate limiting. Waiting [{wait}] seconds, then attempt [{attempt + 1}] of [{RATE_LIMIT_ATTEMPTS}]",
this, GPALObjectType.AIProvider);
84 System.Threading.Thread.Sleep(wait * 1000);
86 response = Send(apiBase, endpoint, accessToken, body);
90 if (429 == _restClient.StatusCode)
92 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"[{_providerType}] is still rate limiting after [{RATE_LIMIT_ATTEMPTS}] attempts.",
this, GPALObjectType.AIProvider);
96 if (
string.IsNullOrEmpty(response))
98 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Empty response from AI provider.",
this, GPALObjectType.AIProvider);
102 string completion = ExtractCompletion(response);
103 if (
string.IsNullOrEmpty(completion))
105 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Unable to extract completion from [{_providerType}] response: [{response}]",
this, GPALObjectType.AIProvider);
109 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Received response from [{_providerType}]",
this, GPALObjectType.AIProvider);
114 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to call AI provider",
this, GPALObjectType.AIProvider, ex);
128 private string Send(
string apiBase,
string endpoint,
string accessToken, Dictionary<string, object> body)
133 .WithAPIBase(apiBase, System.Threading.Timeout.InfiniteTimeSpan)
134 .WithEndpoint(endpoint);
136 if (!
string.IsNullOrEmpty(accessToken))
138 var headerName = _definition?.AuthHeader ??
"Authorization";
139 var prefix = _definition?.AuthPrefix ??
"Bearer ";
140 _restClient.WithHeader(headerName,
string.IsNullOrEmpty(prefix) ? accessToken : $
"{prefix}{accessToken}");
143 if (_definition?.AdditionalHeaders !=
null)
145 foreach (var h
in _definition.AdditionalHeaders)
146 _restClient.WithHeader(h.Key, h.Value);
149 string response = requestBuilder
150 .WithParameters(body)
160 private int RateLimitWait()
162 int wait = _restClient.RetryAfterSeconds;
165 wait = RATE_LIMIT_DEFAULT_WAIT;
167 if (RATE_LIMIT_MAX_WAIT < wait)
168 wait = RATE_LIMIT_MAX_WAIT;
173 private Dictionary<string, object> BuildRequestBody(
string prompt)
175 var messages =
new List<object> {
new Dictionary<string, object> { [
"role"] =
"user", [
"content"] = prompt } };
177 var body =
new Dictionary<string, object>
179 [
"model"] = GetModel(),
180 [
"messages"] = messages
183 if (_definition?.ResponseFormat ==
"anthropic")
185 body[
"max_tokens"] = GetMaxTokens();
188 if (_config.AdditionalParameters !=
null)
190 foreach (var param
in _config.AdditionalParameters)
192 if (IsReservedParameter(param.Key))
194 body[param.Key] = param.Value;
201 private static bool IsReservedParameter(
string key)
203 return key ==
"Endpoint" || key ==
"Model" || key ==
"MaxTokens" || key ==
"AnthropicVersion";
206 private int GetMaxTokens()
208 if (_config?.AdditionalParameters?.ContainsKey(
"MaxTokens") ==
true
209 &&
int.TryParse(_config.AdditionalParameters[
"MaxTokens"].ToString(), out
int maxTokens))
216 private string GetModel()
218 if (_config?.AdditionalParameters?.ContainsKey(
"Model") ==
true)
219 return _config.AdditionalParameters[
"Model"].ToString();
220 return _definition?.DefaultModel;
223 private string GetApiBase() => _definition?.BaseUrl;
225 private string GetEndpoint()
227 if (_config?.AdditionalParameters?.ContainsKey(
"Endpoint") ==
true)
228 return _config.AdditionalParameters[
"Endpoint"].ToString();
229 return _definition?.ChatEndpoint ??
"/chat/completions";
232 private string ExtractCompletion(
string response)
234 using var doc = JsonDocument.Parse(response);
235 var root = doc.RootElement;
237 if (_definition?.ResponseFormat ==
"anthropic")
239 if (root.TryGetProperty(
"content", out var content) && content.ValueKind == JsonValueKind.Array && content.GetArrayLength() > 0)
241 var first = content[0];
242 if (first.TryGetProperty(
"text", out var text))
243 return text.GetString();
248 if (root.TryGetProperty(
"choices", out var choices) && choices.ValueKind == JsonValueKind.Array && choices.GetArrayLength() > 0)
250 var first = choices[0];
251 if (first.TryGetProperty(
"message", out var message) && message.TryGetProperty(
"content", out var msgContent))
252 return msgContent.GetString();