using AIStudio.Models.Matching;
namespace AIStudio.Models.Mistral;
///
/// Reads the release a Mistral model belongs to out of its name.
///
///
/// Mistral names its models after the month they came out: mistral-large-2512 is Mistral Large 3
/// from December 2025. The marketing version lives in the marketing name only, so a rule written
/// against it would miss nearly every model the API actually serves. What a family can state is
/// therefore not "this model reads images" but "this family reads images from this release on",
/// and that is a calculation, not a pattern -- which is what the families do in Refine.
///
public static class MistralReleases
{
///
/// A threshold no release can ever reach, for a family which never gained the ability at all.
///
public const int NEVER = int.MaxValue;
///
/// What a name says when it carries no release at all.
///
///
/// Nothing is granted for it. That is the safe direction: offering an ability the model does
/// not have makes the request fail, while a missing one can be handed back by a person through
/// the expert settings.
///
public const int UNKNOWN = 0;
///
/// How many digits a release is written with.
///
private const int RELEASE_LENGTH = 4;
///
/// Mistral released its first date-named model in 2023.
///
///
/// Anything below that is not a release date but a parameter count or a context size which
/// happens to have four digits.
///
private const int FIRST_RELEASE_YEAR = 23;
///
/// The releases the marketing versions stand for.
///
///
/// Mistral serves some models under their marketing version as well, and writes the version
/// separator both ways: mistral-medium-3.5 and mistral-medium-3-5 are the same model. Those
/// names carry no release date, so they are mapped onto the release they stand for. Ollama
/// leaves the separator out altogether for the Small checkpoints, which is a third spelling of
/// the same statement.
///
/// The order matters, and it is the one place in this rebuild where it still does: these are
/// read as plain text rather than as patterns, so "mistral-medium-3" would answer for
/// "mistral-medium-3.5" if it came first.
///
private static readonly (string VersionName, int Release)[] VERSION_NAMES =
[
("mistral-large-3", 2512),
("mistral-medium-3.5", 2604),
("mistral-medium-3-5", 2604),
("mistral-medium-3.1", 2508),
("mistral-medium-3-1", 2508),
("mistral-medium-3", 2505),
("mistral-small-4", 2603),
("mistral-small-3.2", 2506),
("mistral-small-3-2", 2506),
("mistral-small-3.1", 2503),
("mistral-small-3-1", 2503),
("mistral-small-3", 2501),
("mistral-small4", 2603),
("mistral-small3.2", 2506),
("mistral-small3.1", 2503),
("mistral-small3", 2501),
];
///
/// The release a model name belongs to.
///
/// The model name.
/// The release this family's "latest" alias currently points at.
/// The release as YYMM, or unknown.
public static int Of(in ModelId id, int latestRelease)
{
// The "latest" alias always points at the newest release of its family:
if (id.ContainsSegments("latest"))
return latestRelease;
foreach (var (versionName, release) in VERSION_NAMES)
if (id.ContainsText(versionName))
return release;
return ReadFrom(id.Normalized.AsSpan());
}
///
/// Reads the four-digit release out of a name.
///
///
/// The block has to be exactly four digits long and has to read as a plausible year and month.
/// Without that, the size of a model would be mistaken for its release: ministral-14b-2512 has
/// to resolve to 2512 and not to anything the "14b" part could be read as.
///
/// The normalized model name.
/// The release as YYMM, or unknown.
private static int ReadFrom(ReadOnlySpan modelName)
{
for (var index = 0; index + RELEASE_LENGTH <= modelName.Length; index++)
{
// A digit next to the block means the block is longer than four digits:
if (index > 0 && char.IsAsciiDigit(modelName[index - 1]))
continue;
if (index + RELEASE_LENGTH < modelName.Length && char.IsAsciiDigit(modelName[index + RELEASE_LENGTH]))
continue;
var candidate = modelName.Slice(index, RELEASE_LENGTH);
if (!char.IsAsciiDigit(candidate[0]) || !char.IsAsciiDigit(candidate[1]) ||
!char.IsAsciiDigit(candidate[2]) || !char.IsAsciiDigit(candidate[3]))
continue;
var release = int.Parse(candidate);
var year = release / 100;
var month = release % 100;
if (year < FIRST_RELEASE_YEAR || month is < 1 or > 12)
continue;
return release;
}
return UNKNOWN;
}
}