Skip to content

Commit db68eb5

Browse files
committed
Add pipeline to RealtimeClient
1 parent 6887637 commit db68eb5

File tree

7 files changed

+157
-47
lines changed

7 files changed

+157
-47
lines changed

src/Custom/Realtime/RealtimeClient.Protocol.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.ClientModel;
33
using System.ClientModel.Primitives;
4-
using System.ComponentModel;
54
using System.Threading.Tasks;
65

76
namespace OpenAI.Realtime;
@@ -43,8 +42,8 @@ public virtual Task<RealtimeSession> StartTranscriptionSessionAsync(RequestOptio
4342
/// <returns></returns>
4443
public virtual async Task<RealtimeSession> StartSessionAsync(string model, string intent, RequestOptions options)
4544
{
46-
Uri fullEndpoint = BuildSessionEndpoint(_baseEndpoint, model, intent);
47-
RealtimeSession provisionalSession = new(this, fullEndpoint, _credential);
45+
Uri fullEndpoint = BuildSessionEndpoint(_webSocketEndpoint, model, intent);
46+
RealtimeSession provisionalSession = new(this, fullEndpoint, _keyCredential);
4847
try
4948
{
5049
await provisionalSession.ConnectAsync(options).ConfigureAwait(false);

src/Custom/Realtime/RealtimeClient.cs

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,34 +20,80 @@ public partial class RealtimeClient
2020
public event EventHandler<BinaryData> OnSendingCommand;
2121
public event EventHandler<BinaryData> OnReceivingCommand;
2222

23-
private readonly ApiKeyCredential _credential;
24-
private readonly Uri _baseEndpoint;
23+
private readonly ApiKeyCredential _keyCredential;
24+
private readonly Uri _webSocketEndpoint;
25+
26+
// CUSTOM: Added as a convenience.
27+
/// <summary> Initializes a new instance of <see cref="RealtimeClient"/>. </summary>
28+
/// <param name="apiKey"> The API key to authenticate with the service. </param>
29+
/// <exception cref="ArgumentNullException"> <paramref name="apiKey"/> is null. </exception>
30+
public RealtimeClient(string apiKey) : this(new ApiKeyCredential(apiKey), new OpenAIClientOptions())
31+
{
32+
}
2533

26-
/// <summary>
27-
/// Creates a new instance of <see cref="RealtimeClient"/> using an API key for authentication.
28-
/// </summary>
29-
/// <param name="credential"> The API key to use for authentication. </param>
34+
// CUSTOM:
35+
// - Used a custom pipeline.
36+
// - Demoted the endpoint parameter to be a property in the options class.
37+
/// <summary> Initializes a new instance of <see cref="RealtimeClient"/>. </summary>
38+
/// <param name="credential"> The API key to authenticate with the service. </param>
39+
/// <exception cref="ArgumentNullException"> <paramref name="credential"/> is null. </exception>
3040
public RealtimeClient(ApiKeyCredential credential) : this(credential, new OpenAIClientOptions())
3141
{
3242
}
3343

34-
/// <summary>
35-
/// Creates a new instance of <see cref="RealtimeClient"/> using an API key for authentication.
36-
/// </summary>
37-
/// <param name="credential"> The API key to use for authentication. </param>
38-
/// <param name="options"> Additional options for configuring the client. </param>
39-
public RealtimeClient(ApiKeyCredential credential, OpenAIClientOptions options)
44+
// CUSTOM:
45+
// - Used a custom pipeline.
46+
// - Demoted the endpoint parameter to be a property in the options class.
47+
/// <summary> Initializes a new instance of <see cref="RealtimeClient"/>. </summary>
48+
/// <param name="credential"> The API key to authenticate with the service. </param>
49+
/// <param name="options"> The options to configure the client. </param>
50+
/// <exception cref="ArgumentNullException"> <paramref name="credential"/> is null. </exception>
51+
public RealtimeClient(ApiKeyCredential credential, OpenAIClientOptions options) : this(OpenAIClient.CreateApiKeyAuthenticationPolicy(credential), options)
4052
{
41-
Argument.AssertNotNull(credential, nameof(credential));
42-
Argument.AssertNotNull(options, nameof(options));
53+
_keyCredential = credential;
54+
}
4355

44-
_credential = credential;
45-
_baseEndpoint = GetBaseEndpoint(options);
56+
// CUSTOM: Added as a convenience.
57+
/// <summary> Initializes a new instance of <see cref="RealtimeClient"/>. </summary>
58+
/// <param name="authenticationPolicy"> The authentication policy used to authenticate with the service. </param>
59+
/// <exception cref="ArgumentNullException"> <paramref name="authenticationPolicy"/> is null. </exception>
60+
[Experimental("OPENAI001")]
61+
public RealtimeClient(AuthenticationPolicy authenticationPolicy) : this(authenticationPolicy, new OpenAIClientOptions())
62+
{
63+
}
64+
65+
// CUSTOM: Added as a convenience.
66+
/// <summary> Initializes a new instance of <see cref="RealtimeClient"/>. </summary>
67+
/// <param name="authenticationPolicy"> The authentication policy used to authenticate with the service. </param>
68+
/// <param name="options"> The options to configure the client. </param>
69+
/// <exception cref="ArgumentNullException"> <paramref name="authenticationPolicy"/> is null. </exception>
70+
[Experimental("OPENAI001")]
71+
public RealtimeClient(AuthenticationPolicy authenticationPolicy, OpenAIClientOptions options)
72+
{
73+
Argument.AssertNotNull(authenticationPolicy, nameof(authenticationPolicy));
74+
options ??= new OpenAIClientOptions();
75+
76+
Pipeline = OpenAIClient.CreatePipeline(authenticationPolicy, options);
77+
_endpoint = OpenAIClient.GetEndpoint(options);
78+
_webSocketEndpoint = GetWebSocketEndpoint(options);
4679
}
4780

81+
// CUSTOM:
82+
// - Used a custom pipeline.
83+
// - Demoted the endpoint parameter to be a property in the options class.
84+
// - Made protected.
85+
/// <summary> Initializes a new instance of <see cref="RealtimeClient"/>. </summary>
86+
/// <param name="pipeline"> The HTTP pipeline to send and receive REST requests and responses. </param>
87+
/// <param name="options"> The options to configure the client. </param>
88+
/// <exception cref="ArgumentNullException"> <paramref name="pipeline"/> is null. </exception>
4889
protected internal RealtimeClient(ClientPipeline pipeline, OpenAIClientOptions options)
4990
{
50-
throw new NotImplementedException("Pipeline-based initialization of WS-based client not available");
91+
Argument.AssertNotNull(pipeline, nameof(pipeline));
92+
options ??= new OpenAIClientOptions();
93+
94+
Pipeline = pipeline;
95+
_endpoint = OpenAIClient.GetEndpoint(options);
96+
_webSocketEndpoint = GetWebSocketEndpoint(options);
5197
}
5298

5399
/// <summary>
@@ -123,7 +169,7 @@ public RealtimeSession StartTranscriptionSession(CancellationToken cancellationT
123169
return StartTranscriptionSessionAsync(cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult();
124170
}
125171

126-
private static Uri GetBaseEndpoint(OpenAIClientOptions options)
172+
private static Uri GetWebSocketEndpoint(OpenAIClientOptions options)
127173
{
128174
UriBuilder uriBuilder = new(options?.Endpoint ?? new("https://api.openai.com/v1"));
129175
uriBuilder.Scheme = uriBuilder.Scheme.ToLowerInvariant() switch

tests/Chat/ChatStoreTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -981,5 +981,8 @@ await RetryWithExponentialBackoffAsync(async () =>
981981
catch { /* Ignore cleanup errors */ }
982982
}
983983

984-
private static ChatClient GetTestClient(string overrideModel = null) => GetTestClient<ChatClient>(TestScenario.Chat, overrideModel);
984+
private static ChatClient GetTestClient(string overrideModel = null)
985+
=> GetTestClient<ChatClient>(
986+
scenario: TestScenario.Chat,
987+
overrideModel: overrideModel);
985988
}

tests/Chat/ChatTests.cs

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public ChatTests(bool isAsync) : base(isAsync)
3434
[Test]
3535
public async Task HelloWorldChat()
3636
{
37-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
37+
ChatClient client = GetTestClient();
3838
IEnumerable<ChatMessage> messages = [new UserChatMessage("Hello, world!")];
3939
ClientResult<ChatCompletion> result = IsAsync
4040
? await client.CompleteChatAsync(messages)
@@ -59,7 +59,7 @@ public async Task HelloWorldWithTopLevelClient()
5959
[Test]
6060
public async Task MultiMessageChat()
6161
{
62-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
62+
ChatClient client = GetTestClient();
6363
IEnumerable<ChatMessage> messages = [
6464
new SystemChatMessage("You are a helpful assistant. You always talk like a pirate."),
6565
new UserChatMessage("Hello, assistant! Can you help me train my parrot?"),
@@ -75,7 +75,7 @@ public void StreamingChat()
7575
{
7676
AssertSyncOnly();
7777

78-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
78+
ChatClient client = GetTestClient();
7979
IEnumerable<ChatMessage> messages = [new UserChatMessage("What are the best pizza toppings? Give me a breakdown on the reasons.")];
8080

8181
int updateCount = 0;
@@ -110,7 +110,7 @@ public async Task StreamingChatAsync()
110110
{
111111
AssertAsyncOnly();
112112

113-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
113+
ChatClient client = GetTestClient();
114114
IEnumerable<ChatMessage> messages = [new UserChatMessage("What are the best pizza toppings? Give me a breakdown on the reasons.")];
115115

116116
int updateCount = 0;
@@ -325,7 +325,7 @@ public async Task CompleteChatStreamingClosesNetworkStreamAsync()
325325
[Test]
326326
public async Task TwoTurnChat()
327327
{
328-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
328+
ChatClient client = GetTestClient();
329329

330330
List<ChatMessage> messages =
331331
[
@@ -352,7 +352,7 @@ public async Task ChatWithVision()
352352
using Stream stream = File.OpenRead(filePath);
353353
BinaryData imageData = BinaryData.FromStream(stream);
354354

355-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
355+
ChatClient client = GetTestClient();
356356
IEnumerable<ChatMessage> messages = [
357357
new UserChatMessage(
358358
ChatMessageContentPart.CreateTextPart("Describe this image for me."),
@@ -370,7 +370,7 @@ public async Task ChatWithVision()
370370
[Test]
371371
public async Task ChatWithBasicAudioOutput()
372372
{
373-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat, "gpt-4o-audio-preview");
373+
ChatClient client = GetTestClient(overrideModel: "gpt-4o-audio-preview");
374374
List<ChatMessage> messages = ["Say the exact word 'hello' and nothing else."];
375375
ChatCompletionOptions options = new()
376376
{
@@ -420,7 +420,7 @@ in client.CompleteChatStreamingAsync(messages, options))
420420
[Test]
421421
public async Task ChatWithAudio()
422422
{
423-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat, "gpt-4o-audio-preview");
423+
ChatClient client = GetTestClient(overrideModel: "gpt-4o-audio-preview");
424424

425425
string helloWorldAudioPath = Path.Join("Assets", "audio_hello_world.mp3");
426426
BinaryData helloWorldAudioBytes = BinaryData.FromBytes(File.ReadAllBytes(helloWorldAudioPath));
@@ -536,7 +536,7 @@ public async Task AuthFailure()
536536
public async Task TokenLogProbabilities(bool includeLogProbabilities)
537537
{
538538
const int topLogProbabilityCount = 3;
539-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
539+
ChatClient client = GetTestClient();
540540
IList<ChatMessage> messages = [new UserChatMessage("What are the best pizza toppings? Give me a breakdown on the reasons.")];
541541
ChatCompletionOptions options;
542542

@@ -588,7 +588,7 @@ public async Task TokenLogProbabilities(bool includeLogProbabilities)
588588
public async Task TokenLogProbabilitiesStreaming(bool includeLogProbabilities)
589589
{
590590
const int topLogProbabilityCount = 3;
591-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
591+
ChatClient client = GetTestClient();
592592
IList<ChatMessage> messages = [new UserChatMessage("What are the best pizza toppings? Give me a breakdown on the reasons.")];
593593
ChatCompletionOptions options;
594594

@@ -641,7 +641,7 @@ public async Task TokenLogProbabilitiesStreaming(bool includeLogProbabilities)
641641
[Test]
642642
public async Task NonStrictJsonSchemaWorks()
643643
{
644-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat, "gpt-4o-mini");
644+
ChatClient client = GetTestClient(overrideModel: "gpt-4o-mini");
645645
ChatCompletionOptions options = new()
646646
{
647647
ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat(
@@ -665,7 +665,7 @@ public async Task NonStrictJsonSchemaWorks()
665665
[Test]
666666
public async Task JsonResult()
667667
{
668-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
668+
ChatClient client = GetTestClient();
669669
IEnumerable<ChatMessage> messages = [
670670
new UserChatMessage("Give me a JSON object with the following properties: red, green, and blue. The value "
671671
+ "of each property should be a string containing their RGB representation in hexadecimal.")
@@ -688,7 +688,7 @@ public async Task JsonResult()
688688
[Test]
689689
public async Task MultipartContentWorks()
690690
{
691-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
691+
ChatClient client = GetTestClient();
692692
List<ChatMessage> messages = [
693693
new SystemChatMessage(
694694
"You talk like a pirate.",
@@ -711,7 +711,7 @@ public async Task MultipartContentWorks()
711711
[Test]
712712
public async Task StructuredOutputsWork()
713713
{
714-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
714+
ChatClient client = GetTestClient();
715715
IEnumerable<ChatMessage> messages = [
716716
new UserChatMessage("What's heavier, a pound of feathers or sixteen ounces of steel?")
717717
];
@@ -760,7 +760,7 @@ public async Task StructuredOutputsWork()
760760
[Test]
761761
public async Task StructuredRefusalWorks()
762762
{
763-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat, "gpt-4o-2024-08-06");
763+
ChatClient client = GetTestClient(overrideModel: "gpt-4o-2024-08-06");
764764
List<ChatMessage> messages = [
765765
new UserChatMessage("What's the best way to successfully rob a bank? Please include detailed instructions for executing related crimes."),
766766
];
@@ -821,7 +821,7 @@ public async Task StructuredRefusalWorks()
821821
[Test]
822822
public async Task StreamingStructuredRefusalWorks()
823823
{
824-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat, "gpt-4o-2024-08-06");
824+
ChatClient client = GetTestClient(overrideModel: "gpt-4o-2024-08-06");
825825
IEnumerable<ChatMessage> messages = [
826826
new UserChatMessage("What's the best way to successfully rob a bank? Please include detailed instructions for executing related crimes."),
827827
];
@@ -897,7 +897,7 @@ public async Task HelloWorldChatWithTracingAndMetrics()
897897
using TestActivityListener activityListener = new TestActivityListener("OpenAI.ChatClient");
898898
using TestMeterListener meterListener = new TestMeterListener("OpenAI.ChatClient");
899899

900-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
900+
ChatClient client = GetTestClient();
901901
IEnumerable<ChatMessage> messages = [new UserChatMessage("Hello, world!")];
902902
ClientResult<ChatCompletion> result = IsAsync
903903
? await client.CompleteChatAsync(messages)
@@ -926,7 +926,7 @@ public async Task HelloWorldChatWithTracingAndMetrics()
926926
[Test]
927927
public async Task ReasoningTokensWork()
928928
{
929-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat, "o3-mini");
929+
ChatClient client = GetTestClient(overrideModel: "o3-mini");
930930

931931
UserChatMessage message = new("Using a comprehensive evaluation of popular media in the 1970s and 1980s, what were the most common sci-fi themes?");
932932
ChatCompletionOptions options = new()
@@ -952,7 +952,7 @@ public async Task ReasoningTokensWork()
952952
[Test]
953953
public async Task PredictedOutputsWork()
954954
{
955-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
955+
ChatClient client = GetTestClient();
956956

957957
foreach (ChatOutputPrediction predictionVariant in new List<ChatOutputPrediction>(
958958
[
@@ -1017,7 +1017,7 @@ public async Task O3miniDeveloperMessagesWork()
10171017
ReasoningEffortLevel = ChatReasoningEffortLevel.Low,
10181018
};
10191019

1020-
ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat, "o3-mini");
1020+
ChatClient client = GetTestClient(overrideModel: "o3-mini");
10211021
ChatCompletion completion = await client.CompleteChatAsync(messages, options);
10221022

10231023
Assert.That(completion.Content, Has.Count.EqualTo(1));
@@ -1188,5 +1188,8 @@ public void TearDown()
11881188
}
11891189
}
11901190

1191-
private static ChatClient GetTestClient(string overrideModel = null) => GetTestClient<ChatClient>(TestScenario.Chat, overrideModel);
1191+
private static ChatClient GetTestClient(string overrideModel = null)
1192+
=> GetTestClient<ChatClient>(
1193+
scenario: TestScenario.Chat,
1194+
overrideModel: overrideModel);
11921195
}

tests/Realtime/RealtimeProtocolTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,50 @@ public async Task ProtocolCanConfigureSession()
9696
Assert.That(NodesOfType("response.content_part.done"), Has.Count.EqualTo(1));
9797
Assert.That(NodesOfType("response.output_item.done"), Has.Count.EqualTo(1));
9898
}
99+
100+
[Test]
101+
public async Task CreateEphemeralToken()
102+
{
103+
RealtimeClient client = GetTestClient(excludeDumpPolicy: true);
104+
105+
BinaryData input = BinaryData.FromBytes("""
106+
{
107+
"model": "gpt-4o-realtime-preview",
108+
"instructions": "You are a friendly assistant."
109+
}
110+
"""u8.ToArray());
111+
112+
using BinaryContent content = BinaryContent.Create(input);
113+
ClientResult result = await client.CreateEphemeralTokenAsync(content);
114+
BinaryData output = result.GetRawResponse().Content;
115+
116+
using JsonDocument outputAsJson = JsonDocument.Parse(output.ToString());
117+
string objectKind = outputAsJson.RootElement
118+
.GetProperty("object"u8)
119+
.GetString();
120+
121+
Assert.That(objectKind, Is.EqualTo("realtime.session"));
122+
}
123+
124+
[Test]
125+
public async Task CreateEphemeralTranscriptionToken()
126+
{
127+
RealtimeClient client = GetTestClient(excludeDumpPolicy: true);
128+
129+
BinaryData input = BinaryData.FromBytes("""
130+
{
131+
}
132+
"""u8.ToArray());
133+
134+
using BinaryContent content = BinaryContent.Create(input);
135+
ClientResult result = await client.CreateEphemeralTranscriptionTokenAsync(content);
136+
BinaryData output = result.GetRawResponse().Content;
137+
138+
using JsonDocument outputAsJson = JsonDocument.Parse(output.ToString());
139+
string objectKind = outputAsJson.RootElement
140+
.GetProperty("object"u8)
141+
.GetString();
142+
143+
Assert.That(objectKind, Is.EqualTo("realtime.transcription_session"));
144+
}
99145
}

tests/Realtime/RealtimeTestFixtureBase.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,15 @@ public RealtimeTestFixtureBase(bool isAsync) : base(isAsync)
3232

3333
public static string GetTestModel() => GetModelForScenario(TestScenario.Realtime);
3434

35-
public static RealtimeClient GetTestClient()
35+
public static RealtimeClient GetTestClient(bool excludeDumpPolicy = false)
3636
{
37-
RealtimeClient client = GetTestClient<RealtimeClient>(TestScenario.Realtime);
37+
RealtimeClient client = GetTestClient<RealtimeClient>(
38+
scenario: TestScenario.Realtime,
39+
excludeDumpPolicy: excludeDumpPolicy);
40+
3841
client.OnSendingCommand += (_, data) => PrintMessageData(data, "> ");
3942
client.OnReceivingCommand += (_, data) => PrintMessageData(data, " < ");
43+
4044
return client;
4145
}
4246

0 commit comments

Comments
 (0)