< Summary

Information
Class: Orchestrator.Infrastructure.Factories.OpenAiServiceFactory
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Infrastructure/Factories/OpenAiServiceFactory.cs
Line coverage
91%
Covered lines: 67
Uncovered lines: 6
Coverable lines: 73
Total lines: 176
Line coverage: 91.7%
Branch coverage
90%
Covered branches: 18
Total branches: 20
Branch coverage: 90%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Infrastructure/Factories/OpenAiServiceFactory.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using System.ClientModel;
 3using System.ClientModel.Primitives;
 4using System.Net.Http;
 5using EHonda.KicktippAi.Core;
 6using Microsoft.Extensions.Logging;
 7using OpenAI.Responses;
 8using OpenAiIntegration;
 9using Orchestrator.Infrastructure;
 10
 11namespace Orchestrator.Infrastructure.Factories;
 12
 13/// <summary>
 14/// Default implementation of <see cref="IOpenAiServiceFactory"/>.
 15/// </summary>
 16/// <remarks>
 17/// Reads the API key from OPENAI_API_KEY environment variable.
 18/// Caches services by model to avoid recreating them for repeated requests.
 19/// The <see cref="ITokenUsageTracker"/> is shared across all prediction services.
 20/// </remarks>
 21public sealed class OpenAiServiceFactory : IOpenAiServiceFactory
 22{
 123    private static readonly TimeSpan OpenAiNetworkTimeout = TimeSpan.FromMinutes(15);
 24
 25    private readonly ILoggerFactory _loggerFactory;
 26    private readonly IHttpClientFactory? _httpClientFactory;
 27    private readonly Lazy<string> _apiKey;
 128    private readonly ConcurrentDictionary<string, IPredictionService> _predictionServiceCache = new();
 29    private ITokenUsageTracker? _tokenUsageTracker;
 30    private ICostCalculationService? _costCalculationService;
 31    private IInstructionsTemplateProvider? _instructionsTemplateProvider;
 132    private readonly object _lock = new();
 33
 134    public OpenAiServiceFactory(ILoggerFactory loggerFactory, IHttpClientFactory? httpClientFactory = null)
 35    {
 136        _loggerFactory = loggerFactory;
 137        _httpClientFactory = httpClientFactory;
 138        _apiKey = new Lazy<string>(GetApiKeyFromEnvironment);
 139    }
 40
 41    /// <inheritdoc />
 42    public IPredictionService CreatePredictionService(string model)
 43    {
 144        return CreatePredictionService(model, PredictionServiceOptions.Default);
 45    }
 46
 47    /// <inheritdoc />
 48    public IPredictionService CreatePredictionService(string model, PredictionServiceOptions options)
 49    {
 150        ArgumentException.ThrowIfNullOrWhiteSpace(model);
 151        ArgumentNullException.ThrowIfNull(options);
 52
 153        var apiKey = _apiKey.Value;
 154        var reasoningEffort = string.IsNullOrWhiteSpace(options.ReasoningEffort)
 155            ? string.Empty
 156            : options.ReasoningEffort.Trim().ToLowerInvariant();
 157        var cacheKey = $"{model}|disableFlex={options.DisableFlexProcessing}|reasoningEffort={reasoningEffort}";
 58
 59        // Cache key includes model to handle different configurations
 160        return _predictionServiceCache.GetOrAdd(cacheKey, _ =>
 161        {
 162            return CreatePredictionServiceCore(
 163                model,
 164                options,
 165                GetOrCreateInstructionsTemplateProvider(),
 166                apiKey);
 167        });
 68    }
 69
 70    /// <inheritdoc />
 71    public IPredictionService CreatePredictionService(
 72        string model,
 73        PredictionServiceOptions options,
 74        IInstructionsTemplateProvider templateProvider)
 75    {
 076        ArgumentException.ThrowIfNullOrWhiteSpace(model);
 077        ArgumentNullException.ThrowIfNull(options);
 078        ArgumentNullException.ThrowIfNull(templateProvider);
 79
 080        return CreatePredictionServiceCore(model, options, templateProvider, _apiKey.Value);
 81    }
 82
 83    /// <inheritdoc />
 84    public ITokenUsageTracker GetTokenUsageTracker()
 85    {
 186        if (_tokenUsageTracker == null)
 87        {
 188            lock (_lock)
 89            {
 190                _tokenUsageTracker ??= new TokenUsageTracker(
 191                    _loggerFactory.CreateLogger<TokenUsageTracker>(),
 192                    GetOrCreateCostCalculationService());
 193            }
 94        }
 95
 196        return _tokenUsageTracker;
 97    }
 98
 99    private static string GetApiKeyFromEnvironment()
 100    {
 1101        var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
 1102        if (string.IsNullOrWhiteSpace(apiKey))
 103        {
 1104            throw new InvalidOperationException("OPENAI_API_KEY environment variable is required");
 105        }
 106
 1107        return apiKey;
 108    }
 109
 110    private ICostCalculationService GetOrCreateCostCalculationService()
 111    {
 1112        if (_costCalculationService == null)
 113        {
 1114            lock (_lock)
 115            {
 1116                _costCalculationService ??= new CostCalculationService(
 1117                    _loggerFactory.CreateLogger<CostCalculationService>());
 1118            }
 119        }
 120
 1121        return _costCalculationService;
 122    }
 123
 124    private IInstructionsTemplateProvider GetOrCreateInstructionsTemplateProvider()
 125    {
 1126        if (_instructionsTemplateProvider == null)
 127        {
 1128            lock (_lock)
 129            {
 1130                _instructionsTemplateProvider ??= new InstructionsTemplateProvider(
 1131                    PromptsFileProvider.Create());
 1132            }
 133        }
 134
 1135        return _instructionsTemplateProvider;
 136    }
 137
 138    private IPredictionService CreatePredictionServiceCore(
 139        string model,
 140        PredictionServiceOptions options,
 141        IInstructionsTemplateProvider templateProvider,
 142        string apiKey)
 143    {
 1144        var logger = _loggerFactory.CreateLogger<PredictionService>();
 1145        var responsesClient = new ResponsesClient(new ApiKeyCredential(apiKey), CreateResponsesClientOptions());
 146
 1147        return new PredictionService(
 1148            responsesClient,
 1149            logger,
 1150            GetOrCreateCostCalculationService(),
 1151            GetTokenUsageTracker(),
 1152            templateProvider,
 1153            model,
 1154            options);
 155    }
 156
 157    private ResponsesClientOptions CreateResponsesClientOptions()
 158    {
 1159        var options = new ResponsesClientOptions
 1160        {
 1161            // OpenAI recommends allowing long-running model requests up to 15 minutes.
 1162            // This timeout belongs to the OpenAI client pipeline, while HttpClient.Timeout
 1163            // stays infinite so it does not race the .NET HTTP resilience handler.
 1164            NetworkTimeout = OpenAiNetworkTimeout,
 1165            RetryPolicy = new ClientRetryPolicy(maxRetries: 0)
 1166        };
 167
 1168        if (_httpClientFactory is not null)
 169        {
 0170            options.Transport = new HttpClientPipelineTransport(
 0171                _httpClientFactory.CreateClient(ServiceRegistrationExtensions.OpenAiHttpClientName));
 172        }
 173
 1174        return options;
 175    }
 176}