Skip to content

Commit 871aeac

Browse files
committed
Added support for Groq
1 parent 64c421f commit 871aeac

29 files changed

+747
-3
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Zatomic.AI.Providers
22

3-
C# .NET library that provides chat functionality for the following AI providers: AI21 Labs, Amazon Bedrock, Anthropic, Azure OpenAI, Azure Serverless, Cohere, Deep Infra, Fireworks AI, Google Gemini, Hugging Face, Hyperbolic, Lambda, Meta, Mistral, OpenAI, Together AI, and xAI.
3+
C# .NET library that provides chat functionality for the following AI providers: AI21 Labs, Amazon Bedrock, Anthropic, Azure OpenAI, Azure Serverless, Cohere, Deep Infra, Fireworks AI, Google Gemini, Groq, Hugging Face, Hyperbolic, Lambda, Meta, Mistral, OpenAI, Together AI, and xAI.
44

55
The library calls the chat completions REST APIs and inference endpoints for each of the above AI providers. Everything is strongly-typed with the library handling all JSON serialization/deserialization for all requests and responses. Both non-stream and streaming functionality is supported using `async` methods for improved performance.
66

@@ -86,6 +86,10 @@ The format of the `AppSettigns.Development.json` file is as follows:
8686
"ApiKey": "",
8787
"Model": ""
8888
},
89+
"Groq": {
90+
"ApiKey": "",
91+
"Model": ""
92+
},
8993
"HuggingFace": {
9094
"AccessToken": "",
9195
"Endpoint": "",
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System.Threading.Tasks;
2+
using NUnit.Framework;
3+
using Zatomic.AI.Providers.Groq;
4+
5+
namespace Zatomic.AI.Providers.Samples
6+
{
7+
[TestFixture, Explicit]
8+
public class GroqSamples : BaseSample
9+
{
10+
private readonly string _apiKey;
11+
private readonly string _model;
12+
13+
public GroqSamples()
14+
{
15+
_apiKey = Configuration["Groq:ApiKey"];
16+
_model = Configuration["Groq:Model"];
17+
}
18+
19+
[Test]
20+
public async Task Chat()
21+
{
22+
var client = new GroqChatClient(_apiKey);
23+
var request = new GroqChatRequest(_model);
24+
request.AddSystemMessage(SystemPrompt);
25+
request.AddUserMessage(UserPrompt);
26+
27+
var response = await client.ChatAsync(request);
28+
WriteOutput(response.Choices[0].Message.Content);
29+
WriteOutput(response.Usage.PromptTokens, response.Usage.CompletionTokens, response.Usage.TotalTokens, response.Duration.Value);
30+
}
31+
32+
[Test]
33+
public async Task ChatStream()
34+
{
35+
var client = new GroqChatClient(_apiKey);
36+
var request = new GroqChatRequest(_model);
37+
request.AddSystemMessage(SystemPrompt);
38+
request.AddUserMessage(UserPrompt);
39+
40+
int inputTokens = 0;
41+
int outputTokens = 0;
42+
int totalTokens = 0;
43+
decimal duration = 0;
44+
45+
await foreach (var result in client.ChatStreamAsync(request))
46+
{
47+
WriteOutput(result.Chunk);
48+
49+
if (result.InputTokens.HasValue) inputTokens = result.InputTokens.Value;
50+
if (result.OutputTokens.HasValue) outputTokens = result.OutputTokens.Value;
51+
if (result.TotalTokens.HasValue) totalTokens = result.TotalTokens.Value;
52+
if (result.Duration.HasValue) duration = result.Duration.Value;
53+
}
54+
55+
WriteOutput(inputTokens, outputTokens, totalTokens, duration);
56+
}
57+
}
58+
}

src/Zatomic.AI.Providers/Exceptions/AIExceptionUtility.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Zatomic.AI.Providers.Extensions;
1010
using Zatomic.AI.Providers.FireworksAI;
1111
using Zatomic.AI.Providers.GoogleGemini;
12+
using Zatomic.AI.Providers.Groq;
1213
using Zatomic.AI.Providers.HuggingFace;
1314
using Zatomic.AI.Providers.Hyperbolic;
1415
using Zatomic.AI.Providers.Lambda;
@@ -67,6 +68,11 @@ public static AIException BuildGoogleGeminiAIException(Exception ex, GoogleGemin
6768
return BuildAIException(ex, "Google Gemini", request.Model, request, responseJson);
6869
}
6970

71+
public static AIException BuildGroqAIException(Exception ex, GroqChatRequest request, string responseJson = null)
72+
{
73+
return BuildAIException(ex, "Groq", request.Model, request, responseJson);
74+
}
75+
7076
public static AIException BuildHuggingFaceAIException(Exception ex, HuggingFaceChatRequest request, string responseJson = null)
7177
{
7278
return BuildAIException(ex, "Hugging Face", request.Model, request, responseJson);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Collections.Generic;
2+
using Newtonsoft.Json;
3+
4+
namespace Zatomic.AI.Providers.Groq
5+
{
6+
public class GroqChatAssistantMessage : GroqChatBaseMessage
7+
{
8+
[JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)]
9+
public string Content { get; set; }
10+
11+
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
12+
public string Name { get; set; }
13+
14+
[JsonProperty("reasoning", NullValueHandling = NullValueHandling.Ignore)]
15+
public string Reasoning { get; set; }
16+
17+
[JsonProperty("role", NullValueHandling = NullValueHandling.Ignore)]
18+
public string Role { get; set; }
19+
20+
[JsonProperty("tool_call_id", NullValueHandling = NullValueHandling.Ignore)]
21+
public string ToolCallId { get; set; }
22+
23+
[JsonProperty("tool_calls", NullValueHandling = NullValueHandling.Ignore)]
24+
public List<GroqChatToolCall> ToolCalls { get; set; }
25+
}
26+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Newtonsoft.Json;
2+
3+
namespace Zatomic.AI.Providers.Groq
4+
{
5+
public abstract class GroqChatBaseContent
6+
{
7+
[JsonProperty("type")]
8+
public string Type { get; set; }
9+
}
10+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Zatomic.AI.Providers.Groq
2+
{
3+
public abstract class GroqChatBaseMessage
4+
{
5+
// Just a marker class for now
6+
}
7+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Newtonsoft.Json;
2+
3+
namespace Zatomic.AI.Providers.Groq
4+
{
5+
public class GroqChatChoice
6+
{
7+
[JsonProperty("delta")]
8+
public GroqChatDelta Delta { get; set; }
9+
10+
[JsonProperty("finish_reason")]
11+
public string FinishReason { get; set; }
12+
13+
[JsonProperty("index")]
14+
public int Index { get; set; }
15+
16+
[JsonProperty("message")]
17+
public GroqChatOutputMessage Message { get; set; }
18+
}
19+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.IO;
5+
using System.Net.Http;
6+
using System.Net.Http.Headers;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using Zatomic.AI.Providers.Exceptions;
10+
using Zatomic.AI.Providers.Extensions;
11+
12+
namespace Zatomic.AI.Providers.Groq
13+
{
14+
public class GroqChatClient : BaseClient
15+
{
16+
public string ApiKey { get; set; }
17+
public string ApiUrl { get; } = "https://api.groq.com/openai/v1/chat/completions";
18+
19+
public GroqChatClient()
20+
{
21+
}
22+
23+
public GroqChatClient(string apiKey) : this()
24+
{
25+
ApiKey = apiKey;
26+
}
27+
28+
public async Task<GroqChatResponse> ChatAsync(GroqChatRequest request)
29+
{
30+
GroqChatResponse response = null;
31+
32+
using (var httpClient = new HttpClient())
33+
{
34+
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ApiKey);
35+
36+
var requestJson = request.Serialize();
37+
var content = new StringContent(requestJson, Encoding.UTF8, "application/json");
38+
39+
string responseJson = null;
40+
41+
try
42+
{
43+
var stopwatch = Stopwatch.StartNew();
44+
45+
var postResponse = await DoWithRetryAsync(() => httpClient.PostAsync(ApiUrl, content));
46+
responseJson = await postResponse.Content.ReadAsStringAsync();
47+
postResponse.EnsureSuccessStatusCode();
48+
49+
stopwatch.Stop();
50+
51+
response = responseJson.Deserialize<GroqChatResponse>();
52+
response.Duration = stopwatch.ToDurationInSeconds(2);
53+
}
54+
catch (Exception ex)
55+
{
56+
var aiEx = AIExceptionUtility.BuildGroqAIException(ex, request, responseJson);
57+
throw aiEx;
58+
}
59+
}
60+
61+
return response;
62+
}
63+
64+
public async IAsyncEnumerable<AIStreamResponse> ChatStreamAsync(GroqChatRequest request)
65+
{
66+
request.Stream = true;
67+
request.StreamOptions = new GroqChatStreamOptions { IncludeUsage = true };
68+
69+
using (var httpClient = new HttpClient())
70+
{
71+
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ApiKey);
72+
73+
var requestJson = request.Serialize();
74+
var postRequest = new HttpRequestMessage(HttpMethod.Post, ApiUrl)
75+
{
76+
Content = new StringContent(requestJson, Encoding.UTF8, "application/json")
77+
};
78+
79+
HttpResponseMessage postResponse = null;
80+
81+
try
82+
{
83+
// This is wrapped in a try-catch in case an error occurs at the start
84+
postResponse = await DoWithRetryAsync(() => httpClient.SendAsync(postRequest, HttpCompletionOption.ResponseHeadersRead));
85+
postResponse.EnsureSuccessStatusCode();
86+
}
87+
catch (Exception ex)
88+
{
89+
var aiEx = AIExceptionUtility.BuildGroqAIException(ex, request);
90+
throw aiEx;
91+
}
92+
93+
var streamComplete = false;
94+
var stopwatch = Stopwatch.StartNew();
95+
96+
using (var stream = await postResponse.Content.ReadAsStreamAsync())
97+
using (var reader = new StreamReader(stream))
98+
{
99+
while (!reader.EndOfStream && !streamComplete)
100+
{
101+
string line;
102+
103+
try
104+
{
105+
// This is wrapped in a try-catch in case an error occurs mid-stream
106+
line = await reader.ReadLineAsync();
107+
}
108+
catch (Exception ex)
109+
{
110+
var aiEx = AIExceptionUtility.BuildGroqAIException(ex, request);
111+
throw aiEx;
112+
}
113+
114+
// Event messages start with "data: ", so that's why we substring the line at 6
115+
if (!line.IsNullOrEmpty() && line.StartsWith("data: "))
116+
{
117+
var streamResponse = new AIStreamResponse();
118+
119+
var rsp = line.Substring(6).Deserialize<GroqChatResponse>();
120+
if (rsp.Choices.Count > 0)
121+
{
122+
streamResponse.Chunk = rsp.Choices[0].Delta.Content;
123+
}
124+
125+
// Using the stream options to include usage means that Groq returns an additional chunk
126+
// with the usage information right before the final [DONE] chunk (whereas all prior chunks
127+
// won't have usage in them). This means that we can key off that to determine if the stream
128+
// is complete instead of looking at the finish reason. The finish reason comes in the chunk
129+
// just before the usage chunk, so if we keyed off that we would never get the usage information.
130+
131+
if (rsp.Usage != null)
132+
{
133+
streamComplete = true;
134+
stopwatch.Stop();
135+
136+
streamResponse.InputTokens = rsp.Usage.PromptTokens;
137+
streamResponse.OutputTokens = rsp.Usage.CompletionTokens;
138+
streamResponse.TotalTokens = rsp.Usage.TotalTokens;
139+
streamResponse.Duration = stopwatch.ToDurationInSeconds(2);
140+
}
141+
142+
yield return streamResponse;
143+
}
144+
}
145+
}
146+
}
147+
}
148+
}
149+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Newtonsoft.Json;
4+
using Newtonsoft.Json.Linq;
5+
6+
namespace Zatomic.AI.Providers.Groq
7+
{
8+
public class GroqChatContentListConverter : JsonConverter<List<GroqChatBaseContent>>
9+
{
10+
public override List<GroqChatBaseContent> ReadJson(JsonReader reader, Type objectType, List<GroqChatBaseContent> existingValue, bool hasExistingValue, JsonSerializer serializer)
11+
{
12+
var array = JArray.Load(reader);
13+
var items = new List<GroqChatBaseContent>();
14+
15+
foreach (var token in array)
16+
{
17+
GroqChatBaseContent item;
18+
19+
var type = token["type"]?.Value<string>();
20+
21+
if (type == "text") item = token.ToObject<GroqChatTextContent>(serializer);
22+
else if (type == "image_url") item = token.ToObject<GroqChatImageUrlContent>(serializer);
23+
else throw new JsonSerializationException($"Unknown content type: {type}");
24+
25+
items.Add(item);
26+
}
27+
28+
return items;
29+
}
30+
31+
public override void WriteJson(JsonWriter writer, List<GroqChatBaseContent> value, JsonSerializer serializer)
32+
{
33+
writer.WriteStartArray();
34+
35+
foreach (var item in value)
36+
{
37+
JToken.FromObject(item, serializer).WriteTo(writer);
38+
}
39+
40+
writer.WriteEndArray();
41+
}
42+
}
43+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Newtonsoft.Json;
2+
3+
namespace Zatomic.AI.Providers.Groq
4+
{
5+
public class GroqChatDelta
6+
{
7+
[JsonProperty("content")]
8+
public string Content { get; set; }
9+
}
10+
}

0 commit comments

Comments
 (0)