using AyCode.Core.Loggers; using Mango.Nop.Core.Loggers; using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; using Nop.Plugin.Misc.FruitBankPlugin.Models; using Nop.Services.Configuration; using System.Text; using System.Text.Json; #nullable enable namespace Nop.Plugin.Misc.FruitBankPlugin.Services { public class CerebrasAPIService : IAIAPIService { private readonly ILogger _logger; private readonly ISettingService _settingService; private readonly FruitBankSettings _fruitBankSettings; private readonly HttpClient _httpClient; private static Action? _callback; private static Action? _onComplete; private static Action? _onError; private const string CerebrasEndpoint = "https://api.cerebras.ai/v1/chat/completions"; public CerebrasAPIService(ISettingService settingService, HttpClient httpClient, IEnumerable logWriters) { _logger = new Logger(logWriters.ToArray()); _settingService = settingService; _fruitBankSettings = _settingService.LoadSetting(); _httpClient = httpClient; _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {GetApiKey()}"); } public string GetApiKey() => _fruitBankSettings.ApiKey; //_configuration?.GetSection("Cerebras")?.GetValue("ApiKey") ?? string.Empty; public string GetModelName() => _fruitBankSettings.ModelName; //_configuration?.GetSection("Cerebras")?.GetValue("Model") ?? string.Empty; public void RegisterCallback(Action callback, Action onCompleteCallback, Action onErrorCallback) { _callback = callback; _onComplete = onCompleteCallback; _onError = onErrorCallback; } public async Task GetSimpleResponseAsync(string systemMessage, string userMessage, string? assistantMessage = null) { var modelName = GetModelName(); var requestBody = new CerebrasAIChatRequest { Model = modelName, Temperature = 0.2, Messages = string.IsNullOrEmpty(assistantMessage) ? [ new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "user", Content = userMessage } ] : [ new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "assistant", Content = assistantMessage }, new AIChatMessage { Role = "user", Content = userMessage } ], Stream = false }; var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json"); using var response = await _httpClient.PostAsync(CerebrasEndpoint, requestContent); response.EnsureSuccessStatusCode(); await using var responseStream = await response.Content.ReadAsStreamAsync(); using var document = await JsonDocument.ParseAsync(responseStream); var inputTokens = document.RootElement.GetProperty("usage").GetProperty("prompt_tokens").GetInt32(); var outputTokens = document.RootElement.GetProperty("usage").GetProperty("completion_tokens").GetInt32(); var sum = inputTokens + outputTokens; _logger.Info($"USAGE STATS - Tokens: {inputTokens.ToString()} + {outputTokens.ToString()} = {sum.ToString()}"); return document.RootElement .GetProperty("choices")[0] .GetProperty("message") .GetProperty("content") .GetString() ?? "No response"; } public async Task GetStreamedResponseAsync(string sessionId, string systemMessage, string userMessage, string? assistantMessage = null) { var modelName = GetModelName(); var requestBody = new CerebrasAIChatRequest { Model = modelName, Temperature = 0.2, Messages = assistantMessage == null ? [ new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "user", Content = userMessage } ] : [ new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "assistant", Content = assistantMessage }, new AIChatMessage { Role = "user", Content = userMessage } ], Stream = true }; var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json"); using var httpRequest = new HttpRequestMessage(HttpMethod.Post, CerebrasEndpoint); httpRequest.Content = requestContent; using var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); var stringBuilder = new StringBuilder(); await using var responseStream = await response.Content.ReadAsStreamAsync(); using var reader = new StreamReader(responseStream); try { while (!reader.EndOfStream) { var line = await reader.ReadLineAsync(); if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data: ")) continue; var jsonResponse = line.Substring(6); // ✅ Detect explicit end of stream if (jsonResponse == "[DONE]") { _onComplete?.Invoke(sessionId); // Optional: notify stream end break; } try { using var jsonDoc = JsonDocument.Parse(jsonResponse); if (jsonDoc.RootElement.TryGetProperty("choices", out var choices) && choices[0].TryGetProperty("delta", out var delta) && delta.TryGetProperty("content", out var contentElement)) { var content = contentElement.GetString(); if (!string.IsNullOrEmpty(content)) { stringBuilder.Append(content); _callback?.Invoke(sessionId, stringBuilder.ToString()); } } } catch (JsonException ex) { _onError?.Invoke(sessionId, $"Malformed JSON: {ex.Message}"); break; // Optionally stop stream } } // ✅ Check for unexpected end (in case no [DONE]) if (reader.EndOfStream && !stringBuilder.ToString().EndsWith("[DONE]")) { _onError?.Invoke(sessionId, "Unexpected end of stream"); } } catch (Exception ex) { _onError?.Invoke(sessionId, $"Exception: {ex.Message}"); } return stringBuilder.ToString(); } } }