Skip to content

Commit e8aab11

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 3dbc729 commit e8aab11

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
@@ -165,4 +165,9 @@
165165
url = https://github.com/andrewlock/NetEscapades.Configuration/blob/master/src/NetEscapades.Configuration.Yaml/YamlConfigurationStreamParser.cs
166166
weak
167167
sha = a1ec2c6746d96b4f6f140509aa68dcff09271146
168-
etag = 9e5c6908edc34eb661d647671f79153d8f3a54ebdc848c8765c78d2715f2f657
168+
etag = 9e5c6908edc34eb661d647671f79153d8f3a54ebdc848c8765c78d2715f2f657
169+
[file "src/Tests/Extensions/CallHelpers.cs"]
170+
url = https://github.com/grpc/grpc-dotnet/blob/master/examples/Tester/Tests/Client/Helpers/CallHelpers.cs
171+
sha = a04684ab2306e5a17bad26d3da69636b326cce14
172+
etag = 7faacded709d2bede93356fd58b93af84884949f3bab098b8b8d121a03696449
173+
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)