Files
Playa/tests/Volcanic.AgenticCore.Extensions.Tests/OpenAI/FakeHttpHandler.cs
EugeneTes 89623e14c2 feat(openai): chat completions provider extension
Streams POST {baseUrl}/chat/completions over SSE (System.Net.ServerSentEvents, no NuGet) and maps delta.content, delta.reasoning_content / delta.reasoning and delta.tool_calls onto AssistantMessageEvents via AssistantMessageBuilder. Configurable through OpenAIProviderOptions so one extension serves OpenAI, OpenRouter, Ollama and other compatible servers; ships the gpt-5 / gpt-5-mini / gpt-4.1 catalogue.
2026-09-25 12:45:43 +00:00

53 lines
2.6 KiB
C#

using System.Net;
using System.Text;
using System.Text.Json.Nodes;
namespace Volcanic.AgenticCore.Extensions.Tests.OpenAI;
/// <summary>Captures every request and answers with a scripted response.</summary>
internal sealed class FakeHttpHandler(Func<HttpRequestMessage, HttpResponseMessage> respond) : HttpMessageHandler
{
public List<CapturedRequest> Requests { get; } = [];
public CapturedRequest Single => Assert.Single(Requests);
/// <summary>A 200 <c>text/event-stream</c> response whose body is <c>data:</c> lines for each chunk followed by <c>[DONE]</c>.</summary>
public static FakeHttpHandler Sse(params string[] chunks) => Sse(HttpStatusCode.OK, chunks);
public static FakeHttpHandler Sse(HttpStatusCode status, params string[] chunks) =>
new(_ => new HttpResponseMessage(status) { Content = new StringContent(SseBody(chunks), Encoding.UTF8, "text/event-stream") });
/// <summary>A raw SSE body without the trailing <c>[DONE]</c>.</summary>
public static FakeHttpHandler RawSse(string body) =>
new(_ => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(body, Encoding.UTF8, "text/event-stream") });
public static FakeHttpHandler Json(HttpStatusCode status, string body) =>
new(_ => new HttpResponseMessage(status) { Content = new StringContent(body, Encoding.UTF8, "application/json") });
public static string SseBody(IEnumerable<string> chunks)
{
var sb = new StringBuilder();
foreach (var chunk in chunks) sb.Append("data: ").Append(chunk).Append("\n\n");
sb.Append("data: [DONE]\n\n");
return sb.ToString();
}
public HttpClient CreateClient() => new(this) { Timeout = Timeout.InfiniteTimeSpan };
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var body = request.Content is null ? null : await request.Content.ReadAsStringAsync(cancellationToken);
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var (name, values) in request.Headers) headers[name] = string.Join(", ", values);
if (request.Content is not null)
foreach (var (name, values) in request.Content.Headers) headers[name] = string.Join(", ", values);
Requests.Add(new CapturedRequest(request.Method, request.RequestUri!, headers, body));
return respond(request);
}
}
internal sealed record CapturedRequest(HttpMethod Method, Uri Uri, IReadOnlyDictionary<string, string> Headers, string? Body)
{
public JsonObject Json => JsonNode.Parse(Body ?? "{}")!.AsObject();
}