|
| 1 | +//----------------------------------------------------------------------------- |
| 2 | +// Filename: OpenAIRealtimeRestClient.cs |
| 3 | +// |
| 4 | +// Description: |
| 5 | +// |
| 6 | +// Author(s): |
| 7 | +// Aaron Clauson ([email protected]) |
| 8 | +// |
| 9 | +// History: |
| 10 | +// 23 Jan 2025 Aaron Clauson Created, Dublin, Ireland. |
| 11 | +// |
| 12 | +// License: |
| 13 | +// MIT. |
| 14 | +//----------------------------------------------------------------------------- |
| 15 | + |
| 16 | +using LanguageExt; |
| 17 | +using System.Net.Http.Headers; |
| 18 | +using System.Net.Http; |
| 19 | +using System.Text.Json; |
| 20 | +using System.Text; |
| 21 | +using System.Threading.Tasks; |
| 22 | +using LanguageExt.Common; |
| 23 | + |
| 24 | +namespace demo; |
| 25 | + |
| 26 | +public class OpenAIRealtimeRestClient |
| 27 | +{ |
| 28 | + /// <summary> |
| 29 | + /// Completes the steps required to get an ephemeral key from the OpenAI REST server. The ephemeral key is needed |
| 30 | + /// to send an SDP offer, and get the SDP answer. |
| 31 | + /// </summary> |
| 32 | + public static async Task<Either<Error, string>> CreateEphemeralKeyAsync(string sessionsUrl, string openAIToken, string model, OpenAIVoicesEnum voice) |
| 33 | + => (await SendHttpPostAsync( |
| 34 | + sessionsUrl, |
| 35 | + openAIToken, |
| 36 | + JsonSerializer.Serialize( |
| 37 | + new OpenAISession |
| 38 | + { |
| 39 | + Model = model, |
| 40 | + Voice = voice |
| 41 | + }, JsonOptions.Default), |
| 42 | + "application/json")) |
| 43 | + .Bind(responseContent => |
| 44 | + JsonSerializer.Deserialize<JsonElement>(responseContent) |
| 45 | + .GetProperty("client_secret") |
| 46 | + .GetProperty("value") |
| 47 | + .GetString() ?? |
| 48 | + Prelude.Left<Error, string>(Error.New("Failed to get ephemeral secret.")) |
| 49 | + ); |
| 50 | + |
| 51 | + /// <summary> |
| 52 | + /// Attempts to get the SDP answer from the OpenAI REST server. This is the way OpenAI does the signalling. The |
| 53 | + /// ICE candidates will be returned in the SDP answer and are publicly accessible IP's. |
| 54 | + /// </summary> |
| 55 | + /// <remarks> |
| 56 | + /// See https://platform.openai.com/docs/guides/realtime-webrtc#creating-an-ephemeral-token. |
| 57 | + /// </remarks> |
| 58 | + public static Task<Either<Error, string>> GetOpenAIAnswerSdpAsync(string ephemeralKey, string openAIBaseUrl, string model, string offerSdp) |
| 59 | + => SendHttpPostAsync( |
| 60 | + $"{openAIBaseUrl}?model={model}", |
| 61 | + ephemeralKey, |
| 62 | + offerSdp, |
| 63 | + "application/sdp"); |
| 64 | + |
| 65 | + /// <summary> |
| 66 | + /// Helper method to send an HTTP POST request with the required headers. |
| 67 | + /// </summary> |
| 68 | + public static async Task<Either<Error, string>> SendHttpPostAsync( |
| 69 | + string url, |
| 70 | + string token, |
| 71 | + string body, |
| 72 | + string contentType) |
| 73 | + { |
| 74 | + using var httpClient = new HttpClient(); |
| 75 | + |
| 76 | + httpClient.DefaultRequestHeaders.Clear(); |
| 77 | + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); |
| 78 | + |
| 79 | + var content = new StringContent(body, Encoding.UTF8); |
| 80 | + content.Headers.ContentType = new MediaTypeHeaderValue(contentType); |
| 81 | + |
| 82 | + var response = await httpClient.PostAsync(url, content); |
| 83 | + |
| 84 | + if (!response.IsSuccessStatusCode) |
| 85 | + { |
| 86 | + var errorBody = await response.Content.ReadAsStringAsync(); |
| 87 | + return Error.New($"HTTP POST to {url} failed: {response.StatusCode}. Error body: {errorBody}"); |
| 88 | + } |
| 89 | + |
| 90 | + return await response.Content.ReadAsStringAsync(); |
| 91 | + } |
| 92 | +} |
0 commit comments