11using System . ClientModel ;
22using System . ClientModel . Primitives ;
33using System . Collections . Concurrent ;
4- using System . Text . Json ;
54using Microsoft . Extensions . AI ;
65using OpenAI ;
76
87namespace Devlooped . Extensions . AI ;
98
10- public class GrokClient ( string apiKey , OpenAIClientOptions options )
9+ /// <summary>
10+ /// Provides an OpenAI compability client for Grok. It's recommended you
11+ /// use <see cref="GrokChatClient"/> directly for chat-only scenarios.
12+ /// </summary>
13+ public class GrokClient ( string apiKey , OpenAIClientOptions ? options = null )
1114 : OpenAIClient ( new ApiKeyCredential ( apiKey ) , EnsureEndpoint ( options ) )
1215{
13- // This allows ChatOptions to request a different model than the one configured
14- // in the chat pipeline when GetChatClient(model).AsIChatClient() is called at registration time.
15- readonly ConcurrentDictionary < string , GrokChatClientAdapter > adapters = new ( ) ;
1616 readonly ConcurrentDictionary < string , IChatClient > clients = new ( ) ;
1717
18- public GrokClient ( string apiKey )
19- : this ( apiKey , new ( ) )
20- {
21- }
18+ /// <summary>
19+ /// Initializes a new instance of the <see cref="GrokClient"/> with the specified API key.
20+ /// </summary>
21+ public GrokClient ( string apiKey ) : this ( apiKey , new ( ) ) { }
2222
23- IChatClient GetChatClientImpl ( string model )
24- // Gets the real chat client by prefixing so the overload invokes the base.
25- => clients . GetOrAdd ( model , key => GetChatClient ( "__" + model ) . AsIChatClient ( ) ) ;
23+ IChatClient GetChatClientImpl ( string model ) => clients . GetOrAdd ( model , key => new GrokChatClient ( apiKey , key , options ) ) ;
2624
2725 /// <summary>
2826 /// Returns an adapter that surfaces an <see cref="IChatClient"/> interface that
2927 /// can be used directly in the <see cref="ChatClientBuilder"/> pipeline builder.
3028 /// </summary>
31- public override OpenAI . Chat . ChatClient GetChatClient ( string model )
32- // We need to differentiate getting a real chat client vs an adapter for pipeline setup.
33- // The former is invoked by the adapter when it needs to invoke the actual chat client,
34- // which goes through the GetChatClientImpl. Since the method override is necessary to
35- // satisfy the usage pattern when configuring OpenAIClient with M.E.AI, we differentiate
36- // the internal call by adding a prefix we remove before calling downstream.
37- => model . StartsWith ( "__" ) ? base . GetChatClient ( model [ 2 ..] ) : new GrokChatClientAdapter ( this , model ) ;
38-
39- static OpenAIClientOptions EnsureEndpoint ( OpenAIClientOptions options )
40- {
41- if ( options . Endpoint is null )
42- options . Endpoint = new Uri ( "https://api.x.ai/v1" ) ;
43-
44- return options ;
45- }
29+ public override OpenAI . Chat . ChatClient GetChatClient ( string model ) => new GrokChatClientAdapter ( this , model ) ;
4630
47- static ChatOptions ? SetOptions ( ChatOptions ? options )
31+ static OpenAIClientOptions EnsureEndpoint ( OpenAIClientOptions ? options )
4832 {
49- if ( options is null )
50- return null ;
51-
52- options . RawRepresentationFactory = _ =>
53- {
54- var result = new GrokCompletionOptions ( ) ;
55- var grok = options as GrokChatOptions ;
56- var search = grok ? . Search ;
57-
58- if ( options . Tools != null )
59- {
60- if ( options . Tools . OfType < GrokSearchTool > ( ) . FirstOrDefault ( ) is GrokSearchTool grokSearch )
61- search = grokSearch . Mode ;
62- else if ( options . Tools . OfType < HostedWebSearchTool > ( ) . FirstOrDefault ( ) is HostedWebSearchTool webSearch )
63- search = GrokSearch . Auto ;
64-
65- // Grok doesn't support any other hosted search tools, so remove remaining ones
66- // so they don't get copied over by the OpenAI client.
67- //options.Tools = [.. options.Tools.Where(tool => tool is not HostedWebSearchTool)];
68- }
69-
70- if ( search != null )
71- result . Search = search . Value ;
72-
73- if ( grok ? . ReasoningEffort != null )
74- {
75- result . ReasoningEffortLevel = grok . ReasoningEffort switch
76- {
77- ReasoningEffort . Low => OpenAI . Chat . ChatReasoningEffortLevel . Low ,
78- ReasoningEffort . High => OpenAI . Chat . ChatReasoningEffortLevel . High ,
79- _ => throw new ArgumentException ( $ "Unsupported reasoning effort { grok . ReasoningEffort } ")
80- } ;
81- }
82-
83- return result ;
84- } ;
85-
33+ options ??= new ( ) ;
34+ options . Endpoint ??= new Uri ( "https://api.x.ai/v1" ) ;
8635 return options ;
8736 }
8837
89- class SearchParameters
90- {
91- public GrokSearch Mode { get ; set ; } = GrokSearch . Auto ;
92- }
93-
94- class GrokCompletionOptions : OpenAI . Chat . ChatCompletionOptions
95- {
96- public GrokSearch Search { get ; set ; } = GrokSearch . Auto ;
97-
98- protected override void JsonModelWriteCore ( Utf8JsonWriter writer , ModelReaderWriterOptions ? options )
99- {
100- base . JsonModelWriteCore ( writer , options ) ;
101-
102- // "search_parameters": { "mode": "auto" }
103- writer . WritePropertyName ( "search_parameters" ) ;
104- writer . WriteStartObject ( ) ;
105- writer . WriteString ( "mode" , Search . ToString ( ) . ToLowerInvariant ( ) ) ;
106- writer . WriteEndObject ( ) ;
107- }
108- }
109-
110- public class GrokChatClientAdapter ( GrokClient client , string model ) : OpenAI . Chat . ChatClient , IChatClient
38+ // This adapter is provided for compatibility with the documented usage for
39+ // OpenAI in MEAI docs. Most typical case would be to just create an <see cref="GrokChatClient"/> directly.
40+ // This throws on any non-IChatClient invoked methods in the AsIChatClient adapter, and
41+ // forwards the IChatClient methods to the GrokChatClient implementation which is cached per client.
42+ class GrokChatClientAdapter ( GrokClient client , string model ) : OpenAI . Chat . ChatClient , IChatClient
11143 {
11244 void IDisposable . Dispose ( ) { }
11345
@@ -118,14 +50,14 @@ void IDisposable.Dispose() { }
11850 /// the default model when the adapter was created.
11951 /// </summary>
12052 Task < ChatResponse > IChatClient . GetResponseAsync ( IEnumerable < ChatMessage > messages , ChatOptions ? options , CancellationToken cancellation )
121- => client . GetChatClientImpl ( options ? . ModelId ?? model ) . GetResponseAsync ( messages , SetOptions ( options ) , cancellation ) ;
53+ => client . GetChatClientImpl ( options ? . ModelId ?? model ) . GetResponseAsync ( messages , options , cancellation ) ;
12254
12355 /// <summary>
12456 /// Routes the request to a client that matches the options' ModelId (if set), or
12557 /// the default model when the adapter was created.
12658 /// </summary>
12759 IAsyncEnumerable < ChatResponseUpdate > IChatClient . GetStreamingResponseAsync ( IEnumerable < ChatMessage > messages , ChatOptions ? options , CancellationToken cancellation )
128- => client . GetChatClientImpl ( options ? . ModelId ?? model ) . GetStreamingResponseAsync ( messages , SetOptions ( options ) , cancellation ) ;
60+ => client . GetChatClientImpl ( options ? . ModelId ?? model ) . GetStreamingResponseAsync ( messages , options , cancellation ) ;
12961
13062 // These are the only two methods actually invoked by the AsIChatClient adapter from M.E.AI.OpenAI
13163 public override Task < ClientResult < OpenAI . Chat . ChatCompletion > > CompleteChatAsync ( IEnumerable < OpenAI . Chat . ChatMessage > ? messages , OpenAI . Chat . ChatCompletionOptions ? options = null , CancellationToken cancellationToken = default )
0 commit comments