/
afanasevn
/
PdfEncoder
Обзор
Документация
Войти
/
afanasevn
/
PdfEncoder
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/PdfEncoder.Infrastructure/Llm/OpenAiCompatibleChatClient.cs
154 строки
5 KB
IBS\NAfanasev
Full async product flow
01 июл 2026, 17:49
01 июл 2026, 17:49
d25833e
Код
Авторство
О чём код?
using System.ClientModel; using System.Net; using Microsoft.Extensions.AI; using Microsoft.Extensions.Options; using OpenAI; using OpenAI.Chat; using PdfEncoder.Application.Options; using MeAiChatMessage = Microsoft.Extensions.AI.ChatMessage; using MeAiChatOptions = Microsoft.Extensions.AI.ChatOptions; using MeAiChatResponse = Microsoft.Extensions.AI.ChatResponse; using MeAiChatResponseUpdate = Microsoft.Extensions.AI.ChatResponseUpdate; namespace PdfEncoder.Infrastructure.Llm; /// <summary> /// OpenAI-compatible <see cref="IChatClient"/> через официальный OpenAI SDK (Timeweb AI Gateway и др.). /// </summary> public sealed class OpenAiCompatibleChatClient : IChatClient { private readonly LlmModelOptions _options; private readonly OpenAIClient _openAiClient; /// <summary> /// Создаёт клиент с endpoint и API key из <see cref="LlmModelOptions"/>. /// </summary> public OpenAiCompatibleChatClient(IOptions<LlmModelOptions> options) { _options = options.Value; _openAiClient = CreateOpenAiClient(_options); } /// <inheritdoc /> public async Task<MeAiChatResponse> GetResponseAsync( IEnumerable<MeAiChatMessage> chatMessages, MeAiChatOptions? options = null, CancellationToken cancellationToken = default) { var model = options?.ModelId ?? _options.ClassificationModel; var chatClient = CreateGatewayChatClient(model); return await chatClient.AsIChatClient().GetResponseAsync(chatMessages, options, cancellationToken); } /// <inheritdoc /> public IAsyncEnumerable<MeAiChatResponseUpdate> GetStreamingResponseAsync( IEnumerable<MeAiChatMessage> chatMessages, MeAiChatOptions? options = null, CancellationToken cancellationToken = default) => throw new NotSupportedException("Streaming is not supported in PdfEncoder MVP."); /// <inheritdoc /> public object? GetService(Type serviceType, object? serviceKey = null) => null; /// <inheritdoc /> public void Dispose() { } /// <summary> /// Выполняет chat completion с опциональным json_object response format. /// </summary> internal async Task<OpenAiChatCompletionResult> CompleteAsync( string model, string systemPrompt, string userPrompt, bool useJsonObject, CancellationToken ct) { var chatClient = CreateGatewayChatClient(model); var messages = new List<OpenAI.Chat.ChatMessage> { new SystemChatMessage(systemPrompt), new UserChatMessage(userPrompt) }; var completionOptions = new ChatCompletionOptions { Temperature = (float)_options.Temperature, MaxOutputTokenCount = _options.MaxTokens }; if (useJsonObject) { completionOptions.ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateJsonObjectFormat(); } try { var completion = await chatClient.CompleteChatAsync(messages, completionOptions, ct); var content = completion.Value.Content.Count > 0 ? completion.Value.Content[0].Text ?? string.Empty : string.Empty; return new OpenAiChatCompletionResult { Content = content, PromptTokens = completion.Value.Usage?.InputTokenCount ?? 0, CompletionTokens = completion.Value.Usage?.OutputTokenCount ?? 0 }; } catch (ClientResultException ex) { throw MapClientException(ex); } } /// <summary> /// Создаёт OpenAI SDK client с кастомным endpoint (Timeweb AI Gateway). /// </summary> private static OpenAIClient CreateOpenAiClient(LlmModelOptions options) { var clientOptions = new OpenAIClientOptions(); if (!string.IsNullOrWhiteSpace(options.BaseUrl)) { clientOptions.Endpoint = new Uri(options.BaseUrl.TrimEnd('/')); } return new OpenAIClient(new ApiKeyCredential(options.ApiKey), clientOptions); } /// <summary> /// Возвращает model-scoped ChatClient, как в рекомендации интегратора Timeweb. /// </summary> private ChatClient CreateGatewayChatClient(string model) { return _openAiClient.GetChatClient(model); } /// <summary> /// Преобразует ошибку OpenAI SDK в <see cref="HttpRequestException"/> для существующего retry-слоя. /// </summary> private static HttpRequestException MapClientException(ClientResultException ex) { var statusCode = ex.Status is >= 100 and <= 599 ? (HttpStatusCode)ex.Status : HttpStatusCode.InternalServerError; return new HttpRequestException( $"LLM gateway returned {(int)statusCode}: {ex.Message}", ex, statusCode); } } /// <summary> /// Результат OpenAI chat completion. /// </summary> internal sealed class OpenAiChatCompletionResult { public string Content { get; init; } = string.Empty; public int PromptTokens { get; init; } public int CompletionTokens { get; init; } }