| File: OpenAIRequestPolicies.cs | |
| Project: ..\..\..\src\Libraries\Microsoft.Extensions.AI.OpenAI\Microsoft.Extensions.AI.OpenAI.csproj (Microsoft.Extensions.AI.OpenAI) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.ClientModel.Primitives; using System.Diagnostics.CodeAnalysis; using System.Threading; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; /// <summary> /// Provides an extension hook for adding <see cref="PipelinePolicy"/> instances to the /// <see cref="RequestOptions"/> built by Microsoft.Extensions.AI for every outbound OpenAI request /// made through the owning <c>IChatClient</c> or <c>IEmbeddingGenerator</c>. /// </summary> /// <remarks> /// <para> /// Retrieve the instance via <see cref="IChatClient.GetService(System.Type, object?)"/> /// (or the equivalent on other Microsoft.Extensions.AI client interfaces) using /// <see cref="OpenAIRequestPolicies"/> as the service type. The instance is per-client and /// reachable through any <c>ChatClientBuilder</c> decorator chain. /// </para> /// <para> /// Customer-registered policies are appended <em>after</em> Microsoft.Extensions.AI's own internal /// policies, so a policy that calls <c>message.Request.Headers.Set("User-Agent", ...)</c> /// replaces the existing value, while one that calls <c>Headers.Add(...)</c> stacks an /// additional value. /// </para> /// <para> /// Registration is intended for one-time configuration at startup, but is safe to call /// concurrently with in-flight requests. /// </para> /// </remarks> [Experimental(DiagnosticIds.Experiments.AIOpenAIRequestPolicies, UrlFormat = DiagnosticIds.UrlFormat)] public sealed class OpenAIRequestPolicies { private static readonly Entry[] _empty = Array.Empty<Entry>(); private Entry[] _entries = _empty; /// <summary>Initializes a new instance of the <see cref="OpenAIRequestPolicies"/> class.</summary> public OpenAIRequestPolicies() { } /// <summary> /// Adds a <see cref="PipelinePolicy"/> to be applied to every <see cref="RequestOptions"/> /// produced for outbound OpenAI requests by the owning Microsoft.Extensions.AI client. /// </summary> /// <param name="policy">The pipeline policy to register. Must not be <see langword="null"/>.</param> /// <param name="position"> /// The position in the pipeline at which to place the policy. Defaults to /// <see cref="PipelinePosition.PerCall"/>, which runs the policy once per logical request /// (for example, to stamp a User-Agent or correlation header). /// </param> /// <exception cref="ArgumentNullException"><paramref name="policy"/> is <see langword="null"/>.</exception> public void AddPolicy(PipelinePolicy policy, PipelinePosition position = PipelinePosition.PerCall) { _ = Throw.IfNull(policy); var newEntry = new Entry(policy, position); // Lock-free append: copy-on-write with CAS retry. while (true) { var current = Volatile.Read(ref _entries); var updated = new Entry[current.Length + 1]; Array.Copy(current, updated, current.Length); updated[current.Length] = newEntry; if (Interlocked.CompareExchange(ref _entries, updated, current) == current) { return; } } } /// <summary> /// Applies all registered policies to the supplied <see cref="RequestOptions"/>. /// Called by the Microsoft.Extensions.AI OpenAI clients after their own internal policies /// have been registered. /// </summary> internal void ApplyTo(RequestOptions requestOptions) { var snapshot = Volatile.Read(ref _entries); for (int i = 0; i < snapshot.Length; i++) { var entry = snapshot[i]; requestOptions.AddPolicy(entry.Policy, entry.Position); } } private readonly struct Entry { public Entry(PipelinePolicy policy, PipelinePosition position) { Policy = policy; Position = position; } public PipelinePolicy Policy { get; } public PipelinePosition Position { get; } } }