mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-14 20:03:36 +00:00
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
187 lines
7.4 KiB
C#
187 lines
7.4 KiB
C#
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;
|
|
}
|
|
}
|
|
} |