Implements IAnswerModel over Anthropic's streaming Messages API: SSE parsing (text_delta/usage/error), 429/529 retry with Retry-After or exponential backoff, and a pure AnswerPrompt.Build for the evidence and judgements prompt shape. Health check hits GET /v1/models. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
64 lines
2.4 KiB
C#
64 lines
2.4 KiB
C#
using Wiki.Core.Model;
|
|
using Wiki.Core.Ports;
|
|
|
|
namespace Wiki.Contracts;
|
|
|
|
/// <summary>
|
|
/// Every <see cref="IAnswerModel"/> adapter derives from this. The adapter under test may talk to
|
|
/// a fake server that echoes the evidence back; the contract checks the streaming shape, the
|
|
/// usage record and that the model sees nothing but the request.
|
|
/// </summary>
|
|
public abstract class AnswerModelContract
|
|
{
|
|
protected abstract Task<IAnswerModel> CreateModelAsync();
|
|
|
|
protected static AnswerRequest Request() => new(
|
|
"Does the UDM intercept DNS?",
|
|
[
|
|
new EvidenceParagraph(new Paragraph("udm-dns#p1", "udm-dns", "UDM intercepts DNS", "The UDM answers every UDP/53 query itself."), "UDM intercepts DNS", 0.84),
|
|
new EvidenceParagraph(new Paragraph("udm-dns#p2", "udm-dns", "Details", "Even dig @1.1.1.1 is answered locally."), "UDM intercepts DNS", 0.41),
|
|
],
|
|
new Dictionary<string, Judgement> { ["answered_1"] = new Judgement.Noul(0.93) },
|
|
0.88);
|
|
|
|
[Fact]
|
|
public async Task StreamsTextThenExactlyOneDoneWithUsage()
|
|
{
|
|
var model = await CreateModelAsync();
|
|
var chunks = new List<AnswerChunk>();
|
|
await foreach (var c in model.AnswerAsync(Request(), CancellationToken.None))
|
|
chunks.Add(c);
|
|
|
|
Assert.NotEmpty(chunks);
|
|
Assert.IsType<AnswerChunk.Done>(chunks[^1]);
|
|
Assert.Single(chunks.OfType<AnswerChunk.Done>());
|
|
var text = string.Concat(chunks.OfType<AnswerChunk.Text>().Select(t => t.Delta));
|
|
Assert.False(string.IsNullOrWhiteSpace(text));
|
|
var done = (AnswerChunk.Done)chunks[^1];
|
|
Assert.Equal(model.ModelId, done.ModelId);
|
|
Assert.True(done.InputTokens > 0);
|
|
Assert.True(done.OutputTokens > 0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CancellationStopsTheStream()
|
|
{
|
|
var model = await CreateModelAsync();
|
|
using var cts = new CancellationTokenSource();
|
|
cts.Cancel();
|
|
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
|
|
{
|
|
await foreach (var _ in model.AnswerAsync(Request(), cts.Token)) { }
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ModelIdAndHealth()
|
|
{
|
|
var model = await CreateModelAsync();
|
|
Assert.False(string.IsNullOrWhiteSpace(model.ModelId));
|
|
var health = await model.CheckAsync(CancellationToken.None);
|
|
Assert.True(health.Ok, health.Detail);
|
|
}
|
|
}
|