Skip to content

Commit 9e89efe

Browse files
ElevenLabs-DotNet 2.2.1 (#43)
- misc formatting changes ## ElevenLabs-DotNet-Proxy 2.2.1 - Refactor with modern WebApplication builder - Added ElevenLabs.Proxy.EndpointRouteBuilder
1 parent 9caa655 commit 9e89efe

File tree

6 files changed

+159
-119
lines changed

6 files changed

+159
-119
lines changed

ElevenLabs-DotNet-Proxy/ElevenLabs-DotNet-Proxy.csproj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
<PackageTags>ElevenLabs, AI, ML, API, api-proxy, proxy, gateway</PackageTags>
1616
<Title>ElevenLabs API Proxy</Title>
1717
<PackageId>ElevenLabs-DotNet-Proxy</PackageId>
18-
<Version>1.2.0</Version>
18+
<Version>2.2.1</Version>
1919
<RootNamespace>ElevenLabs.Proxy</RootNamespace>
20-
<PackageReleaseNotes>Initial Release!</PackageReleaseNotes>
20+
<PackageReleaseNotes>Version 2.2.1
21+
- Refactor with modern WebApplication builder
22+
- Added ElevenLabs.Proxy.EndpointRouteBuilder
23+
</PackageReleaseNotes>
2124
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
2225
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
2326
<PackageReadmeFile>Readme.md</PackageReadmeFile>

ElevenLabs-DotNet-Proxy/Proxy/ElevenLabsProxyStartup.cs

Lines changed: 13 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,8 @@
66
using Microsoft.AspNetCore.Server.Kestrel.Core;
77
using Microsoft.Extensions.DependencyInjection;
88
using Microsoft.Extensions.Hosting;
9-
using Microsoft.Net.Http.Headers;
109
using System;
11-
using System.Collections.Generic;
12-
using System.IO;
13-
using System.Linq;
14-
using System.Net.Http;
15-
using System.Security.Authentication;
1610
using System.Threading.Tasks;
17-
using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue;
1811

1912
namespace ElevenLabs.Proxy
2013
{
@@ -26,37 +19,18 @@ public class ElevenLabsProxyStartup
2619
private ElevenLabsClient elevenLabsClient;
2720
private IAuthenticationFilter authenticationFilter;
2821

29-
// Copied from https://github.com/microsoft/reverse-proxy/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/RequestUtilities.cs#L61-L83
30-
private static readonly HashSet<string> excludedHeaders = new()
31-
{
32-
HeaderNames.Connection,
33-
HeaderNames.TransferEncoding,
34-
HeaderNames.KeepAlive,
35-
HeaderNames.Upgrade,
36-
"Proxy-Connection",
37-
"Proxy-Authenticate",
38-
"Proxy-Authentication-Info",
39-
"Proxy-Authorization",
40-
"Proxy-Features",
41-
"Proxy-Instruction",
42-
"Security-Scheme",
43-
"ALPN",
44-
"Close",
45-
HeaderNames.TE,
46-
#if NET
47-
HeaderNames.AltSvc,
48-
#else
49-
"Alt-Svc",
50-
#endif
51-
};
52-
5322
/// <summary>
54-
/// Configures the <see cref="elevenLabsClient"/> and <see cref="IAuthenticationFilter"/> services.
23+
/// Configures the <see cref="ElevenLabsClient"/> and <see cref="IAuthenticationFilter"/> services.
5524
/// </summary>
5625
/// <param name="services"></param>
5726
public void ConfigureServices(IServiceCollection services)
5827
=> SetupServices(services.BuildServiceProvider());
5928

29+
/// <summary>
30+
/// Configures the <see cref="IApplicationBuilder"/> to handle requests and forward them to OpenAI API.
31+
/// </summary>
32+
/// <param name="app"><see cref="IApplicationBuilder"/>.</param>
33+
/// <param name="env"><see cref="IWebHostEnvironment"/>.</param>
6034
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
6135
{
6236
if (env.IsDevelopment())
@@ -71,7 +45,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
7145
app.UseEndpoints(endpoints =>
7246
{
7347
endpoints.MapGet("/health", HealthEndpoint);
74-
endpoints.Map($"{elevenLabsClient.ElevenLabsClientSettings.BaseRequest}{{**endpoint}}", HandleRequest);
48+
endpoints.MapElevenLabsEndpoints(elevenLabsClient, authenticationFilter);
7549
});
7650
}
7751

@@ -94,6 +68,12 @@ public static IHost CreateDefaultHost<T>(string[] args, ElevenLabsClient elevenL
9468
services.AddSingleton<IAuthenticationFilter, T>();
9569
}).Build();
9670

71+
/// <summary>
72+
/// Creates a new <see cref="WebApplication"/> that acts as a proxy web api for OpenAI.
73+
/// </summary>
74+
/// <typeparam name="T"><see cref="IAuthenticationFilter"/> type to use to validate your custom issued tokens.</typeparam>
75+
/// <param name="args">Startup args.</param>
76+
/// <param name="openAIClient"><see cref="OpenAIClient"/> with configured <see cref="OpenAIAuthentication"/> and <see cref="OpenAIClientSettings"/>.</param>
9777
public static WebApplication CreateWebApplication<T>(string[] args, ElevenLabsClient elevenLabsClient) where T : class, IAuthenticationFilter
9878
{
9979
var builder = WebApplication.CreateBuilder(args);
@@ -130,75 +110,5 @@ private static async Task HealthEndpoint(HttpContext context)
130110
const string content = "OK";
131111
await context.Response.WriteAsync(content);
132112
}
133-
134-
/// <summary>
135-
/// Handles incoming requests, validates authentication, and forwards the request to ElevenLabs API
136-
/// </summary>
137-
private async Task HandleRequest(HttpContext httpContext, string endpoint)
138-
{
139-
try
140-
{
141-
// ReSharper disable once MethodHasAsyncOverload
142-
// just in case either method is implemented we call it twice.
143-
authenticationFilter.ValidateAuthentication(httpContext.Request.Headers);
144-
await authenticationFilter.ValidateAuthenticationAsync(httpContext.Request.Headers);
145-
146-
var method = new HttpMethod(httpContext.Request.Method);
147-
var uri = new Uri(string.Format(elevenLabsClient.ElevenLabsClientSettings.BaseRequestUrlFormat, $"{endpoint}{httpContext.Request.QueryString}"));
148-
using var request = new HttpRequestMessage(method, uri);
149-
150-
request.Content = new StreamContent(httpContext.Request.Body);
151-
152-
if (httpContext.Request.ContentType != null)
153-
{
154-
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(httpContext.Request.ContentType);
155-
}
156-
157-
var proxyResponse = await elevenLabsClient.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
158-
httpContext.Response.StatusCode = (int)proxyResponse.StatusCode;
159-
160-
foreach (var (key, value) in proxyResponse.Headers)
161-
{
162-
if (excludedHeaders.Contains(key)) { continue; }
163-
httpContext.Response.Headers[key] = value.ToArray();
164-
}
165-
166-
foreach (var (key, value) in proxyResponse.Content.Headers)
167-
{
168-
if (excludedHeaders.Contains(key)) { continue; }
169-
httpContext.Response.Headers[key] = value.ToArray();
170-
}
171-
172-
httpContext.Response.ContentType = proxyResponse.Content.Headers.ContentType?.ToString() ?? string.Empty;
173-
const string streamingContent = "text/event-stream";
174-
175-
if (httpContext.Response.ContentType.Equals(streamingContent))
176-
{
177-
var stream = await proxyResponse.Content.ReadAsStreamAsync();
178-
await WriteServerStreamEventsAsync(httpContext, stream);
179-
}
180-
else
181-
{
182-
await proxyResponse.Content.CopyToAsync(httpContext.Response.Body);
183-
}
184-
}
185-
catch (AuthenticationException authenticationException)
186-
{
187-
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
188-
await httpContext.Response.WriteAsync(authenticationException.Message);
189-
}
190-
catch (Exception e)
191-
{
192-
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
193-
await httpContext.Response.WriteAsync(e.Message);
194-
}
195-
}
196-
197-
private static async Task WriteServerStreamEventsAsync(HttpContext httpContext, Stream contentStream)
198-
{
199-
var responseStream = httpContext.Response.Body;
200-
await contentStream.CopyToAsync(responseStream, httpContext.RequestAborted);
201-
await responseStream.FlushAsync(httpContext.RequestAborted);
202-
}
203113
}
204114
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Licensed under the MIT License. See LICENSE in the project root for license information.
2+
3+
using Microsoft.AspNetCore.Builder;
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Routing;
6+
using Microsoft.Net.Http.Headers;
7+
using System;
8+
using System.Collections.Generic;
9+
using System.IO;
10+
using System.Linq;
11+
using System.Net.Http;
12+
using System.Security.Authentication;
13+
using System.Text.Json;
14+
using System.Threading.Tasks;
15+
16+
namespace ElevenLabs.Proxy
17+
{
18+
public static class EndpointRouteBuilder
19+
{
20+
// Copied from https://github.com/microsoft/reverse-proxy/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/RequestUtilities.cs#L61-L83
21+
private static readonly HashSet<string> excludedHeaders = new()
22+
{
23+
HeaderNames.Connection,
24+
HeaderNames.TransferEncoding,
25+
HeaderNames.KeepAlive,
26+
HeaderNames.Upgrade,
27+
"Proxy-Connection",
28+
"Proxy-Authenticate",
29+
"Proxy-Authentication-Info",
30+
"Proxy-Authorization",
31+
"Proxy-Features",
32+
"Proxy-Instruction",
33+
"Security-Scheme",
34+
"ALPN",
35+
"Close",
36+
"Set-Cookie",
37+
HeaderNames.TE,
38+
#if NET
39+
HeaderNames.AltSvc,
40+
#else
41+
"Alt-Svc",
42+
#endif
43+
};
44+
45+
public static void MapElevenLabsEndpoints(this IEndpointRouteBuilder endpoints,
46+
ElevenLabsClient elevenLabsClient, IAuthenticationFilter authenticationFilter)
47+
{
48+
endpoints.Map($"{elevenLabsClient.ElevenLabsClientSettings.BaseRequest}{{**endpoint}}", HandleRequest);
49+
50+
async Task HandleRequest(HttpContext httpContext, string endpoint)
51+
{
52+
try
53+
{
54+
// ReSharper disable once MethodHasAsyncOverload
55+
// just in case either method is implemented we call it twice.
56+
authenticationFilter.ValidateAuthentication(httpContext.Request.Headers);
57+
await authenticationFilter.ValidateAuthenticationAsync(httpContext.Request.Headers);
58+
59+
var method = new HttpMethod(httpContext.Request.Method);
60+
var uri = new Uri(string.Format(
61+
elevenLabsClient.ElevenLabsClientSettings.BaseRequestUrlFormat,
62+
$"{endpoint}{httpContext.Request.QueryString}"
63+
));
64+
using var request = new HttpRequestMessage(method, uri);
65+
request.Content = new StreamContent(httpContext.Request.Body);
66+
67+
if (httpContext.Request.ContentType != null)
68+
{
69+
request.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse(httpContext.Request.ContentType);
70+
}
71+
72+
var proxyResponse = await elevenLabsClient.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
73+
httpContext.Response.StatusCode = (int)proxyResponse.StatusCode;
74+
75+
foreach (var (key, value) in proxyResponse.Headers)
76+
{
77+
if (excludedHeaders.Contains(key))
78+
{
79+
continue;
80+
}
81+
82+
httpContext.Response.Headers[key] = value.ToArray();
83+
}
84+
85+
foreach (var (key, value) in proxyResponse.Content.Headers)
86+
{
87+
if (excludedHeaders.Contains(key))
88+
{
89+
continue;
90+
}
91+
92+
httpContext.Response.Headers[key] = value.ToArray();
93+
}
94+
95+
httpContext.Response.ContentType = proxyResponse.Content.Headers.ContentType?.ToString() ?? string.Empty;
96+
const string streamingContent = "text/event-stream";
97+
98+
if (httpContext.Response.ContentType.Equals(streamingContent))
99+
{
100+
var stream = await proxyResponse.Content.ReadAsStreamAsync();
101+
await WriteServerStreamEventsAsync(httpContext, stream);
102+
}
103+
else
104+
{
105+
await proxyResponse.Content.CopyToAsync(httpContext.Response.Body);
106+
}
107+
}
108+
catch (AuthenticationException authenticationException)
109+
{
110+
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
111+
await httpContext.Response.WriteAsync(authenticationException.Message);
112+
}
113+
catch (Exception e)
114+
{
115+
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
116+
var response = JsonSerializer.Serialize(new { error = new { e.Message, e.StackTrace } });
117+
await httpContext.Response.WriteAsync(response);
118+
}
119+
120+
static async Task WriteServerStreamEventsAsync(HttpContext httpContext, Stream contentStream)
121+
{
122+
var responseStream = httpContext.Response.Body;
123+
await contentStream.CopyToAsync(responseStream, httpContext.RequestAborted);
124+
await responseStream.FlushAsync(httpContext.RequestAborted);
125+
}
126+
}
127+
}
128+
}
129+
}

ElevenLabs-DotNet-Tests/Test_Fixture_02_VoicesEndpoint.cs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public async Task Test_03_GetVoice()
4343
var results = await ElevenLabsClient.VoicesEndpoint.GetAllVoicesAsync();
4444
Assert.NotNull(results);
4545
Assert.IsNotEmpty(results);
46-
var voiceToGet = results.OrderBy(voice => voice.Name).FirstOrDefault();
46+
var voiceToGet = results.MinBy(voice => voice.Name);
4747
var result = await ElevenLabsClient.VoicesEndpoint.GetVoiceAsync(voiceToGet);
4848
Assert.NotNull(result);
4949
Console.WriteLine($"{result.Id} | {result.Name} | {result.PreviewUrl}");
@@ -94,7 +94,7 @@ public async Task Test_06_AddVoiceFromByteArray()
9494
{ "accent", "american" }
9595
};
9696
var clipPath = Path.GetFullPath("../../../Assets/test_sample_01.ogg");
97-
byte[] clipData = await File.ReadAllBytesAsync(clipPath);
97+
var clipData = await File.ReadAllBytesAsync(clipPath);
9898
var result = await ElevenLabsClient.VoicesEndpoint.AddVoiceAsync("Test Voice", new[] { clipData }, testLabels);
9999
Assert.NotNull(result);
100100
Console.WriteLine($"{result.Name}");
@@ -112,13 +112,11 @@ public async Task Test_07_AddVoiceFromStream()
112112
};
113113
var clipPath = Path.GetFullPath("../../../Assets/test_sample_01.ogg");
114114

115-
using (FileStream fs = File.OpenRead(clipPath))
116-
{
117-
var result = await ElevenLabsClient.VoicesEndpoint.AddVoiceAsync("Test Voice", new[] { fs }, testLabels);
118-
Assert.NotNull(result);
119-
Console.WriteLine($"{result.Name}");
120-
Assert.IsNotEmpty(result.Samples);
121-
}
115+
await using var fs = File.OpenRead(clipPath);
116+
var result = await ElevenLabsClient.VoicesEndpoint.AddVoiceAsync("Test Voice", new[] { fs }, testLabels);
117+
Assert.NotNull(result);
118+
Console.WriteLine($"{result.Name}");
119+
Assert.IsNotEmpty(result.Samples);
122120
}
123121

124122
[Test]

ElevenLabs-DotNet/ElevenLabs-DotNet.csproj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
<Company>RageAgainstThePixel</Company>
1515
<Copyright>2024</Copyright>
1616
<PackageTags>ElevenLabs, AI, ML, Voice, TTS</PackageTags>
17-
<Version>2.2.0</Version>
18-
<PackageReleaseNotes>Version 2.2.0
17+
<Version>2.2.1</Version>
18+
<PackageReleaseNotes>Version 2.2.1
19+
- Misc formatting changes
20+
Version 2.2.0
1921
- Changed ElevenLabsClient to be IDisposable
2022
- The ElevenLabsClient must now be disposed if you do not pass your own HttpClient
2123
- Updated ElevenLabsClientSettings to accept custom domains

ElevenLabs-DotNet/Voices/VoicesEndpoint.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ public VoicesEndpoint(ElevenLabsClient client) : base(client) { }
4242
/// <param name="cancellationToken"></param>
4343
/// <returns><see cref="IReadOnlyList{T}"/> of <see cref="Voice"/>s.</returns>
4444
public Task<IReadOnlyList<Voice>> GetAllVoicesAsync(CancellationToken cancellationToken = default)
45-
{
46-
return GetAllVoicesAsync(true, cancellationToken);
47-
}
45+
=> GetAllVoicesAsync(true, cancellationToken);
4846

4947
/// <summary>
5048
/// Gets a list of all available voices for a user.

0 commit comments

Comments
 (0)