mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-27 02:33:38 +00:00
Answer the model kind from the registry
This commit is contained in:
parent
51613e3748
commit
849f072f3e
@ -103,7 +103,7 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, ne
|
||||
return this.LoadModelsResponse<ModelsResponse>(
|
||||
storeType,
|
||||
"models",
|
||||
modelResponse => modelResponse.Data.Where(model => model.IsChatModel()),
|
||||
modelResponse => modelResponse.Data.Where(model => model.IsChatModel(this.Provider)),
|
||||
apiKeyProvisional, token: token);
|
||||
}
|
||||
}
|
||||
@ -88,7 +88,7 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("ht
|
||||
var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, apiKeyProvisional, token);
|
||||
return result with
|
||||
{
|
||||
Models = [..result.Models.Where(model => model.IsChatModel())]
|
||||
Models = [..result.Models.Where(model => model.IsChatModel(this.Provider))]
|
||||
};
|
||||
}
|
||||
|
||||
@ -112,7 +112,7 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("ht
|
||||
if (!result.Success)
|
||||
return result;
|
||||
|
||||
var embeddingModels = result.Models.Where(model => model.IsEmbeddingModel()).ToList();
|
||||
var embeddingModels = result.Models.Where(model => model.IsEmbeddingModel(this.Provider)).ToList();
|
||||
if (embeddingModels.Count is 0)
|
||||
return ModelLoadResult.FromModels(KNOWN_EMBEDDING_MODELS);
|
||||
|
||||
|
||||
@ -176,7 +176,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https
|
||||
//
|
||||
..result.Models.Where(model =>
|
||||
model.Id.StartsWith("gemini-", StringComparison.OrdinalIgnoreCase) &&
|
||||
model.IsChatModel())
|
||||
model.IsChatModel(this.Provider))
|
||||
.Select(this.WithDisplayNameFallback)
|
||||
]
|
||||
};
|
||||
@ -195,7 +195,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https
|
||||
{
|
||||
Models =
|
||||
[
|
||||
..result.Models.Where(model => model.IsEmbeddingModel())
|
||||
..result.Models.Where(model => model.IsEmbeddingModel(this.Provider))
|
||||
.Select(this.WithDisplayNameFallback)
|
||||
]
|
||||
};
|
||||
|
||||
@ -83,7 +83,7 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, new Uri("https://a
|
||||
var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, apiKeyProvisional, token);
|
||||
return result with
|
||||
{
|
||||
Models = [..result.Models.Where(model => model.IsChatModel())]
|
||||
Models = [..result.Models.Where(model => model.IsChatModel(this.Provider))]
|
||||
};
|
||||
}
|
||||
|
||||
@ -105,7 +105,7 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, new Uri("https://a
|
||||
var result = await this.LoadModels(SecretStoreType.TRANSCRIPTION_PROVIDER, apiKeyProvisional, token);
|
||||
return result with
|
||||
{
|
||||
Models = [..result.Models.Where(model => model.IsTranscriptionModel())]
|
||||
Models = [..result.Models.Where(model => model.IsTranscriptionModel(this.Provider))]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -84,7 +84,7 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, n
|
||||
{
|
||||
Models =
|
||||
[
|
||||
..result.Models.Where(model => model.IsChatModel())
|
||||
..result.Models.Where(model => model.IsChatModel(this.Provider))
|
||||
]
|
||||
};
|
||||
}
|
||||
@ -103,7 +103,7 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, n
|
||||
{
|
||||
Models =
|
||||
[
|
||||
..result.Models.Where(model => model.IsEmbeddingModel())
|
||||
..result.Models.Where(model => model.IsEmbeddingModel(this.Provider))
|
||||
]
|
||||
};
|
||||
}
|
||||
@ -116,7 +116,7 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, n
|
||||
{
|
||||
Models =
|
||||
[
|
||||
..result.Models.Where(model => model.IsTranscriptionModel())
|
||||
..result.Models.Where(model => model.IsTranscriptionModel(this.Provider))
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@ -69,7 +69,7 @@ public sealed class ProviderHetzner() : BaseProvider(LLMProviders.HETZNER, new U
|
||||
/// <inheritdoc />
|
||||
public override Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModelsResponse<ModelsResponse>(SecretStoreType.LLM_PROVIDER, "models", modelResponse => modelResponse.Data.Where(model => model.IsChatModel()), apiKeyProvisional, token: token);
|
||||
return this.LoadModelsResponse<ModelsResponse>(SecretStoreType.LLM_PROVIDER, "models", modelResponse => modelResponse.Data.Where(model => model.IsChatModel(this.Provider)), apiKeyProvisional, token: token);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -237,7 +237,7 @@ public sealed class ProviderHuggingFace : BaseProvider
|
||||
/// <returns>The models to offer.</returns>
|
||||
private IEnumerable<Model> SelectChatModels(ModelsResponse response)
|
||||
{
|
||||
var chatModels = response.Data.Where(hfModel => new Model(hfModel.Id, null).IsChatModel());
|
||||
var chatModels = response.Data.Where(hfModel => new Model(hfModel.Id, null).IsChatModel(this.Provider));
|
||||
var providerSlug = this.hfProvider.EndpointsId();
|
||||
if (string.IsNullOrEmpty(providerSlug))
|
||||
return ToModels(chatModels);
|
||||
|
||||
@ -84,7 +84,7 @@ public sealed class ProviderIONOS() : BaseProvider(LLMProviders.IONOS, new Uri("
|
||||
/// <inheritdoc />
|
||||
public override Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(SecretStoreType.LLM_PROVIDER, model => model.IsChatModel(), apiKeyProvisional, token);
|
||||
return this.LoadModels(SecretStoreType.LLM_PROVIDER, model => model.IsChatModel(this.Provider), apiKeyProvisional, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -96,7 +96,7 @@ public sealed class ProviderIONOS() : BaseProvider(LLMProviders.IONOS, new Uri("
|
||||
/// <inheritdoc />
|
||||
public override Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, model => model.IsEmbeddingModel(), apiKeyProvisional, token);
|
||||
return this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, model => model.IsEmbeddingModel(this.Provider), apiKeyProvisional, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -77,7 +77,7 @@ public sealed class ProviderLiteLLM(string hostname) : BaseProvider(LLMProviders
|
||||
/// <inheritdoc />
|
||||
public override Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(SecretStoreType.LLM_PROVIDER, static model => model.IsChatModel(), apiKeyProvisional, token);
|
||||
return this.LoadModels(SecretStoreType.LLM_PROVIDER, model => model.IsChatModel(this.Provider), apiKeyProvisional, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -89,13 +89,13 @@ public sealed class ProviderLiteLLM(string hostname) : BaseProvider(LLMProviders
|
||||
/// <inheritdoc />
|
||||
public override Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, static model => model.IsEmbeddingModel(), apiKeyProvisional, token);
|
||||
return this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, model => model.IsEmbeddingModel(this.Provider), apiKeyProvisional, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(SecretStoreType.TRANSCRIPTION_PROVIDER, static model => model.IsTranscriptionModel(), apiKeyProvisional, token);
|
||||
return this.LoadModels(SecretStoreType.TRANSCRIPTION_PROVIDER, model => model.IsTranscriptionModel(this.Provider), apiKeyProvisional, token);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@ -97,7 +97,7 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U
|
||||
// kind detection:
|
||||
..modelResponse.Models.Where(n =>
|
||||
!n.Id.StartsWith("code", StringComparison.OrdinalIgnoreCase) &&
|
||||
n.IsChatModel())
|
||||
n.IsChatModel(this.Provider))
|
||||
]
|
||||
};
|
||||
}
|
||||
@ -111,7 +111,7 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U
|
||||
|
||||
return modelResponse with
|
||||
{
|
||||
Models = [..modelResponse.Models.Where(n => n.IsEmbeddingModel())]
|
||||
Models = [..modelResponse.Models.Where(n => n.IsEmbeddingModel(this.Provider))]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,228 +0,0 @@
|
||||
namespace AIStudio.Provider;
|
||||
|
||||
/// <summary>
|
||||
/// Determines what kind of model we are dealing with, based on its name.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Many providers serve every kind of model through one models endpoint, without telling us what
|
||||
/// kind each model is. Before this class existed, every provider carried its own list of name
|
||||
/// fragments to sort those models apart. Those lists disagreed with each other: a model like
|
||||
/// nomic-embed-text was recognized as an embedding model by some providers, while others offered it
|
||||
/// as a chat model. The knowledge about model families is the same for all providers, so it lives
|
||||
/// here now.
|
||||
///
|
||||
/// This class recognizes what a model is NOT made for. Everything we do not recognize is reported as
|
||||
/// a chat model. That direction matters: when a provider adds a model family we have never seen, the
|
||||
/// user still gets to use it. Getting it wrong the other way around would hide a model the user is
|
||||
/// paying for.
|
||||
///
|
||||
/// What this class must not become is a place for provider-specific knowledge. That a model called
|
||||
/// "codestral" is a fill-in-the-middle model at Mistral, or that Alibaba's chat models all start
|
||||
/// with a "q", is true for that one provider only. Such rules stay in the provider.
|
||||
/// </remarks>
|
||||
public static class ModelKindExtensions
|
||||
{
|
||||
//
|
||||
// Checked first, because these entries are no models at all: whatever else their name might
|
||||
// suggest, none of the other kinds applies to them.
|
||||
//
|
||||
private static readonly string[] OTHER_MARKERS = ["container"];
|
||||
|
||||
//
|
||||
// Reranking is checked before embedding: rerankers are commonly named after the embedding model
|
||||
// they belong to, e.g. Qwen3-VL-Reranker-8B next to Qwen3-VL-Embedding-8B.
|
||||
//
|
||||
private static readonly string[] RERANKING_MARKERS = ["rerank"];
|
||||
|
||||
private static readonly string[] EMBEDDING_MARKERS = ["embed", "bge", "mpnet", "paraphrase", "sentence-transformers", "gte-", "e5-", "gritlm"];
|
||||
|
||||
//
|
||||
// The models from before chat completions existed. Providers keep offering some of them, and
|
||||
// Helmholtz Blablador still reports 'text-davinci-003', but asking any of them for a chat
|
||||
// completion fails. We deliberately do not look for 'ada' here: three letters appear in far too
|
||||
// many unrelated model names, and losing a chat model weighs heavier than keeping a dead one.
|
||||
//
|
||||
private static readonly string[] TEXT_COMPLETION_MARKERS = ["davinci", "babbage", "curie", "gpt-3.5-turbo-instruct"];
|
||||
|
||||
private static readonly string[] IMAGE_GENERATION_MARKERS = ["flux", "stable-diffusion", "sdxl", "dall-e", "midjourney", "gpt-image"];
|
||||
|
||||
//
|
||||
// Google names its image models after the chat model they grew out of and appends "image":
|
||||
// gemini-3-pro-image, gemini-3.1-flash-image, gemini-2.5-flash-image. Read as a plain substring,
|
||||
// that word is too greedy -- it also sits inside "imagenet" and "reimagined", and a chat model
|
||||
// carrying such a word would disappear from the user's list. It therefore counts only where a
|
||||
// name segment begins and ends with it.
|
||||
//
|
||||
private static readonly string[] IMAGE_GENERATION_WORD_MARKERS = ["image"];
|
||||
|
||||
private static readonly string[] VIDEO_GENERATION_MARKERS = ["sora", "veo-", "runway", "hailuo"];
|
||||
|
||||
//
|
||||
// Markers which have to stand as a word of their own. "kling" is such a case: taken as a plain
|
||||
// substring, it also matches the organization "Klingspor", the model "Inkling", and the
|
||||
// fine-tune "Llama-2-7b-chat-klingon" -- all of them models to chat with, which would vanish
|
||||
// from the user's list. The video models themselves are named "kling-v1" or "kling-video",
|
||||
// where the name ends at a separator.
|
||||
//
|
||||
private static readonly string[] VIDEO_GENERATION_WORD_MARKERS = ["kling"];
|
||||
|
||||
//
|
||||
// Voxtral is marketed as an audio model which understands speech, so one could expect it to work
|
||||
// in a chat as well. It does not: asking Mistral for a chat completion with 'voxtral-mini-latest'
|
||||
// is answered with 'Invalid model'. Voxtral therefore belongs here, next to the models which do
|
||||
// nothing but transcribe.
|
||||
//
|
||||
private static readonly string[] TRANSCRIPTION_MARKERS = ["whisper", "-transcribe", "wav2vec", "parakeet", "voxtral"];
|
||||
|
||||
//
|
||||
// Besides the pure text-to-speech models, this covers the models which answer in audio, such as
|
||||
// 'gpt-audio' and 'gpt-4o-audio-preview'. Those do accept a text-only request, but they are made
|
||||
// for spoken conversations, and the providers offering them directly keep them out of their chat
|
||||
// model lists as well.
|
||||
//
|
||||
private static readonly string[] SPEECH_SYNTHESIS_MARKERS = ["-tts", "tts-", "-speech", "speech-", "-audio", "audio-"];
|
||||
|
||||
//
|
||||
// The models for spoken conversations over a live connection. They speak their own protocol,
|
||||
// usually a WebSocket, and answer a chat completion request with an error. Checked before
|
||||
// transcription, because some of them carry the name of a transcription model, such as
|
||||
// OpenAI's 'gpt-realtime-whisper'. Those still need the live connection.
|
||||
//
|
||||
private static readonly string[] REALTIME_MARKERS = ["realtime"];
|
||||
|
||||
private static readonly string[] OCR_MARKERS = ["ocr"];
|
||||
|
||||
private static readonly string[] MODERATION_MARKERS = ["moderation", "guard"];
|
||||
|
||||
/// <summary>
|
||||
/// Determines what kind of model this is, based on its name.
|
||||
/// </summary>
|
||||
/// <param name="model">The model to inspect.</param>
|
||||
/// <returns>The recognized kind, or ModelKind.CHAT when we recognize no other kind.</returns>
|
||||
public static ModelKind DetermineKind(this Model model)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model.Id) || model.IsSystemModel)
|
||||
return ModelKind.CHAT;
|
||||
|
||||
if (HasAnyMarker(model.Id, OTHER_MARKERS))
|
||||
return ModelKind.OTHER;
|
||||
|
||||
if (HasAnyMarker(model.Id, RERANKING_MARKERS))
|
||||
return ModelKind.RERANKING;
|
||||
|
||||
if (HasAnyMarker(model.Id, EMBEDDING_MARKERS))
|
||||
return ModelKind.EMBEDDING;
|
||||
|
||||
if (HasAnyMarker(model.Id, TEXT_COMPLETION_MARKERS))
|
||||
return ModelKind.TEXT_COMPLETION;
|
||||
|
||||
if (HasAnyMarker(model.Id, IMAGE_GENERATION_MARKERS) || HasAnyWordMarker(model.Id, IMAGE_GENERATION_WORD_MARKERS))
|
||||
return ModelKind.IMAGE_GENERATION;
|
||||
|
||||
if (HasAnyMarker(model.Id, VIDEO_GENERATION_MARKERS) || HasAnyWordMarker(model.Id, VIDEO_GENERATION_WORD_MARKERS))
|
||||
return ModelKind.VIDEO_GENERATION;
|
||||
|
||||
if (HasAnyMarker(model.Id, REALTIME_MARKERS))
|
||||
return ModelKind.REALTIME;
|
||||
|
||||
if (HasAnyMarker(model.Id, TRANSCRIPTION_MARKERS))
|
||||
return ModelKind.TRANSCRIPTION;
|
||||
|
||||
if (HasAnyMarker(model.Id, SPEECH_SYNTHESIS_MARKERS))
|
||||
return ModelKind.SPEECH_SYNTHESIS;
|
||||
|
||||
if (HasAnyMarker(model.Id, OCR_MARKERS))
|
||||
return ModelKind.OCR;
|
||||
|
||||
if (HasAnyMarker(model.Id, MODERATION_MARKERS))
|
||||
return ModelKind.MODERATION;
|
||||
|
||||
return ModelKind.CHAT;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether this model can be used for chatting.
|
||||
/// </summary>
|
||||
/// <param name="model">The model to check.</param>
|
||||
/// <returns>True, when the model is a chat model or when we recognize no other kind.</returns>
|
||||
public static bool IsChatModel(this Model model) => model.DetermineKind() is ModelKind.CHAT;
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether this model creates embeddings.
|
||||
/// </summary>
|
||||
/// <param name="model">The model to check.</param>
|
||||
/// <returns>True, when the model is an embedding model.</returns>
|
||||
public static bool IsEmbeddingModel(this Model model) => model.DetermineKind() is ModelKind.EMBEDDING;
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether this model transcribes audio.
|
||||
/// </summary>
|
||||
/// <param name="model">The model to check.</param>
|
||||
/// <returns>True, when the model is a transcription model.</returns>
|
||||
public static bool IsTranscriptionModel(this Model model) => model.DetermineKind() is ModelKind.TRANSCRIPTION;
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether this model generates images.
|
||||
/// </summary>
|
||||
/// <param name="model">The model to check.</param>
|
||||
/// <returns>True, when the model is an image generation model.</returns>
|
||||
public static bool IsImageModel(this Model model) => model.DetermineKind() is ModelKind.IMAGE_GENERATION;
|
||||
|
||||
private static bool HasAnyMarker(string modelId, string[] markers)
|
||||
{
|
||||
foreach (var marker in markers)
|
||||
if (modelId.Contains(marker, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the model name contains one of the markers as a word of its own.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A short marker which is also a common syllable cannot be looked for as a plain substring:
|
||||
/// it would match names which have nothing to do with it, and the model would be sorted into
|
||||
/// the wrong kind. Such a marker counts only where a name segment begins and ends with it.
|
||||
/// </remarks>
|
||||
/// <param name="modelId">The ID of the model.</param>
|
||||
/// <param name="markers">The markers to look for.</param>
|
||||
/// <returns>True, when one of the markers stands as a word of its own.</returns>
|
||||
private static bool HasAnyWordMarker(string modelId, string[] markers)
|
||||
{
|
||||
foreach (var marker in markers)
|
||||
{
|
||||
var searchIndex = 0;
|
||||
while (searchIndex <= modelId.Length - marker.Length)
|
||||
{
|
||||
var markerIndex = modelId.IndexOf(marker, searchIndex, StringComparison.OrdinalIgnoreCase);
|
||||
if (markerIndex is -1)
|
||||
break;
|
||||
|
||||
if (IsWholeWord(modelId, marker, markerIndex))
|
||||
return true;
|
||||
|
||||
// The same marker may appear again later in the name, so we keep looking:
|
||||
searchIndex = markerIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsWholeWord(string modelId, string marker, int markerIndex)
|
||||
{
|
||||
if (markerIndex > 0 && !IsSeparator(modelId[markerIndex - 1]))
|
||||
return false;
|
||||
|
||||
var endIndex = markerIndex + marker.Length;
|
||||
return endIndex >= modelId.Length || IsSeparator(modelId[endIndex]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The characters which separate the parts of a model name, such as in "fal-ai/kling-video".
|
||||
/// </summary>
|
||||
/// <param name="character">The character to check.</param>
|
||||
/// <returns>True, when the character separates two parts of a name.</returns>
|
||||
private static bool IsSeparator(char character) => character is '/' or '-' or '_' or '.' or ' ' or ':';
|
||||
}
|
||||
@ -367,25 +367,25 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
|
||||
/// <inheritdoc />
|
||||
public override Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(SecretStoreType.LLM_PROVIDER, static model => model.IsChatModel(), apiKeyProvisional, token);
|
||||
return this.LoadModels(SecretStoreType.LLM_PROVIDER, model => model.IsChatModel(this.Provider), apiKeyProvisional, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<ModelLoadResult> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(SecretStoreType.IMAGE_PROVIDER, static model => model.IsImageModel(), apiKeyProvisional, token);
|
||||
return this.LoadModels(SecretStoreType.IMAGE_PROVIDER, model => model.IsImageModel(this.Provider), apiKeyProvisional, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, static model => model.IsEmbeddingModel(), apiKeyProvisional, token);
|
||||
return this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, model => model.IsEmbeddingModel(this.Provider), apiKeyProvisional, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(SecretStoreType.TRANSCRIPTION_PROVIDER, static model => model.IsTranscriptionModel(), apiKeyProvisional, token);
|
||||
return this.LoadModels(SecretStoreType.TRANSCRIPTION_PROVIDER, model => model.IsTranscriptionModel(this.Provider), apiKeyProvisional, token);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@ -117,7 +117,7 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
|
||||
"models",
|
||||
modelResponse => modelResponse.Data
|
||||
.Select(n => new Model(n.Id, n.Name))
|
||||
.Where(model => model.IsChatModel()),
|
||||
.Where(model => model.IsChatModel(this.Provider)),
|
||||
apiKeyProvisional,
|
||||
requestConfigurator: (request, secretKey) =>
|
||||
{
|
||||
|
||||
@ -84,7 +84,7 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, new Uri("https://
|
||||
// "-image", which said nothing about grok-imagine-video: that one made films and stood
|
||||
// in the list of things to chat with.
|
||||
//
|
||||
Models = [..result.Models.Where(model => model.IsChatModel())]
|
||||
Models = [..result.Models.Where(model => model.IsChatModel(this.Provider))]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -65,4 +65,47 @@ public static partial class ProviderExtensions
|
||||
/// <param name="provider">The configured provider.</param>
|
||||
/// <returns><c>true</c> when the model accepts image input.</returns>
|
||||
public static bool SupportsImageInput(this Provider provider) => provider.GetModelProfile().HasAny(Capability.SINGLE_IMAGE_INPUT | Capability.MULTIPLE_IMAGE_INPUT);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether this model can be used for chatting.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// What a model can do and what it is made for used to be two questions answered by two pieces
|
||||
/// of code, each walking the same name with rules of its own. They disagreed: a model like
|
||||
/// nomic-embed-text was an embedding model at one provider and a chat model at the next. Both
|
||||
/// come out of the same rules now, which is why this takes the provider -- the same name means
|
||||
/// different things depending on who serves it, and only the provider knows how to unwrap it.
|
||||
///
|
||||
/// The direction of the answer is deliberate. Everything not recognized as something else is a
|
||||
/// chat model, so a provider adding a family we have never seen keeps it visible to the person
|
||||
/// paying for it. Getting it wrong the other way would hide a model.
|
||||
/// </remarks>
|
||||
/// <param name="model">The model to check.</param>
|
||||
/// <param name="provider">The provider serving it.</param>
|
||||
/// <returns>True, when the model is a chat model or when we recognize no other kind.</returns>
|
||||
public static bool IsChatModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.CHAT;
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether this model creates embeddings.
|
||||
/// </summary>
|
||||
/// <param name="model">The model to check.</param>
|
||||
/// <param name="provider">The provider serving it.</param>
|
||||
/// <returns>True, when the model is an embedding model.</returns>
|
||||
public static bool IsEmbeddingModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.EMBEDDING;
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether this model transcribes audio.
|
||||
/// </summary>
|
||||
/// <param name="model">The model to check.</param>
|
||||
/// <param name="provider">The provider serving it.</param>
|
||||
/// <returns>True, when the model is a transcription model.</returns>
|
||||
public static bool IsTranscriptionModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.TRANSCRIPTION;
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether this model generates images.
|
||||
/// </summary>
|
||||
/// <param name="model">The model to check.</param>
|
||||
/// <param name="provider">The provider serving it.</param>
|
||||
/// <returns>True, when the model is an image generation model.</returns>
|
||||
public static bool IsImageModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.IMAGE_GENERATION;
|
||||
}
|
||||
@ -209,13 +209,4 @@ public static class ModelKindCorpus
|
||||
..STILL_CHAT_MODELS,
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// What the markers being replaced answer for a name, where that is not what the rules answer.
|
||||
/// </summary>
|
||||
/// <param name="provider">Who serves the model.</param>
|
||||
/// <param name="modelId">The model ID as that provider reports it.</param>
|
||||
/// <returns>The old answer, or null when nobody recorded a difference for this name.</returns>
|
||||
public static ModelKind? AnsweredTodayAs(LLMProviders provider, string modelId) => ENTRIES
|
||||
.FirstOrDefault(example => example.Provider == provider && string.Equals(example.ModelId, modelId, StringComparison.Ordinal))
|
||||
?.AnsweredTodayAs;
|
||||
}
|
||||
@ -8,6 +8,6 @@ namespace AIStudio.Tests.Models.Corpus;
|
||||
/// <param name="Provider">The provider the model is reached through.</param>
|
||||
/// <param name="ModelId">The model ID exactly as that provider reports it, before any normalization.</param>
|
||||
/// <param name="Kind">What the model is made for.</param>
|
||||
/// <param name="AnsweredTodayAs">What the markers being replaced answer, where that is something else.</param>
|
||||
/// <param name="AnsweredTodayAs">What the markers that used to answer this question said, where they said something else. History now: the code that said it is gone, so nothing checks this any more. It stays because a decision without the thing it decided against reads like an arbitrary statement.</param>
|
||||
/// <param name="Reason">Why the two differ, which is only filled in when they do.</param>
|
||||
public sealed record ModelKindExample(LLMProviders Provider, string ModelId, ModelKind Kind, ModelKind? AnsweredTodayAs = null, string Reason = "");
|
||||
@ -8,14 +8,15 @@ namespace AIStudio.Tests.Models;
|
||||
/// Holds the rules to what a model is made for.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// What a model can do and what it is for are two questions, and until now two pieces of code
|
||||
/// answered them, each walking the same name with rules of its own. This is the test which says the
|
||||
/// second answer did not change when it moved: the marker list is still there and still answers, so
|
||||
/// every example can be put to both and the two have to agree.
|
||||
/// What a model can do and what it is for are two questions, and they used to be answered by two
|
||||
/// pieces of code, each walking the same name with rules of its own. While both existed, the tests
|
||||
/// here held one against the other. The marker list is gone now, and with it the comparison: the
|
||||
/// corpus-wide check moved into the snapshot, which carries the kind of every model in a column of
|
||||
/// its own.
|
||||
///
|
||||
/// The day the call sites move to the profile, the marker list goes and the test below which asks
|
||||
/// it goes with it. What stays is the first test: the examples say what each name is, in words a
|
||||
/// person can check against a model card.
|
||||
/// What is left says what a name is, in words a person can check against a model card -- and holds
|
||||
/// the handful of decisions where the rules deliberately answer something else than the markers did.
|
||||
/// Those stand in the corpus next to the name, with the reason.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class ModelKindTests
|
||||
@ -34,46 +35,6 @@ public sealed class ModelKindTests
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheMarkersBeingReplacedAnswerEveryExampleTheSameWay()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var example in ModelKindCorpus.ENTRIES)
|
||||
{
|
||||
var today = new Model(example.ModelId, null).DetermineKind();
|
||||
var wanted = example.AnsweredTodayAs ?? example.Kind;
|
||||
var because = example.AnsweredTodayAs is null
|
||||
? $"{example.Provider} \"{example.ModelId}\" is sorted differently by the rules than by the markers they replace."
|
||||
: $"{example.Provider} \"{example.ModelId}\": {example.Reason}";
|
||||
|
||||
Assert.That(today, Is.EqualTo(wanted), because);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EveryModelOfTheCapabilityCorpusKeepsTheKindItHasToday()
|
||||
{
|
||||
//
|
||||
// The examples above are names chosen to reach a rule. This asks the other way round: the
|
||||
// corpus is full of models nobody wants sorted anywhere but into a chat, and a word inside
|
||||
// one of those names claiming a kind would take the model out of the user's list without
|
||||
// anything else going wrong.
|
||||
//
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var entry in ModelCorpus.ENTRIES)
|
||||
{
|
||||
var today = new Model(entry.ModelId, null).DetermineKind();
|
||||
var rebuilt = ModelRegistry.Shared.Profile(entry.Provider, entry.ModelId).Kind;
|
||||
var wanted = ModelKindCorpus.AnsweredTodayAs(entry.Provider, entry.ModelId) ?? rebuilt;
|
||||
|
||||
Assert.That(today, Is.EqualTo(wanted), $"{entry.Provider} \"{entry.ModelId}\" is sorted as {rebuilt} by the rules and as {today} by the markers they replace.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AModelWhichIsNoKindOfItsOwnIsAChatModel()
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user