openai/openai-dotnet

Public

mirrored from https://github.com/openai/openai-dotnetAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
OpenAI_2.0.0-beta.1

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/Custom/Internal/TelemetryDetails.cs

150lines · modecode

1using System;
2using System.Net.Http.Headers;
3using System.Reflection;
4using System.Runtime.InteropServices;
5using System.Text;
6
7#nullable enable
8
9namespace OpenAI;
10
11/// <summary>
12/// Details about the package to be included in UserAgent telemetry
13/// </summary>
14internal class TelemetryDetails
15{
16 private const int MaxApplicationIdLength = 24;
17 private readonly string _userAgent;
18
19 /// <summary>
20 /// The package type represented by this <see cref="TelemetryDetails"/> instance.
21 /// </summary>
22 public Assembly Assembly { get; }
23
24 /// <summary>
25 /// The value of the applicationId used to initialize this <see cref="TelemetryDetails"/> instance.
26 /// </summary>
27 public string? ApplicationId { get; }
28
29 /// <summary>
30 /// Initialize an instance of <see cref="TelemetryDetails"/> by extracting the name and version information from the <see cref="System.Reflection.Assembly"/> associated with the <paramref name="assembly"/>.
31 /// </summary>
32 /// <param name="assembly">The <see cref="System.Reflection.Assembly"/> used to generate the package name and version information for the <see cref="TelemetryDetails"/> value.</param>
33 /// <param name="applicationId">An optional value to be prepended to the <see cref="TelemetryDetails"/>.
34 internal TelemetryDetails(Assembly assembly, string? applicationId = null)
35 : this(assembly, applicationId, new RuntimeInformationWrapper())
36 { }
37
38 internal TelemetryDetails(Assembly assembly, string? applicationId = null, RuntimeInformationWrapper? runtimeInformation = default)
39 {
40 Argument.AssertNotNull(assembly, nameof(assembly));
41 if (applicationId?.Length > MaxApplicationIdLength)
42 {
43 throw new ArgumentOutOfRangeException(nameof(applicationId), $"{nameof(applicationId)} must be shorter than {MaxApplicationIdLength + 1} characters");
44 }
45
46 Assembly = assembly;
47 ApplicationId = applicationId;
48 _userAgent = GenerateUserAgentString(assembly, applicationId, runtimeInformation);
49 }
50
51 internal static string GenerateUserAgentString(Assembly clientAssembly, string? applicationId = null, RuntimeInformationWrapper? runtimeInformation = default)
52 {
53 AssemblyInformationalVersionAttribute? versionAttribute
54 = clientAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
55 ?? throw new InvalidOperationException(
56 $"{nameof(AssemblyInformationalVersionAttribute)} is required on client SDK assembly '{clientAssembly.FullName}'.");
57
58 string version = versionAttribute.InformationalVersion;
59
60 string assemblyName = clientAssembly.GetName().Name!;
61
62 int hashSeparator = version.LastIndexOf('+');
63 if (hashSeparator != -1)
64 {
65 version = version.Substring(0, hashSeparator);
66 }
67 runtimeInformation ??= new RuntimeInformationWrapper();
68 var platformInformation = EscapeProductInformation($"({runtimeInformation.FrameworkDescription}; {runtimeInformation.OSDescription})");
69
70 return applicationId != null
71 ? $"{applicationId} {assemblyName}/{version} {platformInformation}"
72 : $"{assemblyName}/{version} {platformInformation}";
73 }
74
75 /// <summary>
76 /// The properly formatted UserAgent string based on this <see cref="TelemetryDetails"/> instance.
77 /// </summary>
78 public override string ToString() => _userAgent;
79
80 /// <summary>
81 /// If the ProductInformation is not in the proper format, this escapes any ')' , '(' or '\' characters per https://www.rfc-editor.org/rfc/rfc7230#section-3.2.6
82 /// </summary>
83 /// <param name="productInfo">The ProductInfo portion of the UserAgent</param>
84 /// <returns></returns>
85 private static string EscapeProductInformation(string productInfo)
86 {
87 // If the string is already valid, we don't need to escape anything
88 bool success = false;
89 try
90 {
91 success = ProductInfoHeaderValue.TryParse(productInfo, out var _);
92 }
93 catch (Exception)
94 {
95 // Invalid values can throw in Framework due to https://github.com/dotnet/runtime/issues/28558
96 // Treat this as a failure to parse.
97 }
98 if (success)
99 {
100 return productInfo;
101 }
102
103 var sb = new StringBuilder(productInfo.Length + 2);
104 sb.Append('(');
105 // exclude the first and last characters, which are the enclosing parentheses
106 for (int i = 1; i < productInfo.Length - 1; i++)
107 {
108 char c = productInfo[i];
109 if (c == ')' || c == '(')
110 {
111 sb.Append('\\');
112 }
113 // If we see a \, we don't need to escape it if it's followed by a '\', '(', or ')', because it is already escaped.
114 else if (c == '\\')
115 {
116 if (i + 1 < (productInfo.Length - 1))
117 {
118 char next = productInfo[i + 1];
119 if (next == '\\' || next == '(' || next == ')')
120 {
121 sb.Append(c);
122 sb.Append(next);
123 i++;
124 continue;
125 }
126 else
127 {
128 sb.Append('\\');
129 }
130 }
131 else
132 {
133 sb.Append('\\');
134 }
135 }
136 sb.Append(c);
137 }
138 sb.Append(')');
139 return sb.ToString();
140 }
141
142 internal class RuntimeInformationWrapper
143 {
144 public virtual string FrameworkDescription => RuntimeInformation.FrameworkDescription;
145 public virtual string OSDescription => RuntimeInformation.OSDescription;
146 public virtual Architecture OSArchitecture => RuntimeInformation.OSArchitecture;
147 public virtual Architecture ProcessArchitecture => RuntimeInformation.ProcessArchitecture;
148 public virtual bool IsOSPlatform(OSPlatform osPlatform) => RuntimeInformation.IsOSPlatform(osPlatform);
149 }
150}