mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Improve fidelity of OpenAI ChatCompletions Hosting (#1785)
* rename, support json serialization * wip * non-streaming * streaming? * proper streaming types * comments + fix audio parse * copilot suggestions * proper stopsequences type * build options as i could * annotations * proper generation of Id for chatcompletions * string length as in chatcompletions api ref * image url * support tools * rework API * introduce tests for chatcompletions * function calling / serialization tests / fixes * more tests and coverage * fix format * sort usings * nit * address PR comments * nits
This commit is contained in:
committed by
GitHub
Unverified
parent
54db13c22f
commit
bb8ef466de
+102
-55
@@ -1,70 +1,44 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Buffers;
|
||||
using System.ClientModel.Primitives;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net.ServerSentEvents;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Utils;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Converters;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using OpenAI.Chat;
|
||||
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions;
|
||||
|
||||
internal sealed class AIAgentChatCompletionsProcessor
|
||||
internal static class AIAgentChatCompletionsProcessor
|
||||
{
|
||||
private readonly AIAgent _agent;
|
||||
|
||||
public AIAgentChatCompletionsProcessor(AIAgent agent)
|
||||
public static async Task<IResult> CreateChatCompletionAsync(AIAgent agent, CreateChatCompletion request, CancellationToken cancellationToken)
|
||||
{
|
||||
this._agent = agent;
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(agent);
|
||||
|
||||
public async Task<IResult> CreateChatCompletionAsync(ChatCompletionOptions chatCompletionOptions, CancellationToken cancellationToken)
|
||||
{
|
||||
AgentThread? agentThread = null; // not supported to resolve from conversationId
|
||||
var chatMessages = request.Messages.Select(i => i.ToChatMessage());
|
||||
var chatClientAgentRunOptions = request.BuildOptions();
|
||||
|
||||
var inputItems = chatCompletionOptions.GetMessages();
|
||||
var chatMessages = inputItems.AsChatMessages();
|
||||
|
||||
if (chatCompletionOptions.GetStream())
|
||||
if (request.Stream == true)
|
||||
{
|
||||
return new OpenAIStreamingChatCompletionResult(this._agent, chatMessages);
|
||||
return new StreamingResponse(agent, request, chatMessages, chatClientAgentRunOptions);
|
||||
}
|
||||
|
||||
var agentResponse = await this._agent.RunAsync(chatMessages, agentThread, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return new OpenAIChatCompletionResult(agentResponse);
|
||||
var response = await agent.RunAsync(chatMessages, options: chatClientAgentRunOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return Results.Ok(response.ToChatCompletion(request));
|
||||
}
|
||||
|
||||
private sealed class OpenAIChatCompletionResult(AgentRunResponse agentRunResponse) : IResult
|
||||
{
|
||||
public async Task ExecuteAsync(HttpContext httpContext)
|
||||
{
|
||||
// note: OpenAI SDK types provide their own serialization implementation
|
||||
// so we cant simply return IResult wrap for the typed-object.
|
||||
// instead writing to the response body can be done.
|
||||
|
||||
var cancellationToken = httpContext.RequestAborted;
|
||||
var response = httpContext.Response;
|
||||
|
||||
var chatResponse = agentRunResponse.AsChatResponse();
|
||||
var openAIChatCompletion = chatResponse.AsOpenAIChatCompletion();
|
||||
var openAIChatCompletionJsonModel = openAIChatCompletion as IJsonModel<ChatCompletion>;
|
||||
Debug.Assert(openAIChatCompletionJsonModel is not null);
|
||||
|
||||
var writer = new Utf8JsonWriter(response.BodyWriter, new JsonWriterOptions { SkipValidation = false });
|
||||
openAIChatCompletionJsonModel.Write(writer, ModelReaderWriterOptions.Json);
|
||||
await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class OpenAIStreamingChatCompletionResult(AIAgent agent, IEnumerable<ChatMessage> chatMessages) : IResult
|
||||
private sealed class StreamingResponse(
|
||||
AIAgent agent,
|
||||
CreateChatCompletion request,
|
||||
IEnumerable<ChatMessage> chatMessages,
|
||||
ChatClientAgentRunOptions? options) : IResult
|
||||
{
|
||||
public Task ExecuteAsync(HttpContext httpContext)
|
||||
{
|
||||
@@ -79,26 +53,99 @@ internal sealed class AIAgentChatCompletionsProcessor
|
||||
httpContext.Features.GetRequiredFeature<IHttpResponseBodyFeature>().DisableBuffering();
|
||||
|
||||
return SseFormatter.WriteAsync(
|
||||
source: this.GetStreamingResponsesAsync(cancellationToken),
|
||||
source: this.GetStreamingChunksAsync(cancellationToken),
|
||||
destination: response.Body,
|
||||
itemFormatter: (sseItem, bufferWriter) =>
|
||||
{
|
||||
var sseDataJsonModel = (IJsonModel<StreamingChatCompletionUpdate>)sseItem.Data;
|
||||
var json = sseDataJsonModel.Write(ModelReaderWriterOptions.Json);
|
||||
bufferWriter.Write(json);
|
||||
using var writer = new Utf8JsonWriter(bufferWriter);
|
||||
JsonSerializer.Serialize(writer, sseItem.Data, ChatCompletionsJsonContext.Default.ChatCompletionChunk);
|
||||
writer.Flush();
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<SseItem<StreamingChatCompletionUpdate>> GetStreamingResponsesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
private async IAsyncEnumerable<SseItem<ChatCompletionChunk>> GetStreamingChunksAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
AgentThread? agentThread = null;
|
||||
// The Unix timestamp (in seconds) of when the chat completion was created. Each chunk has the same timestamp.
|
||||
DateTimeOffset? createdAt = null;
|
||||
var chunkId = IdGeneratorHelpers.NewId(prefix: "chatcmpl", delimiter: "-", stringLength: 13);
|
||||
|
||||
var agentRunResponseUpdates = agent.RunStreamingAsync(chatMessages, thread: agentThread, cancellationToken: cancellationToken);
|
||||
var chatResponseUpdates = agentRunResponseUpdates.AsChatResponseUpdatesAsync();
|
||||
await foreach (var streamingChatCompletionUpdate in chatResponseUpdates.AsOpenAIStreamingChatCompletionUpdatesAsync(cancellationToken).ConfigureAwait(false))
|
||||
await foreach (var agentRunResponseUpdate in agent.RunStreamingAsync(chatMessages, options: options, cancellationToken: cancellationToken).WithCancellation(cancellationToken))
|
||||
{
|
||||
yield return new SseItem<StreamingChatCompletionUpdate>(streamingChatCompletionUpdate);
|
||||
var finishReason = (agentRunResponseUpdate.RawRepresentation is ChatResponseUpdate { FinishReason: not null } chatResponseUpdate)
|
||||
? chatResponseUpdate.FinishReason.ToString()
|
||||
: "stop";
|
||||
|
||||
var choiceChunks = new List<ChatCompletionChoiceChunk>();
|
||||
CompletionUsage? usageDetails = null;
|
||||
|
||||
createdAt ??= agentRunResponseUpdate.CreatedAt;
|
||||
|
||||
foreach (var content in agentRunResponseUpdate.Contents)
|
||||
{
|
||||
// usage content is handled separately
|
||||
if (content is UsageContent usageContent && usageContent.Details != null)
|
||||
{
|
||||
usageDetails = usageContent.Details.ToCompletionUsage();
|
||||
continue;
|
||||
}
|
||||
|
||||
ChatCompletionDelta? delta = content switch
|
||||
{
|
||||
TextContent textContent => new() { Content = textContent.Text },
|
||||
|
||||
// image
|
||||
DataContent imageContent when imageContent.HasTopLevelMediaType("image") => new() { Content = imageContent.Base64Data.ToString() },
|
||||
UriContent urlContent when urlContent.HasTopLevelMediaType("image") => new() { Content = urlContent.Uri.ToString() },
|
||||
|
||||
// audio
|
||||
DataContent audioContent when audioContent.HasTopLevelMediaType("audio") => new() { Content = audioContent.Base64Data.ToString() },
|
||||
|
||||
// file
|
||||
DataContent fileContent => new() { Content = fileContent.Base64Data.ToString() },
|
||||
HostedFileContent fileContent => new() { Content = fileContent.FileId },
|
||||
|
||||
// function call
|
||||
FunctionCallContent functionCallContent => new()
|
||||
{
|
||||
ToolCalls = [functionCallContent.ToChoiceMessageToolCall()]
|
||||
},
|
||||
|
||||
// function result. ChatCompletions dont provide the results of function result per API reference
|
||||
FunctionResultContent functionResultContent => null,
|
||||
|
||||
// ignore
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (delta is null)
|
||||
{
|
||||
// unsupported but expected content type.
|
||||
continue;
|
||||
}
|
||||
|
||||
delta.Role = agentRunResponseUpdate.Role?.Value ?? "user";
|
||||
|
||||
var choiceChunk = new ChatCompletionChoiceChunk
|
||||
{
|
||||
Index = 0,
|
||||
Delta = delta,
|
||||
FinishReason = finishReason
|
||||
};
|
||||
|
||||
choiceChunks.Add(choiceChunk);
|
||||
}
|
||||
|
||||
var chunk = new ChatCompletionChunk
|
||||
{
|
||||
Id = chunkId,
|
||||
Created = (createdAt ?? DateTimeOffset.UtcNow).ToUnixTimeSeconds(),
|
||||
Model = request.Model,
|
||||
Choices = choiceChunks,
|
||||
Usage = usageDetails
|
||||
};
|
||||
|
||||
yield return new(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for converting agent responses to ChatCompletion models.
|
||||
/// </summary>
|
||||
internal static class AgentRunResponseExtensions
|
||||
{
|
||||
public static ChatCompletion ToChatCompletion(this AgentRunResponse agentRunResponse, CreateChatCompletion request)
|
||||
{
|
||||
IList<ChatCompletionChoice> choices = agentRunResponse.ToChoices();
|
||||
|
||||
return new ChatCompletion
|
||||
{
|
||||
Id = IdGeneratorHelpers.NewId(prefix: "chatcmpl", delimiter: "-", stringLength: 13),
|
||||
Choices = choices,
|
||||
Created = (agentRunResponse.CreatedAt ?? DateTimeOffset.UtcNow).ToUnixTimeSeconds(),
|
||||
Model = request.Model,
|
||||
Usage = agentRunResponse.Usage.ToCompletionUsage(),
|
||||
ServiceTier = request.ServiceTier ?? "default"
|
||||
};
|
||||
}
|
||||
|
||||
public static List<ChatCompletionChoice> ToChoices(this AgentRunResponse agentRunResponse)
|
||||
{
|
||||
var chatCompletionChoices = new List<ChatCompletionChoice>();
|
||||
var index = 0;
|
||||
|
||||
var finishReason = (agentRunResponse.RawRepresentation is ChatResponse { FinishReason: not null } chatResponse)
|
||||
? chatResponse.FinishReason.ToString()
|
||||
: "stop"; // "stop" is a natural stop point; returning this by-default
|
||||
|
||||
foreach (var message in agentRunResponse.Messages)
|
||||
{
|
||||
foreach (var content in message.Contents)
|
||||
{
|
||||
ChoiceMessage? choiceMessage = content switch
|
||||
{
|
||||
// text
|
||||
TextContent textContent => new()
|
||||
{
|
||||
Content = textContent.Text
|
||||
},
|
||||
|
||||
// image, see how MessageContentPartConverter packs the content types
|
||||
DataContent imageContent when imageContent.HasTopLevelMediaType("image") => new()
|
||||
{
|
||||
Content = imageContent.Base64Data.ToString()
|
||||
},
|
||||
UriContent urlContent when urlContent.HasTopLevelMediaType("image") => new()
|
||||
{
|
||||
Content = urlContent.Uri.ToString()
|
||||
},
|
||||
|
||||
// audio
|
||||
DataContent audioContent when audioContent.HasTopLevelMediaType("audio") => new()
|
||||
{
|
||||
Audio = new()
|
||||
{
|
||||
Data = audioContent.Base64Data.ToString(),
|
||||
Id = audioContent.Name,
|
||||
//Transcript = ,
|
||||
//ExpiresAt = ,
|
||||
},
|
||||
},
|
||||
|
||||
// file (neither audio nor image)
|
||||
DataContent fileContent => new()
|
||||
{
|
||||
Content = fileContent.Base64Data.ToString()
|
||||
},
|
||||
HostedFileContent fileContent => new()
|
||||
{
|
||||
Content = fileContent.FileId
|
||||
},
|
||||
|
||||
// function call
|
||||
FunctionCallContent functionCallContent => new()
|
||||
{
|
||||
ToolCalls = [functionCallContent.ToChoiceMessageToolCall()]
|
||||
},
|
||||
|
||||
// function result. ChatCompletions dont provide the results of function result per API reference
|
||||
FunctionResultContent functionResultContent => null,
|
||||
|
||||
// ignore
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (choiceMessage is null)
|
||||
{
|
||||
// not supported, but expected content type.
|
||||
continue;
|
||||
}
|
||||
|
||||
choiceMessage.Role = message.Role.Value;
|
||||
choiceMessage.Annotations = content.Annotations?.ToChoiceMessageAnnotations();
|
||||
|
||||
var choice = new ChatCompletionChoice
|
||||
{
|
||||
Index = index++,
|
||||
Message = choiceMessage,
|
||||
FinishReason = finishReason
|
||||
};
|
||||
|
||||
chatCompletionChoices.Add(choice);
|
||||
}
|
||||
}
|
||||
|
||||
return chatCompletionChoices;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts UsageDetails to CompletionUsage.
|
||||
/// </summary>
|
||||
/// <param name="usage">The usage details to convert.</param>
|
||||
/// <returns>A CompletionUsage object with zeros if usage is null.</returns>
|
||||
public static CompletionUsage ToCompletionUsage(this UsageDetails? usage)
|
||||
{
|
||||
if (usage == null)
|
||||
{
|
||||
return CompletionUsage.Zero;
|
||||
}
|
||||
|
||||
var cachedTokens = usage.AdditionalCounts?.TryGetValue("InputTokenDetails.CachedTokenCount", out var cachedInputToken) ?? false
|
||||
? (int)cachedInputToken
|
||||
: 0;
|
||||
var reasoningTokens =
|
||||
usage.AdditionalCounts?.TryGetValue("OutputTokenDetails.ReasoningTokenCount", out var reasoningToken) ?? false
|
||||
? (int)reasoningToken
|
||||
: 0;
|
||||
|
||||
return new CompletionUsage
|
||||
{
|
||||
PromptTokens = (int)(usage.InputTokenCount ?? 0),
|
||||
PromptTokensDetails = new() { CachedTokens = cachedTokens },
|
||||
CompletionTokens = (int)(usage.OutputTokenCount ?? 0),
|
||||
CompletionTokensDetails = new() { ReasoningTokens = reasoningTokens },
|
||||
TotalTokens = (int)(usage.TotalTokenCount ?? 0)
|
||||
};
|
||||
}
|
||||
|
||||
public static IList<ChoiceMessageAnnotation> ToChoiceMessageAnnotations(this IList<AIAnnotation> annotations)
|
||||
{
|
||||
var result = new List<ChoiceMessageAnnotation>();
|
||||
foreach (var annotation in annotations.OfType<CitationAnnotation>())
|
||||
{
|
||||
if (annotation is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// may point to mulitple regions in the AIContent.
|
||||
// we need to unroll another loop for regions then -> chatCompletions only point to single region per annotation
|
||||
|
||||
var regions = annotation.AnnotatedRegions?.OfType<TextSpanAnnotatedRegion>().Where(x => x.StartIndex is not null && x.EndIndex is not null);
|
||||
if (regions is not null)
|
||||
{
|
||||
foreach (var region in regions)
|
||||
{
|
||||
result.Add(new()
|
||||
{
|
||||
AnnotationUrlCitation = new AnnotationUrlCitation
|
||||
{
|
||||
Url = annotation.Url?.ToString(),
|
||||
Title = annotation.Title,
|
||||
StartIndex = region.StartIndex,
|
||||
EndIndex = region.EndIndex
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(new()
|
||||
{
|
||||
AnnotationUrlCitation = new AnnotationUrlCitation
|
||||
{
|
||||
Url = annotation.Url?.ToString(),
|
||||
Title = annotation.Title
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static ChoiceMessageToolCall ToChoiceMessageToolCall(this FunctionCallContent functionCall)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Id = functionCall.CallId,
|
||||
Function = new()
|
||||
{
|
||||
Name = functionCall.Name,
|
||||
Arguments = JsonSerializer.Serialize(functionCall.Arguments, ChatCompletionsJsonContext.Default.DictionaryStringObject)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions;
|
||||
|
||||
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
AllowOutOfOrderMetadataProperties = true,
|
||||
WriteIndented = false)]
|
||||
[JsonSerializable(typeof(Dictionary<string, string>))]
|
||||
[JsonSerializable(typeof(CreateChatCompletion))]
|
||||
[JsonSerializable(typeof(StopSequences))]
|
||||
[JsonSerializable(typeof(ChatCompletion))]
|
||||
[JsonSerializable(typeof(ChatCompletionRequestMessage))]
|
||||
[JsonSerializable(typeof(IList<ChatCompletionRequestMessage>))]
|
||||
[JsonSerializable(typeof(MessageContent))]
|
||||
[JsonSerializable(typeof(MessageContentPart))]
|
||||
[JsonSerializable(typeof(IReadOnlyList<MessageContentPart>))]
|
||||
[JsonSerializable(typeof(TextContentPart))]
|
||||
[JsonSerializable(typeof(ImageContentPart))]
|
||||
[JsonSerializable(typeof(AudioContentPart))]
|
||||
[JsonSerializable(typeof(FileContentPart))]
|
||||
[JsonSerializable(typeof(ChatCompletionChoice))]
|
||||
[JsonSerializable(typeof(IList<ChatCompletionChoice>))]
|
||||
[JsonSerializable(typeof(ChoiceMessage))]
|
||||
[JsonSerializable(typeof(ChoiceMessageAnnotation))]
|
||||
[JsonSerializable(typeof(ChoiceMessageAudio))]
|
||||
[JsonSerializable(typeof(ChoiceMessageFunctionCall))]
|
||||
[JsonSerializable(typeof(ChoiceMessageToolCall))]
|
||||
[JsonSerializable(typeof(AnnotationUrlCitation))]
|
||||
[JsonSerializable(typeof(ChatCompletionChoiceChunk))]
|
||||
[JsonSerializable(typeof(IList<ChatCompletionChoiceChunk>))]
|
||||
[JsonSerializable(typeof(ChatCompletionChunk))]
|
||||
[JsonSerializable(typeof(ChatCompletionDelta))]
|
||||
[JsonSerializable(typeof(ToolChoice))]
|
||||
[JsonSerializable(typeof(AllowedToolsChoice))]
|
||||
[JsonSerializable(typeof(AllowedToolsConfiguration))]
|
||||
[JsonSerializable(typeof(ToolDefinition))]
|
||||
[JsonSerializable(typeof(IList<ToolDefinition>))]
|
||||
[JsonSerializable(typeof(FunctionReference))]
|
||||
[JsonSerializable(typeof(FunctionToolChoice))]
|
||||
[JsonSerializable(typeof(CustomToolChoice))]
|
||||
[JsonSerializable(typeof(CustomToolObject))]
|
||||
[JsonSerializable(typeof(ResponseFormat))]
|
||||
[JsonSerializable(typeof(TextResponseFormat))]
|
||||
[JsonSerializable(typeof(JsonSchemaResponseFormat))]
|
||||
[JsonSerializable(typeof(JsonSchemaConfiguration))]
|
||||
[JsonSerializable(typeof(JsonObjectResponseFormat))]
|
||||
[JsonSerializable(typeof(Tool))]
|
||||
[JsonSerializable(typeof(IList<Tool>))]
|
||||
[JsonSerializable(typeof(FunctionTool))]
|
||||
[JsonSerializable(typeof(FunctionDefinition))]
|
||||
[JsonSerializable(typeof(CustomTool))]
|
||||
[JsonSerializable(typeof(CustomToolProperties))]
|
||||
[JsonSerializable(typeof(CustomToolFormat))]
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal sealed partial class ChatCompletionsJsonContext : JsonSerializerContext;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for JSON serialization.
|
||||
/// </summary>
|
||||
internal static class ChatCompletionsJsonSerializerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default JSON serializer options.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions Default { get; } = Create();
|
||||
|
||||
private static JsonSerializerOptions Create()
|
||||
{
|
||||
JsonSerializerOptions options = new(ChatCompletionsJsonContext.Default.Options);
|
||||
options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!);
|
||||
options.MakeReadOnly();
|
||||
return options;
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Converters;
|
||||
|
||||
internal static class ChatClientAgentRunOptionsConverter
|
||||
{
|
||||
private static readonly JsonElement s_emptyJson = JsonDocument.Parse("{}").RootElement;
|
||||
|
||||
public static ChatClientAgentRunOptions BuildOptions(this CreateChatCompletion request)
|
||||
{
|
||||
ChatOptions chatOptions = new()
|
||||
{
|
||||
Temperature = request.Temperature,
|
||||
MaxOutputTokens = request.MaxCompletionTokens,
|
||||
FrequencyPenalty = request.FrequencyPenalty,
|
||||
PresencePenalty = request.PresencePenalty,
|
||||
Seed = request.Seed,
|
||||
TopP = request.TopP,
|
||||
StopSequences = request.Stop?.SequenceList ?? [],
|
||||
ResponseFormat = request.ResponseFormat?.ToChatResponseFormat()
|
||||
};
|
||||
|
||||
if (request.ToolChoice is not null)
|
||||
{
|
||||
chatOptions.ToolMode = request.ToolChoice.ToChatToolMode();
|
||||
}
|
||||
|
||||
if (request.Tools?.Count > 0)
|
||||
{
|
||||
chatOptions.Tools = request.Tools.Select(x => x.ToAITool()).ToList();
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
ChatOptions = chatOptions
|
||||
};
|
||||
}
|
||||
|
||||
private static ChatResponseFormat ToChatResponseFormat(this ResponseFormat responseFormat)
|
||||
{
|
||||
if (responseFormat.IsText)
|
||||
{
|
||||
return ChatResponseFormat.Text;
|
||||
}
|
||||
if (responseFormat.IsJsonObject)
|
||||
{
|
||||
return ChatResponseFormat.Json;
|
||||
}
|
||||
if (responseFormat.IsJsonSchema)
|
||||
{
|
||||
var schema = responseFormat.JsonSchema.JsonSchema;
|
||||
return ChatResponseFormat.ForJsonSchema(schema.Schema, schema.Name, schema.Description);
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(responseFormat));
|
||||
}
|
||||
|
||||
private static AITool ToAITool(this Tool tool)
|
||||
{
|
||||
if (tool is FunctionTool functionTool)
|
||||
{
|
||||
var function = functionTool.Function;
|
||||
return AIFunctionFactory.CreateDeclaration(function.Name, function.Description, function.Parameters ?? s_emptyJson);
|
||||
}
|
||||
if (tool is CustomTool customTool)
|
||||
{
|
||||
var custom = customTool.Custom;
|
||||
return new CustomAITool(custom.Name, custom.Description, custom.Format?.AdditionalProperties);
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(tool));
|
||||
}
|
||||
|
||||
private static ChatToolMode? ToChatToolMode(this ToolChoice toolChoice)
|
||||
{
|
||||
if (toolChoice.IsMode)
|
||||
{
|
||||
return toolChoice.Mode switch
|
||||
{
|
||||
"auto" => ChatToolMode.Auto,
|
||||
"none" => ChatToolMode.None,
|
||||
"required" => ChatToolMode.RequireAny,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
if (toolChoice.IsAllowedTools)
|
||||
{
|
||||
var mode = toolChoice.AllowedTools.AllowedTools.Mode;
|
||||
return mode switch
|
||||
{
|
||||
"auto" => ChatToolMode.Auto,
|
||||
"required" => ChatToolMode.RequireAny,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
if (toolChoice.IsFunctionTool)
|
||||
{
|
||||
var function = toolChoice.FunctionTool.Function;
|
||||
return ChatToolMode.RequireSpecific(function.Name);
|
||||
}
|
||||
|
||||
if (toolChoice.IsCustomTool)
|
||||
{
|
||||
var custom = toolChoice.CustomTool.Custom;
|
||||
return ChatToolMode.RequireSpecific(custom.Name);
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(toolChoice));
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Converters;
|
||||
|
||||
internal static class MessageContentPartConverter
|
||||
{
|
||||
public static AIContent? ToAIContent(MessageContentPart part)
|
||||
{
|
||||
return part switch
|
||||
{
|
||||
// text
|
||||
TextContentPart textPart => new TextContent(textPart.Text),
|
||||
|
||||
// image
|
||||
ImageContentPart imagePart when !string.IsNullOrEmpty(imagePart.UrlOrData) =>
|
||||
imagePart.UrlOrData.StartsWith("data:", StringComparison.OrdinalIgnoreCase)
|
||||
? new DataContent(imagePart.UrlOrData, "image/*")
|
||||
: new UriContent(imagePart.Url, ImageUriToMediaType(imagePart.Url)),
|
||||
|
||||
// audio
|
||||
AudioContentPart audioPart =>
|
||||
new DataContent(audioPart.InputAudio.Data, audioPart.InputAudio.Format.ToUpperInvariant() switch
|
||||
{
|
||||
"MP3" => "audio/mpeg",
|
||||
"WAV" => "audio/wav",
|
||||
"OPUS" => "audio/opus",
|
||||
"AAC" => "audio/aac",
|
||||
"FLAC" => "audio/flac",
|
||||
"PCM16" => "audio/pcm",
|
||||
_ => "audio/*"
|
||||
}),
|
||||
|
||||
// file
|
||||
FileContentPart filePart when !string.IsNullOrEmpty(filePart.File.FileId)
|
||||
=> new HostedFileContent(filePart.File.FileId),
|
||||
FileContentPart filePart when !string.IsNullOrEmpty(filePart.File.FileData)
|
||||
=> new DataContent(filePart.File.FileData, "application/octet-stream") { Name = filePart.File.Filename },
|
||||
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static string ImageUriToMediaType(Uri uri)
|
||||
{
|
||||
string absoluteUri = uri.AbsoluteUri;
|
||||
return
|
||||
absoluteUri.EndsWith(".png", StringComparison.OrdinalIgnoreCase) ? "image/png" :
|
||||
absoluteUri.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) ? "image/jpeg" :
|
||||
absoluteUri.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) ? "image/jpeg" :
|
||||
absoluteUri.EndsWith(".gif", StringComparison.OrdinalIgnoreCase) ? "image/gif" :
|
||||
absoluteUri.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase) ? "image/bmp" :
|
||||
absoluteUri.EndsWith(".webp", StringComparison.OrdinalIgnoreCase) ? "image/webp" :
|
||||
"image/*";
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a chat completion response returned by the model, based on the provided input.
|
||||
/// </summary>
|
||||
internal sealed record ChatCompletion
|
||||
{
|
||||
/// <summary>
|
||||
/// A unique identifier for the chat completion.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
[JsonRequired]
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The object type, which is always "chat.completion".
|
||||
/// </summary>
|
||||
[JsonPropertyName("object")]
|
||||
public string Object { get; init; } = "chat.completion";
|
||||
|
||||
/// <summary>
|
||||
/// The Unix timestamp (in seconds) of when the chat completion was created.
|
||||
/// </summary>
|
||||
[JsonPropertyName("created")]
|
||||
[JsonRequired]
|
||||
public required long Created { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The model used for the chat completion.
|
||||
/// </summary>
|
||||
[JsonPropertyName("model")]
|
||||
[JsonRequired]
|
||||
public required string Model { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of chat completion choices. Can be more than one if n is greater than 1.
|
||||
/// </summary>
|
||||
[JsonPropertyName("choices")]
|
||||
[JsonRequired]
|
||||
public required IList<ChatCompletionChoice> Choices { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Usage statistics for the completion request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("usage")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public CompletionUsage? Usage { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The service tier used for processing the request. This field is only included if the service_tier parameter is specified in the request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("service_tier")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? ServiceTier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// This fingerprint represents the backend configuration that the model runs with.
|
||||
/// Can be used in conjunction with the seed request parameter to understand when backend changes have been made that might impact determinism.
|
||||
/// </summary>
|
||||
[JsonPropertyName("system_fingerprint")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? SystemFingerprint { get; init; }
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a choice in a chat completion response.
|
||||
/// </summary>
|
||||
internal sealed record ChatCompletionChoice
|
||||
{
|
||||
/// <summary>
|
||||
/// The index of the choice in the list of choices.
|
||||
/// </summary>
|
||||
[JsonPropertyName("index")]
|
||||
public required int Index { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The reason the model stopped generating tokens.
|
||||
/// This will be stop if the model hit a natural stop point or a provided stop sequence, length if the maximum number of tokens specified in the request was reached,
|
||||
/// content_filter if content was omitted due to a flag from our content filters, tool_calls if the model called a tool,
|
||||
/// or function_call (deprecated) if the model called a function.
|
||||
/// </summary>
|
||||
[JsonPropertyName("finish_reason")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? FinishReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// A chat completion message generated by the model.
|
||||
/// </summary>
|
||||
[JsonPropertyName("message")]
|
||||
public required ChoiceMessage Message { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A chat completion message generated by the model.
|
||||
/// </summary>
|
||||
internal sealed record ChoiceMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The role of the author of this message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("role")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Role { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of annotations for this message. Currently used for web search citations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("annotations")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public IList<ChoiceMessageAnnotation>? Annotations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The contents of the message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("content")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The refusal message generated by the model.
|
||||
/// </summary>
|
||||
[JsonPropertyName("refusal")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Refusal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the audio output modality is requested, this object contains data about the audio response from the model.
|
||||
/// </summary>
|
||||
[JsonPropertyName("audio")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public ChoiceMessageAudio? Audio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Deprecated and replaced by tool_calls. The name and arguments of a function that should be called, as generated by the model.
|
||||
/// </summary>
|
||||
[JsonPropertyName("function_call")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public ChoiceMessageFunctionCall? FunctionCall { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tool calls generated by the model, such as function calls.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tool_calls")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public IList<ChoiceMessageToolCall>? ToolCalls { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audio output data in a chat completion message.
|
||||
/// </summary>
|
||||
internal sealed record ChoiceMessageAudio
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64 encoded audio bytes generated by the model, in the format specified in the request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
public string? Data { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The Unix timestamp (in seconds) for when this audio response will no longer be accessible on the server for use in multi-turn conversations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("expires_at")]
|
||||
public int ExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique identifier for this audio response.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Transcript of the audio generated by the model.
|
||||
/// </summary>
|
||||
[JsonPropertyName("transcript")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Transcript { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deprecated. The name and arguments of a function that should be called, as generated by the model.
|
||||
/// </summary>
|
||||
internal sealed record ChoiceMessageFunctionCall
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the function to call.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The arguments to call the function with, as generated by the model in JSON format.
|
||||
/// Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema.
|
||||
/// Validate the arguments in your code before calling your function.
|
||||
/// </summary>
|
||||
[JsonPropertyName("arguments")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Arguments { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a tool call generated by the model.
|
||||
/// </summary>
|
||||
internal sealed record ChoiceMessageToolCall
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the tool call.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of the tool.
|
||||
/// </summary>
|
||||
public string Type => "function";
|
||||
|
||||
/// <summary>
|
||||
/// The function that the model called.
|
||||
/// </summary>
|
||||
[JsonPropertyName("function")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public ChoiceMessageFunctionCall? Function { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An annotation for a message, used for web search citations.
|
||||
/// </summary>
|
||||
internal sealed record ChoiceMessageAnnotation
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of annotation. Always 'url_citation' for web search results.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type => "url_citation";
|
||||
|
||||
/// <summary>
|
||||
/// The URL citation details.
|
||||
/// </summary>
|
||||
[JsonPropertyName("url_citation")]
|
||||
public required AnnotationUrlCitation AnnotationUrlCitation { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A citation to a URL for a web search result.
|
||||
/// </summary>
|
||||
internal sealed record AnnotationUrlCitation
|
||||
{
|
||||
/// <summary>
|
||||
/// The character index in the message content where the citation ends.
|
||||
/// </summary>
|
||||
[JsonPropertyName("end_index")]
|
||||
public int? EndIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The character index in the message content where the citation starts.
|
||||
/// </summary>
|
||||
[JsonPropertyName("start_index")]
|
||||
public int? StartIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The title of the cited resource.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL of the cited resource.
|
||||
/// </summary>
|
||||
[JsonPropertyName("url")]
|
||||
public string? Url { get; set; }
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a chunk of chat completion response returned by the model, based on the provided input.
|
||||
/// </summary>
|
||||
internal sealed record ChatCompletionChunk
|
||||
{
|
||||
/// <summary>
|
||||
/// A unique identifier for the chat completion. Each chunk has the same ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
[JsonRequired]
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of chat completion choices. Can be more than one if n is greater than 1.
|
||||
/// </summary>
|
||||
[JsonPropertyName("choices")]
|
||||
[JsonRequired]
|
||||
public required IList<ChatCompletionChoiceChunk> Choices { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The object type, which is always "chat.completion.chunk".
|
||||
/// </summary>
|
||||
[JsonPropertyName("object")]
|
||||
public string Object => "chat.completion.chunk";
|
||||
|
||||
/// <summary>
|
||||
/// The Unix timestamp (in seconds) of when the chat completion was created. Each chunk has the same timestamp.
|
||||
/// </summary>
|
||||
[JsonPropertyName("created")]
|
||||
[JsonRequired]
|
||||
public required long Created { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The model to generate the completion.
|
||||
/// </summary>
|
||||
[JsonPropertyName("model")]
|
||||
[JsonRequired]
|
||||
public required string Model { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Usage statistics for the completion request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("usage")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public CompletionUsage? Usage { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The service tier used for processing the request. This field is only included if the service_tier parameter is specified in the request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("service_tier")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? ServiceTier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// This fingerprint represents the backend configuration that the model runs with.
|
||||
/// Can be used in conjunction with the seed request parameter to understand when backend changes have been made that might impact determinism.
|
||||
/// </summary>
|
||||
[JsonPropertyName("system_fingerprint")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? SystemFingerprint { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record ChatCompletionChoiceChunk
|
||||
{
|
||||
/// <summary>
|
||||
/// The index of the choice in the list of choices.
|
||||
/// </summary>
|
||||
[JsonPropertyName("index")]
|
||||
public required int Index { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The reason the model stopped generating tokens.
|
||||
/// This will be stop if the model hit a natural stop point or a provided stop sequence, length if the maximum number of tokens specified in the request was reached,
|
||||
/// content_filter if content was omitted due to a flag from our content filters, tool_calls if the model called a tool, or function_call (deprecated) if the model called a function.
|
||||
/// </summary>
|
||||
[JsonPropertyName("finish_reason")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? FinishReason { get; init; }
|
||||
|
||||
[JsonPropertyName("delta")]
|
||||
public required ChatCompletionDelta Delta { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record ChatCompletionDelta
|
||||
{
|
||||
/// <summary>
|
||||
/// The contents of the chunk message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("content")]
|
||||
public string? Content { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The refusal message generated by the model.
|
||||
/// </summary>
|
||||
[JsonPropertyName("refusal")]
|
||||
public string? Refusal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The role of the author of this message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("role")]
|
||||
public string? Role { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Deprecated and replaced by tool_calls. The name and arguments of a function that should be called, as generated by the model.
|
||||
/// </summary>
|
||||
[JsonPropertyName("function_call")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public ChoiceMessageFunctionCall? FunctionCall { get; set; }
|
||||
|
||||
[JsonPropertyName("tool_calls")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public IList<ChoiceMessageToolCall>? ToolCalls { get; set; }
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Converters;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a message in a chat completion request.
|
||||
/// </summary>
|
||||
[JsonPolymorphic(TypeDiscriminatorPropertyName = "role", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]
|
||||
[JsonDerivedType(typeof(DeveloperMessage), "developer")]
|
||||
[JsonDerivedType(typeof(SystemMessage), "system")]
|
||||
[JsonDerivedType(typeof(UserMessage), "user")]
|
||||
[JsonDerivedType(typeof(AssistantMessage), "assistant")]
|
||||
[JsonDerivedType(typeof(ToolMessage), "tool")]
|
||||
[JsonDerivedType(typeof(FunctionMessage), "function")]
|
||||
internal abstract record ChatCompletionRequestMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The role of the content.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public abstract string Role { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The contents of the message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("content")]
|
||||
public required MessageContent Content { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts to a <see cref="ChatMessage"/>.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ChatMessage"/> representing the message.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the content is neither text nor AI contents.</exception>
|
||||
public virtual ChatMessage ToChatMessage()
|
||||
{
|
||||
if (this.Content.IsText)
|
||||
{
|
||||
return new(ChatRole.User, this.Content.Text);
|
||||
}
|
||||
else if (this.Content.IsContents)
|
||||
{
|
||||
var aiContents = this.Content.Contents.Select(MessageContentPartConverter.ToAIContent).Where(c => c is not null).ToList();
|
||||
return new ChatMessage(ChatRole.User, aiContents!);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("MessageContent has no value");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A developer message in a chat completion request.
|
||||
/// Developer messages are used to provide instructions to the model at the system level.
|
||||
/// </summary>
|
||||
internal sealed record DeveloperMessage : ChatCompletionRequestMessage
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public override string Role => "developer";
|
||||
|
||||
/// <summary>
|
||||
/// An optional name for the participant.
|
||||
/// Provides the model information to differentiate between participants of the same role.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A system message in a chat completion request.
|
||||
/// System messages provide high-level instructions for the conversation.
|
||||
/// </summary>
|
||||
internal sealed record SystemMessage : ChatCompletionRequestMessage
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public override string Role => "system";
|
||||
|
||||
/// <summary>
|
||||
/// An optional name for the participant.
|
||||
/// Provides the model information to differentiate between participants of the same role.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A user message in a chat completion request.
|
||||
/// User messages represent input from the end user.
|
||||
/// </summary>
|
||||
internal sealed record UserMessage : ChatCompletionRequestMessage
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public override string Role => "user";
|
||||
|
||||
/// <summary>
|
||||
/// An optional name for the participant.
|
||||
/// Provides the model information to differentiate between participants of the same role.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An assistant message in a chat completion request.
|
||||
/// Assistant messages represent previous responses from the model, used in multi-turn conversations.
|
||||
/// </summary>
|
||||
internal sealed record AssistantMessage : ChatCompletionRequestMessage
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public override string Role => "assistant";
|
||||
|
||||
/// <summary>
|
||||
/// An optional name for the participant.
|
||||
/// Provides the model information to differentiate between participants of the same role.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A tool message in a chat completion request.
|
||||
/// Tool messages contain the result of a tool call made by the assistant.
|
||||
/// </summary>
|
||||
internal sealed record ToolMessage : ChatCompletionRequestMessage
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public override string Role => "tool";
|
||||
|
||||
/// <summary>
|
||||
/// Tool call that this message is responding to.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tool_call_id")]
|
||||
public required string ToolCallId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deprecated. A function message in a chat completion request.
|
||||
/// Function messages have been replaced by tool messages.
|
||||
/// </summary>
|
||||
internal sealed record FunctionMessage : ChatCompletionRequestMessage
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public override string Role => "function";
|
||||
|
||||
/// <summary>
|
||||
/// The name of the function to call.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts to a <see cref="ChatMessage"/>.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ChatMessage"/> representing the message.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the content is not text.</exception>
|
||||
public override ChatMessage ToChatMessage()
|
||||
{
|
||||
if (this.Content.IsText)
|
||||
{
|
||||
return new(ChatRole.User, this.Content.Text);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("FunctionMessage Content must be text");
|
||||
}
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents usage statistics for a chat completion request.
|
||||
/// </summary>
|
||||
internal sealed record CompletionUsage
|
||||
{
|
||||
public static CompletionUsage Zero { get; } = new()
|
||||
{
|
||||
CompletionTokens = 0,
|
||||
PromptTokens = 0,
|
||||
TotalTokens = 0,
|
||||
CompletionTokensDetails = new()
|
||||
{
|
||||
AcceptedPredictionTokens = 0,
|
||||
AudioTokens = 0,
|
||||
ReasoningTokens = 0,
|
||||
RejectedPredictionTokens = 0
|
||||
},
|
||||
PromptTokensDetails = new()
|
||||
{
|
||||
AudioTokens = 0,
|
||||
CachedTokens = 0
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Number of tokens in the generated completion.
|
||||
/// </summary>
|
||||
[JsonPropertyName("completion_tokens")]
|
||||
public int? CompletionTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of tokens in the prompt.
|
||||
/// </summary>
|
||||
[JsonPropertyName("prompt_tokens")]
|
||||
public int? PromptTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of tokens used in the request (prompt + completion).
|
||||
/// </summary>
|
||||
[JsonPropertyName("total_tokens")]
|
||||
public int? TotalTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Breakdown of tokens used in the generated completion.
|
||||
/// </summary>
|
||||
[JsonPropertyName("completion_tokens_details")]
|
||||
public required CompletionTokensDetails CompletionTokensDetails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Breakdown of tokens used in the prompt.
|
||||
/// </summary>
|
||||
[JsonPropertyName("prompt_tokens_details")]
|
||||
public required PromptTokensDetails PromptTokensDetails { get; set; }
|
||||
|
||||
public static CompletionUsage operator +(CompletionUsage left, CompletionUsage right) => new()
|
||||
{
|
||||
CompletionTokens = left.CompletionTokens + right.CompletionTokens,
|
||||
PromptTokens = left.PromptTokens + right.PromptTokens,
|
||||
TotalTokens = left.TotalTokens + right.TotalTokens,
|
||||
CompletionTokensDetails = left.CompletionTokensDetails + right.CompletionTokensDetails,
|
||||
PromptTokensDetails = left.PromptTokensDetails + right.PromptTokensDetails
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Breakdown of tokens used in a completion.
|
||||
/// </summary>
|
||||
internal sealed record CompletionTokensDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// When using Predicted Outputs, the number of tokens in the prediction that appeared in the completion.
|
||||
/// </summary>
|
||||
[JsonPropertyName("accepted_prediction_tokens")]
|
||||
public int AcceptedPredictionTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Audio input tokens generated by the model.
|
||||
/// </summary>
|
||||
[JsonPropertyName("audio_tokens")]
|
||||
public int AudioTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tokens generated by the model for reasoning.
|
||||
/// </summary>
|
||||
[JsonPropertyName("reasoning_tokens")]
|
||||
public int ReasoningTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When using Predicted Outputs, the number of tokens in the prediction that did not appear in the completion.
|
||||
/// However, like reasoning tokens, these tokens are still counted in the total completion tokens for purposes of billing,
|
||||
/// output, and context window limits.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rejected_prediction_tokens")]
|
||||
public int RejectedPredictionTokens { get; set; }
|
||||
|
||||
public static CompletionTokensDetails operator +(CompletionTokensDetails left, CompletionTokensDetails right) => new()
|
||||
{
|
||||
AcceptedPredictionTokens = left.AcceptedPredictionTokens + right.AcceptedPredictionTokens,
|
||||
AudioTokens = left.AudioTokens + right.AudioTokens,
|
||||
ReasoningTokens = left.ReasoningTokens + right.ReasoningTokens,
|
||||
RejectedPredictionTokens = left.RejectedPredictionTokens + right.RejectedPredictionTokens
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Breakdown of tokens used in the prompt.
|
||||
/// </summary>
|
||||
internal sealed record PromptTokensDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// Audio input tokens present in the prompt.
|
||||
/// </summary>
|
||||
[JsonPropertyName("audio_tokens")]
|
||||
public int AudioTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cached tokens present in the prompt.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cached_tokens")]
|
||||
public int CachedTokens { get; set; }
|
||||
|
||||
public static PromptTokensDetails operator +(PromptTokensDetails left, PromptTokensDetails right) => new()
|
||||
{
|
||||
AudioTokens = left.AudioTokens + right.AudioTokens,
|
||||
CachedTokens = left.CachedTokens + right.CachedTokens
|
||||
};
|
||||
}
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Request to create a chat completion.
|
||||
/// </summary>
|
||||
internal sealed record CreateChatCompletion
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of messages comprising the conversation so far.
|
||||
/// </summary>
|
||||
[JsonPropertyName("messages")]
|
||||
[JsonRequired]
|
||||
public required IList<ChatCompletionRequestMessage> Messages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Model ID used to generate the response, like `gpt-4o` or `o3`.
|
||||
/// </summary>
|
||||
[JsonPropertyName("model")]
|
||||
[JsonRequired]
|
||||
public required string Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for audio output. Required when audio output is requested with modalities: ["audio"].
|
||||
/// </summary>
|
||||
[JsonPropertyName("audio")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public object? Audio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far.
|
||||
/// </summary>
|
||||
[JsonPropertyName("frequency_penalty")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public float? FrequencyPenalty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Deprecated in favor of tool_choice. Controls which (if any) function is called by the model.
|
||||
/// </summary>
|
||||
[JsonPropertyName("function_call")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[Obsolete("Deprecated in favor of ToolChoice.")]
|
||||
public object? FunctionCall { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Deprecated in favor of tools. A list of functions the model may generate JSON inputs for.
|
||||
/// </summary>
|
||||
[JsonPropertyName("functions")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[Obsolete("Deprecated in favor of Tools.")]
|
||||
public IList<object>? Functions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Modify the likelihood of specified tokens appearing in the completion.
|
||||
/// </summary>
|
||||
[JsonPropertyName("logit_bias")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public Dictionary<string, int>? LogitBias { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to return log probabilities of the output tokens or not.
|
||||
/// </summary>
|
||||
[JsonPropertyName("logprobs")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public bool? Logprobs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An upper bound for the number of tokens that can be generated for a completion, including visible output tokens and reasoning tokens.
|
||||
/// </summary>
|
||||
[JsonPropertyName("max_completion_tokens")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int? MaxCompletionTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of tokens that can be generated in the chat completion. (Deprecated in favor of max_completion_tokens)
|
||||
/// </summary>
|
||||
[JsonPropertyName("max_tokens")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[Obsolete("Use MaxCompletionTokens instead. This property is deprecated and not compatible with o-series models.")]
|
||||
public int? MaxTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional
|
||||
/// information about the object in a structured format, and querying for objects via API or the dashboard.
|
||||
/// Keys are strings with a maximum length of 64 characters. Values are strings with a maximum length of 512 characters.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public Dictionary<string, string>? Metadata { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Types of content modalities the model can output. Can include "text" and/or "audio".
|
||||
/// </summary>
|
||||
[JsonPropertyName("modalities")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public IList<string>? Modalities { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How many chat completion choices to generate for each input message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("n")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int? N { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to enable parallel function calling during tool use.
|
||||
/// </summary>
|
||||
[JsonPropertyName("parallel_tool_calls")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public bool? ParallelToolCalls { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for a Predicted Output, which can greatly improve response times when large parts of the model response are known ahead of time.
|
||||
/// </summary>
|
||||
[JsonPropertyName("prediction")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public object? Prediction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far.
|
||||
/// </summary>
|
||||
[JsonPropertyName("presence_penalty")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public float? PresencePenalty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used by OpenAI to cache responses for similar requests to optimize your cache hit rates.
|
||||
/// </summary>
|
||||
[JsonPropertyName("prompt_cache_key")]
|
||||
public string? PromptCacheKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The reasoning effort level for o-series models. Can be "low", "medium", or "high".
|
||||
/// </summary>
|
||||
[JsonPropertyName("reasoning_effort")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? ReasoningEffort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An object specifying the format that the model must output.
|
||||
/// </summary>
|
||||
[JsonPropertyName("response_format")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public ResponseFormat? ResponseFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies.
|
||||
/// The IDs should be a string that uniquely identifies each user. We recommend hashing their username or email address,
|
||||
/// in order to avoid sending us any identifying information.
|
||||
/// </summary>
|
||||
[JsonPropertyName("safety_identifier")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? SafetyIdentifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If specified, the system will make a best effort to sample deterministically.
|
||||
/// </summary>
|
||||
[JsonPropertyName("seed")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public long? Seed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the processing type used for serving the request.
|
||||
/// If set to 'auto', the request will be processed with the service tier configured in the Project settings.
|
||||
/// If set to 'default', the request will be processed with standard pricing and performance.
|
||||
/// If set to 'flex' or 'priority', the request will be processed with the corresponding service tier.
|
||||
/// Defaults to 'auto'.
|
||||
/// </summary>
|
||||
[JsonPropertyName("service_tier")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? ServiceTier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Up to 4 sequences where the API will stop generating further tokens.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stop")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public StopSequences? Stop { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to store the output of this chat completion request for use in model distillation or evals products.
|
||||
/// </summary>
|
||||
[JsonPropertyName("store")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public bool? Store { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set to true, the model response data will be streamed to the client using server-sent events.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stream")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public bool? Stream { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Options for streaming response. Only set this when you set stream: true.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stream_options")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public object? StreamOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random,
|
||||
/// while lower values like 0.2 will make it more focused and deterministic.
|
||||
/// We generally recommend altering this or top_p but not both. Defaults to 1.
|
||||
/// </summary>
|
||||
[JsonPropertyName("temperature")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public float? Temperature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controls which (if any) tool is called by the model.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tool_choice")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public ToolChoice? ToolChoice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of tools the model may call. Can include custom tools or function tools.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tools")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public IList<Tool>? Tools { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An integer between 0 and 20 specifying the number of most likely tokens to return at each token position.
|
||||
/// </summary>
|
||||
[JsonPropertyName("top_logprobs")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int? TopLogprobs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of
|
||||
/// the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.
|
||||
/// We generally recommend altering this or temperature but not both.
|
||||
/// </summary>
|
||||
[JsonPropertyName("top_p")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public float? TopP { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Level of detail in the model's output. Can be "standard" or "verbose".
|
||||
/// </summary>
|
||||
[JsonPropertyName("verbosity")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Verbosity { get; set; } = "medium";
|
||||
|
||||
/// <summary>
|
||||
/// Web search tool configuration for searching the web for relevant results.
|
||||
/// </summary>
|
||||
[JsonPropertyName("web_search_options")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public object? WebSearchOptions { get; set; }
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Content which is a part of <see cref="ChatCompletionRequestMessage"/>.
|
||||
/// Can be either a string, or a list of content parts
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(MessageContentJsonConverter))]
|
||||
internal sealed record MessageContent : IEquatable<MessageContent>
|
||||
{
|
||||
private MessageContent(string text)
|
||||
{
|
||||
this.Text = text ?? throw new ArgumentNullException(nameof(text));
|
||||
this.Contents = null;
|
||||
}
|
||||
|
||||
private MessageContent(IReadOnlyList<MessageContentPart> contents)
|
||||
{
|
||||
this.Contents = contents ?? throw new ArgumentNullException(nameof(contents));
|
||||
this.Text = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an MessageContent from a text string.
|
||||
/// </summary>
|
||||
public static MessageContent FromText(string text) => new(text);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an MessageContent from a list of MessageContentPart items.
|
||||
/// </summary>
|
||||
public static MessageContent FromContents(IReadOnlyList<MessageContentPart> contents) => new(contents);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an MessageContent from a list of MessageContentPart items.
|
||||
/// </summary>
|
||||
public static MessageContent FromContents(params MessageContentPart[] contents) => new(contents);
|
||||
|
||||
/// <summary>
|
||||
/// Implicit conversion from string to MessageContent.
|
||||
/// </summary>
|
||||
public static implicit operator MessageContent(string text) => FromText(text);
|
||||
|
||||
/// <summary>
|
||||
/// Implicit conversion from List to MessageContent.
|
||||
/// </summary>
|
||||
public static implicit operator MessageContent(List<MessageContentPart> contents) => FromContents(contents);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this content is text.
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(true, nameof(Text))]
|
||||
public bool IsText => this.Text is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this content is a list of ItemContent items.
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(true, nameof(Contents))]
|
||||
public bool IsContents => this.Contents is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text value, or null if this is not text content.
|
||||
/// </summary>
|
||||
public string? Text { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ItemContent items, or null if this is not a content list.
|
||||
/// </summary>
|
||||
public IReadOnlyList<MessageContentPart>? Contents { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(MessageContent? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Both text
|
||||
if (this.Text is not null && other.Text is not null)
|
||||
{
|
||||
return this.Text == other.Text;
|
||||
}
|
||||
|
||||
// Both contents
|
||||
if (this.Contents is not null
|
||||
&& other.Contents is not null
|
||||
&& this.Contents.Count == other.Contents.Count)
|
||||
{
|
||||
return this.Contents.SequenceEqual(other.Contents);
|
||||
}
|
||||
|
||||
// One is text, one is contents - not equal
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
if (this.Text is not null)
|
||||
{
|
||||
return this.Text.GetHashCode();
|
||||
}
|
||||
|
||||
if (this.Contents is not null)
|
||||
{
|
||||
return this.Contents.Count > 0 ? this.Contents[0].GetHashCode() : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON converter for <see cref="MessageContent"/>.
|
||||
/// </summary>
|
||||
internal sealed class MessageContentJsonConverter : JsonConverter<MessageContent>
|
||||
{
|
||||
public override MessageContent? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
// Check if it's a string
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var text = reader.GetString();
|
||||
return text is not null ? MessageContent.FromText(text) : null;
|
||||
}
|
||||
|
||||
// Check if it's an array of ItemContent
|
||||
if (reader.TokenType == JsonTokenType.StartArray)
|
||||
{
|
||||
var contents = JsonSerializer.Deserialize(ref reader, ChatCompletionsJsonContext.Default.IReadOnlyListMessageContentPart);
|
||||
return contents?.Count > 0
|
||||
? MessageContent.FromContents(contents)
|
||||
: MessageContent.FromText(string.Empty);
|
||||
}
|
||||
|
||||
throw new JsonException($"Unexpected token type for MessageContent: {reader.TokenType}");
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, MessageContent value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value.IsText)
|
||||
{
|
||||
writer.WriteStringValue(value.Text);
|
||||
}
|
||||
else if (value.IsContents)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, value.Contents, ChatCompletionsJsonContext.Default.IReadOnlyListMessageContentPart);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new JsonException("MessageContent has no value");
|
||||
}
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a part of message content in a chat completion request.
|
||||
/// Message content can be text, images, audio, or files.
|
||||
/// </summary>
|
||||
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]
|
||||
[JsonDerivedType(typeof(TextContentPart), "text")]
|
||||
[JsonDerivedType(typeof(ImageContentPart), "image_url")]
|
||||
[JsonDerivedType(typeof(AudioContentPart), "input_audio")]
|
||||
[JsonDerivedType(typeof(FileContentPart), "file")]
|
||||
internal abstract record MessageContentPart
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of the content.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public abstract string Type { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A text content part in a message.
|
||||
/// </summary>
|
||||
internal sealed record TextContentPart : MessageContentPart
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public override string Type => "text";
|
||||
|
||||
/// <summary>
|
||||
/// The text content.
|
||||
/// </summary>
|
||||
[JsonPropertyName("text")]
|
||||
public required string Text { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An image content part in a message.
|
||||
/// </summary>
|
||||
internal sealed record ImageContentPart : MessageContentPart
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public override string Type => "image_url";
|
||||
|
||||
/// <summary>
|
||||
/// Details about the image URL or base64-encoded image data.
|
||||
/// </summary>
|
||||
[JsonPropertyName("image_url")]
|
||||
public required ImageUrl ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URL or base64-encoded data of the image.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string UrlOrData => this.ImageUrl.Url;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URL of the image.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Uri Url => new(this.ImageUrl.Url);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Details about an image for vision-enabled models.
|
||||
/// </summary>
|
||||
internal sealed record ImageUrl
|
||||
{
|
||||
/// <summary>
|
||||
/// Either a URL of the image or the base64 encoded image data
|
||||
/// </summary>
|
||||
[JsonPropertyName("url")]
|
||||
public required string Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the detail level of the image
|
||||
/// </summary>
|
||||
[JsonPropertyName("detail")]
|
||||
public string? Detail { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An audio content part in a message.
|
||||
/// </summary>
|
||||
internal sealed record AudioContentPart : MessageContentPart
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public override string Type => "input_audio";
|
||||
|
||||
/// <summary>
|
||||
/// The input audio data.
|
||||
/// </summary>
|
||||
[JsonPropertyName("input_audio")]
|
||||
public required InputAudio InputAudio { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input audio data for audio-enabled models.
|
||||
/// </summary>
|
||||
internal sealed record InputAudio
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64 encoded audio data.
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
public required string Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The format of the encoded audio data. Currently supports "wav" and "mp3".
|
||||
/// </summary>
|
||||
[JsonPropertyName("format")]
|
||||
public required string Format { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A file content part in a message.
|
||||
/// </summary>
|
||||
internal sealed record FileContentPart : MessageContentPart
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public override string Type => "file";
|
||||
|
||||
/// <summary>
|
||||
/// The input file data.
|
||||
/// </summary>
|
||||
[JsonPropertyName("file")]
|
||||
public required InputFile File { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input file data for file-enabled models.
|
||||
/// </summary>
|
||||
internal sealed record InputFile
|
||||
{
|
||||
/// <summary>
|
||||
/// The base64 encoded file data, used when passing the file to the model as a string.
|
||||
/// </summary>
|
||||
[JsonPropertyName("file_data")]
|
||||
public string? FileData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of an uploaded file to use as input.
|
||||
/// </summary>
|
||||
[JsonPropertyName("file_id")]
|
||||
public string? FileId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the file, used when passing the file to the model as a string.
|
||||
/// </summary>
|
||||
[JsonPropertyName("filename")]
|
||||
public string? Filename { get; set; }
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the format that the model must output.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(ResponseFormatConverter))]
|
||||
internal sealed record ResponseFormat : IEquatable<ResponseFormat>
|
||||
{
|
||||
private ResponseFormat(TextResponseFormat text)
|
||||
{
|
||||
this.Text = text ?? throw new ArgumentNullException(nameof(text));
|
||||
this.JsonSchema = null;
|
||||
this.JsonObject = null;
|
||||
}
|
||||
|
||||
private ResponseFormat(JsonSchemaResponseFormat jsonSchema)
|
||||
{
|
||||
this.JsonSchema = jsonSchema ?? throw new ArgumentNullException(nameof(jsonSchema));
|
||||
this.Text = null;
|
||||
this.JsonObject = null;
|
||||
}
|
||||
|
||||
private ResponseFormat(JsonObjectResponseFormat jsonObject)
|
||||
{
|
||||
this.JsonObject = jsonObject ?? throw new ArgumentNullException(nameof(jsonObject));
|
||||
this.Text = null;
|
||||
this.JsonSchema = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ResponseFormat for text output (default).
|
||||
/// </summary>
|
||||
public static ResponseFormat FromText() => new(new TextResponseFormat());
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ResponseFormat for JSON Schema output with Structured Outputs.
|
||||
/// </summary>
|
||||
public static ResponseFormat FromJsonSchema(JsonSchemaResponseFormat jsonSchema) => new(jsonSchema);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ResponseFormat for JSON object output (older JSON mode).
|
||||
/// </summary>
|
||||
public static ResponseFormat FromJsonObject() => new(new JsonObjectResponseFormat());
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this is a text response format.
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(true, nameof(Text))]
|
||||
public bool IsText => this.Text is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this is a JSON schema response format.
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(true, nameof(JsonSchema))]
|
||||
public bool IsJsonSchema => this.JsonSchema is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this is a JSON object response format.
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(true, nameof(JsonObject))]
|
||||
public bool IsJsonObject => this.JsonObject is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text response format, or null if this is not a text format.
|
||||
/// </summary>
|
||||
public TextResponseFormat? Text { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the JSON schema response format, or null if this is not a JSON schema format.
|
||||
/// </summary>
|
||||
public JsonSchemaResponseFormat? JsonSchema { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the JSON object response format, or null if this is not a JSON object format.
|
||||
/// </summary>
|
||||
public JsonObjectResponseFormat? JsonObject { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(ResponseFormat? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.Text is not null && other.Text is not null)
|
||||
{
|
||||
return this.Text.Equals(other.Text);
|
||||
}
|
||||
|
||||
if (this.JsonSchema is not null && other.JsonSchema is not null)
|
||||
{
|
||||
return this.JsonSchema.Equals(other.JsonSchema);
|
||||
}
|
||||
|
||||
if (this.JsonObject is not null && other.JsonObject is not null)
|
||||
{
|
||||
return this.JsonObject.Equals(other.JsonObject);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
if (this.Text is not null)
|
||||
{
|
||||
return this.Text.GetHashCode();
|
||||
}
|
||||
|
||||
if (this.JsonSchema is not null)
|
||||
{
|
||||
return this.JsonSchema.GetHashCode();
|
||||
}
|
||||
|
||||
if (this.JsonObject is not null)
|
||||
{
|
||||
return this.JsonObject.GetHashCode();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Text response format. Default response format used to generate text responses.
|
||||
/// </summary>
|
||||
internal sealed record TextResponseFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of response format. Always "text".
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type => "text";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON Schema response format. Used to generate structured JSON responses with Structured Outputs.
|
||||
/// </summary>
|
||||
internal sealed record JsonSchemaResponseFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of response format. Always "json_schema".
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type => "json_schema";
|
||||
|
||||
/// <summary>
|
||||
/// Structured Outputs configuration options, including a JSON Schema.
|
||||
/// </summary>
|
||||
[JsonPropertyName("json_schema")]
|
||||
[JsonRequired]
|
||||
public required JsonSchemaConfiguration JsonSchema { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for JSON Schema Structured Outputs.
|
||||
/// </summary>
|
||||
internal sealed record JsonSchemaConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the schema.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
[JsonRequired]
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// A description of the schema.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The JSON Schema definition.
|
||||
/// </summary>
|
||||
[JsonPropertyName("schema")]
|
||||
[JsonRequired]
|
||||
public required JsonElement Schema { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to enable strict schema adherence.
|
||||
/// </summary>
|
||||
[JsonPropertyName("strict")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public bool? Strict { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON object response format. An older method of generating JSON responses.
|
||||
/// Using json_schema is recommended for models that support it.
|
||||
/// </summary>
|
||||
internal sealed record JsonObjectResponseFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of response format. Always "json_object".
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type => "json_object";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON converter for <see cref="ResponseFormat"/> that handles different response format types.
|
||||
/// </summary>
|
||||
internal sealed class ResponseFormatConverter : JsonConverter<ResponseFormat>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ResponseFormat? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonTokenType.StartObject)
|
||||
{
|
||||
using var doc = JsonDocument.ParseValue(ref reader);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.TryGetProperty("type", out var typeProperty))
|
||||
{
|
||||
var type = typeProperty.GetString();
|
||||
return type switch
|
||||
{
|
||||
"text" => ResponseFormat.FromText(),
|
||||
|
||||
"json_schema" => ResponseFormat.FromJsonSchema(
|
||||
JsonSerializer.Deserialize(root.GetRawText(), ChatCompletionsJsonContext.Default.JsonSchemaResponseFormat)!),
|
||||
|
||||
"json_object" => ResponseFormat.FromJsonObject(),
|
||||
|
||||
_ => throw new JsonException($"Unknown response format type: {type}")
|
||||
};
|
||||
}
|
||||
|
||||
throw new JsonException("Response format object must have a 'type' property.");
|
||||
}
|
||||
|
||||
throw new JsonException($"Unexpected token type '{reader.TokenType}' when deserializing ResponseFormat.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, ResponseFormat? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.IsText)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, value.Text, ChatCompletionsJsonContext.Default.TextResponseFormat);
|
||||
}
|
||||
else if (value.IsJsonSchema)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, value.JsonSchema, ChatCompletionsJsonContext.Default.JsonSchemaResponseFormat);
|
||||
}
|
||||
else if (value.IsJsonObject)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, value.JsonObject, ChatCompletionsJsonContext.Default.JsonObjectResponseFormat);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents stop sequences for chat completion generation.
|
||||
/// Up to 4 sequences where the API will stop generating further tokens.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(StopSequencesConverter))]
|
||||
internal sealed record StopSequences : IEquatable<StopSequences>
|
||||
{
|
||||
private StopSequences(string singleSequence)
|
||||
{
|
||||
this.SingleSequence = singleSequence ?? throw new ArgumentNullException(nameof(singleSequence));
|
||||
this.Sequences = null;
|
||||
}
|
||||
|
||||
private StopSequences(IList<string> sequences)
|
||||
{
|
||||
if (sequences is null || sequences.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("Sequences cannot be null or empty.", nameof(sequences));
|
||||
}
|
||||
|
||||
if (sequences.Count > 4)
|
||||
{
|
||||
throw new ArgumentException("Maximum of 4 stop sequences are allowed.", nameof(sequences));
|
||||
}
|
||||
|
||||
this.Sequences = sequences;
|
||||
this.SingleSequence = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a StopSequences from a single stop sequence string.
|
||||
/// </summary>
|
||||
public static StopSequences FromString(string sequence) => new(sequence);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a StopSequences from a list of stop sequences.
|
||||
/// </summary>
|
||||
public static StopSequences FromSequences(IList<string> sequences) => new(sequences);
|
||||
|
||||
/// <summary>
|
||||
/// Implicit conversion from string to StopSequences.
|
||||
/// </summary>
|
||||
public static implicit operator StopSequences(string sequence) => FromString(sequence);
|
||||
|
||||
/// <summary>
|
||||
/// Implicit conversion from string array to StopSequences.
|
||||
/// </summary>
|
||||
public static implicit operator StopSequences(string[] sequences) => FromSequences(sequences);
|
||||
|
||||
/// <summary>
|
||||
/// Implicit conversion from List to StopSequences.
|
||||
/// </summary>
|
||||
public static implicit operator StopSequences(List<string> sequences) => FromSequences(sequences);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this is a single stop sequence.
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(true, nameof(SingleSequence))]
|
||||
public bool IsSingleSequence => this.SingleSequence is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this contains multiple stop sequences.
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(true, nameof(Sequences))]
|
||||
public bool IsSequences => this.Sequences is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the single stop sequence, or null if this contains multiple sequences.
|
||||
/// </summary>
|
||||
public string? SingleSequence { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of stop sequences, or null if this is a single sequence.
|
||||
/// </summary>
|
||||
public IList<string>? Sequences { get; }
|
||||
|
||||
public IList<string> SequenceList =>
|
||||
this.IsSingleSequence ? [this.SingleSequence] :
|
||||
this.IsSequences ? this.Sequences : [];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(StopSequences? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Both single sequences
|
||||
if (this.SingleSequence is not null && other.SingleSequence is not null)
|
||||
{
|
||||
return this.SingleSequence == other.SingleSequence;
|
||||
}
|
||||
|
||||
// Both sequences
|
||||
if (this.Sequences is not null && other.Sequences is not null)
|
||||
{
|
||||
return this.Sequences.SequenceEqual(other.Sequences);
|
||||
}
|
||||
|
||||
// One is single, one is sequences - not equal
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
if (this.SingleSequence is not null)
|
||||
{
|
||||
return this.SingleSequence.GetHashCode();
|
||||
}
|
||||
|
||||
if (this.Sequences is not null)
|
||||
{
|
||||
return this.Sequences.Count > 0 ? this.Sequences[0].GetHashCode() : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON converter for <see cref="StopSequences"/> that handles string, array, and null representations.
|
||||
/// </summary>
|
||||
internal sealed class StopSequencesConverter : JsonConverter<StopSequences>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override StopSequences? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
// Handle null
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle single string
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
string? sequence = reader.GetString();
|
||||
return sequence is not null ? StopSequences.FromString(sequence) : null;
|
||||
}
|
||||
|
||||
// Handle array of strings
|
||||
if (reader.TokenType == JsonTokenType.StartArray)
|
||||
{
|
||||
var sequences = JsonSerializer.Deserialize(ref reader, ChatCompletionsJsonContext.Default.IListString);
|
||||
return sequences?.Count > 0
|
||||
? StopSequences.FromSequences(sequences)
|
||||
: StopSequences.FromString(string.Empty);
|
||||
}
|
||||
|
||||
throw new JsonException($"Unexpected token type '{reader.TokenType}' when deserializing StopSequences. Expected String, StartArray, or Null.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, StopSequences? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.IsSingleSequence)
|
||||
{
|
||||
writer.WriteStringValue(value.SingleSequence);
|
||||
}
|
||||
else if (value.IsSequences)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, value.Sequences, ChatCompletionsJsonContext.Default.IReadOnlyListMessageContentPart);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a tool that the model may call. Can be either a function tool or a custom tool.
|
||||
/// </summary>
|
||||
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
|
||||
[JsonDerivedType(typeof(FunctionTool), "function")]
|
||||
[JsonDerivedType(typeof(CustomTool), "custom")]
|
||||
internal abstract record Tool
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of the tool.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public abstract string Type { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A function tool that can be used to generate a response.
|
||||
/// </summary>
|
||||
internal sealed record FunctionTool : Tool
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of the tool. Always "function".
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public override string Type => "function";
|
||||
|
||||
/// <summary>
|
||||
/// The function definition.
|
||||
/// </summary>
|
||||
[JsonPropertyName("function")]
|
||||
[JsonRequired]
|
||||
public required FunctionDefinition Function { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Definition of a function that can be called by the model.
|
||||
/// </summary>
|
||||
internal sealed record FunctionDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the function to be called.
|
||||
/// Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
[JsonRequired]
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// A description of what the function does, used by the model to choose when and how to call the function.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The parameters the function accepts, described as a JSON Schema object.
|
||||
/// Omitting parameters defines a function with an empty parameter list.
|
||||
/// </summary>
|
||||
[JsonPropertyName("parameters")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public JsonElement? Parameters { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to enable strict schema adherence when generating the function call.
|
||||
/// If set to true, the model will follow the exact schema defined in the parameters field.
|
||||
/// Only a subset of JSON Schema is supported when strict is true.
|
||||
/// Defaults to false.
|
||||
/// </summary>
|
||||
[JsonPropertyName("strict")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public bool? Strict { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom tool that processes input using a specified format.
|
||||
/// </summary>
|
||||
internal sealed record CustomTool : Tool
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of the tool. Always "custom".
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public override string Type => "custom";
|
||||
|
||||
/// <summary>
|
||||
/// Properties of the custom tool.
|
||||
/// </summary>
|
||||
[JsonPropertyName("custom")]
|
||||
[JsonRequired]
|
||||
public required CustomToolProperties Custom { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A wrapper for MEAI <see cref="AITool"/>
|
||||
/// </summary>
|
||||
internal sealed class CustomAITool : AITool
|
||||
{
|
||||
public CustomAITool(string name, string? description, IReadOnlyDictionary<string, object?>? additionalProperties)
|
||||
: base()
|
||||
{
|
||||
this.Name = name;
|
||||
this.Description = description ?? string.Empty;
|
||||
this.AdditionalProperties = additionalProperties ?? new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
public override string Name { get; }
|
||||
public override string Description { get; }
|
||||
public override IReadOnlyDictionary<string, object?> AdditionalProperties { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Properties of a custom tool.
|
||||
/// </summary>
|
||||
internal sealed record CustomToolProperties
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the custom tool, used to identify it in tool calls.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
[JsonRequired]
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional description of the custom tool, used to provide more context.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The input format for the custom tool. Default is unconstrained text.
|
||||
/// </summary>
|
||||
[JsonPropertyName("format")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public CustomToolFormat? Format { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The input format for a custom tool.
|
||||
/// </summary>
|
||||
internal sealed record CustomToolFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of format. Can be various schema types.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional format properties (schema definition).
|
||||
/// </summary>
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, object?>? AdditionalProperties { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Controls which (if any) tool is called by the model.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(ToolChoiceConverter))]
|
||||
internal sealed record ToolChoice : IEquatable<ToolChoice>
|
||||
{
|
||||
private ToolChoice(string mode)
|
||||
{
|
||||
this.Mode = mode ?? throw new ArgumentNullException(nameof(mode));
|
||||
this.AllowedTools = null;
|
||||
this.FunctionTool = null;
|
||||
this.CustomTool = null;
|
||||
}
|
||||
|
||||
private ToolChoice(AllowedToolsChoice allowedTools)
|
||||
{
|
||||
this.AllowedTools = allowedTools ?? throw new ArgumentNullException(nameof(allowedTools));
|
||||
this.Mode = null;
|
||||
this.FunctionTool = null;
|
||||
this.CustomTool = null;
|
||||
}
|
||||
|
||||
private ToolChoice(FunctionToolChoice functionTool)
|
||||
{
|
||||
this.FunctionTool = functionTool ?? throw new ArgumentNullException(nameof(functionTool));
|
||||
this.Mode = null;
|
||||
this.AllowedTools = null;
|
||||
this.CustomTool = null;
|
||||
}
|
||||
|
||||
private ToolChoice(CustomToolChoice customTool)
|
||||
{
|
||||
this.CustomTool = customTool ?? throw new ArgumentNullException(nameof(customTool));
|
||||
this.Mode = null;
|
||||
this.AllowedTools = null;
|
||||
this.FunctionTool = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ToolChoice from a mode string ("none", "auto", or "required").
|
||||
/// </summary>
|
||||
public static ToolChoice FromMode(string mode) => new(mode);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ToolChoice that constrains tools to a pre-defined set.
|
||||
/// </summary>
|
||||
public static ToolChoice FromAllowedTools(AllowedToolsChoice allowedTools) => new(allowedTools);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ToolChoice that forces the model to call a specific function.
|
||||
/// </summary>
|
||||
public static ToolChoice FromFunction(FunctionToolChoice functionTool) => new(functionTool);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ToolChoice that forces the model to call a specific custom tool.
|
||||
/// </summary>
|
||||
public static ToolChoice FromCustom(CustomToolChoice customTool) => new(customTool);
|
||||
|
||||
/// <summary>
|
||||
/// Implicit conversion from string to ToolChoice.
|
||||
/// </summary>
|
||||
public static implicit operator ToolChoice(string mode) => FromMode(mode);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this is a mode string.
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(true, nameof(Mode))]
|
||||
public bool IsMode => this.Mode is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this is an allowed tools configuration.
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(true, nameof(AllowedTools))]
|
||||
public bool IsAllowedTools => this.AllowedTools is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this is a function tool choice.
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(true, nameof(FunctionTool))]
|
||||
public bool IsFunctionTool => this.FunctionTool is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this is a custom tool choice.
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(true, nameof(CustomTool))]
|
||||
public bool IsCustomTool => this.CustomTool is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mode string, or null if this is not a mode.
|
||||
/// </summary>
|
||||
public string? Mode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the allowed tools configuration, or null if this is not an allowed tools choice.
|
||||
/// </summary>
|
||||
public AllowedToolsChoice? AllowedTools { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the function tool choice, or null if this is not a function tool choice.
|
||||
/// </summary>
|
||||
public FunctionToolChoice? FunctionTool { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the custom tool choice, or null if this is not a custom tool choice.
|
||||
/// </summary>
|
||||
public CustomToolChoice? CustomTool { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(ToolChoice? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.Mode is not null && other.Mode is not null)
|
||||
{
|
||||
return this.Mode == other.Mode;
|
||||
}
|
||||
|
||||
if (this.AllowedTools is not null && other.AllowedTools is not null)
|
||||
{
|
||||
return this.AllowedTools.Equals(other.AllowedTools);
|
||||
}
|
||||
|
||||
if (this.FunctionTool is not null && other.FunctionTool is not null)
|
||||
{
|
||||
return this.FunctionTool.Equals(other.FunctionTool);
|
||||
}
|
||||
|
||||
if (this.CustomTool is not null && other.CustomTool is not null)
|
||||
{
|
||||
return this.CustomTool.Equals(other.CustomTool);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
if (this.Mode is not null)
|
||||
{
|
||||
return this.Mode.GetHashCode();
|
||||
}
|
||||
|
||||
if (this.AllowedTools is not null)
|
||||
{
|
||||
return this.AllowedTools.GetHashCode();
|
||||
}
|
||||
|
||||
if (this.FunctionTool is not null)
|
||||
{
|
||||
return this.FunctionTool.GetHashCode();
|
||||
}
|
||||
|
||||
if (this.CustomTool is not null)
|
||||
{
|
||||
return this.CustomTool.GetHashCode();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constrains the tools available to the model to a pre-defined set.
|
||||
/// </summary>
|
||||
internal sealed record AllowedToolsChoice
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of tool choice. Always "allowed_tools".
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type => "allowed_tools";
|
||||
|
||||
/// <summary>
|
||||
/// Constrains the tools available to the model to a pre-defined set.
|
||||
/// </summary>
|
||||
[JsonPropertyName("allowed_tools")]
|
||||
[JsonRequired]
|
||||
public required AllowedToolsConfiguration AllowedTools { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for allowed tools.
|
||||
/// </summary>
|
||||
internal sealed record AllowedToolsConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Constrains the tools available to the model to a pre-defined set.
|
||||
/// auto allows the model to pick from among the allowed tools and generate a message.
|
||||
/// required requires the model to call one or more of the allowed tools.
|
||||
/// </summary>
|
||||
[JsonPropertyName("mode")]
|
||||
[JsonRequired]
|
||||
public required string Mode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of tool definitions that the model should be allowed to call.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tools")]
|
||||
[JsonRequired]
|
||||
public required IList<ToolDefinition> Tools { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A tool definition in the allowed tools list.
|
||||
/// </summary>
|
||||
internal sealed record ToolDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of tool (e.g., "function" or "custom").
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
[JsonRequired]
|
||||
public required string Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The function details if type is "function".
|
||||
/// </summary>
|
||||
[JsonPropertyName("function")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public FunctionReference? Function { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A reference to a function by name.
|
||||
/// </summary>
|
||||
internal sealed record FunctionReference
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the function.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
[JsonRequired]
|
||||
public required string Name { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies a function tool the model should use.
|
||||
/// </summary>
|
||||
internal sealed record FunctionToolChoice
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of tool. Always "function".
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type => "function";
|
||||
|
||||
/// <summary>
|
||||
/// The function to call.
|
||||
/// </summary>
|
||||
[JsonPropertyName("function")]
|
||||
[JsonRequired]
|
||||
public required FunctionReference Function { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies a custom tool the model should use.
|
||||
/// </summary>
|
||||
internal sealed record CustomToolChoice
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of tool. Always "custom".
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type => "custom";
|
||||
|
||||
/// <summary>
|
||||
/// The custom tool configuration.
|
||||
/// </summary>
|
||||
[JsonPropertyName("custom")]
|
||||
[JsonRequired]
|
||||
public required CustomToolObject Custom { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A reference to a custom tool object.
|
||||
/// </summary>
|
||||
internal sealed record CustomToolObject
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the function.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
[JsonRequired]
|
||||
public required string Name { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON converter for <see cref="ToolChoice"/> that handles string and object representations.
|
||||
/// </summary>
|
||||
internal sealed class ToolChoiceConverter : JsonConverter<ToolChoice>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ToolChoice? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
string? mode = reader.GetString();
|
||||
return mode is not null ? ToolChoice.FromMode(mode) : null;
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonTokenType.StartObject)
|
||||
{
|
||||
using var doc = JsonDocument.ParseValue(ref reader);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.TryGetProperty("type", out var typeProperty))
|
||||
{
|
||||
var type = typeProperty.GetString();
|
||||
return type switch
|
||||
{
|
||||
"allowed_tools" => ToolChoice.FromAllowedTools(
|
||||
JsonSerializer.Deserialize(root.GetRawText(), ChatCompletionsJsonContext.Default.AllowedToolsChoice)!),
|
||||
|
||||
"function" => ToolChoice.FromFunction(
|
||||
JsonSerializer.Deserialize(root.GetRawText(), ChatCompletionsJsonContext.Default.FunctionToolChoice)!),
|
||||
|
||||
"custom" => ToolChoice.FromCustom(
|
||||
JsonSerializer.Deserialize(root.GetRawText(), ChatCompletionsJsonContext.Default.CustomToolChoice)!),
|
||||
|
||||
_ => throw new JsonException($"Unknown tool choice type: {type}")
|
||||
};
|
||||
}
|
||||
|
||||
throw new JsonException("Tool choice object must have a 'type' property.");
|
||||
}
|
||||
|
||||
throw new JsonException($"Unexpected token type '{reader.TokenType}' when deserializing ToolChoice.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, ToolChoice? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.IsMode)
|
||||
{
|
||||
writer.WriteStringValue(value.Mode);
|
||||
}
|
||||
else if (value.IsAllowedTools)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, value.AllowedTools, ChatCompletionsJsonContext.Default.AllowedToolsChoice);
|
||||
}
|
||||
else if (value.IsFunctionTool)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, value.FunctionTool, ChatCompletionsJsonContext.Default.FunctionToolChoice);
|
||||
}
|
||||
else if (value.IsCustomTool)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, value.CustomTool, ChatCompletionsJsonContext.Default.CustomToolChoice);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using OpenAI.Chat;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Utils;
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline", Justification = "Specifically for accessing hidden members")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations", Justification = "Specifically for accessing hidden members")]
|
||||
internal static class ChatCompletionsOptionsExtensions
|
||||
{
|
||||
private static readonly Func<ChatCompletionOptions, bool?> s_getStreamNullable;
|
||||
private static readonly Func<ChatCompletionOptions, IList<ChatMessage>> s_getMessages;
|
||||
|
||||
static ChatCompletionsOptionsExtensions()
|
||||
{
|
||||
// OpenAI SDK does not have a simple way to get the input as a c# object.
|
||||
// However, it does parse most of the interesting fields into internal properties of `ChatCompletionsOptions` object.
|
||||
|
||||
// --- Stream (internal bool? Stream { get; set; }) ---
|
||||
const string StreamPropName = "Stream";
|
||||
var streamProp = typeof(ChatCompletionOptions).GetProperty(StreamPropName, BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
?? throw new MissingMemberException(typeof(ChatCompletionOptions).FullName!, StreamPropName);
|
||||
var streamGetter = streamProp.GetGetMethod(nonPublic: true) ?? throw new MissingMethodException($"{StreamPropName} getter not found.");
|
||||
|
||||
s_getStreamNullable = streamGetter.CreateDelegate<Func<ChatCompletionOptions, bool?>>();
|
||||
|
||||
// --- Messages (internal IList<OpenAI.Chat.ChatMessage> Messages { get; set; }) ---
|
||||
const string InputPropName = "Messages";
|
||||
var inputProp = typeof(ChatCompletionOptions).GetProperty(InputPropName, BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
?? throw new MissingMemberException(typeof(ChatCompletionOptions).FullName!, InputPropName);
|
||||
var inputGetter = inputProp.GetGetMethod(nonPublic: true)
|
||||
?? throw new MissingMethodException($"{InputPropName} getter not found.");
|
||||
|
||||
s_getMessages = inputGetter.CreateDelegate<Func<ChatCompletionOptions, IList<ChatMessage>>>();
|
||||
}
|
||||
|
||||
public static IList<ChatMessage> GetMessages(this ChatCompletionOptions options)
|
||||
{
|
||||
Throw.IfNull(options);
|
||||
return s_getMessages(options);
|
||||
}
|
||||
|
||||
public static bool GetStream(this ChatCompletionOptions options)
|
||||
{
|
||||
Throw.IfNull(options);
|
||||
return s_getStreamNullable(options) ?? false;
|
||||
}
|
||||
}
|
||||
+44
-38
@@ -1,16 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.ClientModel.Primitives;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenAI.Chat;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder;
|
||||
|
||||
@@ -20,47 +19,54 @@ public static partial class MicrosoftAgentAIHostingOpenAIEndpointRouteBuilderExt
|
||||
/// Maps OpenAI ChatCompletions API endpoints to the specified <see cref="IEndpointRouteBuilder"/> for the given <see cref="AIAgent"/>.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the OpenAI ChatCompletions endpoints to.</param>
|
||||
/// <param name="agentName">The name of the AI agent service registered in the dependency injection container. This name is used to resolve the <see cref="AIAgent"/> instance from the keyed services.</param>
|
||||
/// <param name="agentBuilder">The builder for <see cref="AIAgent"/> to map the OpenAI ChatCompletions endpoints for.</param>
|
||||
public static IEndpointConventionBuilder MapOpenAIChatCompletions(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder)
|
||||
=> MapOpenAIChatCompletions(endpoints, agentBuilder, path: null);
|
||||
|
||||
/// <summary>
|
||||
/// Maps OpenAI ChatCompletions API endpoints to the specified <see cref="IEndpointRouteBuilder"/> for the given <see cref="AIAgent"/>.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the OpenAI ChatCompletions endpoints to.</param>
|
||||
/// <param name="agentBuilder">The builder for <see cref="AIAgent"/> to map the OpenAI ChatCompletions endpoints for.</param>
|
||||
/// <param name="path">Custom route path for the chat completions endpoint.</param>
|
||||
public static void MapOpenAIChatCompletions(
|
||||
public static IEndpointConventionBuilder MapOpenAIChatCompletions(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string? path)
|
||||
{
|
||||
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentBuilder.Name);
|
||||
return MapOpenAIChatCompletions(endpoints, agent, path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps OpenAI ChatCompletions API endpoints to the specified <see cref="IEndpointRouteBuilder"/> for the given <see cref="AIAgent"/>.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the OpenAI ChatCompletions endpoints to.</param>
|
||||
/// <param name="agent">The <see cref="AIAgent"/> instance to map the OpenAI ChatCompletions endpoints for.</param>
|
||||
public static IEndpointConventionBuilder MapOpenAIChatCompletions(this IEndpointRouteBuilder endpoints, AIAgent agent)
|
||||
=> MapOpenAIChatCompletions(endpoints, agent, path: null);
|
||||
|
||||
/// <summary>
|
||||
/// Maps OpenAI ChatCompletions API endpoints to the specified <see cref="IEndpointRouteBuilder"/> for the given <see cref="AIAgent"/>.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the OpenAI ChatCompletions endpoints to.</param>
|
||||
/// <param name="agent">The <see cref="AIAgent"/> instance to map the OpenAI ChatCompletions endpoints for.</param>
|
||||
/// <param name="path">Custom route path for the chat completions endpoint.</param>
|
||||
public static IEndpointConventionBuilder MapOpenAIChatCompletions(
|
||||
this IEndpointRouteBuilder endpoints,
|
||||
string agentName,
|
||||
[StringSyntax("Route")] string? path = null)
|
||||
AIAgent agent,
|
||||
[StringSyntax("Route")] string? path)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
ArgumentNullException.ThrowIfNull(agentName);
|
||||
if (path is null)
|
||||
{
|
||||
ValidateAgentName(agentName);
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(agent);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(agent.Name, nameof(agent.Name));
|
||||
ValidateAgentName(agent.Name);
|
||||
|
||||
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
|
||||
|
||||
path ??= $"/{agentName}/v1/chat/completions";
|
||||
var chatCompletionsRouteGroup = endpoints.MapGroup(path);
|
||||
MapChatCompletions(chatCompletionsRouteGroup, agent);
|
||||
}
|
||||
|
||||
private static void MapChatCompletions(IEndpointRouteBuilder routeGroup, AIAgent agent)
|
||||
{
|
||||
path ??= $"/{agent.Name}/v1/chat/completions";
|
||||
var group = endpoints.MapGroup(path);
|
||||
var endpointAgentName = agent.DisplayName;
|
||||
var chatCompletionsProcessor = new AIAgentChatCompletionsProcessor(agent);
|
||||
|
||||
routeGroup.MapPost("/", async (HttpContext requestContext, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var requestBinary = await BinaryData.FromStreamAsync(requestContext.Request.Body, cancellationToken).ConfigureAwait(false);
|
||||
group.MapPost("/", async ([FromBody] CreateChatCompletion request, CancellationToken cancellationToken)
|
||||
=> await AIAgentChatCompletionsProcessor.CreateChatCompletionAsync(agent, request, cancellationToken).ConfigureAwait(false))
|
||||
.WithName(endpointAgentName + "/CreateChatCompletion");
|
||||
|
||||
var chatCompletionOptions = new ChatCompletionOptions();
|
||||
var chatCompletionOptionsJsonModel = chatCompletionOptions as IJsonModel<ChatCompletionOptions>;
|
||||
Debug.Assert(chatCompletionOptionsJsonModel is not null);
|
||||
|
||||
chatCompletionOptions = chatCompletionOptionsJsonModel.Create(requestBinary, ModelReaderWriterOptions.Json);
|
||||
if (chatCompletionOptions is null)
|
||||
{
|
||||
return Results.BadRequest("Invalid request payload.");
|
||||
}
|
||||
|
||||
return await chatCompletionsProcessor.CreateChatCompletionAsync(chatCompletionOptions, cancellationToken).ConfigureAwait(false);
|
||||
}).WithName(endpointAgentName + "/CreateChatCompletion");
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,24 @@ using Microsoft.Extensions.Hosting;
|
||||
namespace Microsoft.Extensions.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IHostApplicationBuilder"/> to configure OpenAI Responses support.
|
||||
/// Extension methods for <see cref="IHostApplicationBuilder"/> to configure OpenAI support.
|
||||
/// </summary>
|
||||
public static class MicrosoftAgentAIHostingOpenAIHostApplicationBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds support for exposing <see cref="AIAgent"/> instances via OpenAI ChatCompletions.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHostApplicationBuilder"/> to configure.</param>
|
||||
/// <returns>The <see cref="IHostApplicationBuilder"/> for method chaining.</returns>
|
||||
public static IHostApplicationBuilder AddOpenAIChatCompletions(this IHostApplicationBuilder builder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
builder.Services.AddOpenAIChatCompletions();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds support for exposing <see cref="AIAgent"/> instances via OpenAI Responses.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Shared helpers to generate IDs.
|
||||
/// </summary>
|
||||
internal static partial class IdGeneratorHelpers
|
||||
{
|
||||
#if NET9_0_OR_GREATER
|
||||
[GeneratedRegex("^[A-Za-z0-9]+$")]
|
||||
private static partial Regex WatermarkRegex();
|
||||
#else
|
||||
private static readonly Regex s_watermarkRegex = new("^[A-Za-z0-9]+$", RegexOptions.Compiled);
|
||||
private static Regex WatermarkRegex() => s_watermarkRegex;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Generates a new ID with a structured format that includes a partition key.
|
||||
/// </summary>
|
||||
/// <param name="prefix">The prefix to add to the ID, typically indicating the resource type.</param>
|
||||
/// <param name="stringLength">The length of the random entropy string in the ID.</param>
|
||||
/// <param name="partitionKeyLength">The length of the partition key if generating a new one.</param>
|
||||
/// <param name="infix">Optional additional text to insert between the prefix and the entropy.</param>
|
||||
/// <param name="watermark">Optional text to insert in the middle of the entropy string for traceability.</param>
|
||||
/// <param name="delimiter">The delimiter character used to separate parts of the ID.</param>
|
||||
/// <param name="partitionKey">An explicit partition key to use. When provided, this value will be used instead of generating a new one.</param>
|
||||
/// <param name="partitionKeyHint">An existing ID to extract the partition key from. When provided, the same partition key will be used instead of generating a new one.</param>
|
||||
/// <returns>A new ID with format "{prefix}{delimiter}{infix}{entropy}{delimiter}{partitionKey}".</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the watermark contains non-alphanumeric characters.</exception>
|
||||
public static string NewId(string prefix, int stringLength = 32, int partitionKeyLength = 16, string infix = "",
|
||||
string watermark = "", string delimiter = "_", string? partitionKey = null, string partitionKeyHint = "")
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(stringLength, 1);
|
||||
var entropy = GetRandomString(stringLength);
|
||||
|
||||
string pKey = partitionKey ?? GetPartitionIdOrDefault(partitionKeyHint) ?? GetRandomString(partitionKeyLength);
|
||||
|
||||
if (!string.IsNullOrEmpty(watermark))
|
||||
{
|
||||
if (!WatermarkRegex().IsMatch(watermark))
|
||||
{
|
||||
throw new ArgumentException($"Only alphanumeric characters may be in watermark: {watermark}",
|
||||
nameof(watermark));
|
||||
}
|
||||
|
||||
entropy = $"{entropy[..(stringLength / 2)]}{watermark}{entropy[(stringLength / 2)..]}";
|
||||
}
|
||||
|
||||
infix ??= "";
|
||||
prefix = !string.IsNullOrEmpty(prefix) ? $"{prefix}{delimiter}" : "";
|
||||
return $"{prefix}{infix}{entropy}{pKey}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a secure random alphanumeric string of the specified length.
|
||||
/// </summary>
|
||||
/// <param name="stringLength">The desired length of the random string.</param>
|
||||
/// <returns>A random alphanumeric string.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when stringLength is less than 1.</exception>
|
||||
public static string GetRandomString(int stringLength) =>
|
||||
RandomNumberGenerator.GetString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", stringLength);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the partition key from an existing ID, or returns null if extraction fails.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID to extract the partition key from.</param>
|
||||
/// <param name="stringLength">The length of the random entropy string in the ID.</param>
|
||||
/// <param name="partitionKeyLength">The length of the partition key if generating a new one.</param>
|
||||
/// <param name="delimiter">The delimiter character used in the ID.</param>
|
||||
/// <returns>The partition key if successfully extracted; otherwise, null.</returns>
|
||||
public static string? GetPartitionIdOrDefault(string? id, int stringLength = 32, int partitionKeyLength = 16,
|
||||
string delimiter = "_")
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var parts = id.Split([delimiter], StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length < 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parts[1].Length < stringLength + partitionKeyLength)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// get last partitionKeyLength characters from the last part as the partition key
|
||||
return parts[1][^partitionKeyLength..];
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
|
||||
@@ -14,14 +11,6 @@ internal sealed partial class IdGenerator
|
||||
{
|
||||
private readonly string _partitionId;
|
||||
|
||||
#if NET9_0_OR_GREATER
|
||||
[GeneratedRegex("^[A-Za-z0-9]+$")]
|
||||
private static partial Regex WatermarkRegex();
|
||||
#else
|
||||
private static readonly Regex s_watermarkRegex = new("^[A-Za-z0-9]+$", RegexOptions.Compiled);
|
||||
private static Regex WatermarkRegex() => s_watermarkRegex;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IdGenerator"/> class.
|
||||
/// </summary>
|
||||
@@ -29,9 +18,9 @@ internal sealed partial class IdGenerator
|
||||
/// <param name="conversationId">The conversation ID.</param>
|
||||
public IdGenerator(string? responseId, string? conversationId)
|
||||
{
|
||||
this.ResponseId = responseId ?? NewId("resp");
|
||||
this.ConversationId = conversationId ?? NewId("conv");
|
||||
this._partitionId = GetPartitionIdOrDefault(this.ConversationId) ?? string.Empty;
|
||||
this.ResponseId = responseId ?? IdGeneratorHelpers.NewId("resp");
|
||||
this.ConversationId = conversationId ?? IdGeneratorHelpers.NewId("conv");
|
||||
this._partitionId = IdGeneratorHelpers.GetPartitionIdOrDefault(this.ConversationId) ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -64,7 +53,7 @@ internal sealed partial class IdGenerator
|
||||
public string Generate(string? category = null)
|
||||
{
|
||||
var prefix = string.IsNullOrEmpty(category) ? "id" : category;
|
||||
return NewId(prefix, partitionKey: this._partitionId);
|
||||
return IdGeneratorHelpers.NewId(prefix, partitionKey: this._partitionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -90,81 +79,4 @@ internal sealed partial class IdGenerator
|
||||
/// </summary>
|
||||
/// <returns>A reasoning ID.</returns>
|
||||
public string GenerateReasoningId() => this.Generate("rs");
|
||||
|
||||
/// <summary>
|
||||
/// Generates a new ID with a structured format that includes a partition key.
|
||||
/// </summary>
|
||||
/// <param name="prefix">The prefix to add to the ID, typically indicating the resource type.</param>
|
||||
/// <param name="stringLength">The length of the random entropy string in the ID.</param>
|
||||
/// <param name="partitionKeyLength">The length of the partition key if generating a new one.</param>
|
||||
/// <param name="infix">Optional additional text to insert between the prefix and the entropy.</param>
|
||||
/// <param name="watermark">Optional text to insert in the middle of the entropy string for traceability.</param>
|
||||
/// <param name="delimiter">The delimiter character used to separate parts of the ID.</param>
|
||||
/// <param name="partitionKey">An explicit partition key to use. When provided, this value will be used instead of generating a new one.</param>
|
||||
/// <param name="partitionKeyHint">An existing ID to extract the partition key from. When provided, the same partition key will be used instead of generating a new one.</param>
|
||||
/// <returns>A new ID with format "{prefix}{delimiter}{infix}{entropy}{delimiter}{partitionKey}".</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the watermark contains non-alphanumeric characters.</exception>
|
||||
private static string NewId(string prefix, int stringLength = 32, int partitionKeyLength = 16, string infix = "",
|
||||
string watermark = "", string delimiter = "_", string? partitionKey = null, string partitionKeyHint = "")
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(stringLength, 1);
|
||||
var entropy = GetRandomString(stringLength);
|
||||
|
||||
string pKey = partitionKey ?? GetPartitionIdOrDefault(partitionKeyHint) ?? GetRandomString(partitionKeyLength);
|
||||
|
||||
if (!string.IsNullOrEmpty(watermark))
|
||||
{
|
||||
if (!WatermarkRegex().IsMatch(watermark))
|
||||
{
|
||||
throw new ArgumentException($"Only alphanumeric characters may be in watermark: {watermark}",
|
||||
nameof(watermark));
|
||||
}
|
||||
|
||||
entropy = $"{entropy[..(stringLength / 2)]}{watermark}{entropy[(stringLength / 2)..]}";
|
||||
}
|
||||
|
||||
infix ??= "";
|
||||
prefix = !string.IsNullOrEmpty(prefix) ? $"{prefix}{delimiter}" : "";
|
||||
return $"{prefix}{infix}{entropy}{pKey}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a secure random alphanumeric string of the specified length.
|
||||
/// </summary>
|
||||
/// <param name="stringLength">The desired length of the random string.</param>
|
||||
/// <returns>A random alphanumeric string.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when stringLength is less than 1.</exception>
|
||||
private static string GetRandomString(int stringLength) =>
|
||||
RandomNumberGenerator.GetString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", stringLength);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the partition key from an existing ID, or returns null if extraction fails.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID to extract the partition key from.</param>
|
||||
/// <param name="stringLength">The length of the random entropy string in the ID.</param>
|
||||
/// <param name="partitionKeyLength">The length of the partition key if generating a new one.</param>
|
||||
/// <param name="delimiter">The delimiter character used in the ID.</param>
|
||||
/// <returns>The partition key if successfully extracted; otherwise, null.</returns>
|
||||
private static string? GetPartitionIdOrDefault(string? id, int stringLength = 32, int partitionKeyLength = 16,
|
||||
string delimiter = "_")
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var parts = id.Split([delimiter], StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length < 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parts[1].Length < stringLength + partitionKeyLength)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// get last partitionKeyLength characters from the last part as the partition key
|
||||
return parts[1][^partitionKeyLength..];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,31 @@
|
||||
|
||||
using System;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.Responses;
|
||||
using Microsoft.AspNetCore.Http.Json;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IServiceCollection"/> to configure OpenAI Responses support.
|
||||
/// Extension methods for <see cref="IServiceCollection"/> to configure OpenAI support.
|
||||
/// </summary>
|
||||
public static class MicrosoftAgentAIHostingOpenAIServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds support for exposing <see cref="AIAgent"/> instances via OpenAI ChatCompletions.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to configure.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> for method chaining.</returns>
|
||||
public static IServiceCollection AddOpenAIChatCompletions(this IServiceCollection services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
services.Configure<JsonOptions>(options => options.SerializerOptions.TypeInfoResolverChain.Add(ChatCompletionsJsonSerializerOptions.Default.TypeInfoResolver!));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds support for exposing <see cref="AIAgent"/> instances via OpenAI Responses.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user