diff --git a/src/CompactifAI.Client/CompactifAI.Client.csproj b/src/CompactifAI.Client/CompactifAI.Client.csproj index 55a82f1..c6720a6 100644 --- a/src/CompactifAI.Client/CompactifAI.Client.csproj +++ b/src/CompactifAI.Client/CompactifAI.Client.csproj @@ -21,6 +21,10 @@ false true + + + true + true diff --git a/src/CompactifAI.Client/CompactifAIClient.cs b/src/CompactifAI.Client/CompactifAIClient.cs index 75a1cd5..bea89c3 100644 --- a/src/CompactifAI.Client/CompactifAIClient.cs +++ b/src/CompactifAI.Client/CompactifAIClient.cs @@ -1,7 +1,9 @@ using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using CompactifAI.Client.Models; +using CompactifAI.Client.Serialization; using Microsoft.Extensions.Options; namespace CompactifAI.Client; @@ -13,7 +15,6 @@ public class CompactifAIClient : ICompactifAIClient { private readonly HttpClient _httpClient; private readonly CompactifAIOptions _options; - private readonly JsonSerializerOptions _jsonOptions; /// /// Creates a new CompactifAI client. @@ -24,12 +25,6 @@ public CompactifAIClient(HttpClient httpClient, IOptions opt { _httpClient = httpClient; _options = options.Value; - _jsonOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, - DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull - }; - ConfigureHttpClient(); } @@ -46,12 +41,6 @@ public CompactifAIClient(string apiKey, string? baseUrl = null) BaseUrl = baseUrl ?? "https://api.compactif.ai/v1" }; _httpClient = new HttpClient(); - _jsonOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, - DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull - }; - ConfigureHttpClient(); } @@ -76,10 +65,10 @@ public async Task CreateChatCompletionAsync( var response = await _httpClient.PostAsJsonAsync( "chat/completions", request, - _jsonOptions, + CompactifAIJsonContext.Default.ChatCompletionRequest, cancellationToken); - return await HandleResponseAsync(response, cancellationToken); + return await HandleResponseAsync(response, CompactifAIJsonContext.Default.ChatCompletionResponse, cancellationToken); } /// @@ -120,10 +109,10 @@ public async Task CreateCompletionAsync( var response = await _httpClient.PostAsJsonAsync( "completions", request, - _jsonOptions, + CompactifAIJsonContext.Default.CompletionRequest, cancellationToken); - return await HandleResponseAsync(response, cancellationToken); + return await HandleResponseAsync(response, CompactifAIJsonContext.Default.CompletionResponse, cancellationToken); } /// @@ -176,7 +165,7 @@ public async Task TranscribeAsync( var response = await _httpClient.PostAsync("audio/transcriptions", content, cancellationToken); - return await HandleResponseAsync(response, cancellationToken); + return await HandleResponseAsync(response, CompactifAIJsonContext.Default.TranscriptionResponse, cancellationToken); } /// @@ -209,7 +198,7 @@ public async Task ListModelsAsync(CancellationToken cancellation { var response = await _httpClient.GetAsync("models", cancellationToken); - return await HandleResponseAsync(response, cancellationToken); + return await HandleResponseAsync(response, CompactifAIJsonContext.Default.ModelsResponse, cancellationToken); } /// @@ -217,7 +206,7 @@ public async Task GetModelAsync(string modelId, CancellationToken can { var response = await _httpClient.GetAsync($"models/{modelId}", cancellationToken); - return await HandleResponseAsync(response, cancellationToken); + return await HandleResponseAsync(response, CompactifAIJsonContext.Default.ModelInfo, cancellationToken); } #endregion @@ -226,6 +215,7 @@ public async Task GetModelAsync(string modelId, CancellationToken can private async Task HandleResponseAsync( HttpResponseMessage response, + JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken) { var responseBody = await response.Content.ReadAsStringAsync(cancellationToken); @@ -238,7 +228,7 @@ private async Task HandleResponseAsync( responseBody); } - var result = JsonSerializer.Deserialize(responseBody, _jsonOptions); + var result = JsonSerializer.Deserialize(responseBody, jsonTypeInfo); if (result is null) { diff --git a/src/CompactifAI.Client/Extensions/ServiceCollectionExtensions.cs b/src/CompactifAI.Client/Extensions/ServiceCollectionExtensions.cs index 78f00b1..c5c6111 100644 --- a/src/CompactifAI.Client/Extensions/ServiceCollectionExtensions.cs +++ b/src/CompactifAI.Client/Extensions/ServiceCollectionExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -49,6 +50,12 @@ public static IServiceCollection AddCompactifAI( /// The service collection. /// The configuration section to bind from. /// The service collection for chaining. + /// + /// This method uses configuration binding which requires reflection and is not AOT-compatible. + /// For AOT scenarios, use the overload that accepts an Action<CompactifAIOptions>. + /// + [RequiresUnreferencedCode("Configuration binding uses reflection. Use the Action overload for AOT scenarios.")] + [RequiresDynamicCode("Configuration binding uses reflection. Use the Action overload for AOT scenarios.")] public static IServiceCollection AddCompactifAI( this IServiceCollection services, Microsoft.Extensions.Configuration.IConfiguration configuration) diff --git a/src/CompactifAI.Client/Models/ChatModels.cs b/src/CompactifAI.Client/Models/ChatModels.cs index 5bf9669..8900ded 100644 --- a/src/CompactifAI.Client/Models/ChatModels.cs +++ b/src/CompactifAI.Client/Models/ChatModels.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using System.Text.Json.Serialization; namespace CompactifAI.Client.Models; @@ -150,7 +151,7 @@ public class ToolFunction /// [JsonPropertyName("parameters")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public object? Parameters { get; set; } + public JsonElement? Parameters { get; set; } } /// diff --git a/src/CompactifAI.Client/Serialization/CompactifAIJsonContext.cs b/src/CompactifAI.Client/Serialization/CompactifAIJsonContext.cs new file mode 100644 index 0000000..8bc6ffe --- /dev/null +++ b/src/CompactifAI.Client/Serialization/CompactifAIJsonContext.cs @@ -0,0 +1,45 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using CompactifAI.Client.Models; + +namespace CompactifAI.Client.Serialization; + +/// +/// Source-generated JSON serialization context for CompactifAI models. +/// Provides high-performance, AOT-compatible serialization without reflection. +/// +[JsonSourceGenerationOptions( + PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + GenerationMode = JsonSourceGenerationMode.Default)] +// Chat models +[JsonSerializable(typeof(ChatCompletionRequest))] +[JsonSerializable(typeof(ChatCompletionResponse))] +[JsonSerializable(typeof(ChatMessage))] +[JsonSerializable(typeof(ChatChoice))] +[JsonSerializable(typeof(Tool))] +[JsonSerializable(typeof(ToolFunction))] +[JsonSerializable(typeof(Usage))] +// Completion models +[JsonSerializable(typeof(CompletionRequest))] +[JsonSerializable(typeof(CompletionResponse))] +[JsonSerializable(typeof(CompletionChoice))] +// Transcription models +[JsonSerializable(typeof(TranscriptionResponse))] +[JsonSerializable(typeof(TranscriptionSegment))] +// Model info +[JsonSerializable(typeof(ModelsResponse))] +[JsonSerializable(typeof(ModelInfo))] +[JsonSerializable(typeof(ModelCapabilities))] +// Supporting types +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(JsonElement))] +public partial class CompactifAIJsonContext : JsonSerializerContext +{ +} diff --git a/tests/CompactifAI.Client.Tests/ModelsTests.cs b/tests/CompactifAI.Client.Tests/ModelsTests.cs index de89db6..d715e1b 100644 --- a/tests/CompactifAI.Client.Tests/ModelsTests.cs +++ b/tests/CompactifAI.Client.Tests/ModelsTests.cs @@ -1,15 +1,14 @@ using System.Text.Json; using CompactifAI.Client.Models; +using CompactifAI.Client.Serialization; using Xunit; namespace CompactifAI.Client.Tests; public class ModelsTests { - private readonly JsonSerializerOptions _jsonOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower - }; + // Use source-generated context for high-performance serialization + private static CompactifAIJsonContext JsonContext => CompactifAIJsonContext.Default; #region ChatMessage Tests @@ -56,7 +55,7 @@ public void ChatCompletionRequest_Serializes_WithRequiredFields() } }; - var json = JsonSerializer.Serialize(request, _jsonOptions); + var json = JsonSerializer.Serialize(request, JsonContext.ChatCompletionRequest); Assert.Contains("\"model\":\"test-model\"", json); Assert.Contains("\"messages\":", json); @@ -73,7 +72,7 @@ public void ChatCompletionRequest_Serializes_OptionalFieldsOmittedWhenNull() Messages = new List { ChatMessage.User("Test") } }; - var json = JsonSerializer.Serialize(request, _jsonOptions); + var json = JsonSerializer.Serialize(request, JsonContext.ChatCompletionRequest); Assert.DoesNotContain("temperature", json); Assert.DoesNotContain("max_tokens", json); @@ -93,7 +92,7 @@ public void ChatCompletionRequest_Serializes_WithAllOptionalFields() FrequencyPenalty = 0.5 }; - var json = JsonSerializer.Serialize(request, _jsonOptions); + var json = JsonSerializer.Serialize(request, JsonContext.ChatCompletionRequest); Assert.Contains("\"temperature\":0.7", json); Assert.Contains("\"max_tokens\":100", json); @@ -132,7 +131,7 @@ public void ChatCompletionResponse_Deserializes_Correctly() } """; - var response = JsonSerializer.Deserialize(json, _jsonOptions); + var response = JsonSerializer.Deserialize(json, JsonContext.ChatCompletionResponse); Assert.NotNull(response); Assert.Equal("chatcmpl-123", response.Id); @@ -161,7 +160,7 @@ public void CompletionRequest_Serializes_Correctly() TopP = 0.95 }; - var json = JsonSerializer.Serialize(request, _jsonOptions); + var json = JsonSerializer.Serialize(request, JsonContext.CompletionRequest); Assert.Contains("\"model\":\"test-model\"", json); Assert.Contains("\"prompt\":\"Once upon a time\"", json); @@ -198,7 +197,7 @@ public void CompletionResponse_Deserializes_Correctly() } """; - var response = JsonSerializer.Deserialize(json, _jsonOptions); + var response = JsonSerializer.Deserialize(json, JsonContext.CompletionResponse); Assert.NotNull(response); Assert.Equal("cmpl-123", response.Id); @@ -230,7 +229,7 @@ public void TranscriptionResponse_Deserializes_Correctly() } """; - var response = JsonSerializer.Deserialize(json, _jsonOptions); + var response = JsonSerializer.Deserialize(json, JsonContext.TranscriptionResponse); Assert.NotNull(response); Assert.Equal("transcribe", response.Task); @@ -270,7 +269,7 @@ public void ModelsResponse_Deserializes_Correctly() } """; - var response = JsonSerializer.Deserialize(json, _jsonOptions); + var response = JsonSerializer.Deserialize(json, JsonContext.ModelsResponse); Assert.NotNull(response); Assert.Equal("list", response.Object); @@ -309,7 +308,7 @@ public void ModelsResponse_ParametersNumber_HandlesVariousFormats(string paramet } """; - var response = JsonSerializer.Deserialize(json, _jsonOptions); + var response = JsonSerializer.Deserialize(json, JsonContext.ModelsResponse); Assert.NotNull(response); Assert.Single(response.Data); @@ -338,7 +337,7 @@ public void ModelsResponse_Deserializes_WithMissingParametersNumber() } """; - var response = JsonSerializer.Deserialize(json, _jsonOptions); + var response = JsonSerializer.Deserialize(json, JsonContext.ModelsResponse); Assert.NotNull(response); Assert.Single(response.Data); @@ -352,6 +351,9 @@ public void ModelsResponse_Deserializes_WithMissingParametersNumber() [Fact] public void Tool_Serializes_Correctly() { + // Create JsonElement from anonymous object for parameters + var parametersJson = JsonSerializer.SerializeToElement(new { type = "object", properties = new { location = new { type = "string" } } }); + var tool = new Tool { Type = "function", @@ -359,11 +361,11 @@ public void Tool_Serializes_Correctly() { Name = "get_weather", Description = "Get the current weather", - Parameters = new { type = "object", properties = new { location = new { type = "string" } } } + Parameters = parametersJson } }; - var json = JsonSerializer.Serialize(tool, _jsonOptions); + var json = JsonSerializer.Serialize(tool, JsonContext.Tool); Assert.Contains("\"type\":\"function\"", json); Assert.Contains("\"name\":\"get_weather\"", json);