mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 22:47:21 +00:00
Generated the JSON contracts of the providers at compile time
This commit is contained in:
7 files changed
+163
-13
No files matched your search
@@ -117,8 +117,8 @@ public sealed class AnthropicContentBlockBuilder
|
|||||||
case TYPE_TEXT:
|
case TYPE_TEXT:
|
||||||
return this.BuildFromSeed(new()
|
return this.BuildFromSeed(new()
|
||||||
{
|
{
|
||||||
["type"] = JsonSerializer.Serialize(TYPE_TEXT),
|
["type"] = JsonSerializer.Serialize(TYPE_TEXT, ProviderJsonOptions.OPTIONS),
|
||||||
["text"] = JsonSerializer.Serialize(this.text.ToString()),
|
["text"] = JsonSerializer.Serialize(this.text.ToString(), ProviderJsonOptions.OPTIONS),
|
||||||
});
|
});
|
||||||
|
|
||||||
case TYPE_THINKING:
|
case TYPE_THINKING:
|
||||||
@@ -129,9 +129,9 @@ public sealed class AnthropicContentBlockBuilder
|
|||||||
//
|
//
|
||||||
return this.BuildFromSeed(new()
|
return this.BuildFromSeed(new()
|
||||||
{
|
{
|
||||||
["type"] = JsonSerializer.Serialize(TYPE_THINKING),
|
["type"] = JsonSerializer.Serialize(TYPE_THINKING, ProviderJsonOptions.OPTIONS),
|
||||||
["thinking"] = JsonSerializer.Serialize(this.thinking.ToString()),
|
["thinking"] = JsonSerializer.Serialize(this.thinking.ToString(), ProviderJsonOptions.OPTIONS),
|
||||||
["signature"] = JsonSerializer.Serialize(this.signature),
|
["signature"] = JsonSerializer.Serialize(this.signature, ProviderJsonOptions.OPTIONS),
|
||||||
});
|
});
|
||||||
|
|
||||||
case TYPE_TOOL_USE:
|
case TYPE_TOOL_USE:
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ public sealed class ChatCompletionToolCallAccumulator(Func<ServerSentEvent, ILis
|
|||||||
// No text means no content field, the way a round which only calls a tool arrives
|
// No text means no content field, the way a round which only calls a tool arrives
|
||||||
// when it is not streamed. Some providers reject an empty string in its place.
|
// when it is not streamed. Some providers reject an empty string in its place.
|
||||||
//
|
//
|
||||||
RawContent = answer.Length is 0 ? null : JsonSerializer.SerializeToElement(answer),
|
RawContent = answer.Length is 0 ? null : JsonSerializer.SerializeToElement(answer, ProviderJsonOptions.OPTIONS),
|
||||||
ReasoningContent = this.reasoning.Length is 0 ? null : this.reasoning.ToString(),
|
ReasoningContent = this.reasoning.Length is 0 ? null : this.reasoning.ToString(),
|
||||||
ToolCalls = this.toolCalls.Count is 0
|
ToolCalls = this.toolCalls.Count is 0
|
||||||
? null
|
? null
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public static class OpenAIStrictToolSchema
|
|||||||
// schema stays stable across requests, which prompt caching depends on.
|
// schema stays stable across requests, which prompt caching depends on.
|
||||||
//
|
//
|
||||||
schema["required"] = new JsonArray([..properties.Select(property => JsonValue.Create(property.Key))]);
|
schema["required"] = new JsonArray([..properties.Select(property => JsonValue.Create(property.Key))]);
|
||||||
return JsonSerializer.Deserialize<JsonElement>(schema.ToJsonString());
|
return JsonSerializer.Deserialize<JsonElement>(schema.ToJsonString(), ProviderJsonOptions.OPTIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AllowNullType(JsonObject property)
|
private static void AllowNullType(JsonObject property)
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
using AIStudio.Provider.Anthropic;
|
||||||
|
using AIStudio.Provider.Google;
|
||||||
|
using AIStudio.Provider.Groq;
|
||||||
|
using AIStudio.Provider.HuggingFace;
|
||||||
|
using AIStudio.Provider.OpenAI;
|
||||||
|
using AIStudio.Provider.OpenRouter;
|
||||||
|
using AIStudio.Provider.Requesty;
|
||||||
|
|
||||||
|
using AnthropicDelta = AIStudio.Provider.Anthropic.Delta;
|
||||||
|
using AnthropicImageUrl = AIStudio.Provider.Anthropic.SubContentImageUrl;
|
||||||
|
using AnthropicResponseStreamLine = AIStudio.Provider.Anthropic.ResponseStreamLine;
|
||||||
|
using FireworksChoice = AIStudio.Provider.Fireworks.Choice;
|
||||||
|
using FireworksDelta = AIStudio.Provider.Fireworks.Delta;
|
||||||
|
using FireworksResponseStreamLine = AIStudio.Provider.Fireworks.ResponseStreamLine;
|
||||||
|
using HuggingFaceModelsResponse = AIStudio.Provider.HuggingFace.ModelsResponse;
|
||||||
|
using MistralModel = AIStudio.Provider.Mistral.Model;
|
||||||
|
using MistralModelsResponse = AIStudio.Provider.Mistral.ModelsResponse;
|
||||||
|
using OpenAIImageUrl = AIStudio.Provider.OpenAI.SubContentImageUrl;
|
||||||
|
using OpenAIModelsResponse = AIStudio.Provider.OpenAI.ModelsResponse;
|
||||||
|
using PerplexityChoice = AIStudio.Provider.Perplexity.Choice;
|
||||||
|
using PerplexityDelta = AIStudio.Provider.Perplexity.Delta;
|
||||||
|
using PerplexityResponseStreamLine = AIStudio.Provider.Perplexity.ResponseStreamLine;
|
||||||
|
using SelfHostedModel = AIStudio.Provider.SelfHosted.Model;
|
||||||
|
using SelfHostedModelsResponse = AIStudio.Provider.SelfHosted.ModelsResponse;
|
||||||
|
|
||||||
|
namespace AIStudio.Provider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The contracts with which System.Text.Json reads and writes the requests and answers of LLM providers, built
|
||||||
|
/// at compile time.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Some types System.Text.Json meets only while it runs, since no property names them: the messages and their
|
||||||
|
/// parts, which converters write as the type they are of; the annotations, which a converter reads as the type
|
||||||
|
/// their JSON tells; and whatever a request holds as an object, i.e., the values of the additional API
|
||||||
|
/// parameters, the tools, and the items of an earlier answer. A test fails for such a type which is missing
|
||||||
|
/// here.<br/><br/>
|
||||||
|
///
|
||||||
|
/// Two types with the same name need a name of their own for their contract, else the generator builds only
|
||||||
|
/// the first of them and warns with SYSLIB1031.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
|
||||||
|
|
||||||
|
// The requests:
|
||||||
|
[JsonSerializable(typeof(ChatCompletionAPIRequest))]
|
||||||
|
[JsonSerializable(typeof(ResponsesAPIRequest))]
|
||||||
|
[JsonSerializable(typeof(ChatRequest))]
|
||||||
|
[JsonSerializable(typeof(EmbeddingRequest))]
|
||||||
|
[JsonSerializable(typeof(GoogleEmbeddingRequest))]
|
||||||
|
|
||||||
|
// The messages and their parts:
|
||||||
|
[JsonSerializable(typeof(TextMessage))]
|
||||||
|
[JsonSerializable(typeof(MultimodalMessage))]
|
||||||
|
[JsonSerializable(typeof(AssistantToolCallMessage))]
|
||||||
|
[JsonSerializable(typeof(ToolResultMessage))]
|
||||||
|
[JsonSerializable(typeof(AnthropicMessage))]
|
||||||
|
[JsonSerializable(typeof(AnthropicToolResultMessage))]
|
||||||
|
[JsonSerializable(typeof(SubContentText))]
|
||||||
|
[JsonSerializable(typeof(OpenAIImageUrl), TypeInfoPropertyName = "OpenAISubContentImageUrl")]
|
||||||
|
[JsonSerializable(typeof(SubContentImageUrlNested))]
|
||||||
|
[JsonSerializable(typeof(SubContentImageUrlData))]
|
||||||
|
[JsonSerializable(typeof(SubContentInputText))]
|
||||||
|
[JsonSerializable(typeof(SubContentInputImage))]
|
||||||
|
[JsonSerializable(typeof(SubContentImage))]
|
||||||
|
[JsonSerializable(typeof(SubContentBase64Image))]
|
||||||
|
[JsonSerializable(typeof(AnthropicImageUrl), TypeInfoPropertyName = "AnthropicSubContentImageUrl")]
|
||||||
|
|
||||||
|
// What a request holds as an object:
|
||||||
|
[JsonSerializable(typeof(string))]
|
||||||
|
[JsonSerializable(typeof(int))]
|
||||||
|
[JsonSerializable(typeof(long))]
|
||||||
|
[JsonSerializable(typeof(double))]
|
||||||
|
[JsonSerializable(typeof(decimal))]
|
||||||
|
[JsonSerializable(typeof(bool))]
|
||||||
|
[JsonSerializable(typeof(Dictionary<string, object>))]
|
||||||
|
[JsonSerializable(typeof(List<object>))]
|
||||||
|
[JsonSerializable(typeof(JsonElement))]
|
||||||
|
[JsonSerializable(typeof(ChatCompletionFunctionTool))]
|
||||||
|
[JsonSerializable(typeof(ResponsesFunctionTool))]
|
||||||
|
[JsonSerializable(typeof(ResponsesFunctionCallOutputItem))]
|
||||||
|
|
||||||
|
// The lists of models:
|
||||||
|
[JsonSerializable(typeof(OpenAIModelsResponse), TypeInfoPropertyName = "OpenAIModelsResponse")]
|
||||||
|
[JsonSerializable(typeof(MistralModelsResponse), TypeInfoPropertyName = "MistralModelsResponse")]
|
||||||
|
[JsonSerializable(typeof(SelfHostedModelsResponse), TypeInfoPropertyName = "SelfHostedModelsResponse")]
|
||||||
|
[JsonSerializable(typeof(HuggingFaceModelsResponse), TypeInfoPropertyName = "HuggingFaceModelsResponse")]
|
||||||
|
[JsonSerializable(typeof(IList<HubModel>))]
|
||||||
|
[JsonSerializable(typeof(RequestyModelsResponse))]
|
||||||
|
[JsonSerializable(typeof(OpenRouterModelsResponse))]
|
||||||
|
[JsonSerializable(typeof(GroqModelsResponse))]
|
||||||
|
|
||||||
|
// The lines of streamed answers, with the annotations:
|
||||||
|
[JsonSerializable(typeof(ChatCompletionDeltaStreamLine))]
|
||||||
|
[JsonSerializable(typeof(ChatCompletionAnnotationStreamLine))]
|
||||||
|
[JsonSerializable(typeof(ChatCompletionToolStreamLine))]
|
||||||
|
[JsonSerializable(typeof(ResponsesDeltaStreamLine))]
|
||||||
|
[JsonSerializable(typeof(ResponsesAnnotationStreamLine))]
|
||||||
|
[JsonSerializable(typeof(ResponsesCompletedStreamLine))]
|
||||||
|
[JsonSerializable(typeof(AnthropicResponseStreamLine), TypeInfoPropertyName = "AnthropicResponseStreamLine")]
|
||||||
|
[JsonSerializable(typeof(AnthropicStreamLine))]
|
||||||
|
[JsonSerializable(typeof(FireworksResponseStreamLine), TypeInfoPropertyName = "FireworksResponseStreamLine")]
|
||||||
|
[JsonSerializable(typeof(PerplexityResponseStreamLine), TypeInfoPropertyName = "PerplexityResponseStreamLine")]
|
||||||
|
[JsonSerializable(typeof(AnnotatingUnknown))]
|
||||||
|
[JsonSerializable(typeof(ChatCompletionAnnotatingURL))]
|
||||||
|
[JsonSerializable(typeof(ResponsesAnnotatingUrlCitationData))]
|
||||||
|
|
||||||
|
// Transcriptions and embeddings:
|
||||||
|
[JsonSerializable(typeof(TranscriptionResponse))]
|
||||||
|
[JsonSerializable(typeof(EmbeddingResponse))]
|
||||||
|
[JsonSerializable(typeof(GoogleEmbeddingResponse))]
|
||||||
|
[JsonSerializable(typeof(GoogleEmbedding))]
|
||||||
|
[JsonSerializable(typeof(List<GoogleEmbedding>))]
|
||||||
|
|
||||||
|
// The types inside the answers which share their names with others:
|
||||||
|
[JsonSerializable(typeof(MistralModel), TypeInfoPropertyName = "MistralModel")]
|
||||||
|
[JsonSerializable(typeof(MistralModel[]), TypeInfoPropertyName = "MistralModelArray")]
|
||||||
|
[JsonSerializable(typeof(SelfHostedModel), TypeInfoPropertyName = "SelfHostedModel")]
|
||||||
|
[JsonSerializable(typeof(SelfHostedModel[]), TypeInfoPropertyName = "SelfHostedModelArray")]
|
||||||
|
[JsonSerializable(typeof(FireworksChoice), TypeInfoPropertyName = "FireworksChoice")]
|
||||||
|
[JsonSerializable(typeof(IList<FireworksChoice>), TypeInfoPropertyName = "IListFireworksChoice")]
|
||||||
|
[JsonSerializable(typeof(PerplexityChoice), TypeInfoPropertyName = "PerplexityChoice")]
|
||||||
|
[JsonSerializable(typeof(IList<PerplexityChoice>), TypeInfoPropertyName = "IListPerplexityChoice")]
|
||||||
|
[JsonSerializable(typeof(AnthropicDelta), TypeInfoPropertyName = "AnthropicDelta")]
|
||||||
|
[JsonSerializable(typeof(FireworksDelta), TypeInfoPropertyName = "FireworksDelta")]
|
||||||
|
[JsonSerializable(typeof(PerplexityDelta), TypeInfoPropertyName = "PerplexityDelta")]
|
||||||
|
internal sealed partial class ProviderJsonContext : JsonSerializerContext;
|
||||||
@@ -21,7 +21,7 @@ public static class ProviderJsonOptions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The shared options.
|
/// The shared options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly JsonSerializerOptions OPTIONS = new()
|
public static readonly JsonSerializerOptions OPTIONS = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
Converters =
|
Converters =
|
||||||
@@ -33,6 +33,7 @@ public static class ProviderJsonOptions
|
|||||||
new SubContentImageSourceConverter(),
|
new SubContentImageSourceConverter(),
|
||||||
new SubContentImageUrlConverter(),
|
new SubContentImageUrlConverter(),
|
||||||
},
|
},
|
||||||
AllowTrailingCommas = false
|
AllowTrailingCommas = false,
|
||||||
};
|
TypeInfoResolver = ProviderJsonContext.Default,
|
||||||
|
}.AsReadOnly();
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Provider.OpenAI;
|
using AIStudio.Provider.OpenAI;
|
||||||
@@ -198,10 +199,27 @@ public sealed class ChatCompletionToolCallingAdapterTests
|
|||||||
/// <returns>The call as the loop sees it, and the request of the next round as it goes over the wire.</returns>
|
/// <returns>The call as the loop sees it, and the request of the next round as it goes over the wire.</returns>
|
||||||
private static async Task<(ToolCallingRequestedCall Call, string NextRequest)> AnswerOneCall(string functionName, string arguments, string result)
|
private static async Task<(ToolCallingRequestedCall Call, string NextRequest)> AnswerOneCall(string functionName, string arguments, string result)
|
||||||
{
|
{
|
||||||
var toolCallLine = JsonSerializer.Serialize(new
|
//
|
||||||
|
// Built as nodes, since the options of the providers know no anonymous types. The nodes escape the name and the
|
||||||
|
// arguments the way a provider would:
|
||||||
|
//
|
||||||
|
var toolCallLine = new JsonObject
|
||||||
{
|
{
|
||||||
Choices = new[] { new { Index = 0, Delta = new { ToolCalls = new[] { new { Index = 0, Id = "call_1", Type = "function", Function = new { Name = functionName, Arguments = arguments } } } } } },
|
["choices"] = new JsonArray(new JsonObject
|
||||||
}, ProviderJsonOptions.OPTIONS);
|
{
|
||||||
|
["index"] = 0,
|
||||||
|
["delta"] = new JsonObject
|
||||||
|
{
|
||||||
|
["tool_calls"] = new JsonArray(new JsonObject
|
||||||
|
{
|
||||||
|
["index"] = 0,
|
||||||
|
["id"] = "call_1",
|
||||||
|
["type"] = "function",
|
||||||
|
["function"] = new JsonObject { ["name"] = functionName, ["arguments"] = arguments },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}.ToJsonString();
|
||||||
|
|
||||||
var sent = new List<ChatCompletionAPIRequest>();
|
var sent = new List<ChatCompletionAPIRequest>();
|
||||||
var adapter = Adapter(true, sent.Add,
|
var adapter = Adapter(true, sent.Add,
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ namespace AIStudio.Tests.Serialization;
|
|||||||
public sealed class JsonCoverageTests
|
public sealed class JsonCoverageTests
|
||||||
{
|
{
|
||||||
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.Stored))]
|
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.Stored))]
|
||||||
|
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ProviderRequests))]
|
||||||
|
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ProviderAnswers))]
|
||||||
public void EveryContractComesFromSourceGeneration(JsonTestRoot root)
|
public void EveryContractComesFromSourceGeneration(JsonTestRoot root)
|
||||||
{
|
{
|
||||||
var typesWithoutGeneratedContract = JsonTypeGraph.Collect(root.Type, root.Options)
|
var typesWithoutGeneratedContract = JsonTypeGraph.Collect(root.Type, root.Options)
|
||||||
|
|||||||
Reference in new issue
Block a user