forked from admin/VolcanicAgenticCore
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.
53 lines
2.6 KiB
C#
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();
|
|
}
|