From 74b37cd74b36a8849bcd7a58eeb5a3b0c69ba52d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 15 Jan 2026 23:15:21 +0000
Subject: [PATCH 1/2] Initial plan
From b2f4a0f340eb2f5ce107f0af84e39a2ee197278f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 15 Jan 2026 23:33:43 +0000
Subject: [PATCH 2/2] Replace Extensions.Grok project with xAI NuGet package
Co-authored-by: kzu <169707+kzu@users.noreply.github.com>
---
Extensions.AI.slnx | 1 -
readme.md | 180 +---
src/Agents/ConfigurableAIAgent.cs | 2 +-
src/Extensions.Grok/Extensions.Grok.csproj | 32 -
src/Extensions.Grok/Extensions/Throw.cs | 992 ------------------
src/Extensions.Grok/GrokChatClient.cs | 465 --------
src/Extensions.Grok/GrokChatOptions.cs | 41 -
src/Extensions.Grok/GrokClient.cs | 47 -
src/Extensions.Grok/GrokClientExtensions.cs | 13 -
src/Extensions.Grok/GrokClientOptions.cs | 16 -
src/Extensions.Grok/GrokSearchTool.cs | 23 -
src/Extensions.Grok/GrokXSearch.cs | 24 -
src/Extensions.Grok/HostedToolCallContent.cs | 13 -
.../HostedToolResultContent.cs | 18 -
src/Extensions.Grok/readme.md | 9 -
src/Extensions/ConfigurableChatClient.cs | 2 +-
src/Extensions/Extensions.csproj | 2 +-
src/Tests/GrokTests.cs | 506 ---------
src/Tests/Tests.csproj | 1 -
19 files changed, 6 insertions(+), 2381 deletions(-)
delete mode 100644 src/Extensions.Grok/Extensions.Grok.csproj
delete mode 100644 src/Extensions.Grok/Extensions/Throw.cs
delete mode 100644 src/Extensions.Grok/GrokChatClient.cs
delete mode 100644 src/Extensions.Grok/GrokChatOptions.cs
delete mode 100644 src/Extensions.Grok/GrokClient.cs
delete mode 100644 src/Extensions.Grok/GrokClientExtensions.cs
delete mode 100644 src/Extensions.Grok/GrokClientOptions.cs
delete mode 100644 src/Extensions.Grok/GrokSearchTool.cs
delete mode 100644 src/Extensions.Grok/GrokXSearch.cs
delete mode 100644 src/Extensions.Grok/HostedToolCallContent.cs
delete mode 100644 src/Extensions.Grok/HostedToolResultContent.cs
delete mode 100644 src/Extensions.Grok/readme.md
delete mode 100644 src/Tests/GrokTests.cs
diff --git a/Extensions.AI.slnx b/Extensions.AI.slnx
index 0e6ed7b..9690911 100644
--- a/Extensions.AI.slnx
+++ b/Extensions.AI.slnx
@@ -5,7 +5,6 @@
-
diff --git a/readme.md b/readme.md
index 6f5d754..10c83ed 100644
--- a/readme.md
+++ b/readme.md
@@ -261,184 +261,10 @@ IChatClient chat = new OpenAIChatClient(Environment.GetEnvironmentVariable("OPEN
```
-# Devlooped.Extensions.AI.Grok
+## xAI/Grok
-[](https://www.nuget.org/packages/Devlooped.Extensions.AI.Grok)
-[](https://www.nuget.org/packages/Devlooped.Extensions.AI.Grok)
-
-
-Microsoft.Extensions.AI `IChatClient` for Grok with full support for all
-[agentic tools](https://docs.x.ai/docs/guides/tools/overview):
-
-```csharp
-var grok = new GrokClient(Environment.GetEnvironmentVariable("XAI_API_KEY")!)
- .AsIChatClient("grok-4.1-fast");
-```
-
-
-## Web Search
-
-```csharp
-var messages = new Chat()
-{
- { "system", "You are an AI assistant that knows how to search the web." },
- { "user", "What's Tesla stock worth today? Search X and the news for latest info." },
-};
-
-var grok = new GrokClient(Environment.GetEnvironmentVariable("XAI_API_KEY")!).AsIChatClient("grok-4.1-fast");
-
-var options = new ChatOptions
-{
- Tools = [new HostedWebSearchTool()] // 👈 compatible with OpenAI
-};
-
-var response = await grok.GetResponseAsync(messages, options);
-```
-
-In addition to basic web search as shown above, Grok supports more
-[advanced search](https://docs.x.ai/docs/guides/tools/search-tools) scenarios,
-which can be opted-in by using Grok-specific types:
-
-```csharp
-var grok = new GrokChatClient(Environment.GetEnvironmentVariable("XAI_API_KEY")!).AsIChatClient("grok-4.1-fast");
-var response = await grok.GetResponseAsync(
- "What are the latest product news by Tesla?",
- new ChatOptions
- {
- Tools = [new GrokSearchTool()
- {
- AllowedDomains = [ "ir.tesla.com" ]
- }]
- });
-```
-
-You can alternatively set `ExcludedDomains` instead, and enable image
-understanding with `EnableImageUndestanding`. Learn more about these filters
-at [web search parameters](https://docs.x.ai/docs/guides/tools/search-tools#web-search-parameters).
-
-## X Search
-
-In addition to web search, Grok also supports searching on X (formerly Twitter):
-
-```csharp
-var response = await grok.GetResponseAsync(
- "What's the latest on Optimus?",
- new ChatOptions
- {
- Tools = [new GrokXSearchTool
- {
- // AllowedHandles = [...],
- // ExcludedHandles = [...],
- // EnableImageUnderstanding = true,
- // EnableVideoUnderstanding = true,
- // FromDate = ...,
- // ToDate = ...,
- }]
- });
-```
-
-Learn more about available filters at [X search parameters](https://docs.x.ai/docs/guides/tools/search-tools#x-search-parameters).
-
-You can combine both web and X search in the same request by adding both tools.
-
-## Code Execution
-
-The code execution tool enables Grok to write and execute Python code in real-time,
-dramatically expanding its capabilities beyond text generation. This powerful feature
-allows Grok to perform precise calculations, complex data analysis, statistical
-computations, and solve mathematical problems that would be impossible through text alone.
-
-This is Grok's equivalent of the OpenAI code interpreter, and is configured the same way:
-
-```csharp
-var grok = new GrokClient(Configuration["XAI_API_KEY"]!).AsIChatClient("grok-4-fast");
-var response = await grok.GetResponseAsync(
- "Calculate the compound interest for $10,000 at 5% annually for 10 years",
- new ChatOptions
- {
- Tools = [new HostedCodeInterpreterTool()]
- });
-
-var text = response.Text;
-Assert.Contains("$6,288.95", text);
-```
-
-If you want to access the output from the code execution, you can add that as an
-include in the options:
-
-```csharp
-var grok = new GrokClient(Configuration["XAI_API_KEY"]!).AsIChatClient("grok-4-fast");
-var options = new GrokChatOptions
-{
- Include = { IncludeOption.CodeExecutionCallOutput },
- Tools = [new HostedCodeInterpreterTool()]
-};
-
-var response = await grok.GetResponseAsync(
- "Calculate the compound interest for $10,000 at 5% annually for 10 years",
- options);
-
-var content = response.Messages
- .SelectMany(x => x.Contents)
- .OfType()
- .First();
-
-foreach (AIContent output in content.Outputs)
- // process outputs from code interpreter
-```
-
-Learn more about the [code execution tool](https://docs.x.ai/docs/guides/tools/code-execution-tool).
-
-## Collection Search
-
-If you maintain a [collection](https://docs.x.ai/docs/key-information/collections),
-Grok can perform semantic search on it:
-
-```csharp
-var options = new ChatOptions
-{
- Tools = [new HostedFileSearchTool {
- Inputs = [new HostedVectorStoreContent("[collection_id]")]
- }]
-};
-```
-
-Learn more about [collection search](https://docs.x.ai/docs/guides/tools/collections-search-tool).
-
-## Remote MCP
-
-Remote MCP Tools allow Grok to connect to external MCP (Model Context Protocol) servers.
-This example sets up the GitHub MCP server so queries about releases (limited specifically
-in this case):
-
-```csharp
-var options = new ChatOptions
-{
- Tools = [new HostedMcpServerTool("GitHub", "https://api.githubcopilot.com/mcp/") {
- AuthorizationToken = Configuration["GITHUB_TOKEN"]!,
- AllowedTools = ["list_releases"],
- }]
-};
-```
-
-Just like with code execution, you can opt-in to surfacing the MCP outputs in
-the response:
-
-```csharp
-var options = new GrokChatOptions
-{
- // Exposes McpServerToolResultContent in responses
- Include = { IncludeOption.McpCallOutput },
- Tools = [new HostedMcpServerTool("GitHub", "https://api.githubcopilot.com/mcp/") {
- AuthorizationToken = Configuration["GITHUB_TOKEN"]!,
- AllowedTools = ["list_releases"],
- }]
-};
-
-```
-
-Learn more about [Remote MCP tools](https://docs.x.ai/docs/guides/tools/remote-mcp-tools).
-
+For Grok integration with full support for all [agentic tools](https://docs.x.ai/docs/guides/tools/overview),
+use the [xAI](https://www.nuget.org/packages/xAI) package which provides the same behavior.
# Sponsors
diff --git a/src/Agents/ConfigurableAIAgent.cs b/src/Agents/ConfigurableAIAgent.cs
index c24176a..e5e28a6 100644
--- a/src/Agents/ConfigurableAIAgent.cs
+++ b/src/Agents/ConfigurableAIAgent.cs
@@ -2,12 +2,12 @@
using System.Diagnostics;
using System.Text.Json;
using Devlooped.Extensions.AI;
-using Devlooped.Extensions.AI.Grok;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using xAI;
namespace Devlooped.Agents.AI;
diff --git a/src/Extensions.Grok/Extensions.Grok.csproj b/src/Extensions.Grok/Extensions.Grok.csproj
deleted file mode 100644
index 9cbaf07..0000000
--- a/src/Extensions.Grok/Extensions.Grok.csproj
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
- net8.0;net10.0
- Devlooped.Extensions.AI.Grok
- $(AssemblyName)
- $(AssemblyName)
- Grok implementation for Microsoft.Extensions.AI
-
- OSMFEULA.txt
- true
- true
- MEAI001;DEAI001;$(NoWarn)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Extensions.Grok/Extensions/Throw.cs b/src/Extensions.Grok/Extensions/Throw.cs
deleted file mode 100644
index eea3e12..0000000
--- a/src/Extensions.Grok/Extensions/Throw.cs
+++ /dev/null
@@ -1,992 +0,0 @@
-//
-#region License
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// Adapted from https://github.com/dotnet/extensions/blob/main/src/Shared/Throw/Throw.cs
-#endregion
-
-#nullable enable
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Runtime.CompilerServices;
-
-#pragma warning disable CA1716
-namespace System;
-#pragma warning restore CA1716
-
-///
-/// Defines static methods used to throw exceptions.
-///
-///
-/// The main purpose is to reduce code size, improve performance, and standardize exception
-/// messages.
-///
-[SuppressMessage("Minor Code Smell", "S4136:Method overloads should be grouped together", Justification = "Doesn't work with the region layout")]
-[SuppressMessage("Minor Code Smell", "S2333:Partial is gratuitous in this context", Justification = "Some projects add additional partial parts.")]
-[SuppressMessage("Design", "CA1716", Justification = "Not part of an API")]
-
-#if !SHARED_PROJECT
-[ExcludeFromCodeCoverage]
-#endif
-
-static partial class Throw
-{
- #region For Object
-
- ///
- /// Throws an if the specified argument is .
- ///
- /// Argument type to be checked for .
- /// Object to be checked for .
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- [return: NotNull]
- public static T IfNull([NotNull] T argument, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument is null)
- {
- ArgumentNullException(paramName);
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified argument is ,
- /// or if the specified member is .
- ///
- /// Argument type to be checked for .
- /// Member type to be checked for .
- /// Argument to be checked for .
- /// Object member to be checked for .
- /// The name of the parameter being checked.
- /// The name of the member.
- /// The original value of .
- ///
- ///
- /// Throws.IfNullOrMemberNull(myObject, myObject?.MyProperty)
- ///
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- [return: NotNull]
- public static TMember IfNullOrMemberNull(
- [NotNull] TParameter argument,
- [NotNull] TMember member,
- [CallerArgumentExpression(nameof(argument))] string paramName = "",
- [CallerArgumentExpression(nameof(member))] string memberName = "")
- {
- if (argument is null)
- {
- ArgumentNullException(paramName);
- }
-
- if (member is null)
- {
- ArgumentException(paramName, $"Member {memberName} of {paramName} is null");
- }
-
- return member;
- }
-
- ///
- /// Throws an if the specified member is .
- ///
- /// Argument type.
- /// Member type to be checked for .
- /// Argument to which member belongs.
- /// Object member to be checked for .
- /// The name of the parameter being checked.
- /// The name of the member.
- /// The original value of .
- ///
- ///
- /// Throws.IfMemberNull(myObject, myObject.MyProperty)
- ///
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- [return: NotNull]
- [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Analyzer isn't seeing the reference to 'argument' in the attribute")]
- public static TMember IfMemberNull(
- TParameter argument,
- [NotNull] TMember member,
- [CallerArgumentExpression(nameof(argument))] string paramName = "",
- [CallerArgumentExpression(nameof(member))] string memberName = "")
- where TParameter : notnull
- {
- if (member is null)
- {
- ArgumentException(paramName, $"Member {memberName} of {paramName} is null");
- }
-
- return member;
- }
-
- #endregion
-
- #region For String
-
- ///
- /// Throws either an or an
- /// if the specified string is or whitespace respectively.
- ///
- /// String to be checked for or whitespace.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- [return: NotNull]
- public static string IfNullOrWhitespace([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
-#if !NETCOREAPP3_1_OR_GREATER
- if (argument == null)
- {
- ArgumentNullException(paramName);
- }
-#endif
-
- if (string.IsNullOrWhiteSpace(argument))
- {
- if (argument == null)
- {
- ArgumentNullException(paramName);
- }
- else
- {
- ArgumentException(paramName, "Argument is whitespace");
- }
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the string is ,
- /// or if it is empty.
- ///
- /// String to be checked for or empty.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- [return: NotNull]
- public static string IfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
-#if !NETCOREAPP3_1_OR_GREATER
- if (argument == null)
- {
- ArgumentNullException(paramName);
- }
-#endif
-
- if (string.IsNullOrEmpty(argument))
- {
- if (argument == null)
- {
- ArgumentNullException(paramName);
- }
- else
- {
- ArgumentException(paramName, "Argument is an empty string");
- }
- }
-
- return argument;
- }
-
- #endregion
-
- #region For Buffer
-
- ///
- /// Throws an if the argument's buffer size is less than the required buffer size.
- ///
- /// The actual buffer size.
- /// The required buffer size.
- /// The name of the parameter to be checked.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void IfBufferTooSmall(int bufferSize, int requiredSize, string paramName = "")
- {
- if (bufferSize < requiredSize)
- {
- ArgumentException(paramName, $"Buffer too small, needed a size of {requiredSize} but got {bufferSize}");
- }
- }
-
- #endregion
-
- #region For Enums
-
- ///
- /// Throws an if the enum value is not valid.
- ///
- /// The argument to evaluate.
- /// The name of the parameter being checked.
- /// The type of the enumeration.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static T IfOutOfRange(T argument, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- where T : struct, Enum
- {
-#if NET5_0_OR_GREATER
- if (!Enum.IsDefined(argument))
-#else
- if (!Enum.IsDefined(typeof(T), argument))
-#endif
- {
- ArgumentOutOfRangeException(paramName, $"{argument} is an invalid value for enum type {typeof(T)}");
- }
-
- return argument;
- }
-
- #endregion
-
- #region For Collections
-
- ///
- /// Throws an if the collection is ,
- /// or if it is empty.
- ///
- /// The collection to evaluate.
- /// The name of the parameter being checked.
- /// The type of objects in the collection.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- [return: NotNull]
-
- // The method has actually 100% coverage, but due to a bug in the code coverage tool,
- // a lower number is reported. Therefore, we temporarily exclude this method
- // from the coverage measurements. Once the bug in the code coverage tool is fixed,
- // the exclusion attribute can be removed.
- [ExcludeFromCodeCoverage]
- public static IEnumerable IfNullOrEmpty([NotNull] IEnumerable? argument, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument == null)
- {
- ArgumentNullException(paramName);
- }
- else
- {
- switch (argument)
- {
- case ICollection collection:
- if (collection.Count == 0)
- {
- ArgumentException(paramName, "Collection is empty");
- }
-
- break;
- case IReadOnlyCollection readOnlyCollection:
- if (readOnlyCollection.Count == 0)
- {
- ArgumentException(paramName, "Collection is empty");
- }
-
- break;
- default:
- using (IEnumerator enumerator = argument.GetEnumerator())
- {
- if (!enumerator.MoveNext())
- {
- ArgumentException(paramName, "Collection is empty");
- }
- }
-
- break;
- }
- }
-
- return argument;
- }
-
- #endregion
-
- #region Exceptions
-
- ///
- /// Throws an .
- ///
- /// The name of the parameter that caused the exception.
-#if !NET6_0_OR_GREATER
- [MethodImpl(MethodImplOptions.NoInlining)]
-#endif
- [DoesNotReturn]
- public static void ArgumentNullException(string paramName)
- => throw new ArgumentNullException(paramName);
-
- ///
- /// Throws an .
- ///
- /// The name of the parameter that caused the exception.
- /// A message that describes the error.
-#if !NET6_0_OR_GREATER
- [MethodImpl(MethodImplOptions.NoInlining)]
-#endif
- [DoesNotReturn]
- public static void ArgumentNullException(string paramName, string? message)
- => throw new ArgumentNullException(paramName, message);
-
- ///
- /// Throws an .
- ///
- /// The name of the parameter that caused the exception.
-#if !NET6_0_OR_GREATER
- [MethodImpl(MethodImplOptions.NoInlining)]
-#endif
- [DoesNotReturn]
- public static void ArgumentOutOfRangeException(string paramName)
- => throw new ArgumentOutOfRangeException(paramName);
-
- ///
- /// Throws an .
- ///
- /// The name of the parameter that caused the exception.
- /// A message that describes the error.
-#if !NET6_0_OR_GREATER
- [MethodImpl(MethodImplOptions.NoInlining)]
-#endif
- [DoesNotReturn]
- public static void ArgumentOutOfRangeException(string paramName, string? message)
- => throw new ArgumentOutOfRangeException(paramName, message);
-
- ///
- /// Throws an .
- ///
- /// The name of the parameter that caused the exception.
- /// The value of the argument that caused this exception.
- /// A message that describes the error.
-#if !NET6_0_OR_GREATER
- [MethodImpl(MethodImplOptions.NoInlining)]
-#endif
- [DoesNotReturn]
- public static void ArgumentOutOfRangeException(string paramName, object? actualValue, string? message)
- => throw new ArgumentOutOfRangeException(paramName, actualValue, message);
-
- ///
- /// Throws an .
- ///
- /// The name of the parameter that caused the exception.
- /// A message that describes the error.
-#if !NET6_0_OR_GREATER
- [MethodImpl(MethodImplOptions.NoInlining)]
-#endif
- [DoesNotReturn]
- public static void ArgumentException(string paramName, string? message)
- => throw new ArgumentException(message, paramName);
-
- ///
- /// Throws an .
- ///
- /// The name of the parameter that caused the exception.
- /// A message that describes the error.
- /// The exception that is the cause of the current exception.
- ///
- /// If the is not a , the current exception is raised in a catch
- /// block that handles the inner exception.
- ///
-#if !NET6_0_OR_GREATER
- [MethodImpl(MethodImplOptions.NoInlining)]
-#endif
- [DoesNotReturn]
- public static void ArgumentException(string paramName, string? message, Exception? innerException)
- => throw new ArgumentException(message, paramName, innerException);
-
- ///
- /// Throws an .
- ///
- /// A message that describes the error.
-#if !NET6_0_OR_GREATER
- [MethodImpl(MethodImplOptions.NoInlining)]
-#endif
- [DoesNotReturn]
- public static void InvalidOperationException(string message)
- => throw new InvalidOperationException(message);
-
- ///
- /// Throws an .
- ///
- /// A message that describes the error.
- /// The exception that is the cause of the current exception.
-#if !NET6_0_OR_GREATER
- [MethodImpl(MethodImplOptions.NoInlining)]
-#endif
- [DoesNotReturn]
- public static void InvalidOperationException(string message, Exception? innerException)
- => throw new InvalidOperationException(message, innerException);
-
- #endregion
-
- #region For Integer
-
- ///
- /// Throws an if the specified number is less than min.
- ///
- /// Number to be expected being less than min.
- /// The number that must be less than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static int IfLessThan(int argument, int min, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument < min)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is greater than max.
- ///
- /// Number to be expected being greater than max.
- /// The number that must be greater than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static int IfGreaterThan(int argument, int max, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument > max)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument greater than maximum value {max}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is less or equal than min.
- ///
- /// Number to be expected being less or equal than min.
- /// The number that must be less or equal than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static int IfLessThanOrEqual(int argument, int min, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument <= min)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument less or equal than minimum value {min}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is greater or equal than max.
- ///
- /// Number to be expected being greater or equal than max.
- /// The number that must be greater or equal than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static int IfGreaterThanOrEqual(int argument, int max, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument >= max)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument greater or equal than maximum value {max}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is not in the specified range.
- ///
- /// Number to be expected being greater or equal than max.
- /// The lower bound of the allowed range of argument values.
- /// The upper bound of the allowed range of argument values.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static int IfOutOfRange(int argument, int min, int max, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument < min || argument > max)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument not in the range [{min}..{max}]");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is equal to 0.
- ///
- /// Number to be expected being not equal to zero.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static int IfZero(int argument, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument == 0)
- {
- ArgumentOutOfRangeException(paramName, "Argument is zero");
- }
-
- return argument;
- }
-
- #endregion
-
- #region For Unsigned Integer
-
- ///
- /// Throws an if the specified number is less than min.
- ///
- /// Number to be expected being less than min.
- /// The number that must be less than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static uint IfLessThan(uint argument, uint min, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument < min)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is greater than max.
- ///
- /// Number to be expected being greater than max.
- /// The number that must be greater than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static uint IfGreaterThan(uint argument, uint max, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument > max)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument greater than maximum value {max}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is less or equal than min.
- ///
- /// Number to be expected being less or equal than min.
- /// The number that must be less or equal than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static uint IfLessThanOrEqual(uint argument, uint min, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument <= min)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument less or equal than minimum value {min}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is greater or equal than max.
- ///
- /// Number to be expected being greater or equal than max.
- /// The number that must be greater or equal than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static uint IfGreaterThanOrEqual(uint argument, uint max, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument >= max)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument greater or equal than maximum value {max}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is not in the specified range.
- ///
- /// Number to be expected being greater or equal than max.
- /// The lower bound of the allowed range of argument values.
- /// The upper bound of the allowed range of argument values.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static uint IfOutOfRange(uint argument, uint min, uint max, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument < min || argument > max)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument not in the range [{min}..{max}]");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is equal to 0.
- ///
- /// Number to be expected being not equal to zero.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static uint IfZero(uint argument, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument == 0U)
- {
- ArgumentOutOfRangeException(paramName, "Argument is zero");
- }
-
- return argument;
- }
-
- #endregion
-
- #region For Long
-
- ///
- /// Throws an if the specified number is less than min.
- ///
- /// Number to be expected being less than min.
- /// The number that must be less than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static long IfLessThan(long argument, long min, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument < min)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is greater than max.
- ///
- /// Number to be expected being greater than max.
- /// The number that must be greater than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static long IfGreaterThan(long argument, long max, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument > max)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument greater than maximum value {max}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is less or equal than min.
- ///
- /// Number to be expected being less or equal than min.
- /// The number that must be less or equal than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static long IfLessThanOrEqual(long argument, long min, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument <= min)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument less or equal than minimum value {min}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is greater or equal than max.
- ///
- /// Number to be expected being greater or equal than max.
- /// The number that must be greater or equal than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static long IfGreaterThanOrEqual(long argument, long max, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument >= max)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument greater or equal than maximum value {max}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is not in the specified range.
- ///
- /// Number to be expected being greater or equal than max.
- /// The lower bound of the allowed range of argument values.
- /// The upper bound of the allowed range of argument values.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static long IfOutOfRange(long argument, long min, long max, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument < min || argument > max)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument not in the range [{min}..{max}]");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is equal to 0.
- ///
- /// Number to be expected being not equal to zero.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static long IfZero(long argument, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument == 0L)
- {
- ArgumentOutOfRangeException(paramName, "Argument is zero");
- }
-
- return argument;
- }
-
- #endregion
-
- #region For Unsigned Long
-
- ///
- /// Throws an if the specified number is less than min.
- ///
- /// Number to be expected being less than min.
- /// The number that must be less than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static ulong IfLessThan(ulong argument, ulong min, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument < min)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is greater than max.
- ///
- /// Number to be expected being greater than max.
- /// The number that must be greater than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static ulong IfGreaterThan(ulong argument, ulong max, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument > max)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument greater than maximum value {max}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is less or equal than min.
- ///
- /// Number to be expected being less or equal than min.
- /// The number that must be less or equal than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static ulong IfLessThanOrEqual(ulong argument, ulong min, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument <= min)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument less or equal than minimum value {min}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is greater or equal than max.
- ///
- /// Number to be expected being greater or equal than max.
- /// The number that must be greater or equal than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static ulong IfGreaterThanOrEqual(ulong argument, ulong max, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument >= max)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument greater or equal than maximum value {max}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is not in the specified range.
- ///
- /// Number to be expected being greater or equal than max.
- /// The lower bound of the allowed range of argument values.
- /// The upper bound of the allowed range of argument values.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static ulong IfOutOfRange(ulong argument, ulong min, ulong max, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument < min || argument > max)
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument not in the range [{min}..{max}]");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is equal to 0.
- ///
- /// Number to be expected being not equal to zero.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static ulong IfZero(ulong argument, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- if (argument == 0UL)
- {
- ArgumentOutOfRangeException(paramName, "Argument is zero");
- }
-
- return argument;
- }
-
- #endregion
-
- #region For Double
-
- ///
- /// Throws an if the specified number is less than min.
- ///
- /// Number to be expected being less than min.
- /// The number that must be less than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static double IfLessThan(double argument, double min, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- // strange conditional needed in order to handle NaN values correctly
-#pragma warning disable S1940 // Boolean checks should not be inverted
- if (!(argument >= min))
-#pragma warning restore S1940 // Boolean checks should not be inverted
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is greater than max.
- ///
- /// Number to be expected being greater than max.
- /// The number that must be greater than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static double IfGreaterThan(double argument, double max, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- // strange conditional needed in order to handle NaN values correctly
-#pragma warning disable S1940 // Boolean checks should not be inverted
- if (!(argument <= max))
-#pragma warning restore S1940 // Boolean checks should not be inverted
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument greater than maximum value {max}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is less or equal than min.
- ///
- /// Number to be expected being less or equal than min.
- /// The number that must be less or equal than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static double IfLessThanOrEqual(double argument, double min, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- // strange conditional needed in order to handle NaN values correctly
-#pragma warning disable S1940 // Boolean checks should not be inverted
- if (!(argument > min))
-#pragma warning restore S1940 // Boolean checks should not be inverted
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument less or equal than minimum value {min}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is greater or equal than max.
- ///
- /// Number to be expected being greater or equal than max.
- /// The number that must be greater or equal than the argument.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static double IfGreaterThanOrEqual(double argument, double max, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- // strange conditional needed in order to handle NaN values correctly
-#pragma warning disable S1940 // Boolean checks should not be inverted
- if (!(argument < max))
-#pragma warning restore S1940 // Boolean checks should not be inverted
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument greater or equal than maximum value {max}");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is not in the specified range.
- ///
- /// Number to be expected being greater or equal than max.
- /// The lower bound of the allowed range of argument values.
- /// The upper bound of the allowed range of argument values.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static double IfOutOfRange(double argument, double min, double max, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
- // strange conditional needed in order to handle NaN values correctly
- if (!(min <= argument && argument <= max))
- {
- ArgumentOutOfRangeException(paramName, argument, $"Argument not in the range [{min}..{max}]");
- }
-
- return argument;
- }
-
- ///
- /// Throws an if the specified number is equal to 0.
- ///
- /// Number to be expected being not equal to zero.
- /// The name of the parameter being checked.
- /// The original value of .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static double IfZero(double argument, [CallerArgumentExpression(nameof(argument))] string paramName = "")
- {
-#pragma warning disable S1244 // Floating point numbers should not be tested for equality
- if (argument == 0.0)
-#pragma warning restore S1244 // Floating point numbers should not be tested for equality
- {
- ArgumentOutOfRangeException(paramName, "Argument is zero");
- }
-
- return argument;
- }
-
- #endregion
-}
diff --git a/src/Extensions.Grok/GrokChatClient.cs b/src/Extensions.Grok/GrokChatClient.cs
deleted file mode 100644
index 48eee2a..0000000
--- a/src/Extensions.Grok/GrokChatClient.cs
+++ /dev/null
@@ -1,465 +0,0 @@
-using System.Text.Json;
-using Devlooped.Grok;
-using Grpc.Core;
-using Grpc.Net.Client;
-using Microsoft.Extensions.AI;
-using static Devlooped.Grok.Chat;
-
-namespace Devlooped.Extensions.AI.Grok;
-
-class GrokChatClient : IChatClient
-{
- readonly ChatClientMetadata metadata;
- readonly ChatClient client;
- readonly string defaultModelId;
- readonly GrokClientOptions clientOptions;
-
- internal GrokChatClient(GrpcChannel channel, GrokClientOptions clientOptions, string defaultModelId)
- : this(new ChatClient(channel), clientOptions, defaultModelId)
- { }
-
- ///
- /// Test constructor.
- ///
- internal GrokChatClient(ChatClient client, string defaultModelId)
- : this(client, new(), defaultModelId)
- { }
-
- GrokChatClient(ChatClient client, GrokClientOptions clientOptions, string defaultModelId)
- {
- this.client = client;
- this.clientOptions = clientOptions;
- this.defaultModelId = defaultModelId;
- metadata = new ChatClientMetadata("xai", clientOptions.Endpoint, defaultModelId);
- }
-
- public async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
- {
- var request = MapToRequest(messages, options);
- var response = await client.GetCompletionAsync(request, cancellationToken: cancellationToken);
- var lastOutput = response.Outputs.OrderByDescending(x => x.Index).FirstOrDefault();
-
- if (lastOutput == null)
- {
- return new ChatResponse()
- {
- ResponseId = response.Id,
- ModelId = response.Model,
- CreatedAt = response.Created.ToDateTimeOffset(),
- Usage = MapToUsage(response.Usage),
- };
- }
-
- var message = new ChatMessage(MapRole(lastOutput.Message.Role), default(string));
- var citations = response.Citations?.Distinct().Select(MapCitation).ToList();
-
- foreach (var output in response.Outputs.OrderBy(x => x.Index))
- {
- if (output.Message.Content is { Length: > 0 } text)
- {
- // Special-case output from tools
- if (output.Message.Role == MessageRole.RoleTool &&
- output.Message.ToolCalls.Count == 1 &&
- output.Message.ToolCalls[0] is { } toolCall)
- {
- if (toolCall.Type == ToolCallType.McpTool)
- {
- message.Contents.Add(new McpServerToolCallContent(toolCall.Id, toolCall.Function.Name, null)
- {
- RawRepresentation = toolCall
- });
- message.Contents.Add(new McpServerToolResultContent(toolCall.Id)
- {
- RawRepresentation = toolCall,
- Output = [new TextContent(text)]
- });
- continue;
- }
- else if (toolCall.Type == ToolCallType.CodeExecutionTool)
- {
- message.Contents.Add(new CodeInterpreterToolCallContent()
- {
- CallId = toolCall.Id,
- RawRepresentation = toolCall
- });
- message.Contents.Add(new CodeInterpreterToolResultContent()
- {
- CallId = toolCall.Id,
- RawRepresentation = toolCall,
- Outputs = [new TextContent(text)]
- });
- continue;
- }
- }
-
- var content = new TextContent(text) { Annotations = citations };
-
- foreach (var citation in output.Message.Citations)
- (content.Annotations ??= []).Add(MapInlineCitation(citation));
-
- message.Contents.Add(content);
- }
-
- foreach (var toolCall in output.Message.ToolCalls)
- message.Contents.Add(MapToolCall(toolCall));
- }
-
- return new ChatResponse(message)
- {
- ResponseId = response.Id,
- ModelId = response.Model,
- CreatedAt = response.Created?.ToDateTimeOffset(),
- FinishReason = lastOutput != null ? MapFinishReason(lastOutput.FinishReason) : null,
- Usage = MapToUsage(response.Usage),
- };
- }
-
- AIContent MapToolCall(ToolCall toolCall) => toolCall.Type switch
- {
- ToolCallType.ClientSideTool => new FunctionCallContent(
- toolCall.Id,
- toolCall.Function.Name,
- !string.IsNullOrEmpty(toolCall.Function.Arguments)
- ? JsonSerializer.Deserialize>(toolCall.Function.Arguments)
- : null)
- {
- RawRepresentation = toolCall
- },
- ToolCallType.McpTool => new McpServerToolCallContent(toolCall.Id, toolCall.Function.Name, null)
- {
- RawRepresentation = toolCall
- },
- ToolCallType.CodeExecutionTool => new CodeInterpreterToolCallContent()
- {
- CallId = toolCall.Id,
- RawRepresentation = toolCall
- },
- _ => new HostedToolCallContent()
- {
- CallId = toolCall.Id,
- RawRepresentation = toolCall
- }
- };
-
- public IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
- {
- return CompleteChatStreamingCore(messages, options, cancellationToken);
-
- async IAsyncEnumerable CompleteChatStreamingCore(IEnumerable messages, ChatOptions? options, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
- {
- var request = MapToRequest(messages, options);
- var call = client.GetCompletionChunk(request, cancellationToken: cancellationToken);
-
- await foreach (var chunk in call.ResponseStream.ReadAllAsync(cancellationToken))
- {
- var output = chunk.Outputs[0];
- var text = output.Delta.Content is { Length: > 0 } delta ? delta : null;
-
- // Use positional arguments for ChatResponseUpdate
- var update = new ChatResponseUpdate(MapRole(output.Delta.Role), text)
- {
- ResponseId = chunk.Id,
- ModelId = chunk.Model,
- CreatedAt = chunk.Created?.ToDateTimeOffset(),
- FinishReason = output.FinishReason != FinishReason.ReasonInvalid ? MapFinishReason(output.FinishReason) : null,
- };
-
- if (chunk.Citations is { Count: > 0 } citations)
- {
- var textContent = update.Contents.OfType().FirstOrDefault();
- if (textContent == null)
- {
- textContent = new TextContent(string.Empty);
- update.Contents.Add(textContent);
- }
-
- foreach (var citation in citations.Distinct())
- (textContent.Annotations ??= []).Add(MapCitation(citation));
- }
-
- foreach (var toolCall in output.Delta.ToolCalls)
- update.Contents.Add(MapToolCall(toolCall));
-
- if (update.Contents.Any())
- yield return update;
- }
- }
- }
-
- static CitationAnnotation MapInlineCitation(InlineCitation citation) => citation.CitationCase switch
- {
- InlineCitation.CitationOneofCase.WebCitation => new CitationAnnotation { Url = new(citation.WebCitation.Url) },
- InlineCitation.CitationOneofCase.XCitation => new CitationAnnotation { Url = new(citation.XCitation.Url) },
- InlineCitation.CitationOneofCase.CollectionsCitation => new CitationAnnotation
- {
- FileId = citation.CollectionsCitation.FileId,
- Snippet = citation.CollectionsCitation.ChunkContent,
- ToolName = "file_search",
- },
- _ => new CitationAnnotation()
- };
-
- static CitationAnnotation MapCitation(string citation)
- {
- var url = new Uri(citation);
- if (url.Scheme != "collections")
- return new CitationAnnotation { Url = url };
-
- // Special-case collection citations so we get better metadata
- var collection = url.Host;
- var file = url.AbsolutePath[7..];
- return new CitationAnnotation
- {
- ToolName = "collections_search",
- FileId = file,
- AdditionalProperties = new AdditionalPropertiesDictionary
- {
- { "collection_id", collection }
- }
- };
- }
-
- GetCompletionsRequest MapToRequest(IEnumerable messages, ChatOptions? options)
- {
- var request = options?.RawRepresentationFactory?.Invoke(this) as GetCompletionsRequest ?? new GetCompletionsRequest()
- {
- // By default always include citations in the final output if available
- Include = { IncludeOption.InlineCitations },
- Model = options?.ModelId ?? defaultModelId,
- };
-
- if (string.IsNullOrEmpty(request.Model))
- request.Model = options?.ModelId ?? defaultModelId;
-
- if ((options?.EndUserId ?? clientOptions.EndUserId) is { } user) request.User = user;
- if (options?.MaxOutputTokens is { } maxTokens) request.MaxTokens = maxTokens;
- if (options?.Temperature is { } temperature) request.Temperature = temperature;
- if (options?.TopP is { } topP) request.TopP = topP;
- if (options?.FrequencyPenalty is { } frequencyPenalty) request.FrequencyPenalty = frequencyPenalty;
- if (options?.PresencePenalty is { } presencePenalty) request.PresencePenalty = presencePenalty;
-
- foreach (var message in messages)
- {
- var gmsg = new Message { Role = MapRole(message.Role) };
-
- foreach (var content in message.Contents)
- {
- if (content is TextContent textContent && !string.IsNullOrEmpty(textContent.Text))
- {
- gmsg.Content.Add(new Content { Text = textContent.Text });
- }
- else if (content.RawRepresentation is ToolCall toolCall)
- {
- gmsg.ToolCalls.Add(toolCall);
- }
- else if (content is FunctionCallContent functionCall)
- {
- gmsg.ToolCalls.Add(new ToolCall
- {
- Id = functionCall.CallId,
- Type = ToolCallType.ClientSideTool,
- Function = new FunctionCall
- {
- Name = functionCall.Name,
- Arguments = JsonSerializer.Serialize(functionCall.Arguments)
- }
- });
- }
- else if (content is FunctionResultContent resultContent)
- {
- request.Messages.Add(new Message
- {
- Role = MessageRole.RoleTool,
- Content = { new Content { Text = JsonSerializer.Serialize(resultContent.Result) ?? "null" } }
- });
- }
- else if (content is McpServerToolResultContent mcpResult &&
- mcpResult.RawRepresentation is ToolCall mcpToolCall &&
- mcpResult.Output is { Count: 1 } &&
- mcpResult.Output[0] is TextContent mcpText)
- {
- request.Messages.Add(new Message
- {
- Role = MessageRole.RoleTool,
- ToolCalls = { mcpToolCall },
- Content = { new Content { Text = mcpText.Text } }
- });
- }
- else if (content is CodeInterpreterToolResultContent codeResult &&
- codeResult.RawRepresentation is ToolCall codeToolCall &&
- codeResult.Outputs is { Count: 1 } &&
- codeResult.Outputs[0] is TextContent codeText)
- {
- request.Messages.Add(new Message
- {
- Role = MessageRole.RoleTool,
- ToolCalls = { codeToolCall },
- Content = { new Content { Text = codeText.Text } }
- });
- }
- }
-
- if (gmsg.Content.Count == 0 && gmsg.ToolCalls.Count == 0)
- continue;
-
- // If we have only tool calls and no content, the gRPC enpoint fails, so add an empty one.
- if (gmsg.Content.Count == 0)
- gmsg.Content.Add(new Content());
-
- request.Messages.Add(gmsg);
- }
-
- IList includes = [IncludeOption.InlineCitations];
- if (options is GrokChatOptions grokOptions)
- {
- // NOTE: overrides our default include for inline citations, potentially.
- request.Include.Clear();
- request.Include.AddRange(grokOptions.Include);
-
- if (grokOptions.Search.HasFlag(GrokSearch.X))
- {
- (options.Tools ??= []).Insert(0, new GrokXSearchTool());
- }
- else if (grokOptions.Search.HasFlag(GrokSearch.Web))
- {
- (options.Tools ??= []).Insert(0, new GrokSearchTool());
- }
- }
-
- if (options?.Tools is not null)
- {
- foreach (var tool in options.Tools)
- {
- if (tool is AIFunction functionTool)
- {
- var function = new Function
- {
- Name = functionTool.Name,
- Description = functionTool.Description,
- Parameters = JsonSerializer.Serialize(functionTool.JsonSchema)
- };
- request.Tools.Add(new Tool { Function = function });
- }
- else if (tool is HostedWebSearchTool webSearchTool)
- {
- if (webSearchTool is GrokXSearchTool xSearch)
- {
- var toolProto = new XSearch
- {
- EnableImageUnderstanding = xSearch.EnableImageUnderstanding,
- EnableVideoUnderstanding = xSearch.EnableVideoUnderstanding,
- };
-
- if (xSearch.AllowedHandles is { } allowed) toolProto.AllowedXHandles.AddRange(allowed);
- if (xSearch.ExcludedHandles is { } excluded) toolProto.ExcludedXHandles.AddRange(excluded);
- if (xSearch.FromDate is { } from) toolProto.FromDate = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(from.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc));
- if (xSearch.ToDate is { } to) toolProto.ToDate = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(to.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc));
-
- request.Tools.Add(new Tool { XSearch = toolProto });
- }
- else if (webSearchTool is GrokSearchTool grokSearch)
- {
- var toolProto = new WebSearch
- {
- EnableImageUnderstanding = grokSearch.EnableImageUnderstanding,
- };
-
- if (grokSearch.AllowedDomains is { } allowed) toolProto.AllowedDomains.AddRange(allowed);
- if (grokSearch.ExcludedDomains is { } excluded) toolProto.ExcludedDomains.AddRange(excluded);
-
- request.Tools.Add(new Tool { WebSearch = toolProto });
- }
- else
- {
- request.Tools.Add(new Tool { WebSearch = new WebSearch() });
- }
- }
- else if (tool is HostedCodeInterpreterTool)
- {
- request.Tools.Add(new Tool { CodeExecution = new CodeExecution { } });
- }
- else if (tool is HostedFileSearchTool fileSearch)
- {
- var toolProto = new CollectionsSearch();
-
- if (fileSearch.Inputs?.OfType() is { } vectorStores)
- toolProto.CollectionIds.AddRange(vectorStores.Select(x => x.VectorStoreId).Distinct());
-
- if (fileSearch.MaximumResultCount is { } maxResults)
- toolProto.Limit = maxResults;
-
- request.Tools.Add(new Tool { CollectionsSearch = toolProto });
- }
- else if (tool is HostedMcpServerTool mcpTool)
- {
- request.Tools.Add(new Tool
- {
- Mcp = new MCP
- {
- Authorization = mcpTool.AuthorizationToken,
- ServerLabel = mcpTool.ServerName,
- ServerUrl = mcpTool.ServerAddress,
- AllowedToolNames = { mcpTool.AllowedTools ?? Array.Empty() }
- }
- });
- }
- }
- }
-
- if (options?.ResponseFormat is ChatResponseFormatJson)
- {
- request.ResponseFormat = new ResponseFormat
- {
- FormatType = FormatType.JsonObject
- };
- }
-
- return request;
- }
-
- static MessageRole MapRole(ChatRole role) => role switch
- {
- _ when role == ChatRole.System => MessageRole.RoleSystem,
- _ when role == ChatRole.User => MessageRole.RoleUser,
- _ when role == ChatRole.Assistant => MessageRole.RoleAssistant,
- _ when role == ChatRole.Tool => MessageRole.RoleTool,
- _ => MessageRole.RoleUser
- };
-
- static ChatRole MapRole(MessageRole role) => role switch
- {
- MessageRole.RoleSystem => ChatRole.System,
- MessageRole.RoleUser => ChatRole.User,
- MessageRole.RoleAssistant => ChatRole.Assistant,
- MessageRole.RoleTool => ChatRole.Tool,
- _ => ChatRole.Assistant
- };
-
- static ChatFinishReason? MapFinishReason(FinishReason finishReason) => finishReason switch
- {
- FinishReason.ReasonStop => ChatFinishReason.Stop,
- FinishReason.ReasonMaxLen => ChatFinishReason.Length,
- FinishReason.ReasonToolCalls => ChatFinishReason.ToolCalls,
- FinishReason.ReasonMaxContext => ChatFinishReason.Length,
- FinishReason.ReasonTimeLimit => ChatFinishReason.Length,
- _ => null
- };
-
- static UsageDetails? MapToUsage(SamplingUsage usage) => usage == null ? null : new()
- {
- InputTokenCount = usage.PromptTokens,
- OutputTokenCount = usage.CompletionTokens,
- TotalTokenCount = usage.TotalTokens
- };
-
- ///
- public object? GetService(Type serviceType, object? serviceKey = null) => serviceType switch
- {
- Type t when t == typeof(ChatClientMetadata) => metadata,
- Type t when t == typeof(GrokChatClient) => this,
- _ => null
- };
-
- ///
- public void Dispose() { }
-}
diff --git a/src/Extensions.Grok/GrokChatOptions.cs b/src/Extensions.Grok/GrokChatOptions.cs
deleted file mode 100644
index ba7bf65..0000000
--- a/src/Extensions.Grok/GrokChatOptions.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System.ComponentModel;
-using Devlooped.Grok;
-using Microsoft.Extensions.AI;
-
-namespace Devlooped.Extensions.AI.Grok;
-
-/// Customizes Grok's agentic search tools.
-/// See https://docs.x.ai/docs/guides/tools/search-tools.
-[Flags]
-public enum GrokSearch
-{
- /// Disables agentic search capabilities.
- None = 0,
- /// Enables all available agentic search capabilities.
- All = Web | X,
- /// Allows the agent to search the web and browse pages.
- Web = 1,
- /// Allows the agent to perform keyword search, semantic search, user search, and thread fetch on X.
- X = 2,
- [EditorBrowsable(EditorBrowsableState.Never)]
- [Obsolete("Use either GrokSearch.Web or GrokSearch.X")]
- Auto = Web,
- [EditorBrowsable(EditorBrowsableState.Never)]
- [Obsolete("Use either GrokSearch.Web or GrokSearch.X")]
- On = Web,
- [EditorBrowsable(EditorBrowsableState.Never)]
- [Obsolete("Use GrokSearch.None")]
- Off = None
-}
-
-/// Grok-specific chat options that extend the base .
-public class GrokChatOptions : ChatOptions
-{
- /// Configures Grok's agentic search capabilities.
- /// See https://docs.x.ai/docs/guides/tools/search-tools.
- public GrokSearch Search { get; set; } = GrokSearch.None;
-
- /// Additional outputs to include in responses.
- /// Defaults to including .
- public IList Include { get; set; } = [IncludeOption.InlineCitations];
-}
diff --git a/src/Extensions.Grok/GrokClient.cs b/src/Extensions.Grok/GrokClient.cs
deleted file mode 100644
index 607ad0e..0000000
--- a/src/Extensions.Grok/GrokClient.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using System.Collections.Concurrent;
-using System.Net.Http.Headers;
-using Grpc.Net.Client;
-
-namespace Devlooped.Extensions.AI.Grok;
-
-/// Client for interacting with the Grok service.
-/// The API key used for authentication.
-/// The options used to configure the client.
-public class GrokClient(string apiKey, GrokClientOptions options)
-{
- static ConcurrentDictionary<(Uri, string), GrpcChannel> channels = [];
-
- /// Initializes a new instance of the class with default options.
- public GrokClient(string apiKey) : this(apiKey, new GrokClientOptions()) { }
-
- /// Gets the API key used for authentication.
- public string ApiKey { get; } = apiKey;
-
- /// Gets or sets the endpoint for the service.
- public Uri Endpoint { get; set; } = options.Endpoint;
-
- /// Gets the options used to configure the client.
- public GrokClientOptions Options { get; } = options;
-
- internal GrpcChannel Channel => channels.GetOrAdd((Endpoint, ApiKey), key =>
- {
- var handler = new AuthenticationHeaderHandler(ApiKey)
- {
- InnerHandler = Options.ChannelOptions?.HttpHandler ?? new HttpClientHandler()
- };
-
- var options = Options.ChannelOptions ?? new GrpcChannelOptions();
- options.HttpHandler = handler;
-
- return GrpcChannel.ForAddress(Endpoint, options);
- });
-
- class AuthenticationHeaderHandler(string apiKey) : DelegatingHandler
- {
- protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
- return base.SendAsync(request, cancellationToken);
- }
- }
-}
diff --git a/src/Extensions.Grok/GrokClientExtensions.cs b/src/Extensions.Grok/GrokClientExtensions.cs
deleted file mode 100644
index 9784177..0000000
--- a/src/Extensions.Grok/GrokClientExtensions.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System.ComponentModel;
-using Microsoft.Extensions.AI;
-
-namespace Devlooped.Extensions.AI.Grok;
-
-/// Provides extension methods for .
-[EditorBrowsable(EditorBrowsableState.Never)]
-public static class GrokClientExtensions
-{
- /// Creates a new from the specified using the given model as the default.
- public static IChatClient AsIChatClient(this GrokClient client, string defaultModelId)
- => new GrokChatClient(client.Channel, client.Options, defaultModelId);
-}
diff --git a/src/Extensions.Grok/GrokClientOptions.cs b/src/Extensions.Grok/GrokClientOptions.cs
deleted file mode 100644
index 4aea877..0000000
--- a/src/Extensions.Grok/GrokClientOptions.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Grpc.Net.Client;
-
-namespace Devlooped.Extensions.AI.Grok;
-
-/// Options for configuring the .
-public class GrokClientOptions
-{
- /// Gets or sets the service endpoint.
- public Uri Endpoint { get; set; } = new("https://api.x.ai");
-
- /// Gets or sets the gRPC channel options.
- public GrpcChannelOptions? ChannelOptions { get; set; }
-
- /// Gets or sets the end user ID for the chat session.
- public string? EndUserId { get; set; }
-}
diff --git a/src/Extensions.Grok/GrokSearchTool.cs b/src/Extensions.Grok/GrokSearchTool.cs
deleted file mode 100644
index 602e1f9..0000000
--- a/src/Extensions.Grok/GrokSearchTool.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Microsoft.Extensions.AI;
-
-namespace Devlooped.Extensions.AI.Grok;
-
-/// Configures Grok's agentic search tool.
-/// See https://docs.x.ai/docs/guides/tools/search-tools
-public class GrokSearchTool : HostedWebSearchTool
-{
- ///
- public override string Name => "web_search";
-
- ///
- public override string Description => "Performs agentic web search";
-
- /// Use to make the web search only perform the search and web browsing on web pages that fall within the specified domains. Can include a maximum of five domains.
- public IList? AllowedDomains { get; set; }
-
- /// Use to prevent the model from including the specified domains in any web search tool invocations and from browsing any pages on those domains. Can include a maximum of five domains.
- public IList? ExcludedDomains { get; set; }
-
- /// See https://docs.x.ai/docs/guides/tools/search-tools#enable-image-understanding
- public bool EnableImageUnderstanding { get; set; }
-}
\ No newline at end of file
diff --git a/src/Extensions.Grok/GrokXSearch.cs b/src/Extensions.Grok/GrokXSearch.cs
deleted file mode 100644
index 4d42538..0000000
--- a/src/Extensions.Grok/GrokXSearch.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System.Text.Json.Serialization;
-using Microsoft.Extensions.AI;
-
-namespace Devlooped.Extensions.AI.Grok;
-
-/// Configures Grok's agentic search tool for X.
-/// See https://docs.x.ai/docs/guides/tools/search-tools#x-search-parameters
-public class GrokXSearchTool : HostedWebSearchTool
-{
- /// See https://docs.x.ai/docs/guides/tools/search-tools#only-consider-x-posts-from-specific-handles
- [JsonPropertyName("allowed_x_handles")]
- public IList? AllowedHandles { get; set; }
- /// See https://docs.x.ai/docs/guides/tools/search-tools#exclude-x-posts-from-specific-handles
- [JsonPropertyName("excluded_x_handles")]
- public IList? ExcludedHandles { get; set; }
- /// See https://docs.x.ai/docs/guides/tools/search-tools#date-range
- public DateOnly? FromDate { get; set; }
- /// See https://docs.x.ai/docs/guides/tools/search-tools#date-range
- public DateOnly? ToDate { get; set; }
- /// See https://docs.x.ai/docs/guides/tools/search-tools#enable-image-understanding-1
- public bool EnableImageUnderstanding { get; set; }
- /// See https://docs.x.ai/docs/guides/tools/search-tools#enable-video-understanding
- public bool EnableVideoUnderstanding { get; set; }
-}
\ No newline at end of file
diff --git a/src/Extensions.Grok/HostedToolCallContent.cs b/src/Extensions.Grok/HostedToolCallContent.cs
deleted file mode 100644
index 52bcafd..0000000
--- a/src/Extensions.Grok/HostedToolCallContent.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.Extensions.AI;
-
-namespace Devlooped.Extensions.AI;
-
-/// Represents a hosted tool agentic call.
-/// The tool call details.
-[Experimental("DEAI001")]
-public class HostedToolCallContent : AIContent
-{
- /// Gets or sets the tool call ID.
- public virtual string? CallId { get; set; }
-}
diff --git a/src/Extensions.Grok/HostedToolResultContent.cs b/src/Extensions.Grok/HostedToolResultContent.cs
deleted file mode 100644
index 272b55b..0000000
--- a/src/Extensions.Grok/HostedToolResultContent.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.Extensions.AI;
-
-namespace Devlooped.Extensions.AI;
-
-/// Represents a hosted tool agentic call.
-/// The tool call details.
-[DebuggerDisplay("{DebuggerDisplay,nq}")]
-[Experimental("DEAI001")]
-public class HostedToolResultContent : AIContent
-{
- /// Gets or sets the tool call ID.
- public virtual string? CallId { get; set; }
-
- /// Gets or sets the resulting contents from the tool.
- public virtual IList? Outputs { get; set; }
-}
\ No newline at end of file
diff --git a/src/Extensions.Grok/readme.md b/src/Extensions.Grok/readme.md
deleted file mode 100644
index 8b79e90..0000000
--- a/src/Extensions.Grok/readme.md
+++ /dev/null
@@ -1,9 +0,0 @@
-[](osmfeula.txt)
-[](license.txt)
-[](https://github.com/devlooped/AI)
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Extensions/ConfigurableChatClient.cs b/src/Extensions/ConfigurableChatClient.cs
index 491fcb9..c38b540 100644
--- a/src/Extensions/ConfigurableChatClient.cs
+++ b/src/Extensions/ConfigurableChatClient.cs
@@ -3,12 +3,12 @@
using Azure;
using Azure.AI.Inference;
using Azure.AI.OpenAI;
-using Devlooped.Extensions.AI.Grok;
using Devlooped.Extensions.AI.OpenAI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using OpenAI;
+using xAI;
namespace Devlooped.Extensions.AI;
diff --git a/src/Extensions/Extensions.csproj b/src/Extensions/Extensions.csproj
index 54b3242..74eff7c 100644
--- a/src/Extensions/Extensions.csproj
+++ b/src/Extensions/Extensions.csproj
@@ -30,11 +30,11 @@
+
-
diff --git a/src/Tests/GrokTests.cs b/src/Tests/GrokTests.cs
deleted file mode 100644
index a608051..0000000
--- a/src/Tests/GrokTests.cs
+++ /dev/null
@@ -1,506 +0,0 @@
-using System.Text.Json;
-using System.Text.Json.Nodes;
-using Azure;
-using Devlooped.Extensions.AI.Grok;
-using Devlooped.Grok;
-using Microsoft.Extensions.AI;
-using Moq;
-using Tests.Client.Helpers;
-using static ConfigurationExtensions;
-using OpenAIClientOptions = OpenAI.OpenAIClientOptions;
-
-namespace Devlooped.Extensions.AI;
-
-public class GrokTests(ITestOutputHelper output)
-{
- [SecretsFact("XAI_API_KEY")]
- public async Task GrokInvokesTools()
- {
- var messages = new Chat()
- {
- { "system", "You are a bot that invokes the tool get_date when asked for the date." },
- { "user", "What day is today?" },
- };
-
- var chat = new GrokClient(Configuration["XAI_API_KEY"]!).AsIChatClient("grok-4")
- .AsBuilder()
- .UseLogging(output.AsLoggerFactory())
- .Build();
-
- var options = new GrokChatOptions
- {
- ModelId = "grok-4-fast-non-reasoning",
- Tools = [AIFunctionFactory.Create(() => DateTimeOffset.Now.ToString("O"), "get_date")],
- AdditionalProperties = new()
- {
- { "foo", "bar" }
- }
- };
-
- var response = await chat.GetResponseAsync(messages, options);
- var getdate = response.Messages
- .SelectMany(x => x.Contents.OfType())
- .Any(x => x.Name == "get_date");
-
- Assert.True(getdate);
- // NOTE: the chat client was requested as grok-3 but the chat options wanted a
- // different model and the grok client honors that choice.
- Assert.Equal(options.ModelId, response.ModelId);
- }
-
- [SecretsFact("XAI_API_KEY")]
- public async Task GrokInvokesToolAndSearch()
- {
- var messages = new Chat()
- {
- { "system", "You use Nasdaq for stocks news and prices." },
- { "user", "What's Tesla stock worth today?" },
- };
-
- var grok = new GrokClient(Configuration["XAI_API_KEY"]!).AsIChatClient("grok-4")
- .AsBuilder()
- .UseFunctionInvocation()
- .UseLogging(output.AsLoggerFactory())
- .Build();
-
- var getDateCalls = 0;
- var options = new GrokChatOptions
- {
- ModelId = "grok-4-1-fast-non-reasoning",
- Search = GrokSearch.Web,
- Tools = [AIFunctionFactory.Create(() =>
- {
- getDateCalls++;
- return DateTimeOffset.Now.ToString("O");
- }, "get_date", "Gets the current date")],
- };
-
- var response = await grok.GetResponseAsync(messages, options);
-
- // The get_date result shows up as a tool role
- Assert.Contains(response.Messages, x => x.Role == ChatRole.Tool);
-
- // Citations include nasdaq.com at least as a web search source
- var urls = response.Messages
- .SelectMany(x => x.Contents)
- .SelectMany(x => x.Annotations?.OfType() ?? [])
- .Where(x => x.Url is not null)
- .Select(x => x.Url!)
- .ToList();
-
- Assert.Equal(1, getDateCalls);
- Assert.Contains(urls, x => x.Host.EndsWith("nasdaq.com"));
- Assert.Contains(urls, x => x.PathAndQuery.Contains("/TSLA"));
- Assert.Equal(options.ModelId, response.ModelId);
-
- var calls = response.Messages
- .SelectMany(x => x.Contents.OfType())
- .Select(x => x.RawRepresentation as Devlooped.Grok.ToolCall)
- .Where(x => x is not null)
- .ToList();
-
- Assert.NotEmpty(calls);
- Assert.Contains(calls, x => x?.Type == Devlooped.Grok.ToolCallType.WebSearchTool);
- }
-
- [SecretsFact("XAI_API_KEY")]
- public async Task GrokInvokesSpecificSearchUrl()
- {
- var messages = new Chat()
- {
- { "system", "Sos un asistente del Cerro Catedral, usas la funcionalidad de Live Search en el sitio oficial." },
- { "system", $"Hoy es {DateTime.Now.ToString("o")}" },
- { "user", "Que calidad de nieve hay hoy?" },
- };
-
- var grok = new GrokClient(Configuration["XAI_API_KEY"]!).AsIChatClient("grok-4-1-fast-non-reasoning");
-
- var options = new ChatOptions
- {
- Tools = [new GrokSearchTool()
- {
- AllowedDomains = [ "catedralaltapatagonia.com" ]
- }]
- };
-
- var response = await grok.GetResponseAsync(messages, options);
- var text = response.Text;
-
- var citations = response.Messages
- .SelectMany(x => x.Contents)
- .SelectMany(x => x.Annotations ?? [])
- .OfType()
- .Where(x => x.Url != null)
- .Select(x => x.Url!.AbsoluteUri)
- .ToList();
-
- Assert.Contains("https://partediario.catedralaltapatagonia.com/partediario/", citations);
- }
-
- [SecretsFact("XAI_API_KEY")]
- public async Task GrokInvokesHostedSearchTool()
- {
- var messages = new Chat()
- {
- { "system", "You are an AI assistant that knows how to search the web." },
- { "user", "What's Tesla stock worth today? Search X, Yahoo and the news for latest info." },
- };
-
- var grok = new GrokClient(Configuration["XAI_API_KEY"]!).AsIChatClient("grok-4-fast");
-
- var options = new GrokChatOptions
- {
- Include = { Devlooped.Grok.IncludeOption.WebSearchCallOutput },
- Tools = [new HostedWebSearchTool()]
- };
-
- var response = await grok.GetResponseAsync(messages, options);
- var text = response.Text;
-
- Assert.Contains("TSLA", text);
- Assert.NotNull(response.ModelId);
-
- var urls = response.Messages
- .SelectMany(x => x.Contents)
- .SelectMany(x => x.Annotations?.OfType() ?? [])
- .Where(x => x.Url is not null)
- .Select(x => x.Url!)
- .ToList();
-
- Assert.Contains(urls, x => x.Host == "finance.yahoo.com");
- Assert.Contains(urls, x => x.PathAndQuery.Contains("/TSLA"));
- }
-
- [SecretsFact("XAI_API_KEY")]
- public async Task GrokInvokesGrokSearchToolIncludesDomain()
- {
- var messages = new Chat()
- {
- { "system", "You are an AI assistant that knows how to search the web." },
- { "user", "What is the latest news about Microsoft?" },
- };
-
- var grok = new GrokClient(Configuration["XAI_API_KEY"]!).AsIChatClient("grok-4-fast");
-
- var options = new ChatOptions
- {
- Tools = [new GrokSearchTool
- {
- AllowedDomains = ["microsoft.com", "news.microsoft.com"],
- }]
- };
-
- var response = await grok.GetResponseAsync(messages, options);
-
- Assert.NotNull(response.Text);
- Assert.Contains("Microsoft", response.Text);
-
- var urls = response.Messages
- .SelectMany(x => x.Contents)
- .SelectMany(x => x.Annotations?.OfType() ?? [])
- .Where(x => x.Url is not null)
- .Select(x => x.Url!)
- .ToList();
-
- foreach (var url in urls)
- {
- output.WriteLine(url.ToString());
- }
-
- Assert.All(urls, x => x.Host.EndsWith(".microsoft.com"));
- }
-
- [SecretsFact("XAI_API_KEY")]
- public async Task GrokInvokesGrokSearchToolExcludesDomain()
- {
- var messages = new Chat()
- {
- { "system", "You are an AI assistant that knows how to search the web." },
- { "user", "What is the latest news about Microsoft?" },
- };
-
- var grok = new GrokClient(Configuration["XAI_API_KEY"]!).AsIChatClient("grok-4-fast");
-
- var options = new ChatOptions
- {
- Tools = [new GrokSearchTool
- {
- ExcludedDomains = ["blogs.microsoft.com"]
- }]
- };
-
- var response = await grok.GetResponseAsync(messages, options);
-
- Assert.NotNull(response.Text);
- Assert.Contains("Microsoft", response.Text);
-
- var urls = response.Messages
- .SelectMany(x => x.Contents)
- .SelectMany(x => x.Annotations?.OfType() ?? [])
- .Where(x => x.Url is not null)
- .Select(x => x.Url!)
- .ToList();
-
- foreach (var url in urls)
- {
- output.WriteLine(url.ToString());
- }
-
- Assert.DoesNotContain(urls, x => x.Host == "blogs.microsoft.com");
- }
-
- [SecretsFact("XAI_API_KEY")]
- public async Task GrokInvokesHostedCodeExecution()
- {
- var grok = new GrokClient(Configuration["XAI_API_KEY"]!).AsIChatClient("grok-4-fast");
-
- var response = await grok.GetResponseAsync(
- "Calculate the compound interest for $10,000 at 5% annually for 10 years",
- new ChatOptions
- {
- Tools = [new HostedCodeInterpreterTool()]
- });
-
- var text = response.Text;
-
- Assert.Contains("$6,288.95", text);
- Assert.NotEmpty(response.Messages
- .SelectMany(x => x.Contents)
- .OfType());
-
- // result content is not available by default
- Assert.Empty(response.Messages
- .SelectMany(x => x.Contents)
- .OfType());
- }
-
- [SecretsFact("XAI_API_KEY")]
- public async Task GrokInvokesHostedCodeExecutionWithOutput()
- {
- var messages = new Chat()
- {
- { "user", "Calculate the compound interest for $10,000 at 5% annually for 10 years" },
- };
-
- var grok = new GrokClient(Configuration["XAI_API_KEY"]!).AsIChatClient("grok-4-fast");
-
- var options = new GrokChatOptions
- {
- Include = { Devlooped.Grok.IncludeOption.CodeExecutionCallOutput },
- Tools = [new HostedCodeInterpreterTool()]
- };
-
- var response = await grok.GetResponseAsync(messages, options);
-
- Assert.Contains("$6,288.95", response.Text);
- Assert.NotEmpty(response.Messages
- .SelectMany(x => x.Contents)
- .OfType());
-
- // result content opted-in is found
- Assert.NotEmpty(response.Messages
- .SelectMany(x => x.Contents)
- .OfType());
- }
-
- [SecretsFact("XAI_API_KEY")]
- public async Task GrokInvokesHostedCollectionSearch()
- {
- var messages = new Chat()
- {
- { "user", "¿Cuál es el monto exacto del rango de la multa por inasistencia injustificada a la audiencia señalada por el juez en el proceso sucesorio, según lo establecido en el Artículo 691 del Código Procesal Civil y Comercial de la Nación (Ley 17.454)?" },
- };
-
- var grok = new GrokClient(Configuration["XAI_API_KEY"]!).AsIChatClient("grok-4-fast");
-
- var options = new ChatOptions
- {
- Tools = [new HostedFileSearchTool {
- Inputs = [new HostedVectorStoreContent("collection_91559d9b-a55d-42fe-b2ad-ecf8904d9049")]
- }]
- };
-
- var response = await grok.GetResponseAsync(messages, options);
- var text = response.Text;
-
- Assert.Contains("11,74", text);
- Assert.Contains(response.Messages
- .SelectMany(x => x.Contents)
- .OfType()
- .Select(x => x.RawRepresentation as Devlooped.Grok.ToolCall),
- x => x?.Type == Devlooped.Grok.ToolCallType.CollectionsSearchTool);
- }
-
- [SecretsFact("XAI_API_KEY", "GITHUB_TOKEN")]
- public async Task GrokInvokesHostedMcp()
- {
- var messages = new Chat()
- {
- { "user", "When was GrokClient v1.0.0 released on the devlooped/GrokClient repo? Respond with just the date, in YYYY-MM-DD format." },
- };
-
- var grok = new GrokClient(Configuration["XAI_API_KEY"]!).AsIChatClient("grok-4-fast");
-
- var options = new ChatOptions
- {
- Tools = [new HostedMcpServerTool("GitHub", "https://api.githubcopilot.com/mcp/") {
- AuthorizationToken = Configuration["GITHUB_TOKEN"]!,
- AllowedTools = ["list_releases"],
- }]
- };
-
- var response = await grok.GetResponseAsync(messages, options);
- var text = response.Text;
-
- Assert.Equal("2025-11-29", text);
- var call = Assert.Single(response.Messages
- .SelectMany(x => x.Contents)
- .OfType());
-
- Assert.Equal("GitHub.list_releases", call.ToolName);
- }
-
- [SecretsFact("XAI_API_KEY", "GITHUB_TOKEN")]
- public async Task GrokInvokesHostedMcpWithOutput()
- {
- var messages = new Chat()
- {
- { "user", "When was GrokClient v1.0.0 released on the devlooped/GrokClient repo? Respond with just the date, in YYYY-MM-DD format." },
- };
-
- var grok = new GrokClient(Configuration["XAI_API_KEY"]!).AsIChatClient("grok-4-fast");
-
- var options = new GrokChatOptions
- {
- Include = { Devlooped.Grok.IncludeOption.McpCallOutput },
- Tools = [new HostedMcpServerTool("GitHub", "https://api.githubcopilot.com/mcp/") {
- AuthorizationToken = Configuration["GITHUB_TOKEN"]!,
- AllowedTools = ["list_releases"],
- }]
- };
-
- var response = await grok.GetResponseAsync(messages, options);
-
- // Can include result of MCP tool
- var output = Assert.Single(response.Messages
- .SelectMany(x => x.Contents)
- .OfType());
-
- Assert.NotNull(output.Output);
- Assert.Single(output.Output);
- var json = Assert.Single(output.Output!.OfType()).Text;
- var tags = JsonSerializer.Deserialize>(json, new JsonSerializerOptions(JsonSerializerDefaults.Web)
- {
- PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
- });
-
- Assert.NotNull(tags);
- Assert.Contains(tags, x => x.TagName == "v1.0.0");
- }
-
- record Release(string TagName, DateTimeOffset CreatedAt);
-
- [SecretsFact("XAI_API_KEY", "GITHUB_TOKEN")]
- public async Task GrokStreamsUpdatesFromAllTools()
- {
- var messages = new Chat()
- {
- { "user",
- """
- What's the oldest stable version released on the devlooped/GrokClient repo on GitHub?,
- what is the current price of Tesla stock,
- and what is the current date? Respond with the following JSON:
- {
- "today": "[get_date result]",
- "release": "[first stable release of devlooped/GrokClient, using GitHub MCP tool]",
- "price": [$TSLA price using web search tool]
- }
- """
- },
- };
-
- var grok = new GrokClient(Configuration["XAI_API_KEY"]!)
- .AsIChatClient("grok-4-fast")
- .AsBuilder()
- .UseFunctionInvocation()
- .UseLogging(output.AsLoggerFactory())
- .Build();
-
- var getDateCalls = 0;
- var options = new GrokChatOptions
- {
- Include = { IncludeOption.McpCallOutput },
- Tools =
- [
- new HostedWebSearchTool(),
- new HostedMcpServerTool("GitHub", "https://api.githubcopilot.com/mcp/") {
- AuthorizationToken = Configuration["GITHUB_TOKEN"]!,
- AllowedTools = ["list_releases", "get_release_by_tag"],
- },
- AIFunctionFactory.Create(() => {
- getDateCalls++;
- return DateTimeOffset.Now.ToString("O");
- }, "get_date", "Gets the current date")
- ]
- };
-
- var updates = await grok.GetStreamingResponseAsync(messages, options).ToListAsync();
- var response = updates.ToChatResponse();
- var typed = JsonSerializer.Deserialize(response.Messages.Last().Text, new JsonSerializerOptions(JsonSerializerDefaults.Web));
-
- Assert.NotNull(typed);
-
- Assert.NotEmpty(response.Messages
- .SelectMany(x => x.Contents)
- .OfType());
-
- Assert.Contains(response.Messages
- .SelectMany(x => x.Contents)
- .OfType()
- .Select(x => x.RawRepresentation as Devlooped.Grok.ToolCall),
- x => x?.Type == Devlooped.Grok.ToolCallType.WebSearchTool);
-
- Assert.Equal(1, getDateCalls);
-
- Assert.Equal(DateOnly.FromDateTime(DateTime.Today), typed.Today);
- Assert.EndsWith("1.0.0", typed.Release);
- Assert.True(typed.Price > 100);
- }
-
- [Fact]
- public async Task GrokCustomFactoryInvokedFromOptions()
- {
- var invoked = false;
- var client = new Mock(MockBehavior.Strict);
- client.Setup(x => x.GetCompletionAsync(It.IsAny(), null, null, CancellationToken.None))
- .Returns(CallHelpers.CreateAsyncUnaryCall(new GetChatCompletionResponse
- {
- Outputs =
- {
- new CompletionOutput
- {
- Message = new CompletionMessage
- {
- Content = "Hey Cazzulino!"
- }
- }
- }
- }));
-
- var grok = new GrokChatClient(client.Object, "grok-4-1-fast");
- var response = await grok.GetResponseAsync("Hi, my internet alias is kzu. Lookup my real full name online.",
- new GrokChatOptions
- {
- RawRepresentationFactory = (client) =>
- {
- invoked = true;
- return new GetCompletionsRequest();
- }
- });
-
- Assert.True(invoked);
- Assert.Equal("Hey Cazzulino!", response.Text);
- }
-
- record Response(DateOnly Today, string Release, decimal Price);
-}
diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj
index c8d8f5c..65ac868 100644
--- a/src/Tests/Tests.csproj
+++ b/src/Tests/Tests.csproj
@@ -32,7 +32,6 @@
-