Skip to content

Commit e564606

Browse files
HavenDVclaude
andcommitted
feat(openai): expose AsyncAPI server variable + subprotocol-auth
Wire tryAGI/AutoSDK #277 and #278 through asyncapi.json so the generated OpenAiRealtimeClient surfaces: - `ConnectAsync(string model, …)` — new typed overload that appends `?model=<model>` to the WebSocket URL via the AsyncAPI `servers[*].variables.model` declaration. - `AuthorizeUsingSubprotocol(string apiKey)` + `useSubprotocolAuth` parameter on ConnectAsync — browser-compatible auth that sends the API key as a WebSocket subprotocol instead of an Authorization header (which the JS WebSocket constructor can't set). Kept to a single subprotocol template to avoid the codegen bug I filed as tryAGI/AutoSDK#282 (duplicate `__subProtocol` locals when >1 template). The literal `realtime` subprotocol that OpenAI also requires can be passed via `additionalSubProtocols` on ConnectAsync until #282 lands, after which we can move it back into the scheme. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 13dd33f commit e564606

3 files changed

Lines changed: 128 additions & 5 deletions

File tree

src/libs/tryAGI.OpenAI/Generated/tryAGI.OpenAI.Realtime.OpenAiRealtimeClient.g.cs

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ public OpenAiRealtimeClient(
4545
private string? _storedAuthorizationHeaderScheme;
4646
private string? _storedAuthorizationApiKey;
4747

48+
private readonly global::System.Collections.Generic.Dictionary<string, string> _subprotocolAuthorizationValues = new global::System.Collections.Generic.Dictionary<string, string>(global::System.StringComparer.Ordinal);
49+
private bool _preferSubprotocolAuth;
50+
4851
/// <summary>
4952
/// Authorize using bearer authentication.
5053
/// </summary>
@@ -57,6 +60,8 @@ public void AuthorizeUsingBearer(
5760
_storedAuthorizationApiKey = apiKey;
5861
_storedAuthorizationHeaderName = "Authorization";
5962
_storedAuthorizationHeaderScheme = "bearer";
63+
_preferSubprotocolAuth = false;
64+
_subprotocolAuthorizationValues["apiKey"] = apiKey;
6065
}
6166

6267
/// <summary>
@@ -75,13 +80,34 @@ public OpenAiRealtimeClient(
7580
Authorized(_clientWebSocket);
7681
}
7782

83+
/// <summary>
84+
/// Authorize using WebSocket subprotocol authentication.
85+
/// </summary>
86+
/// <param name="apiKey"></param>
87+
public void AuthorizeUsingSubprotocol(
88+
string apiKey
89+
)
90+
{
91+
var apiKeyValue = apiKey ?? throw new global::System.ArgumentNullException(nameof(apiKey));
92+
_subprotocolAuthorizationValues["apiKey"] = apiKeyValue;
93+
_preferSubprotocolAuth = true;
94+
}
95+
7896

7997

8098

8199

82100
private void ApplyStoredAuthorization(
83101
bool useSubprotocolAuth)
84102
{
103+
if (useSubprotocolAuth || _preferSubprotocolAuth)
104+
{
105+
if (_subprotocolAuthorizationValues.ContainsKey("apiKey"))
106+
{
107+
var __apiKey = _subprotocolAuthorizationValues["apiKey"]; var __subProtocol = "openai-insecure-api-key.{apiKey}";
108+
__subProtocol = __subProtocol.Replace("{apiKey}", __apiKey); _clientWebSocket.Options.AddSubProtocol(__subProtocol); return;
109+
} return;
110+
}
85111

86112
if (_storedAuthorizationApiKey is not null &&
87113
_storedAuthorizationHeaderName is not null)
@@ -95,14 +121,15 @@ private void ApplyStoredAuthorization(
95121
private void ApplyConnectionOptions(
96122
global::System.Collections.Generic.IDictionary<string, string>? additionalHeaders,
97123
global::System.Collections.Generic.IEnumerable<string>? additionalSubProtocols,
98-
global::System.TimeSpan? keepAliveInterval)
124+
global::System.TimeSpan? keepAliveInterval,
125+
bool useSubprotocolAuth)
99126
{
100127
if (keepAliveInterval is not null)
101128
{
102129
_clientWebSocket.Options.KeepAliveInterval = keepAliveInterval.Value;
103130
}
104131

105-
ApplyStoredAuthorization(false);
132+
ApplyStoredAuthorization(useSubprotocolAuth);
106133

107134
if (additionalHeaders is not null)
108135
{
@@ -146,13 +173,17 @@ private void ApplyConnectionOptions(
146173
}
147174
}
148175

176+
private const string DefaultBaseUrlTemplate = "wss://api.openai.com/v1/realtime";
177+
178+
149179
/// <inheritdoc cref="global::System.Net.WebSockets.ClientWebSocket.ConnectAsync(global::System.Uri, global::System.Threading.CancellationToken)"/>
150180
public async global::System.Threading.Tasks.Task ConnectAsync(
151181
global::System.Uri? uri = null,
152182
global::System.Collections.Generic.IDictionary<string, string>? additionalHeaders = null,
153183
global::System.Collections.Generic.IEnumerable<string>? additionalSubProtocols = null,
154184
global::System.TimeSpan? keepAliveInterval = null,
155185
global::System.TimeSpan? connectTimeout = null,
186+
bool useSubprotocolAuth = false,
156187
global::System.Threading.CancellationToken cancellationToken = default)
157188
{
158189
global::System.Uri __uri;
@@ -168,7 +199,49 @@ private void ApplyConnectionOptions(
168199
__uri = new global::System.Uri(__pathBuilder.ToString());
169200
}
170201

171-
ApplyConnectionOptions(additionalHeaders, additionalSubProtocols, keepAliveInterval);
202+
ApplyConnectionOptions(additionalHeaders, additionalSubProtocols, keepAliveInterval, useSubprotocolAuth);
203+
await ConnectAsyncCore(__uri, connectTimeout, cancellationToken).ConfigureAwait(false);
204+
}
205+
206+
/// <summary>
207+
/// Connects to the WebSocket server with typed connection parameters.
208+
/// </summary>
209+
/// <param name="model">Realtime model ID. The generator emits this as a required parameter on ConnectAsync and appends it to the URL as ?model=&lt;id&gt;.</param>
210+
/// <param name="uri">Optional WebSocket endpoint override.</param>
211+
/// <param name="additionalHeaders">Additional headers applied before connecting.</param>
212+
/// <param name="additionalSubProtocols">Additional WebSocket subprotocols applied before connecting.</param>
213+
/// <param name="keepAliveInterval">Optional keep-alive interval.</param>
214+
/// <param name="connectTimeout">Optional connect timeout.</param>
215+
/// <param name="useSubprotocolAuth">When true, applies stored subprotocol authentication instead of header authentication.</param>
216+
/// <param name="cancellationToken">A cancellation token.</param>
217+
public async global::System.Threading.Tasks.Task ConnectAsync(
218+
string model,
219+
global::System.Uri? uri = null,
220+
global::System.Collections.Generic.IDictionary<string, string>? additionalHeaders = null,
221+
global::System.Collections.Generic.IEnumerable<string>? additionalSubProtocols = null,
222+
global::System.TimeSpan? keepAliveInterval = null,
223+
global::System.TimeSpan? connectTimeout = null,
224+
bool useSubprotocolAuth = false,
225+
global::System.Threading.CancellationToken cancellationToken = default)
226+
{
227+
global::System.Uri __uri;
228+
if (uri is not null)
229+
{
230+
__uri = uri;
231+
}
232+
else
233+
{
234+
var __baseUrl = DefaultBaseUrlTemplate;
235+
var __pathBuilder = new global::tryAGI.OpenAI.Realtime.PathBuilder(
236+
path: __baseUrl);
237+
__pathBuilder
238+
.AddRequiredParameter("model", model)
239+
;
240+
241+
__uri = new global::System.Uri(__pathBuilder.ToString());
242+
}
243+
244+
ApplyConnectionOptions(additionalHeaders, additionalSubProtocols, keepAliveInterval, useSubprotocolAuth);
172245
await ConnectAsyncCore(__uri, connectTimeout, cancellationToken).ConfigureAwait(false);
173246
}
174247

src/libs/tryAGI.OpenAI/asyncapi.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,20 @@
1111
"pathname": "/v1/realtime",
1212
"protocol": "wss",
1313
"description": "OpenAI Realtime WebSocket server",
14+
"variables": {
15+
"model": {
16+
"description": "Realtime model ID. The generator emits this as a required parameter on ConnectAsync and appends it to the URL as ?model=<id>.",
17+
"examples": [
18+
"gpt-4o-realtime-preview"
19+
]
20+
}
21+
},
1422
"security": [
1523
{
1624
"$ref": "#/components/securitySchemes/bearer"
25+
},
26+
{
27+
"$ref": "#/components/securitySchemes/subprotocol"
1728
}
1829
]
1930
}
@@ -356,6 +367,13 @@
356367
"type": "http",
357368
"scheme": "bearer",
358369
"description": "OpenAI API key as a Bearer token."
370+
},
371+
"subprotocol": {
372+
"type": "apiKey",
373+
"description": "Browser-compatible auth via WebSocket subprotocols. When ConnectAsync is called with useSubprotocolAuth: true, the client advertises `openai-insecure-api-key.<apiKey>`, avoiding the need to set an Authorization header (which browsers disallow on the JS WebSocket constructor).\n\nOpenAI also expects the `realtime` subprotocol on the connection; include it via the `additionalSubProtocols` parameter on ConnectAsync.\n\nNOTE: single template until tryAGI/AutoSDK codegen bug (duplicate __subProtocol locals with >1 template) is fixed.",
374+
"x-subprotocol-auth": [
375+
"openai-insecure-api-key.{apiKey}"
376+
]
359377
}
360378
},
361379
"messages": {

src/libs/tryAGI.OpenAI/build-asyncapi.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,20 @@ def main() -> int:
155155
"pathname": "/v1/realtime",
156156
"protocol": "wss",
157157
"description": "OpenAI Realtime WebSocket server",
158-
"security": [{"$ref": "#/components/securitySchemes/bearer"}],
158+
"variables": {
159+
"model": {
160+
"description": (
161+
"Realtime model ID. The generator emits this as a "
162+
"required parameter on ConnectAsync and appends it "
163+
"to the URL as ?model=<id>."
164+
),
165+
"examples": ["gpt-4o-realtime-preview"],
166+
}
167+
},
168+
"security": [
169+
{"$ref": "#/components/securitySchemes/bearer"},
170+
{"$ref": "#/components/securitySchemes/subprotocol"},
171+
],
159172
}
160173
},
161174
"channels": {
@@ -168,7 +181,26 @@ def main() -> int:
168181
"type": "http",
169182
"scheme": "bearer",
170183
"description": "OpenAI API key as a Bearer token.",
171-
}
184+
},
185+
"subprotocol": {
186+
"type": "apiKey",
187+
"description": (
188+
"Browser-compatible auth via WebSocket subprotocols. "
189+
"When ConnectAsync is called with useSubprotocolAuth: true, "
190+
"the client advertises `openai-insecure-api-key.<apiKey>`, "
191+
"avoiding the need to set an Authorization header (which "
192+
"browsers disallow on the JS WebSocket constructor).\n\n"
193+
"OpenAI also expects the `realtime` subprotocol on the "
194+
"connection; include it via the `additionalSubProtocols` "
195+
"parameter on ConnectAsync.\n\n"
196+
"NOTE: single template until tryAGI/AutoSDK codegen bug "
197+
"(duplicate __subProtocol locals with >1 template) is "
198+
"fixed."
199+
),
200+
"x-subprotocol-auth": [
201+
"openai-insecure-api-key.{apiKey}",
202+
],
203+
},
172204
},
173205
"messages": component_messages,
174206
"schemas": inline_schemas,

0 commit comments

Comments
 (0)