Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Converters;
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
using Microsoft.Agents.AI.Hosting.OpenAI.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.AI;
Expand All @@ -18,27 +19,48 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions;

internal static class AIAgentChatCompletionsProcessor
{
public static async Task<IResult> CreateChatCompletionAsync(AIAgent agent, CreateChatCompletion request, CancellationToken cancellationToken)
public static async Task<IResult> CreateChatCompletionAsync(AIAgent agent, CreateChatCompletion request, OpenAIChatCompletionsMapOptions? mapOptions, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(agent);

var runOptionsFactory = (mapOptions ?? new OpenAIChatCompletionsMapOptions()).RunOptionsFactory;

AgentRunOptions? runOptions;
try
{
// The hosting developer controls, via OpenAIChatCompletionsMapOptions.RunOptionsFactory, which (if any)
// request settings are mapped onto the agent run. By default no request setting is mapped.
runOptions = runOptionsFactory(request.ToRequestInfo());
}
catch (NotSupportedException ex)
{
return Results.BadRequest(new ErrorResponse
{
Error = new ErrorDetails
{
Message = ex.Message,
Type = "invalid_request_error",
Code = "unsupported_parameter"
}
});
}

var chatMessages = request.Messages.Select(i => i.ToChatMessage());
var chatClientAgentRunOptions = request.BuildOptions();

if (request.Stream == true)
{
return new StreamingResponse(agent, request, chatMessages, chatClientAgentRunOptions);
return new StreamingResponse(agent, request, chatMessages, runOptions);
}

var response = await agent.RunAsync(chatMessages, options: chatClientAgentRunOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
var response = await agent.RunAsync(chatMessages, options: runOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
return Results.Ok(response.ToChatCompletion(request));
}

private sealed class StreamingResponse(
AIAgent agent,
CreateChatCompletion request,
IEnumerable<ChatMessage> chatMessages,
ChatClientAgentRunOptions? options) : IResult
AgentRunOptions? options) : IResult
{
public Task ExecuteAsync(HttpContext httpContext)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,24 @@ internal static class ChatClientAgentRunOptionsConverter
{
private static readonly JsonElement s_emptyJson = JsonElement.Parse("{}");

public static ChatClientAgentRunOptions BuildOptions(this CreateChatCompletion request)
/// <summary>
/// Projects the request-supplied generation and tool settings into a public
/// <see cref="OpenAIChatCompletionRequestInfo"/> for use by a hosting-developer mapping callback.
/// </summary>
public static OpenAIChatCompletionRequestInfo ToRequestInfo(this CreateChatCompletion request) => new()
{
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
};
}
Temperature = request.Temperature,
TopP = request.TopP,
MaxOutputTokens = request.MaxCompletionTokens,
FrequencyPenalty = request.FrequencyPenalty,
PresencePenalty = request.PresencePenalty,
Seed = request.Seed,
StopSequences = request.Stop?.SequenceList is { Count: > 0 } sequences ? [.. sequences] : null,
ResponseFormat = request.ResponseFormat?.ToChatResponseFormat(),
Model = request.Model,
ToolChoice = request.ToolChoice?.ToChatToolMode(),
Tools = request.Tools is { Count: > 0 } tools ? tools.Select(x => x.ToAITool()).ToList() : null,
};

private static ChatResponseFormat ToChatResponseFormat(this ResponseFormat responseFormat)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI.Hosting.OpenAI;
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions;
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -29,10 +30,11 @@ public static IEndpointConventionBuilder MapOpenAIChatCompletions(this IEndpoint
/// <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 IEndpointConventionBuilder MapOpenAIChatCompletions(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string? path)
/// <param name="mapOptions">Optional options controlling how incoming requests are mapped onto the agent run.</param>
public static IEndpointConventionBuilder MapOpenAIChatCompletions(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string? path, OpenAIChatCompletionsMapOptions? mapOptions = null)
Comment thread
westey-m marked this conversation as resolved.
{
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentBuilder.Name);
return MapOpenAIChatCompletions(endpoints, agent, path);
return MapOpenAIChatCompletions(endpoints, agent, path, mapOptions);
}

/// <summary>
Expand All @@ -49,10 +51,12 @@ public static IEndpointConventionBuilder MapOpenAIChatCompletions(this IEndpoint
/// <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>
/// <param name="mapOptions">Optional options controlling how incoming requests are mapped onto the agent run.</param>
public static IEndpointConventionBuilder MapOpenAIChatCompletions(
this IEndpointRouteBuilder endpoints,
AIAgent agent,
[StringSyntax("Route")] string? path)
[StringSyntax("Route")] string? path,
OpenAIChatCompletionsMapOptions? mapOptions = null)
{
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentNullException.ThrowIfNull(agent);
Expand All @@ -64,7 +68,7 @@ public static IEndpointConventionBuilder MapOpenAIChatCompletions(
var endpointAgentName = agent.Name ?? agent.Id;

group.MapPost("/", async ([FromBody] CreateChatCompletion request, CancellationToken cancellationToken)
=> await AIAgentChatCompletionsProcessor.CreateChatCompletionAsync(agent, request, cancellationToken).ConfigureAwait(false))
=> await AIAgentChatCompletionsProcessor.CreateChatCompletionAsync(agent, request, mapOptions, cancellationToken).ConfigureAwait(false))
.WithName(endpointAgentName + "/CreateChatCompletion");

return group;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ public static IEndpointConventionBuilder MapOpenAIResponses(this IEndpointRouteB
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the OpenAI Responses endpoints to.</param>
/// <param name="agentBuilder">The builder for <see cref="AIAgent"/> to map the OpenAI Responses endpoints for.</param>
/// <param name="path">Custom route path for the OpenAI Responses endpoint.</param>
public static IEndpointConventionBuilder MapOpenAIResponses(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string? path)
/// <param name="mapOptions">Optional options controlling how incoming requests are mapped onto the agent run.</param>
public static IEndpointConventionBuilder MapOpenAIResponses(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string? path, OpenAIResponsesMapOptions? mapOptions = null)
Comment thread
westey-m marked this conversation as resolved.
{
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentNullException.ThrowIfNull(agentBuilder);

var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentBuilder.Name);
return MapOpenAIResponses(endpoints, agent, path);
return MapOpenAIResponses(endpoints, agent, path, mapOptions);
}

/// <summary>
Expand All @@ -55,10 +56,12 @@ public static IEndpointConventionBuilder MapOpenAIResponses(this IEndpointRouteB
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the OpenAI Responses endpoints to.</param>
/// <param name="agent">The <see cref="AIAgent"/> instance to map the OpenAI Responses endpoints for.</param>
/// <param name="responsesPath">Custom route path for the responses endpoint.</param>
/// <param name="mapOptions">Optional options controlling how incoming requests are mapped onto the agent run.</param>
public static IEndpointConventionBuilder MapOpenAIResponses(
this IEndpointRouteBuilder endpoints,
AIAgent agent,
[StringSyntax("Route")] string? responsesPath)
[StringSyntax("Route")] string? responsesPath,
OpenAIResponsesMapOptions? mapOptions = null)
{
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentNullException.ThrowIfNull(agent);
Expand All @@ -68,7 +71,7 @@ public static IEndpointConventionBuilder MapOpenAIResponses(
responsesPath ??= $"/{agent.Name}/v1/responses";

// Create an executor for this agent
var executor = new AIAgentResponseExecutor(agent);
var executor = new AIAgentResponseExecutor(agent, mapOptions);
var storageOptions = endpoints.ServiceProvider.GetService<InMemoryStorageOptions>() ?? new InMemoryStorageOptions();
var conversationStorage = endpoints.ServiceProvider.GetService<IConversationStorage>();
var responsesService = new InMemoryResponsesService(executor, storageOptions, conversationStorage);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using Microsoft.Extensions.AI;

namespace Microsoft.Agents.AI.Hosting.OpenAI;

/// <summary>
/// Exposes the request-supplied generation and tool settings of an OpenAI ChatCompletions
/// <c>create chat completion</c> request that a hosting developer may choose to map onto the
/// <see cref="AgentRunOptions"/> used to run the target <see cref="AIAgent"/>.
/// </summary>
/// <remarks>
/// <para>
/// This type is passed to <see cref="OpenAIChatCompletionsMapOptions.RunOptionsFactory"/>. By default no
/// request setting is mapped onto the agent, because an agent is typically self-contained and
/// allowing callers to override its configuration (for example which tools it may invoke) can cause
/// it to behave in ways its author did not intend.
/// </para>
/// <para>
/// Tool and response-format settings are surfaced using their <c>Microsoft.Extensions.AI</c>
/// equivalents so that a hosting developer can map them directly without re-parsing the wire format.
/// </para>
/// </remarks>
public sealed class OpenAIChatCompletionRequestInfo
{
/// <summary>
/// Gets or sets the sampling temperature supplied on the request, if any.
/// </summary>
public float? Temperature { get; set; }

/// <summary>
/// Gets or sets the nucleus sampling value (<c>top_p</c>) supplied on the request, if any.
/// </summary>
public float? TopP { get; set; }

/// <summary>
/// Gets or sets the maximum number of completion tokens (<c>max_completion_tokens</c>) supplied on the request, if any.
/// </summary>
public int? MaxOutputTokens { get; set; }

/// <summary>
/// Gets or sets the frequency penalty supplied on the request, if any.
/// </summary>
public float? FrequencyPenalty { get; set; }

/// <summary>
/// Gets or sets the presence penalty supplied on the request, if any.
/// </summary>
public float? PresencePenalty { get; set; }

/// <summary>
/// Gets or sets the deterministic sampling seed supplied on the request, if any.
/// </summary>
public long? Seed { get; set; }

/// <summary>
/// Gets or sets the stop sequences supplied on the request, if any.
/// </summary>
public IReadOnlyList<string>? StopSequences { get; set; }

/// <summary>
/// Gets or sets the response format supplied on the request, if any.
/// </summary>
public ChatResponseFormat? ResponseFormat { get; set; }

/// <summary>
/// Gets or sets the model identifier supplied on the request, if any.
/// </summary>
/// <remarks>
/// This value is informational. It is not applied to local agent execution (the agent runs with
/// its own <see cref="IChatClient"/>), and the OpenAI ChatCompletions
/// wire format requires it on every request, so it is intentionally excluded from the default
/// <see cref="OpenAIChatCompletionsMapOptions.RejectRequestSettings"/> rejection.
/// </remarks>
public string? Model { get; set; }

/// <summary>
/// Gets or sets the tool selection mode (<c>tool_choice</c>) supplied on the request, if any.
/// </summary>
public ChatToolMode? ToolChoice { get; set; }

/// <summary>
/// Gets or sets the tools supplied on the request, if any.
/// </summary>
public IReadOnlyList<AITool>? Tools { get; set; }
}
Loading
Loading