Skip to content

Commit 96c8d05

Browse files
committed
Make it possible to construct the raw completion request from gRPC
This is the pattern followed by the OpenAI implementation too. Allows exposing ALL the underlying options, albeit quite a bit less discoverable. This should be sufficient fallback for any new (auto-updated) features in the upstream GrokClient generated from .proto files if we ever lag behind it on this library. We make the test fully isolated from xAI calls to avoid spending tokens on a test that doesn't really require hitting the backend (unlike most of the others).
1 parent c719389 commit 96c8d05

File tree

5 files changed

+107
-5
lines changed

5 files changed

+107
-5
lines changed

.netconfig

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,9 @@
168168
url = https://github.com/andrewlock/NetEscapades.Configuration/blob/master/src/NetEscapades.Configuration.Yaml/YamlConfigurationStreamParser.cs
169169
weak
170170
sha = a1ec2c6746d96b4f6f140509aa68dcff09271146
171-
etag = 9e5c6908edc34eb661d647671f79153d8f3a54ebdc848c8765c78d2715f2f657
171+
etag = 9e5c6908edc34eb661d647671f79153d8f3a54ebdc848c8765c78d2715f2f657
172+
[file "src/Tests/Extensions/CallHelpers.cs"]
173+
url = https://github.com/grpc/grpc-dotnet/blob/master/examples/Tester/Tests/Client/Helpers/CallHelpers.cs
174+
sha = a04684ab2306e5a17bad26d3da69636b326cce14
175+
etag = 7faacded709d2bede93356fd58b93af84884949f3bab098b8b8d121a03696449
176+
weak

src/Extensions.Grok/Extensions.Grok.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<ItemGroup>
2727
<Compile Include="..\Extensions\Extensions\ChatOptionsExtensions.cs" Link="Extensions\ChatOptionsExtensions.cs" />
2828
<None Include="..\..\osmfeula.txt" Link="osmfeula.txt" PackagePath="OSMFEULA.txt" />
29+
<InternalsVisibleTo Include="Tests"/>
2930
</ItemGroup>
3031

3132
</Project>

src/Extensions.Grok/GrokChatClient.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,19 @@ class GrokChatClient : IChatClient
1515
readonly GrokClientOptions clientOptions;
1616

1717
internal GrokChatClient(GrpcChannel channel, GrokClientOptions clientOptions, string defaultModelId)
18+
: this(new ChatClient(channel), clientOptions, defaultModelId)
19+
{ }
20+
21+
/// <summary>
22+
/// Test constructor.
23+
/// </summary>
24+
internal GrokChatClient(ChatClient client, string defaultModelId)
25+
: this(client, new(), defaultModelId)
26+
{ }
27+
28+
GrokChatClient(ChatClient client, GrokClientOptions clientOptions, string defaultModelId)
1829
{
19-
client = new ChatClient(channel);
30+
this.client = client;
2031
this.clientOptions = clientOptions;
2132
this.defaultModelId = defaultModelId;
2233
metadata = new ChatClientMetadata("xai", clientOptions.Endpoint, defaultModelId);
@@ -97,7 +108,7 @@ public async Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messag
97108
{
98109
ResponseId = response.Id,
99110
ModelId = response.Model,
100-
CreatedAt = response.Created.ToDateTimeOffset(),
111+
CreatedAt = response.Created?.ToDateTimeOffset(),
101112
FinishReason = lastOutput != null ? MapFinishReason(lastOutput.FinishReason) : null,
102113
Usage = MapToUsage(response.Usage),
103114
};
@@ -210,13 +221,16 @@ static CitationAnnotation MapCitation(string citation)
210221

211222
GetCompletionsRequest MapToRequest(IEnumerable<ChatMessage> messages, ChatOptions? options)
212223
{
213-
var request = new GetCompletionsRequest
224+
var request = options?.RawRepresentationFactory?.Invoke(this) as GetCompletionsRequest ?? new GetCompletionsRequest()
214225
{
215226
// By default always include citations in the final output if available
216227
Include = { IncludeOption.InlineCitations },
217228
Model = options?.ModelId ?? defaultModelId,
218229
};
219230

231+
if (string.IsNullOrEmpty(request.Model))
232+
request.Model = options?.ModelId ?? defaultModelId;
233+
220234
if ((options?.EndUserId ?? clientOptions.EndUserId) is { } user) request.User = user;
221235
if (options?.MaxOutputTokens is { } maxTokens) request.MaxTokens = maxTokens;
222236
if (options?.Temperature is { } temperature) request.Temperature = temperature;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#region Copyright notice and license
2+
3+
// Copyright 2019 The gRPC Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
#endregion
18+
19+
using Grpc.Core;
20+
21+
namespace Tests.Client.Helpers
22+
{
23+
static class CallHelpers
24+
{
25+
public static AsyncUnaryCall<TResponse> CreateAsyncUnaryCall<TResponse>(TResponse response)
26+
{
27+
return new AsyncUnaryCall<TResponse>(
28+
Task.FromResult(response),
29+
Task.FromResult(new Metadata()),
30+
() => Status.DefaultSuccess,
31+
() => new Metadata(),
32+
() => { });
33+
}
34+
35+
public static AsyncUnaryCall<TResponse> CreateAsyncUnaryCall<TResponse>(StatusCode statusCode)
36+
{
37+
var status = new Status(statusCode, string.Empty);
38+
return new AsyncUnaryCall<TResponse>(
39+
Task.FromException<TResponse>(new RpcException(status)),
40+
Task.FromResult(new Metadata()),
41+
() => status,
42+
() => new Metadata(),
43+
() => { });
44+
}
45+
}
46+
}

src/Tests/GrokTests.cs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
using Devlooped.Extensions.AI.Grok;
55
using Devlooped.Grok;
66
using Microsoft.Extensions.AI;
7-
using OpenAI.Realtime;
7+
using Moq;
8+
using Tests.Client.Helpers;
89
using static ConfigurationExtensions;
910
using OpenAIClientOptions = OpenAI.OpenAIClientOptions;
1011

@@ -466,5 +467,40 @@ public async Task GrokStreamsUpdatesFromAllTools()
466467
Assert.True(typed.Price > 100);
467468
}
468469

470+
[Fact]
471+
public async Task GrokCustomFactoryInvokedFromOptions()
472+
{
473+
var invoked = false;
474+
var client = new Mock<Devlooped.Grok.Chat.ChatClient>(MockBehavior.Strict);
475+
client.Setup(x => x.GetCompletionAsync(It.IsAny<GetCompletionsRequest>(), null, null, CancellationToken.None))
476+
.Returns(CallHelpers.CreateAsyncUnaryCall(new GetChatCompletionResponse
477+
{
478+
Outputs =
479+
{
480+
new CompletionOutput
481+
{
482+
Message = new CompletionMessage
483+
{
484+
Content = "Hey Cazzulino!"
485+
}
486+
}
487+
}
488+
}));
489+
490+
var grok = new GrokChatClient(client.Object, "grok-4-1-fast");
491+
var response = await grok.GetResponseAsync("Hi, my internet alias is kzu. Lookup my real full name online.",
492+
new GrokChatOptions
493+
{
494+
RawRepresentationFactory = (client) =>
495+
{
496+
invoked = true;
497+
return new GetCompletionsRequest();
498+
}
499+
});
500+
501+
Assert.True(invoked);
502+
Assert.Equal("Hey Cazzulino!", response.Text);
503+
}
504+
469505
record Response(DateOnly Today, string Release, decimal Price);
470506
}

0 commit comments

Comments
 (0)