Files
AI-Studio/app/MindWork AI Studio/Tools/AppUserAgent.cs
T
Dominic NeuburgandThorsten Sommer 50aa11a34c
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Added opt-in sharing of feature usage with self-hosted servers (#1020)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
2026-10-04 14:44:16 +02:00

117 lines
5.0 KiB
C#

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;
/// <summary>
/// The User-Agent which AI Studio sends to self-hosted servers, when the user allows it.
/// </summary>
/// <remarks>
/// 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.<br/><br/>
/// 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.
/// </remarks>
public static class AppUserAgent
{
private static readonly MetaDataAttribute META_DATA = Assembly.GetExecutingAssembly().GetCustomAttribute<MetaDataAttribute>()!;
/// <summary>
/// The User-Agent naming the app, its version, and the platform, e.g., <c>MindWorkAIStudio/26.9.1 (osx-arm64)</c>.
/// </summary>
public static readonly string BASE = CreateBase();
/// <summary>
/// Whether requests to the given provider may carry the User-Agent.
/// </summary>
/// <param name="provider">The provider which receives the requests.</param>
/// <param name="settingsManager">The settings, which say whether the user shares the feature usage.</param>
/// <returns>True when the provider runs on a self-hosted server and sharing is switched on; otherwise, false.</returns>
public static bool IsAllowedFor(LLMProviders provider, SettingsManager settingsManager) =>
provider.RunsOnSelfHostedServer() && settingsManager.ConfigurationData.App.ShareFeatureUsageWithSelfHostedServerOperators;
/// <summary>
/// Sets the User-Agent as default header of the given HTTP client.
/// </summary>
/// <param name="httpClient">The HTTP client, whose requests should carry the User-Agent.</param>
public static void Apply(HttpClient httpClient) => httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd(BASE);
/// <summary>
/// Replaces the User-Agent of a single request by one naming the component of the given chat thread.
/// </summary>
/// <param name="headers">The headers of the request.</param>
/// <param name="chatThread">The chat thread, whose component sent the request.</param>
public static void ApplyComponent(HttpRequestHeaders headers, ChatThread chatThread)
{
headers.Remove("User-Agent");
headers.TryAddWithoutValidation("User-Agent", ForComponent(chatThread.RuntimeComponent, chatThread.RuntimeAssistantName));
}
/// <summary>
/// Creates the User-Agent naming the given component, e.g., <c>MindWorkAIStudio/26.9.1 (osx-arm64) Component/TRANSLATION_ASSISTANT</c>.
/// </summary>
/// <param name="component">The component which sends the request.</param>
/// <param name="assistantName">The name of the assistant plugin, if any.</param>
/// <returns>The User-Agent.</returns>
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}";
}
/// <summary>
/// Converts a free-text name into a valid product token of a User-Agent.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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})";
}
}