mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-12 20:33:48 +00:00
Rebuilt how AI Studio knows what a model can do (#960)
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
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
This commit is contained in:
1 parent
d21e09dd1e
commit
d85b4e71b6
287 files changed
+18341
-3677
No files matched your search
@@ -0,0 +1,188 @@
|
||||
using AIStudio.Models;
|
||||
using AIStudio.Models.Live;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tests.Models.Corpus;
|
||||
|
||||
namespace AIStudio.Tests.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Checks the door the app asks its question through.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Behind it stand the links of the chain in the order they win: what a person said about their own
|
||||
/// installation, then what that installation reported about itself, then what the rules worked out,
|
||||
/// then what the app assumes. The rules themselves are measured elsewhere, against the whole corpus.
|
||||
/// What is measured here is everything around them -- the last link, which nothing held to account
|
||||
/// until now because a model falling through looked exactly like a model nobody had asked about,
|
||||
/// and the order of the three links above it, each of which can speak about the same number.
|
||||
///
|
||||
/// What a provider reported lands in the store the app shares, so these tests must not run next to
|
||||
/// anything else touching it.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
[NonParallelizable]
|
||||
public sealed class ModelProfileChainTests
|
||||
{
|
||||
private const string MACHINE = "33333333-3333-3333-3333-333333333333";
|
||||
|
||||
/// <summary>
|
||||
/// A window no rule would ever state, so that finding it proves where the answer came from.
|
||||
/// </summary>
|
||||
private const int WHAT_THE_MACHINE_REPORTS = 33_333;
|
||||
|
||||
private static readonly Model MODEL = new("qwen3-32b", null);
|
||||
|
||||
[SetUp]
|
||||
public void ForgetWhatTheMachineSaidBefore() => ListedModels.Shared.Report(MACHINE, []);
|
||||
|
||||
[Test]
|
||||
public void EveryModelLeftToTheDefaultIsAnsweredByTheAssumption()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var left in LeftToTheDefault.ENTRIES)
|
||||
{
|
||||
var profile = left.Provider.GetModelProfile(new Model(left.ModelId, null));
|
||||
var wanted = left.Provider is LLMProviders.NONE || string.IsNullOrWhiteSpace(left.ModelId)
|
||||
? Capability.NONE
|
||||
: ModelProfile.ASSUMED.Capabilities;
|
||||
|
||||
Assert.That(profile.Capabilities, Is.EqualTo(wanted), $"{left.Provider} \"{left.ModelId}\": {left.Reason}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AModelNoRuleKnowsReadsAndWritesTextAndCallsFunctions()
|
||||
{
|
||||
var profile = LLMProviders.SELF_HOSTED.GetModelProfile(new Model("a-model-nobody-has-heard-of", null));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(profile.Has(Capability.TEXT_INPUT), Is.True);
|
||||
Assert.That(profile.Has(Capability.TEXT_OUTPUT), Is.True);
|
||||
Assert.That(profile.Has(Capability.CHAT_COMPLETION_API), Is.True);
|
||||
Assert.That(profile.Has(Capability.FUNCTION_CALLING), Is.True);
|
||||
Assert.That(profile.Has(Capability.MULTIPLE_IMAGE_INPUT), Is.False, "The assumption says nothing about what a model reads besides text.");
|
||||
Assert.That(profile.Reasoning, Is.EqualTo(ReasoningSupport.NONE));
|
||||
Assert.That(profile.Context.IsKnown, Is.False, "A context window nobody stated is unknown, not a number somebody picked.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AModelWhoseKindIsKnownKeepsItWhenTheAssumptionFillsInTheRest()
|
||||
{
|
||||
//
|
||||
// The assumption fills in the capabilities and nothing else. An embedding model nobody wrote
|
||||
// a rule for is still an embedding model, and must not turn into a chat model on the way
|
||||
// through -- it would appear in the user's chat model list and answer every request with an
|
||||
// error.
|
||||
//
|
||||
var profile = LLMProviders.SELF_HOSTED.GetModelProfile(new Model("bge-m3:567m", null));
|
||||
|
||||
Assert.That(profile.Kind, Is.EqualTo(ModelKind.EMBEDDING));
|
||||
}
|
||||
|
||||
[TestCase("")]
|
||||
[TestCase(" ")]
|
||||
public void AProviderWhichNamedNoModelIsAnsweredWithNothing(string modelId)
|
||||
{
|
||||
var profile = LLMProviders.OPEN_AI.GetModelProfile(new Model(modelId, null));
|
||||
|
||||
Assert.That(profile.Capabilities, Is.EqualTo(Capability.NONE), "There is nothing to assume about a model nobody picked.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WithoutAProviderThereIsNothingToAssumeEither()
|
||||
{
|
||||
var profile = LLMProviders.NONE.GetModelProfile(new Model("gpt-5.6", null));
|
||||
|
||||
Assert.That(profile.Capabilities, Is.EqualTo(Capability.NONE), "There is no way to reach the model, so there is nothing to say about how it could be used.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhatAPersonSaidAboutTheirOwnInstallationWinsOverTheRules()
|
||||
{
|
||||
var configured = new AIStudio.Settings.Provider(0, "test", "Test", LLMProviders.SELF_HOSTED, new Model("llama3.3:70b", null))
|
||||
{
|
||||
CapabilityOverrides = new() { MultipleImageInput = true, FunctionCalling = false },
|
||||
};
|
||||
|
||||
var profile = configured.GetModelProfile();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(profile.Has(Capability.MULTIPLE_IMAGE_INPUT), Is.True, "The rules say this model reads text only; the person says otherwise and can see their installation.");
|
||||
Assert.That(profile.Has(Capability.FUNCTION_CALLING), Is.False, "And the other way round.");
|
||||
Assert.That(profile.Has(Capability.TEXT_INPUT), Is.True, "Everything nobody said anything about stays as the rules had it.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhatThePersonTypedBeatsWhatTheMachineReported()
|
||||
{
|
||||
//
|
||||
// Somebody who types a window has a reason for it, and the app is not in a position to know
|
||||
// it better -- they may be working around an engine reporting nonsense.
|
||||
//
|
||||
ListedModels.Shared.Report(MACHINE, [new(MODEL.Id, ContextWindow.Of(WHAT_THE_MACHINE_REPORTS))]);
|
||||
var configured = ProviderWith(new() { ContextWindowTokens = 8_192 });
|
||||
|
||||
Assert.That(configured.GetModelProfile().Context.DefaultTokens, Is.EqualTo(8_192));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhatTheMachineReportedBeatsWhatTheRulesWorkedOut()
|
||||
{
|
||||
ListedModels.Shared.Report(MACHINE, [new(MODEL.Id, ContextWindow.Of(WHAT_THE_MACHINE_REPORTS))]);
|
||||
var configured = ProviderWith(null);
|
||||
|
||||
Assert.That(configured.GetModelProfile().Context.DefaultTokens, Is.EqualTo(WHAT_THE_MACHINE_REPORTS));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ASilentMachineLeavesTheRulesStanding()
|
||||
{
|
||||
var configured = ProviderWith(null);
|
||||
|
||||
Assert.That(configured.GetModelProfile().Context, Is.EqualTo(LLMProviders.SELF_HOSTED.GetModelProfile(MODEL).Context));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheAutomaticAnswerIsWhatHappensWithoutTheSwitches()
|
||||
{
|
||||
//
|
||||
// This is the number the expert dialog offers as its placeholder. Showing the rules there
|
||||
// while the chat goes by the reported window would tell a person that emptying the field
|
||||
// gets them something it does not.
|
||||
//
|
||||
ListedModels.Shared.Report(MACHINE, [new(MODEL.Id, ContextWindow.Of(WHAT_THE_MACHINE_REPORTS))]);
|
||||
var configured = ProviderWith(new() { ContextWindowTokens = 8_192 });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(configured.GetAutomaticModelProfile().Context.DefaultTokens, Is.EqualTo(WHAT_THE_MACHINE_REPORTS));
|
||||
Assert.That(configured.GetModelProfile().Context.DefaultTokens, Is.EqualTo(8_192), "What the person typed is still what counts everywhere else.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhatOneMachineReportsIsNoAnswerForAnother()
|
||||
{
|
||||
ListedModels.Shared.Report(MACHINE, [new(MODEL.Id, ContextWindow.Of(WHAT_THE_MACHINE_REPORTS))]);
|
||||
var somebodyElse = ProviderWith(null) with { Id = "44444444-4444-4444-4444-444444444444" };
|
||||
|
||||
Assert.That(somebodyElse.GetModelProfile().Context, Is.EqualTo(LLMProviders.SELF_HOSTED.GetModelProfile(MODEL).Context));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A configured self-hosted provider, the way the settings hold one.
|
||||
/// </summary>
|
||||
/// <param name="overrides">What the person switched, or nothing when they switched nothing.</param>
|
||||
/// <returns>The configured provider.</returns>
|
||||
private static AIStudio.Settings.Provider ProviderWith(ProviderCapabilityOverrides? overrides) => new(1, MACHINE, "A machine of my own", LLMProviders.SELF_HOSTED, MODEL, IsSelfHosted: true)
|
||||
{
|
||||
CapabilityOverrides = overrides,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using AIStudio.Models;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Tests.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Checks what a person's own settings do to what the rules worked out.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The expert dialog writes the three reasoning words together, in five combinations. Those five
|
||||
/// are the whole surface the app produces, so each of them is stated below with the one thing it
|
||||
/// means -- whatever the rules said about the model, because that is what choosing from a list of
|
||||
/// five does.
|
||||
///
|
||||
/// These used to be measured against the repair they replaced, by running both and comparing. That
|
||||
/// comparison is gone with the repair itself: keeping a dead implementation alive so a test can ask
|
||||
/// it questions makes the test the only reason it still exists, and the next reader cannot tell
|
||||
/// which of the two is the real one. What it guaranteed is written out instead.
|
||||
///
|
||||
/// A configuration plugin can write the three words one at a time, and there the two differed on
|
||||
/// purpose. The repair took a word away unless another one stood next to it, so an override about
|
||||
/// something else destroyed an answer nobody had touched. Those cases are stated below, one by one,
|
||||
/// with what they answer now.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class ProviderCapabilityOverridesTests
|
||||
{
|
||||
private static readonly ReasoningSupport[] EVERY_STATE = [ReasoningSupport.NONE, ReasoningSupport.OPTIONAL, ReasoningSupport.ON_BY_DEFAULT, ReasoningSupport.ALWAYS];
|
||||
|
||||
/// <summary>
|
||||
/// The four combinations the expert dialog writes, in the order its list shows them, and the
|
||||
/// one state each of them means.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// "Automatic" is the fifth choice and is not among them. It means the person said nothing, and
|
||||
/// a provider carrying nothing but nothing is saved without an override record at all, so it
|
||||
/// never reaches here -- which is exactly why the defect below went unnoticed for so long: it
|
||||
/// needed a second, unrelated switch to become visible.
|
||||
/// </remarks>
|
||||
private static readonly (ProviderCapabilityOverrides Overrides, ReasoningSupport Means)[] WHAT_THE_DIALOG_WRITES =
|
||||
[
|
||||
(new() { AlwaysReasoning = false, OptionalReasoning = false, ReasoningByDefault = false }, ReasoningSupport.NONE),
|
||||
(new() { AlwaysReasoning = false, OptionalReasoning = true, ReasoningByDefault = false }, ReasoningSupport.OPTIONAL),
|
||||
(new() { AlwaysReasoning = false, OptionalReasoning = true, ReasoningByDefault = true }, ReasoningSupport.ON_BY_DEFAULT),
|
||||
(new() { AlwaysReasoning = true, OptionalReasoning = false, ReasoningByDefault = false }, ReasoningSupport.ALWAYS),
|
||||
];
|
||||
|
||||
[Test]
|
||||
public void EveryChoiceTheExpertDialogOffersMeansOneStateAndNothingElse()
|
||||
{
|
||||
//
|
||||
// Whatever the rules said about the model is beside the point here: somebody picked one of
|
||||
// five entries from a list, and each entry says outright how this model reasons. That is
|
||||
// also what makes these four the cheapest guard there is against somebody rearranging the
|
||||
// resolution below them.
|
||||
//
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var (overrides, means) in WHAT_THE_DIALOG_WRITES)
|
||||
foreach (var stated in EVERY_STATE)
|
||||
Assert.That(overrides.ApplyTo(ProfileWhichReasons(stated)).Reasoning, Is.EqualTo(means), $"A model which reasons {stated}, with {Describe(overrides)}.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnOverrideAboutSomethingElseLeavesTheThinkingAlone()
|
||||
{
|
||||
//
|
||||
// The defect this replaced. Turning tool calling off said nothing about reasoning, and yet
|
||||
// a model which thinks unless asked not to came out of it as a model which never thinks --
|
||||
// because the repair kept "on by default" only where "on request" stood next to it, which
|
||||
// no rule has ever stated. It cannot come back through this door: the answer is one value
|
||||
// now, and the combination the repair existed for cannot be written down any more.
|
||||
//
|
||||
var overrides = new ProviderCapabilityOverrides { FunctionCalling = false };
|
||||
|
||||
Assert.That(overrides.ApplyTo(ProfileWhichReasons(ReasoningSupport.ON_BY_DEFAULT)).Reasoning, Is.EqualTo(ReasoningSupport.ON_BY_DEFAULT));
|
||||
}
|
||||
|
||||
[TestCase(ReasoningSupport.ALWAYS, ReasoningSupport.ALWAYS, Description = "Saying it is not on by default says nothing about a model which cannot turn it off.")]
|
||||
[TestCase(ReasoningSupport.ON_BY_DEFAULT, ReasoningSupport.NONE, Description = "Here it names the state the model is in.")]
|
||||
[TestCase(ReasoningSupport.OPTIONAL, ReasoningSupport.OPTIONAL, Description = "A model which reasons on request was never on by default.")]
|
||||
public void ANoOnlyTakesAwayTheStateItNames(ReasoningSupport stated, ReasoningSupport wanted)
|
||||
{
|
||||
var overrides = new ProviderCapabilityOverrides { ReasoningByDefault = false };
|
||||
|
||||
Assert.That(overrides.ApplyTo(ProfileWhichReasons(stated)).Reasoning, Is.EqualTo(wanted));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AYesIsTheWholeAnswer()
|
||||
{
|
||||
var alwaysOn = new ProviderCapabilityOverrides { AlwaysReasoning = true, OptionalReasoning = false, ReasoningByDefault = false };
|
||||
|
||||
Assert.That(alwaysOn.ApplyTo(ProfileWhichReasons(ReasoningSupport.NONE)).Reasoning, Is.EqualTo(ReasoningSupport.ALWAYS));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SwitchingACapabilityOnAndOffTouchesNothingElse()
|
||||
{
|
||||
var profile = new ModelProfile
|
||||
{
|
||||
Capabilities = Capability.TEXT_INPUT | Capability.TEXT_OUTPUT | Capability.SINGLE_IMAGE_INPUT | Capability.FUNCTION_CALLING,
|
||||
Kind = ModelKind.CHAT,
|
||||
Context = ContextWindow.Of(128_000),
|
||||
};
|
||||
|
||||
var overrides = new ProviderCapabilityOverrides { FunctionCalling = false, AudioInput = true };
|
||||
var after = overrides.ApplyTo(profile);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(after.Has(Capability.FUNCTION_CALLING), Is.False);
|
||||
Assert.That(after.Has(Capability.AUDIO_INPUT), Is.True);
|
||||
Assert.That(after.Has(Capability.TEXT_INPUT), Is.True);
|
||||
Assert.That(after.Has(Capability.SINGLE_IMAGE_INPUT), Is.True, "Turning several images off is what removes several images; one image is a statement of its own.");
|
||||
Assert.That(after.Context, Is.EqualTo(profile.Context));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A profile which reasons the given way and says nothing else.
|
||||
/// </summary>
|
||||
/// <param name="reasoning">How the model reasons.</param>
|
||||
/// <returns>The profile.</returns>
|
||||
private static ModelProfile ProfileWhichReasons(ReasoningSupport reasoning) => new()
|
||||
{
|
||||
Capabilities = Capability.TEXT_INPUT | Capability.TEXT_OUTPUT,
|
||||
Reasoning = reasoning,
|
||||
};
|
||||
|
||||
private static string Describe(ProviderCapabilityOverrides overrides) => $"always={overrides.AlwaysReasoning?.ToString() ?? "auto"}, optional={overrides.OptionalReasoning?.ToString() ?? "auto"}, byDefault={overrides.ReasoningByDefault?.ToString() ?? "auto"}";
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Models;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Lua;
|
||||
using Lua.Standard;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace AIStudio.Tests.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Checks what a person's own numbers do to what the rules worked out.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Three surfaces write these numbers and all three are checked here, because a number which
|
||||
/// survives one of them and is lost by another is worse than no number at all: the expert dialog
|
||||
/// writes the record, an organization writes a Lua table, and both end up in a settings file which
|
||||
/// has to be read back the way it was written.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class ProviderNumberOverridesTests
|
||||
{
|
||||
private static readonly Guid PLUGIN_ID = new("22222222-2222-2222-2222-222222222222");
|
||||
|
||||
/// <summary>
|
||||
/// A model the rules have a lot to say about, so that an override has something to contradict.
|
||||
/// </summary>
|
||||
private static readonly ModelProfile WHAT_THE_RULES_SAY = new()
|
||||
{
|
||||
Capabilities = Capability.TEXT_INPUT | Capability.TEXT_OUTPUT | Capability.MULTIPLE_IMAGE_INPUT,
|
||||
Kind = ModelKind.CHAT,
|
||||
Context = ContextWindow.Of(131_072, 262_144),
|
||||
Images = new(null, 20),
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void AStatedWindowReplacesTheWholeWindow()
|
||||
{
|
||||
var overrides = new ProviderCapabilityOverrides { ContextWindowTokens = 32_768 };
|
||||
var after = overrides.ApplyTo(WHAT_THE_RULES_SAY);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(after.Context.DefaultTokens, Is.EqualTo(32_768));
|
||||
Assert.That(after.Context.RaisableToTokens, Is.Null, "What the model card says it could be raised to is not a property of this installation.");
|
||||
Assert.That(after.Images, Is.EqualTo(WHAT_THE_RULES_SAY.Images), "Stating a window says nothing about pictures.");
|
||||
Assert.That(after.Capabilities, Is.EqualTo(WHAT_THE_RULES_SAY.Capabilities));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SayingNothingKeepsEverythingTheRulesWorkedOut()
|
||||
{
|
||||
var after = new ProviderCapabilityOverrides().ApplyTo(WHAT_THE_RULES_SAY);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(after.Context, Is.EqualTo(WHAT_THE_RULES_SAY.Context));
|
||||
Assert.That(after.Images, Is.EqualTo(WHAT_THE_RULES_SAY.Images));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EachImageLimitStandsForItself()
|
||||
{
|
||||
var overrides = new ProviderCapabilityOverrides { MaxImagesPerMessage = 4 };
|
||||
var after = overrides.ApplyTo(WHAT_THE_RULES_SAY);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(after.Images.MaxPerMessage, Is.EqualTo(4));
|
||||
Assert.That(after.Images.MaxPerRequest, Is.EqualTo(20), "Nobody contradicted the request limit, so it stands.");
|
||||
Assert.That(after.Images.MaxInOneMessage, Is.EqualTo(4));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheSmallerLimitStillDecidesWhatFitsIntoAMessage()
|
||||
{
|
||||
//
|
||||
// A person raising the request limit alone may well see no change, and that is the right
|
||||
// answer rather than a defect: the limit standing in their way is the other one, which they
|
||||
// have not said anything about. The dialog shows them what is in effect for that reason.
|
||||
//
|
||||
var rules = WHAT_THE_RULES_SAY with { Images = new(3, null) };
|
||||
var after = new ProviderCapabilityOverrides { MaxImagesPerRequest = 100 }.ApplyTo(rules);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(after.Images.MaxPerRequest, Is.EqualTo(100));
|
||||
Assert.That(after.Images.MaxInOneMessage, Is.EqualTo(3));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NoImagesAtAllIsAnAnswerAndNotAGap()
|
||||
{
|
||||
var after = new ProviderCapabilityOverrides { MaxImagesPerRequest = 0 }.ApplyTo(WHAT_THE_RULES_SAY);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(after.Images.IsKnown, Is.True);
|
||||
Assert.That(after.Images.MaxInOneMessage, Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(0)]
|
||||
[TestCase(-1)]
|
||||
public void AWindowWhichIsNoWidthIsIgnoredRatherThanRepaired(int tokens)
|
||||
{
|
||||
//
|
||||
// Both surfaces which take a number refuse this one with a message, so a value like it came
|
||||
// out of a settings file somebody edited by hand. Falling back to what the rules say is the
|
||||
// one answer nobody has to invent.
|
||||
//
|
||||
var after = new ProviderCapabilityOverrides { ContextWindowTokens = tokens }.ApplyTo(WHAT_THE_RULES_SAY);
|
||||
Assert.That(after.Context, Is.EqualTo(WHAT_THE_RULES_SAY.Context));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ANegativeCountOfImagesIsIgnoredRatherThanRepaired()
|
||||
{
|
||||
var after = new ProviderCapabilityOverrides { MaxImagesPerRequest = -5 }.ApplyTo(WHAT_THE_RULES_SAY);
|
||||
Assert.That(after.Images, Is.EqualTo(WHAT_THE_RULES_SAY.Images));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AProviderCarryingNothingButANumberIsStillWorthSaving()
|
||||
{
|
||||
//
|
||||
// The dialog throws the record away when this says false, so a person who set nothing but a
|
||||
// window would watch their number disappear on the way out of the dialog.
|
||||
//
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(new ProviderCapabilityOverrides { ContextWindowTokens = 8_192 }.HasOverrides, Is.True);
|
||||
Assert.That(new ProviderCapabilityOverrides { MaxImagesPerMessage = 1 }.HasOverrides, Is.True);
|
||||
Assert.That(new ProviderCapabilityOverrides { MaxImagesPerRequest = 0 }.HasOverrides, Is.True, "Zero is a statement, and the person made it.");
|
||||
Assert.That(new ProviderCapabilityOverrides().HasOverrides, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ASettingsFileReadsBackWhatItWasWritten()
|
||||
{
|
||||
var written = new ProviderCapabilityOverrides
|
||||
{
|
||||
VideoInput = false,
|
||||
ContextWindowTokens = 32_768,
|
||||
MaxImagesPerMessage = 4,
|
||||
MaxImagesPerRequest = 0,
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(written);
|
||||
var read = JsonSerializer.Deserialize<ProviderCapabilityOverrides>(json);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(read, Is.EqualTo(written));
|
||||
Assert.That(json, Does.Contain("\"CONTEXT_WINDOW\""), "The key names are the surface an administrator sees; they are not free to change.");
|
||||
Assert.That(json, Does.Contain("\"MAX_IMAGES_PER_MESSAGE\""));
|
||||
Assert.That(json, Does.Contain("\"MAX_IMAGES_PER_REQUEST\""));
|
||||
Assert.That(json, Does.Not.Contain("AUDIO_INPUT"), "Saying nothing is not the same as saying null, and a settings file should not be full of it.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhatTheAppExportsIsWhatAConfigurationPluginCanReadBack()
|
||||
{
|
||||
var written = new ProviderCapabilityOverrides
|
||||
{
|
||||
FunctionCalling = true,
|
||||
ContextWindowTokens = 65_536,
|
||||
MaxImagesPerMessage = 2,
|
||||
MaxImagesPerRequest = 8,
|
||||
};
|
||||
|
||||
var read = await ParseAsync(written.ExportAsLuaTable(string.Empty));
|
||||
Assert.That(read, Is.EqualTo(written));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ANumberIsReadTheWayAnAdministratorWroteIt()
|
||||
{
|
||||
var read = await ParseAsync("""
|
||||
["CapabilityOverrides"] = {
|
||||
["CONTEXT_WINDOW"] = 32768,
|
||||
["max_images_per_request"] = 4,
|
||||
},
|
||||
""");
|
||||
|
||||
Assert.That(read, Is.Not.Null);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(read!.ContextWindowTokens, Is.EqualTo(32_768));
|
||||
Assert.That(read.MaxImagesPerRequest, Is.EqualTo(4), "The capability words are read loosely too, and a table is read by the app rather than by a compiler.");
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase("[\"CONTEXT_WINDOW\"] = 0", TestName = "A window of no tokens")]
|
||||
[TestCase("[\"CONTEXT_WINDOW\"] = -1", TestName = "A window of negative tokens")]
|
||||
[TestCase("[\"CONTEXT_WINDOW\"] = \"32768\"", TestName = "A window written as text")]
|
||||
[TestCase("[\"CONTEXT_WINDOW\"] = true", TestName = "A window written as a switch")]
|
||||
[TestCase("[\"MAX_IMAGES_PER_REQUEST\"] = -1", TestName = "A negative count of images")]
|
||||
[TestCase("[\"MAX_IMAGES_PER_REQUEST\"] = false", TestName = "A count of images written as a switch")]
|
||||
public async Task ANumberWhichIsNoneLeavesTheRestOfTheTableStanding(string entry)
|
||||
{
|
||||
//
|
||||
// One unusable line is the line to lose, not the table around it. An organization rolling
|
||||
// out a typo would otherwise lose every switch they got right along with it.
|
||||
//
|
||||
var read = await ParseAsync($$"""
|
||||
["CapabilityOverrides"] = {
|
||||
["VIDEO_INPUT"] = false,
|
||||
{{entry}},
|
||||
},
|
||||
""");
|
||||
|
||||
Assert.That(read, Is.Not.Null);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(read!.VideoInput, Is.EqualTo(false));
|
||||
Assert.That(read.ContextWindowTokens, Is.Null);
|
||||
Assert.That(read.MaxImagesPerRequest, Is.Null);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ATableOfNothingUsableIsNoOverrideAtAll()
|
||||
{
|
||||
var read = await ParseAsync("""
|
||||
["CapabilityOverrides"] = {
|
||||
["CONTEXT_WINDOW"] = 0,
|
||||
},
|
||||
""");
|
||||
|
||||
Assert.That(read, Is.Null, "A provider with nothing to say about itself is saved without a record, the way it was before anybody typed.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a provider entry the way a configuration plugin states it.
|
||||
/// </summary>
|
||||
/// <param name="providerEntry">The lines of the provider table.</param>
|
||||
/// <returns>The overrides read from it, or null when there are none.</returns>
|
||||
private static async Task<ProviderCapabilityOverrides?> ParseAsync(string providerEntry)
|
||||
{
|
||||
var state = LuaState.Create();
|
||||
state.OpenBasicLibrary();
|
||||
state.OpenTableLibrary();
|
||||
|
||||
await state.DoStringAsync($$"""
|
||||
PROVIDER = {
|
||||
{{providerEntry}}
|
||||
}
|
||||
""");
|
||||
|
||||
if (!state.Environment["PROVIDER"].TryRead<LuaTable>(out var table))
|
||||
throw new InvalidOperationException("The entry of this test is not a Lua table.");
|
||||
|
||||
return ProviderCapabilityOverrides.TryParseFromLuaTable(1, table, PLUGIN_ID, NullLogger.Instance);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user