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; this.unparsableToolArguments[builder.ToolUseId] = builder.UnparsableToolArguments;
} }
private static JsonElement EmptyTextBlock() => JsonSerializer.SerializeToElement(new private static JsonElement EmptyTextBlock()
{ {
type = "text", using var document = JsonDocument.Parse("""{"type":"text","text":""}""");
text = string.Empty, return document.RootElement.Clone();
}); }
} }
@@ -1377,7 +1377,7 @@ public abstract class BaseProvider : IProvider, ISecretId
if (runnableTools.Count > 0) if (runnableTools.Count > 0)
{ {
var adapter = new ChatCompletionToolCallingAdapter<TRequest>(requestFactory, systemPrompt, apiParameters, 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), (requestDto, requestToken) => this.StreamChatCompletionRequest(requestDto, providerName, requestPath, requestedSecret, requestHeadersAction, requestToken),
ChatCompletionSourceReader.Read<TDelta, TAnnotation>, ChatCompletionSourceReader.Read<TDelta, TAnnotation>,
this.logger); this.logger);
@@ -1662,13 +1662,7 @@ public abstract class BaseProvider : IProvider, ISecretId
modelName = "placeholder"; modelName = "placeholder";
// Prepare the HTTP embedding request: // Prepare the HTTP embedding request:
var payload = new var payload = new EmbeddingRequest(modelName, texts);
{
model = modelName,
input = texts,
encoding_format = "float"
};
var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS); var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS);
using var request = new HttpRequestMessage(HttpMethod.Post, host.EmbeddingURL()); 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: // Prepare the Google Gemini embedding request:
var payload = new var payload = GoogleEmbeddingRequest.Of(texts);
{
content = new
{
parts = texts.Select(text => new { text }).ToArray()
},
taskType = "SEMANTIC_SIMILARITY"
};
var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS); var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS);
var embedUrl = $"https://generativelanguage.googleapis.com/v1beta/models/{modelName}:embedContent"; var embedUrl = $"https://generativelanguage.googleapis.com/v1beta/models/{modelName}:embedContent";
using var request = new HttpRequestMessage(HttpMethod.Post, embedUrl); 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> /// </summary>
/// <param name="definition">The tool to describe.</param> /// <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> /// <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; var isStrict = definition.Function.Strict && hostEnforcesStrict;
return new return new()
{ {
type = "function", Function = new()
function = new
{ {
name = definition.Function.Name, Name = definition.Function.Name,
description = definition.Function.DescriptionForLLM, Description = definition.Function.DescriptionForLLM,
parameters = ToOpenAIParameters(definition, isStrict), Parameters = ToOpenAIParameters(definition, isStrict),
strict = 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("chat-completion-request.json", JsonSampleRequests.ChatCompletion, ProviderJsonOptions.OPTIONS);
yield return new("responses-request.json", JsonSampleRequests.Responses, 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("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))] [TestCaseSource(nameof(GoldenRequests))]
@@ -2,6 +2,7 @@ using System.Text.Json;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Provider.Anthropic; using AIStudio.Provider.Anthropic;
using AIStudio.Provider.Google;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
using AIStudio.Tools.ToolCallingSystem; using AIStudio.Tools.ToolCallingSystem;
@@ -91,6 +92,16 @@ public static class JsonSampleRequests
AdditionalApiParameters = ApiParameters(), 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() => private static List<IMessageBase> Messages() =>
[ [
new TextMessage("You answer briefly.", "system"), 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(ChatCompletionAPIRequest), options);
yield return CaseWithNamespace(typeof(ResponsesAPIRequest), options); yield return CaseWithNamespace(typeof(ResponsesAPIRequest), options);
yield return CaseWithNamespace(typeof(ChatRequest), 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: // 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) }) 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(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(ResponsesFunctionTool), options);
yield return CaseWithNamespace(typeof(ResponsesFunctionCallOutputItem), options); yield return CaseWithNamespace(typeof(ResponsesFunctionCallOutputItem), options);
} }