Replaced the anonymous request shapes of the providers with records

This commit is contained in:
Thorsten Sommer committed 2026-11-02 18:47:27 +01:00
1 parent 9e5aee766c
commit 5d80d3894b
15 files changed
+111 -32

No files matched your search

@@ -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();
}
}
@@ -1377,7 +1377,7 @@ public abstract class BaseProvider : IProvider, ISecretId
if (runnableTools.Count > 0)
{
var adapter = new ChatCompletionToolCallingAdapter<TRequest>(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<TDelta, TAnnotation>,
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());
@@ -0,0 +1,14 @@
namespace AIStudio.Provider;
/// <summary>
/// A request for embeddings, in the shape of the OpenAI API, which most providers take.
/// </summary>
/// <param name="Model">The name of the embedding model.</param>
/// <param name="Input">The texts to embed.</param>
public sealed record EmbeddingRequest(string Model, IList<string> Input)
{
/// <summary>
/// Asks for the vectors as numbers, instead of as base64.
/// </summary>
public string EncodingFormat => "float";
}
@@ -0,0 +1,7 @@
namespace AIStudio.Provider.Google;
/// <summary>
/// The content of a request for an embedding to the Gemini API.
/// </summary>
/// <param name="Parts">The texts, each of them as a part of its own.</param>
public sealed record GoogleEmbeddingContent(IList<GoogleEmbeddingPart> Parts);
@@ -0,0 +1,7 @@
namespace AIStudio.Provider.Google;
/// <summary>
/// One text in a request for an embedding to the Gemini API.
/// </summary>
/// <param name="Text">The text to embed.</param>
public sealed record GoogleEmbeddingPart(string Text);
@@ -0,0 +1,22 @@
namespace AIStudio.Provider.Google;
/// <summary>
/// A request for an embedding to the Gemini API.
/// </summary>
/// <param name="Content">The texts to embed.</param>
public sealed record GoogleEmbeddingRequest(GoogleEmbeddingContent Content)
{
/// <summary>
/// Asks for vectors which tell how similar texts are.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public string TaskType => "SEMANTIC_SIMILARITY";
/// <summary>
/// Creates the request for the given texts, each of them as a part of its own.
/// </summary>
public static GoogleEmbeddingRequest Of(IEnumerable<string> texts) => new(new GoogleEmbeddingContent([..texts.Select(text => new GoogleEmbeddingPart(text))]));
}
@@ -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);
@@ -0,0 +1,17 @@
using System.Text.Json;
namespace AIStudio.Provider.OpenAI;
/// <summary>
/// The function a tool of a Chat Completions compatible API offers to the model.
/// </summary>
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; }
}
@@ -0,0 +1,11 @@
namespace AIStudio.Provider.OpenAI;
/// <summary>
/// The function tool definition shape expected by Chat Completions compatible APIs, with the function nested.
/// </summary>
public sealed record ChatCompletionFunctionTool
{
public string Type { get; init; } = "function";
public ChatCompletionFunctionDeclaration Function { get; init; } = new();
}
@@ -24,19 +24,18 @@ public static class ProviderToolAdapters
/// </summary>
/// <param name="definition">The tool to describe.</param>
/// <param name="hostEnforcesStrict">Whether the host binds the model's tool calls to a strict schema. Only then is the tool sent in strict mode.</param>
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,
},
};
}
@@ -0,0 +1 @@
{"model":"embedding-model","input":["The first text.","The second text."],"encoding_format":"float"}
@@ -0,0 +1 @@
{"content":{"parts":[{"text":"The first text."},{"text":"The second text."}]},"task_type":"SEMANTIC_SIMILARITY"}
@@ -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))]
@@ -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(),
};
/// <summary>
/// A request for embeddings in the shape of the OpenAI API.
/// </summary>
public static EmbeddingRequest Embedding() => new("embedding-model", ["The first text.", "The second text."]);
/// <summary>
/// A request for an embedding to the Gemini API.
/// </summary>
public static GoogleEmbeddingRequest GoogleEmbedding() => GoogleEmbeddingRequest.Of(["The first text.", "The second text."]);
private static List<IMessageBase> Messages() =>
[
new TextMessage("You answer briefly.", "system"),
@@ -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<string, object>), typeof(List<object>), 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);
}