using System.Globalization; using System.Net.Http.Headers; using System.Reflection; using System.Text; using AIStudio.Chat; using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Tools.Metadata; using SharedTools; namespace AIStudio.Tools; /// /// The User-Agent which AI Studio sends to self-hosted servers, when the user allows it. /// /// /// It lets the operators of a self-hosted server or a gateway such as LiteLLM tell requests from /// AI Studio apart from other clients, e.g., in their logs, and see which feature sent them.

/// Who receives it is decided in IsAllowedFor, and nowhere else: only self-hosted servers, and only /// when the user or their organization switched on sharing the feature usage. Without that, AI Studio /// sends no User-Agent at all, the same as before this existed. Cloud providers never receive it. ///
public static class AppUserAgent { private static readonly MetaDataAttribute META_DATA = Assembly.GetExecutingAssembly().GetCustomAttribute()!; /// /// The User-Agent naming the app, its version, and the platform, e.g., MindWorkAIStudio/26.9.1 (osx-arm64). /// public static readonly string BASE = CreateBase(); /// /// Whether requests to the given provider may carry the User-Agent. /// /// The provider which receives the requests. /// The settings, which say whether the user shares the feature usage. /// True when the provider runs on a self-hosted server and sharing is switched on; otherwise, false. public static bool IsAllowedFor(LLMProviders provider, SettingsManager settingsManager) => provider.RunsOnSelfHostedServer() && settingsManager.ConfigurationData.App.ShareFeatureUsageWithSelfHostedServerOperators; /// /// Sets the User-Agent as default header of the given HTTP client. /// /// The HTTP client, whose requests should carry the User-Agent. public static void Apply(HttpClient httpClient) => httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd(BASE); /// /// Replaces the User-Agent of a single request by one naming the component of the given chat thread. /// /// The headers of the request. /// The chat thread, whose component sent the request. public static void ApplyComponent(HttpRequestHeaders headers, ChatThread chatThread) { headers.Remove("User-Agent"); headers.TryAddWithoutValidation("User-Agent", ForComponent(chatThread.RuntimeComponent, chatThread.RuntimeAssistantName)); } /// /// Creates the User-Agent naming the given component, e.g., MindWorkAIStudio/26.9.1 (osx-arm64) Component/TRANSLATION_ASSISTANT. /// /// The component which sends the request. /// The name of the assistant plugin, if any. /// The User-Agent. public static string ForComponent(Components component, string assistantName = "") { if (component is Components.NONE) return BASE; var userAgent = $"{BASE} Component/{component}"; var assistantToken = ToProductToken(assistantName); return string.IsNullOrWhiteSpace(assistantToken) ? userAgent : $"{userAgent} Assistant/{assistantToken}"; } /// /// Converts a free-text name into a valid product token of a User-Agent. /// /// /// Header values are ASCII only, and a product token must not contain separators like spaces, /// slashes, or parentheses. Diacritics are dropped (Ü becomes U), everything else is replaced by a dash. /// private static string ToProductToken(string name) { if (string.IsNullOrWhiteSpace(name)) return string.Empty; const int MAX_LENGTH = 64; var normalized = name.Trim().Replace("ß", "ss").Normalize(NormalizationForm.FormD); var token = new StringBuilder(normalized.Length); foreach (var character in normalized) { if (CharUnicodeInfo.GetUnicodeCategory(character) is UnicodeCategory.NonSpacingMark) continue; if (char.IsAsciiLetterOrDigit(character) || character is '.' or '_' or '-') token.Append(character); else if (token.Length > 0 && token[^1] is not '-') token.Append('-'); if (token.Length >= MAX_LENGTH) break; } return token.ToString().TrimEnd('-'); } private static string CreateBase() { var rid = RIDExtensions.GetCurrentRID().AsMicrosoftRid(); return string.IsNullOrWhiteSpace(rid) ? $"MindWorkAIStudio/{META_DATA.Version}" : $"MindWorkAIStudio/{META_DATA.Version} ({rid})"; } }