AI-Studio/app/Tests/Models/Hosting/ModelHostIndexTests.cs

187 lines
7.4 KiB
C#
Raw Normal View History

using AIStudio.Models;
using AIStudio.Models.Hosting;
using AIStudio.Models.Matching;
using AIStudio.Provider;
namespace AIStudio.Tests.Models.Hosting;
/// <summary>
/// Checks the walk which takes a name apart, and what happens when nobody wrote a host.
/// </summary>
/// <remarks>
/// How deep a wrapping goes is the host's business, not the caller's: Hugging Face has two, Fireworks
/// has three, most have none. Asking over and over until the host says no is what covers all of
/// them, and what has to be bounded so that a host which never says no cannot hang the app.
/// </remarks>
[TestFixture]
public sealed class ModelHostIndexTests
{
[Test]
public void TheHostsAreKeptInTheOrderOfTheProvidersTheyAnswerFor()
{
var index = ModelHostIndex.Build([new SplittingHost(), new StubbornHost()]);
Assert.That(index.Hosts.Select(host => host.Provider), Is.EqualTo(new[] { LLMProviders.OPEN_ROUTER, LLMProviders.LITE_LLM }));
}
[Test]
public void TwoHostsForOneProviderIsRefused()
{
var refused = Assert.Throws<InvalidOperationException>(() => ModelHostIndex.Build([new SplittingHost(), new SecondHostForTheSameProvider()]));
Assert.That(refused?.Message, Does.Contain("OPEN_ROUTER"));
}
[Test]
public void AHostAnsweringForNoProviderIsRefused()
{
//
// The default value of the provider enum is NONE, so a host which gets this wrong gets it
// wrong quietly: it would sit in the index answering for a provider nobody can configure.
//
var refused = Assert.Throws<InvalidOperationException>(() => ModelHostIndex.Build([new HostForNobody()]));
Assert.That(refused?.Message, Does.Contain(nameof(HostForNobody)));
}
[Test]
public void ProvidersNobodyWroteAHostForAreNamed()
{
var index = ModelHostIndex.Build([new SplittingHost()]);
Assert.Multiple(() =>
{
Assert.That(index.ProvidersWithoutAHost, Does.Contain(LLMProviders.ANTHROPIC));
Assert.That(index.ProvidersWithoutAHost, Does.Not.Contain(LLMProviders.OPEN_ROUTER));
Assert.That(index.ProvidersWithoutAHost, Does.Not.Contain(LLMProviders.NONE), "Nobody can configure it, so nobody has to write a host for it.");
});
}
[Test]
public void AProviderWithoutAHostGetsItsNameBackUntouched()
{
var index = ModelHostIndex.Build([new SplittingHost()]);
var unwrapped = index.Unwrap(new ModelId("anthropic/claude-opus-5"), LLMProviders.ANTHROPIC, out var vendor);
Assert.Multiple(() =>
{
Assert.That(index.Of(LLMProviders.ANTHROPIC), Is.Null);
Assert.That(unwrapped.Original, Is.EqualTo("anthropic/claude-opus-5"));
Assert.That(vendor, Is.Null);
});
}
[Test]
public void AProviderWithoutAHostStillLosesTheResponsesApi()
{
//
// The safe direction: claiming an API which is not there turns into a failed request, while
// not claiming one only means the app does not use it.
//
var index = ModelHostIndex.Build([new SplittingHost()]);
var profile = new ModelProfile { Capabilities = Capability.TEXT_INPUT | Capability.RESPONSES_API };
var throughTheProvider = index.ApplyTransport(profile, LLMProviders.ANTHROPIC);
Assert.Multiple(() =>
{
Assert.That(throughTheProvider.Has(Capability.RESPONSES_API), Is.False);
Assert.That(throughTheProvider.Has(Capability.CHAT_COMPLETION_API), Is.True);
});
}
[Test]
public void TheWalkKeepsAskingUntilTheHostSaysNo()
{
var index = ModelHostIndex.Build([new SplittingHost()]);
var unwrapped = index.Unwrap(new ModelId("accounts/fireworks/models/llama-v3p1-405b-instruct"), LLMProviders.OPEN_ROUTER, out _);
Assert.That(unwrapped.Original, Is.EqualTo("llama-v3p1-405b-instruct"));
}
[Test]
public void TheInnermostWrappingIsTheOneWhichSaysWhoBuiltTheModel()
{
var index = ModelHostIndex.Build([new SplittingHost()]);
index.Unwrap(new ModelId("anthropic/openai/gpt-5"), LLMProviders.OPEN_ROUTER, out var vendor);
Assert.That(vendor, Is.EqualTo(ModelVendor.OPEN_AI), "A wrapping closer to the model knows more about it than one further out.");
}
[Test]
public void AWrappingWhichSaysNothingDoesNotEraseWhatAnOuterOneSaid()
{
var index = ModelHostIndex.Build([new SplittingHost()]);
index.Unwrap(new ModelId("anthropic/somebody-else/claude-opus-5"), LLMProviders.OPEN_ROUTER, out var vendor);
Assert.That(vendor, Is.EqualTo(ModelVendor.ANTHROPIC));
}
[Test]
public void AHostHandingBackWhatItWasGivenIsNotAskedAgain()
{
var index = ModelHostIndex.Build([new StubbornHost()]);
var unwrapped = index.Unwrap(new ModelId("the-fast-one"), LLMProviders.LITE_LLM, out _);
Assert.That(unwrapped.Original, Is.EqualTo("the-fast-one"));
}
[Test]
public void AHostWhichNeverSaysNoIsStoppedRatherThanFollowedForever()
{
var index = ModelHostIndex.Build([new GrowingHost()]);
var unwrapped = index.Unwrap(new ModelId("thing"), LLMProviders.GROQ, out _);
Assert.That(unwrapped.Original.Split("-more"), Has.Length.EqualTo(ModelHostIndex.MAX_UNWRAPPING_STEPS + 1));
}
private sealed class SplittingHost : ModelHost
{
public override LLMProviders Provider => LLMProviders.OPEN_ROUTER;
public override ModelSource Source => new("https://example.invalid/splitting", new DateOnly(2026, 9, 11), "A host taking off one organization at a time.");
public override bool TryUnwrap(in ModelId id, out ModelId inner, out ModelVendor? declaredVendor) => HostNaming.TrySplitOrganization(id, out inner, out declaredVendor);
}
private sealed class SecondHostForTheSameProvider : ModelHost
{
public override LLMProviders Provider => LLMProviders.OPEN_ROUTER;
public override ModelSource Source => new("https://example.invalid/second", new DateOnly(2026, 9, 11), "A second host claiming a provider which already has one.");
}
private sealed class HostForNobody : ModelHost
{
public override LLMProviders Provider => LLMProviders.NONE;
public override ModelSource Source => new("https://example.invalid/nobody", new DateOnly(2026, 9, 11), "A host which names no provider.");
}
private sealed class StubbornHost : ModelHost
{
public override LLMProviders Provider => LLMProviders.LITE_LLM;
public override ModelSource Source => new("https://example.invalid/stubborn", new DateOnly(2026, 9, 11), "A host saying it unwrapped something without shortening anything.");
public override bool TryUnwrap(in ModelId id, out ModelId inner, out ModelVendor? declaredVendor)
{
inner = id;
declaredVendor = null;
return true;
}
}
private sealed class GrowingHost : ModelHost
{
public override LLMProviders Provider => LLMProviders.GROQ;
public override ModelSource Source => new("https://example.invalid/growing", new DateOnly(2026, 9, 11), "A host handing back a longer name every time it is asked.");
public override bool TryUnwrap(in ModelId id, out ModelId inner, out ModelVendor? declaredVendor)
{
inner = new($"{id.Original}-more");
declaredVendor = null;
return true;
}
}
}