From 5d80d3894bf92dc198bdf4748466df315a27b11f Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 2 Nov 2026 18:47:27 +0100 Subject: [PATCH] Replaced the anonymous request shapes of the providers with records --- .../AnthropicMessageStreamAccumulator.cs | 8 +++---- .../Provider/BaseProvider.cs | 10 ++------- .../Provider/EmbeddingRequest.cs | 14 ++++++++++++ .../Provider/Google/GoogleEmbeddingContent.cs | 7 ++++++ .../Provider/Google/GoogleEmbeddingPart.cs | 7 ++++++ .../Provider/Google/GoogleEmbeddingRequest.cs | 22 +++++++++++++++++++ .../Provider/Google/ProviderGoogle.cs | 11 +--------- .../ChatCompletionFunctionDeclaration.cs | 17 ++++++++++++++ .../OpenAI/ChatCompletionFunctionTool.cs | 11 ++++++++++ .../Provider/ProviderToolAdapters.cs | 17 +++++++------- .../Fixtures/embedding-request.json | 1 + .../Fixtures/google-embedding-request.json | 1 + .../Serialization/JsonGoldenRequestTests.cs | 2 ++ app/Tests/Serialization/JsonSampleRequests.cs | 11 ++++++++++ .../Serialization/JsonTestRoots.Providers.cs | 4 +++- 15 files changed, 111 insertions(+), 32 deletions(-) create mode 100644 app/MindWork AI Studio/Provider/EmbeddingRequest.cs create mode 100644 app/MindWork AI Studio/Provider/Google/GoogleEmbeddingContent.cs create mode 100644 app/MindWork AI Studio/Provider/Google/GoogleEmbeddingPart.cs create mode 100644 app/MindWork AI Studio/Provider/Google/GoogleEmbeddingRequest.cs create mode 100644 app/MindWork AI Studio/Provider/OpenAI/ChatCompletionFunctionDeclaration.cs create mode 100644 app/MindWork AI Studio/Provider/OpenAI/ChatCompletionFunctionTool.cs create mode 100644 app/Tests/Serialization/Fixtures/embedding-request.json create mode 100644 app/Tests/Serialization/Fixtures/google-embedding-request.json diff --git a/app/MindWork AI Studio/Provider/Anthropic/AnthropicMessageStreamAccumulator.cs b/app/MindWork AI Studio/Provider/Anthropic/AnthropicMessageStreamAccumulator.cs index f12c157f1..95845673f 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/AnthropicMessageStreamAccumulator.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/AnthropicMessageStreamAccumulator.cs @@ -152,9 +152,9 @@ public sealed class AnthropicMessageStreamAccumulator this.unparsableToolArguments[builder.ToolUseId] = builder.UnparsableToolArguments; } - private static JsonElement EmptyTextBlock() => JsonSerializer.SerializeToElement(new + private static JsonElement EmptyTextBlock() { - type = "text", - text = string.Empty, - }); + using var document = JsonDocument.Parse("""{"type":"text","text":""}"""); + return document.RootElement.Clone(); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index eddb53ade..ceec463f6 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -1377,7 +1377,7 @@ public abstract class BaseProvider : IProvider, ISecretId if (runnableTools.Count > 0) { var adapter = new ChatCompletionToolCallingAdapter(requestFactory, systemPrompt, apiParameters, - runnableTools.Select(x => ProviderToolAdapters.ToChatCompletionTool(x.Definition, enforcesStrictToolSchemas)).ToList(), mayAskForSequentialToolCalls, runnableTools, + runnableTools.Select(x => (object)ProviderToolAdapters.ToChatCompletionTool(x.Definition, enforcesStrictToolSchemas)).ToList(), mayAskForSequentialToolCalls, runnableTools, (requestDto, requestToken) => this.StreamChatCompletionRequest(requestDto, providerName, requestPath, requestedSecret, requestHeadersAction, requestToken), ChatCompletionSourceReader.Read, this.logger); @@ -1662,13 +1662,7 @@ public abstract class BaseProvider : IProvider, ISecretId modelName = "placeholder"; // Prepare the HTTP embedding request: - var payload = new - { - model = modelName, - input = texts, - encoding_format = "float" - }; - + var payload = new EmbeddingRequest(modelName, texts); var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS); using var request = new HttpRequestMessage(HttpMethod.Post, host.EmbeddingURL()); diff --git a/app/MindWork AI Studio/Provider/EmbeddingRequest.cs b/app/MindWork AI Studio/Provider/EmbeddingRequest.cs new file mode 100644 index 000000000..1b833726c --- /dev/null +++ b/app/MindWork AI Studio/Provider/EmbeddingRequest.cs @@ -0,0 +1,14 @@ +namespace AIStudio.Provider; + +/// +/// A request for embeddings, in the shape of the OpenAI API, which most providers take. +/// +/// The name of the embedding model. +/// The texts to embed. +public sealed record EmbeddingRequest(string Model, IList Input) +{ + /// + /// Asks for the vectors as numbers, instead of as base64. + /// + public string EncodingFormat => "float"; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Google/GoogleEmbeddingContent.cs b/app/MindWork AI Studio/Provider/Google/GoogleEmbeddingContent.cs new file mode 100644 index 000000000..45a27ea3b --- /dev/null +++ b/app/MindWork AI Studio/Provider/Google/GoogleEmbeddingContent.cs @@ -0,0 +1,7 @@ +namespace AIStudio.Provider.Google; + +/// +/// The content of a request for an embedding to the Gemini API. +/// +/// The texts, each of them as a part of its own. +public sealed record GoogleEmbeddingContent(IList Parts); \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Google/GoogleEmbeddingPart.cs b/app/MindWork AI Studio/Provider/Google/GoogleEmbeddingPart.cs new file mode 100644 index 000000000..6950c3b95 --- /dev/null +++ b/app/MindWork AI Studio/Provider/Google/GoogleEmbeddingPart.cs @@ -0,0 +1,7 @@ +namespace AIStudio.Provider.Google; + +/// +/// One text in a request for an embedding to the Gemini API. +/// +/// The text to embed. +public sealed record GoogleEmbeddingPart(string Text); \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Google/GoogleEmbeddingRequest.cs b/app/MindWork AI Studio/Provider/Google/GoogleEmbeddingRequest.cs new file mode 100644 index 000000000..9dba8790d --- /dev/null +++ b/app/MindWork AI Studio/Provider/Google/GoogleEmbeddingRequest.cs @@ -0,0 +1,22 @@ +namespace AIStudio.Provider.Google; + +/// +/// A request for an embedding to the Gemini API. +/// +/// The texts to embed. +public sealed record GoogleEmbeddingRequest(GoogleEmbeddingContent Content) +{ + /// + /// Asks for vectors which tell how similar texts are. + /// + /// + /// Written as task_type, like every other field of our requests. The API reads both its field names in + /// snake case and in camel case. + /// + public string TaskType => "SEMANTIC_SIMILARITY"; + + /// + /// Creates the request for the given texts, each of them as a part of its own. + /// + public static GoogleEmbeddingRequest Of(IEnumerable texts) => new(new GoogleEmbeddingContent([..texts.Select(text => new GoogleEmbeddingPart(text))])); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs index bc08b617f..0cdc533e2 100644 --- a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs +++ b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs @@ -92,16 +92,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https } // Prepare the Google Gemini embedding request: - var payload = new - { - content = new - { - parts = texts.Select(text => new { text }).ToArray() - }, - - taskType = "SEMANTIC_SIMILARITY" - }; - + var payload = GoogleEmbeddingRequest.Of(texts); var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS); var embedUrl = $"https://generativelanguage.googleapis.com/v1beta/models/{modelName}:embedContent"; using var request = new HttpRequestMessage(HttpMethod.Post, embedUrl); diff --git a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionFunctionDeclaration.cs b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionFunctionDeclaration.cs new file mode 100644 index 000000000..f00a4744c --- /dev/null +++ b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionFunctionDeclaration.cs @@ -0,0 +1,17 @@ +using System.Text.Json; + +namespace AIStudio.Provider.OpenAI; + +/// +/// The function a tool of a Chat Completions compatible API offers to the model. +/// +public sealed record ChatCompletionFunctionDeclaration +{ + public string Name { get; init; } = string.Empty; + + public string Description { get; init; } = string.Empty; + + public JsonElement Parameters { get; init; } + + public bool Strict { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionFunctionTool.cs b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionFunctionTool.cs new file mode 100644 index 000000000..8441c296c --- /dev/null +++ b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionFunctionTool.cs @@ -0,0 +1,11 @@ +namespace AIStudio.Provider.OpenAI; + +/// +/// The function tool definition shape expected by Chat Completions compatible APIs, with the function nested. +/// +public sealed record ChatCompletionFunctionTool +{ + public string Type { get; init; } = "function"; + + public ChatCompletionFunctionDeclaration Function { get; init; } = new(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/ProviderToolAdapters.cs b/app/MindWork AI Studio/Provider/ProviderToolAdapters.cs index d8016bb2d..3187153ab 100644 --- a/app/MindWork AI Studio/Provider/ProviderToolAdapters.cs +++ b/app/MindWork AI Studio/Provider/ProviderToolAdapters.cs @@ -24,19 +24,18 @@ public static class ProviderToolAdapters /// /// The tool to describe. /// Whether the host binds the model's tool calls to a strict schema. Only then is the tool sent in strict mode. - public static object ToChatCompletionTool(ToolDefinition definition, bool hostEnforcesStrict) + public static ChatCompletionFunctionTool ToChatCompletionTool(ToolDefinition definition, bool hostEnforcesStrict) { var isStrict = definition.Function.Strict && hostEnforcesStrict; - return new + return new() { - type = "function", - function = new + Function = new() { - name = definition.Function.Name, - description = definition.Function.DescriptionForLLM, - parameters = ToOpenAIParameters(definition, isStrict), - strict = isStrict, - } + Name = definition.Function.Name, + Description = definition.Function.DescriptionForLLM, + Parameters = ToOpenAIParameters(definition, isStrict), + Strict = isStrict, + }, }; } diff --git a/app/Tests/Serialization/Fixtures/embedding-request.json b/app/Tests/Serialization/Fixtures/embedding-request.json new file mode 100644 index 000000000..e8bd13f1e --- /dev/null +++ b/app/Tests/Serialization/Fixtures/embedding-request.json @@ -0,0 +1 @@ +{"model":"embedding-model","input":["The first text.","The second text."],"encoding_format":"float"} \ No newline at end of file diff --git a/app/Tests/Serialization/Fixtures/google-embedding-request.json b/app/Tests/Serialization/Fixtures/google-embedding-request.json new file mode 100644 index 000000000..91529ea26 --- /dev/null +++ b/app/Tests/Serialization/Fixtures/google-embedding-request.json @@ -0,0 +1 @@ +{"content":{"parts":[{"text":"The first text."},{"text":"The second text."}]},"task_type":"SEMANTIC_SIMILARITY"} \ No newline at end of file diff --git a/app/Tests/Serialization/JsonGoldenRequestTests.cs b/app/Tests/Serialization/JsonGoldenRequestTests.cs index 0d8895a5a..408e2384a 100644 --- a/app/Tests/Serialization/JsonGoldenRequestTests.cs +++ b/app/Tests/Serialization/JsonGoldenRequestTests.cs @@ -22,6 +22,8 @@ public sealed class JsonGoldenRequestTests yield return new("chat-completion-request.json", JsonSampleRequests.ChatCompletion, ProviderJsonOptions.OPTIONS); yield return new("responses-request.json", JsonSampleRequests.Responses, ProviderJsonOptions.OPTIONS); yield return new("anthropic-messages-request.json", () => JsonSampleRequests.Anthropic(), ProviderJsonOptions.OPTIONS); + yield return new("embedding-request.json", JsonSampleRequests.Embedding, ProviderJsonOptions.OPTIONS); + yield return new("google-embedding-request.json", JsonSampleRequests.GoogleEmbedding, ProviderJsonOptions.OPTIONS); } [TestCaseSource(nameof(GoldenRequests))] diff --git a/app/Tests/Serialization/JsonSampleRequests.cs b/app/Tests/Serialization/JsonSampleRequests.cs index ab5171a27..74cc5115d 100644 --- a/app/Tests/Serialization/JsonSampleRequests.cs +++ b/app/Tests/Serialization/JsonSampleRequests.cs @@ -2,6 +2,7 @@ using System.Text.Json; using AIStudio.Provider; using AIStudio.Provider.Anthropic; +using AIStudio.Provider.Google; using AIStudio.Provider.OpenAI; using AIStudio.Tools.ToolCallingSystem; @@ -91,6 +92,16 @@ public static class JsonSampleRequests AdditionalApiParameters = ApiParameters(), }; + /// + /// A request for embeddings in the shape of the OpenAI API. + /// + public static EmbeddingRequest Embedding() => new("embedding-model", ["The first text.", "The second text."]); + + /// + /// A request for an embedding to the Gemini API. + /// + public static GoogleEmbeddingRequest GoogleEmbedding() => GoogleEmbeddingRequest.Of(["The first text.", "The second text."]); + private static List Messages() => [ new TextMessage("You answer briefly.", "system"), diff --git a/app/Tests/Serialization/JsonTestRoots.Providers.cs b/app/Tests/Serialization/JsonTestRoots.Providers.cs index 78bf557bf..db4b25620 100644 --- a/app/Tests/Serialization/JsonTestRoots.Providers.cs +++ b/app/Tests/Serialization/JsonTestRoots.Providers.cs @@ -28,6 +28,8 @@ public static partial class JsonTestRoots yield return CaseWithNamespace(typeof(ChatCompletionAPIRequest), options); yield return CaseWithNamespace(typeof(ResponsesAPIRequest), options); yield return CaseWithNamespace(typeof(ChatRequest), options); + yield return CaseWithNamespace(typeof(EmbeddingRequest), options); + yield return CaseWithNamespace(typeof(GoogleEmbeddingRequest), options); // // The messages and their parts, which converters write as the type they are of: @@ -43,7 +45,7 @@ public static partial class JsonTestRoots foreach (var type in new[] { typeof(string), typeof(int), typeof(long), typeof(double), typeof(decimal), typeof(bool), typeof(Dictionary), typeof(List), typeof(JsonElement) }) yield return Case(new(type, options), "value of a request"); - yield return Case(new(ProviderToolAdapters.ToChatCompletionTool(JsonSampleRequests.Tool(), hostEnforcesStrict: true).GetType(), options), "tool of a Chat Completions API"); + yield return CaseWithNamespace(typeof(ChatCompletionFunctionTool), options); yield return CaseWithNamespace(typeof(ResponsesFunctionTool), options); yield return CaseWithNamespace(typeof(ResponsesFunctionCallOutputItem), options); }