Skip to content

Commit 7e6b1c8

Browse files
committed
Add first-class support for Grok unique features
* Live search support: On/Off/Auto setting * Reasoning effort: for grok-3-mini Example: ```csharp var messages = new Chat() { { "system", "You are a highly intelligent AI assistant." }, { "user", "What is 101*3?" }, }; var grok = new GrokClient(Env.Get("XAI_API_KEY")!); var options = new GrokChatOptions { ModelId = "grok-3-mini", // or "grok-3-mini-fast" Temperature = 0.7f, ReasoningEffort = ReasoningEffort.High, // or Low Search = GrokSearch.Auto, // or GrokSearch.On or GrokSearch.Off }; var response = await grok.GetResponseAsync(messages, options); AnsiConsole.MarkupLine($":robot: {response.Text}"); ```
1 parent bb93932 commit 7e6b1c8

File tree

11 files changed

+257
-9
lines changed

11 files changed

+257
-9
lines changed

.netconfig

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,7 @@
9696
[file "Directory.Build.rsp"]
9797
url = https://github.com/devlooped/oss/blob/main/Directory.Build.rsp
9898
sha = 0f7f7f7e8a29de9b535676f75fe7c67e629a5e8c
99-
100-
etag = 0ccae83fc51f400bfd7058170bfec7aba11455e24a46a0d7e6a358da6486e255
101-
weak
99+
skip
102100
[file "_config.yml"]
103101
url = https://github.com/devlooped/oss/blob/main/_config.yml
104102
sha = 68b409c486842062e0de0e5b11e6fdb7cd12d6e2

Directory.Build.rsp

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/AI.Tests/GrokTests.cs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
namespace Devlooped.Extensions.AI;
2+
3+
using Microsoft.Extensions.AI;
4+
using static ConfigurationExtensions;
5+
6+
public class GrokTests
7+
{
8+
[SecretsFact("XAI_API_KEY")]
9+
public async Task GrokInvokesTools()
10+
{
11+
var messages = new Chat()
12+
{
13+
{ "system", "You are a bot that invokes the tool get_date when asked for the date." },
14+
{ "user", "What day is today?" },
15+
};
16+
17+
var grok = new GrokClient(Configuration["XAI_API_KEY"]!);
18+
19+
var options = new GrokChatOptions
20+
{
21+
ModelId = "grok-3-mini",
22+
Search = GrokSearch.Auto,
23+
Tools = [AIFunctionFactory.Create(() => DateTimeOffset.Now.ToString("O"), "get_date")]
24+
};
25+
26+
var response = await grok.GetResponseAsync(messages, options);
27+
var getdate = response.Messages
28+
.SelectMany(x => x.Contents.OfType<FunctionCallContent>())
29+
.Any(x => x.Name == "get_date");
30+
31+
Assert.True(getdate);
32+
}
33+
34+
[SecretsFact("XAI_API_KEY")]
35+
public async Task GrokInvokesToolAndSearch()
36+
{
37+
var messages = new Chat()
38+
{
39+
{ "system", "You are a bot that invokes the tool 'get_date' before responding to anything since it's important context." },
40+
{ "user", "What's Tesla stock worth today?" },
41+
};
42+
43+
var grok = new GrokClient(Configuration["XAI_API_KEY"]!)
44+
.AsBuilder()
45+
.UseFunctionInvocation()
46+
.Build();
47+
48+
var options = new GrokChatOptions
49+
{
50+
ModelId = "grok-3-mini",
51+
Search = GrokSearch.On,
52+
Tools = [AIFunctionFactory.Create(() => DateTimeOffset.Now.ToString("O"), "get_date")]
53+
};
54+
55+
var response = await grok.GetResponseAsync(messages, options);
56+
57+
// The get_date result shows up as a tool role
58+
Assert.Contains(response.Messages, x => x.Role == ChatRole.Tool);
59+
60+
var text = response.Text;
61+
62+
Assert.Contains("TSLA", text);
63+
Assert.Contains("$", text);
64+
Assert.Contains("Nasdaq", text, StringComparison.OrdinalIgnoreCase);
65+
}
66+
67+
[SecretsFact("XAI_API_KEY")]
68+
public async Task GrokThinksHard()
69+
{
70+
var messages = new Chat()
71+
{
72+
{ "system", "You are an intelligent AI assistant that's an expert on financial matters." },
73+
{ "user", "If you have a debt of 100k and accumulate a compounding 5% debt on top of it every year, how long before you are a negative millonaire?" },
74+
};
75+
76+
var grok = new GrokClient(Configuration["XAI_API_KEY"]!)
77+
.AsBuilder()
78+
.UseFunctionInvocation()
79+
.Build();
80+
81+
var options = new GrokChatOptions
82+
{
83+
ModelId = "grok-3-mini",
84+
Search = GrokSearch.Off,
85+
ReasoningEffort = ReasoningEffort.High,
86+
};
87+
88+
var response = await grok.GetResponseAsync(messages, options);
89+
90+
var text = response.Text;
91+
92+
Assert.Contains("48 years", text);
93+
}
94+
}

src/AI/ChatExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ public static class ChatExtensions
2020
public Task<ChatResponse> GetResponseAsync(Chat chat, ChatOptions? options = null, CancellationToken cancellation = default)
2121
=> client.GetResponseAsync((IEnumerable<ChatMessage>)chat, options, cancellation);
2222
}
23-
}
23+
}

src/AI/Grok/GrokChatOptions.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Microsoft.Extensions.AI;
2+
3+
namespace Devlooped.Extensions.AI;
4+
5+
/// <summary>
6+
/// Grok-specific chat options that extend the base <see cref="ChatOptions"/>
7+
/// with <see cref="Search"/> and <see cref="ReasoningEffort"/> properties.
8+
/// </summary>
9+
public class GrokChatOptions : ChatOptions
10+
{
11+
/// <summary>
12+
/// Configures Grok's live search capabilities.
13+
/// See https://docs.x.ai/docs/guides/live-search.
14+
/// </summary>
15+
public GrokSearch Search { get; set; } = GrokSearch.Auto;
16+
17+
/// <summary>
18+
/// Configures the reasoning effort level for Grok's responses.
19+
/// See https://docs.x.ai/docs/guides/reasoning.
20+
/// </summary>
21+
public ReasoningEffort? ReasoningEffort { get; set; }
22+
}

src/AI/Grok/GrokClient.cs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using System.ClientModel;
2+
using System.ClientModel.Primitives;
3+
using System.Collections.Concurrent;
4+
using System.Text.Json;
5+
using Microsoft.Extensions.AI;
6+
using OpenAI;
7+
8+
namespace Devlooped.Extensions.AI;
9+
10+
public class GrokClient(string apiKey, GrokClientOptions options)
11+
: OpenAIClient(new ApiKeyCredential(apiKey), options), IChatClient
12+
{
13+
readonly GrokClientOptions clientOptions = options;
14+
readonly ConcurrentDictionary<string, IChatClient> clients = new();
15+
16+
public GrokClient(string apiKey)
17+
: this(apiKey, new())
18+
{
19+
}
20+
21+
void IDisposable.Dispose() { }
22+
object? IChatClient.GetService(Type serviceType, object? serviceKey) => default;
23+
24+
Task<ChatResponse> IChatClient.GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options, CancellationToken cancellation)
25+
=> GetClient(options).GetResponseAsync(messages, SetOptions(options), cancellation);
26+
27+
IAsyncEnumerable<ChatResponseUpdate> IChatClient.GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options, CancellationToken cancellation)
28+
=> GetClient(options).GetStreamingResponseAsync(messages, SetOptions(options), cancellation);
29+
30+
IChatClient GetClient(ChatOptions? options) => clients.GetOrAdd(
31+
options?.ModelId ?? clientOptions.Model,
32+
model => base.GetChatClient(model).AsIChatClient());
33+
34+
ChatOptions? SetOptions(ChatOptions? options)
35+
{
36+
if (options is null || options is not GrokChatOptions grok)
37+
return null;
38+
39+
options.RawRepresentationFactory = _ =>
40+
{
41+
var result = new GrokCompletionOptions
42+
{
43+
Search = grok.Search
44+
};
45+
46+
if (grok.ReasoningEffort != null)
47+
{
48+
result.ReasoningEffortLevel = grok.ReasoningEffort switch
49+
{
50+
ReasoningEffort.Low => OpenAI.Chat.ChatReasoningEffortLevel.Low,
51+
ReasoningEffort.High => OpenAI.Chat.ChatReasoningEffortLevel.High,
52+
_ => throw new ArgumentException($"Unsupported reasoning effort {grok.ReasoningEffort}")
53+
};
54+
}
55+
56+
return result;
57+
};
58+
59+
return options;
60+
}
61+
62+
class SearchParameters
63+
{
64+
public GrokSearch Mode { get; set; } = GrokSearch.Auto;
65+
}
66+
67+
class GrokCompletionOptions : OpenAI.Chat.ChatCompletionOptions
68+
{
69+
public GrokSearch Search { get; set; } = GrokSearch.Auto;
70+
71+
protected override void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options)
72+
{
73+
base.JsonModelWriteCore(writer, options);
74+
75+
// "search_parameters": { "mode": "auto" }
76+
writer.WritePropertyName("search_parameters");
77+
writer.WriteStartObject();
78+
writer.WriteString("mode", Search.ToString().ToLowerInvariant());
79+
writer.WriteEndObject();
80+
}
81+
}
82+
}
83+

src/AI/Grok/GrokClientOptions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using OpenAI;
2+
3+
namespace Devlooped.Extensions.AI;
4+
5+
public class GrokClientOptions : OpenAIClientOptions
6+
{
7+
public GrokClientOptions() : this("grok-3") { }
8+
9+
public GrokClientOptions(string model)
10+
{
11+
Endpoint = new Uri("https://api.x.ai/v1");
12+
Model = Throw.IfNullOrEmpty(model);
13+
}
14+
15+
public string Model { get; }
16+
}

src/AI/Grok/GrokSearch.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace Devlooped.Extensions.AI;
2+
3+
public enum GrokSearch { Auto, On, Off }
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.ComponentModel;
2+
using Devlooped.Extensions.AI;
3+
using Microsoft.Extensions.AI;
4+
using Microsoft.Extensions.Configuration;
5+
using Microsoft.Extensions.DependencyInjection;
6+
7+
namespace Devlooped.Extensions.AI;
8+
9+
/// <summary>
10+
/// Extensions for registering the <see cref="GrokClient"/> as a chat client in the service collection.
11+
/// </summary>
12+
[EditorBrowsable(EditorBrowsableState.Never)]
13+
public static class GrokServiceCollectionExtensions
14+
{
15+
extension(IServiceCollection services)
16+
{
17+
/// <summary>
18+
/// Registers the <see cref="GrokClient"/> as a chat client in the service collection.
19+
/// </summary>
20+
/// <param name="factory">The factory to create the Grok client.</param>
21+
/// <param name="lifetime">The optional service lifetime.</param>
22+
/// <returns>The <see cref="ChatClientBuilder"/> to further build the pipeline.</returns>
23+
public ChatClientBuilder AddGrok(Func<IConfiguration, GrokClient> factory, ServiceLifetime lifetime = ServiceLifetime.Singleton)
24+
=> services.AddChatClient(services
25+
=> factory(services.GetRequiredService<IConfiguration>()), lifetime);
26+
}
27+
}

src/AI/ReasoningEffort.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Devlooped.Extensions.AI;
2+
3+
/// <summary>
4+
/// Reasoning effort an AI should apply when generating a response.
5+
/// </summary>
6+
public enum ReasoningEffort { Low, High }

0 commit comments

Comments
 (0)