Sorted every model list by what a model is made for (#984)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
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:
Thorsten Sommer authored and GitHub committed 2026-09-19 17:54:10 +02:00
1 parent e42277beba
commit f9c6075c50
36 files changed
+947 -333

No files matched your search

@@ -76,40 +76,8 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
/// <inheritdoc />
public override async Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
var additionalModels = new[]
{
new Model("qwq-plus", "QwQ plus"), // reasoning model
new Model("qwen-max-latest", "Qwen-Max (Latest)"),
new Model("qwen-plus-latest", "Qwen-Plus (Latest)"),
new Model("qwen-turbo-latest", "Qwen-Turbo (Latest)"),
new Model("qvq-max", "QVQ Max"), // visual reasoning model
new Model("qvq-max-latest", "QVQ Max (Latest)"), // visual reasoning model
new Model("qwen-vl-max", "Qwen-VL Max"), // text generation model that can understand and process images
new Model("qwen-vl-plus", "Qwen-VL Plus"), // text generation model that can understand and process images
new Model("qwen-mt-plus", "Qwen-MT Plus"), // machine translation
new Model("qwen-mt-turbo", "Qwen-MT Turbo"), // machine translation
//Open source
new Model("qwen2.5-14b-instruct-1m", "Qwen2.5 14b 1m context"),
new Model("qwen2.5-7b-instruct-1m", "Qwen2.5 7b 1m context"),
new Model("qwen2.5-72b-instruct", "Qwen2.5 72b"),
new Model("qwen2.5-32b-instruct", "Qwen2.5 32b"),
new Model("qwen2.5-14b-instruct", "Qwen2.5 14b"),
new Model("qwen2.5-7b-instruct", "Qwen2.5 7b"),
new Model("qwen2.5-omni-7b", "Qwen2.5-Omni 7b"), // omni-modal understanding and generation model
new Model("qwen2.5-vl-72b-instruct", "Qwen2.5-VL 72b"),
new Model("qwen2.5-vl-32b-instruct", "Qwen2.5-VL 32b"),
new Model("qwen2.5-vl-7b-instruct", "Qwen2.5-VL 7b"),
new Model("qwen2.5-vl-3b-instruct", "Qwen2.5-VL 3b"),
};
var result = await this.LoadModels(["q"], SecretStoreType.LLM_PROVIDER, apiKeyProvisional, token);
return result with
{
// The API is the authority: when it reports a model we also keep as a fallback above,
// its entry comes first and the fallback is dropped.
Models = [..result.Models.Concat(additionalModels).DistinctBy(x => x.Id).OrderBy(x => x.Id)]
};
var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, apiKeyProvisional, token);
return result with { Models = [..result.Models.Where(model => model.IsChatModel(this.Provider)).OrderBy(x => x.Id)] };
}
/// <inheritdoc />
@@ -121,19 +89,8 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
/// <inheritdoc />
public override async Task<ModelLoadResult> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
var additionalModels = new[]
{
new Model("text-embedding-v3", "text-embedding-v3"),
};
var result = await this.LoadModels(["text-embedding-"], SecretStoreType.EMBEDDING_PROVIDER, apiKeyProvisional, token);
return result with
{
// The API is the authority: when it reports a model we also keep as a fallback above,
// its entry comes first and the fallback is dropped.
Models = [..result.Models.Concat(additionalModels).DistinctBy(x => x.Id).OrderBy(x => x.Id)]
};
var result = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, apiKeyProvisional, token);
return result with { Models = [..result.Models.Where(model => model.IsEmbeddingModel(this.Provider)).OrderBy(x => x.Id)] };
}
#region Overrides of BaseProvider
@@ -148,12 +105,23 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
#endregion
private Task<ModelLoadResult> LoadModels(string[] prefixes, SecretStoreType storeType, string? apiKeyProvisional, CancellationToken token)
/// <summary>
/// Reads Model Studio's catalog, whole.
/// </summary>
/// <remarks>
/// It used to be read through a prefix per list -- a single "q" for the models to talk to, and
/// "text-embedding-" for the ones which answer in vectors. Neither survived what Model Studio
/// became: the letter also brings qwen-image, qwen-tts, qwen3-asr and qwen-vl-ocr into the chat
/// list, while it locks out DeepSeek, Kimi, GLM and MiniMax, which Alibaba serves through this
/// very endpoint. A name has never been a statement about what a model is for; the callers ask
/// the registry instead.
/// </remarks>
private Task<ModelLoadResult> LoadModels(SecretStoreType storeType, string? apiKeyProvisional, CancellationToken token)
{
return this.LoadModelsResponse<ModelsResponse>(
storeType,
"models",
modelResponse => modelResponse.Data.Where(model => prefixes.Any(prefix => model.Id.StartsWith(prefix, StringComparison.InvariantCulture))),
modelResponse => modelResponse.Data,
apiKeyProvisional, token: token);
}
}
@@ -213,9 +213,14 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n
var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, apiKeyProvisional, token);
return result with
{
//
// The API is the authority: when it reports a model we also keep as a fallback above,
// its entry comes first and the fallback is dropped.
Models = [..result.Models.Concat(additionalModels).DistinctBy(x => x.Id).OrderBy(x => x.Id)]
// its entry comes first and the fallback is dropped. What it reports is asked about
// first, though -- the route says nothing about what a model is made for, and Claude
// has not always been only something to talk to. The six above skip that question
// because they are not a catalog: every one of them was picked by hand.
//
Models = [..result.Models.Where(model => model.IsChatModel(this.Provider)).Concat(additionalModels).DistinctBy(x => x.Id).OrderBy(x => x.Id)]
};
}
@@ -93,6 +93,16 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, new Uri(
}
/// <inheritdoc />
/// <remarks>
/// The one transcription list which stays a plain list, where GWDG and Mistral ask their
/// endpoint first. There is nothing to ask here: HasModelLoadingCapability is false and every
/// other method above answers with nothing, which is why a chat model at Fireworks has to be
/// typed in by hand. A list is all there is.
///
/// The commented-out entry is no oversight either. The documentation names Whisper v3 Turbo,
/// and trying it does not work -- which is worth keeping written down, so that nobody adds it
/// back and finds out the same way again.
/// </remarks>
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
// Source: https://docs.fireworks.ai/api-reference/audio-transcriptions#param-model
@@ -18,6 +18,12 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("ht
new("qwen3-embedding-4b", "Qwen3 Embedding 4B"),
];
// Source: https://docs.hpc.gwdg.de/services/saia/index.html#voice-to-text
private static readonly Model[] KNOWN_TRANSCRIPTION_MODELS =
[
new("whisper-large-v2", "Whisper v2 Large"),
];
#region Implementation of IProvider
/// <inheritdoc />
@@ -123,13 +129,27 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("ht
}
/// <inheritdoc />
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
/// <remarks>
/// Built the same way as the embedding models above, and for the same reason: SAIA answers the
/// models endpoint with its chat models only, so this comes back empty and the documented list
/// stands in. Asking first costs nothing and means a speech model appearing in that answer one
/// day shows up on its own, rather than waiting for somebody to notice and edit this file. A
/// failed request is passed on unchanged, so a wrong API key stays visible as such.
/// </remarks>
public override async Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
// Source: https://docs.hpc.gwdg.de/services/saia/index.html#voice-to-text
return Task.FromResult(ModelLoadResult.FromModels(
[
new Model("whisper-large-v2", "Whisper v2 Large"),
]));
var result = await this.LoadModels(SecretStoreType.TRANSCRIPTION_PROVIDER, apiKeyProvisional, token);
if (!result.Success)
return result;
var transcriptionModels = result.Models.Where(model => model.IsTranscriptionModel(this.Provider)).ToList();
if (transcriptionModels.Count is 0)
return ModelLoadResult.FromModels(KNOWN_TRANSCRIPTION_MODELS);
return result with
{
Models = [..transcriptionModels]
};
}
#endregion
@@ -172,11 +172,15 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https
// Asking what a model is made for, rather than only ruling out the embedding ones.
// Google names everything after the chat model it grew out of, so the catalog is
// full of names which look like something to talk to and are not: the image models,
// and the computer use model whose API refuses a request without its tool.
// the computer use model whose API refuses a request without its tool, and the live
// line which wants a connection held open in both directions.
//
..result.Models.Where(model =>
model.Id.StartsWith("gemini-", StringComparison.OrdinalIgnoreCase) &&
model.IsChatModel(this.Provider))
// The question used to be asked of names beginning with "gemini" alone, and that
// cost the two Gemma models Google serves on this very route. What the prefix kept
// out besides them -- Lyria, Imagen, Veo, the research and coding agents, AQA --
// is kept out by a rule now, where the reason is written down.
//
..result.Models.Where(model => model.IsChatModel(this.Provider))
.Select(this.WithDisplayNameFallback)
]
};
@@ -421,17 +421,6 @@ public static class LLMProvidersExtensions
_ => false,
};
public static bool IsEmbeddingModelProvidedManually(this LLMProviders provider, Host host) => provider switch
{
LLMProviders.SELF_HOSTED => host is not Host.LM_STUDIO,
_ => false,
};
public static bool IsTranscriptionModelProvidedManually(this LLMProviders provider, Host host) => provider switch
{
_ => false,
};
/// <summary>
/// Determines if the model selection should be completely hidden for LLM providers.
/// This is the case when the host does not support model selection.
@@ -93,12 +93,13 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U
{
Models =
[
// Codestral is a fill-in-the-middle model, which we cannot use for chats. That is
// specific to Mistral's catalog, which is why it is not part of the shared model
// kind detection:
..modelResponse.Models.Where(n =>
!n.Id.StartsWith("code", StringComparison.OrdinalIgnoreCase) &&
n.IsChatModel(this.Provider))
//
// Codestral is a fill-in-the-middle model, which we cannot use for chats. Its own
// family says so now, bound to this provider, so the word "code" no longer has to
// be tested for here -- and testing for it never reached mistral-code-fim-latest,
// which does the same job under a name that begins differently.
//
..modelResponse.Models.Where(n => n.IsChatModel(this.Provider))
]
};
}
@@ -123,13 +124,16 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U
}
/// <inheritdoc />
public override Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
public override async Task<ModelLoadResult> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
// Source: https://docs.mistral.ai/capabilities/audio_transcription
return Task.FromResult(ModelLoadResult.FromModels(
[
new Provider.Model("voxtral-mini-latest", "Voxtral Mini Latest"),
]));
var modelResponse = await this.LoadModelList(SecretStoreType.TRANSCRIPTION_PROVIDER, apiKeyProvisional, token);
if (!modelResponse.Success)
return modelResponse;
return modelResponse with
{
Models = [..modelResponse.Models.Where(n => n.IsTranscriptionModel(this.Provider))]
};
}
#endregion
@@ -144,4 +148,4 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U
listingFactory: modelResponse => modelResponse.Data.Select(n => ModelListing.For(n.Id, n.ContextWindowTokens)),
token: token);
}
}
}
@@ -52,6 +52,16 @@ public enum ModelKind
/// </summary>
VIDEO_GENERATION,
/// <summary>
/// The model composes music.
/// </summary>
/// <remarks>
/// Audio comes out of it, but not speech: instruments, arrangement, and in Lyria's case singing
/// with lyrics. Neither the speech synthesis list nor any other one fits, and a chat request to
/// such a model gets nothing back that reads like an answer.
/// </remarks>
MUSIC_GENERATION,
/// <summary>
/// The model transcribes audio into text.
/// </summary>
@@ -86,6 +96,27 @@ public enum ModelKind
/// </remarks>
COMPUTER_USE,
/// <summary>
/// The model runs an errand of its own instead of answering.
/// </summary>
/// <remarks>
/// One request starts a loop which plans, calls tools, runs code and reads the web, and it can
/// take minutes. Google serves its research and coding agents this way, through an API of their
/// own which a chat request never reaches. Note that the name alone decides nothing here: what
/// Perplexity calls deep research is an ordinary chat model with web search.
/// </remarks>
AGENT,
/// <summary>
/// The model answers a question out of sources handed to it, and says where the answer came from.
/// </summary>
/// <remarks>
/// Built for retrieval rather than for conversation: it is given passages along with the
/// question, and returns the answer, the citations, and an estimate of whether the question
/// could be answered from them at all. Reached through a route of its own.
/// </remarks>
GROUNDED_ANSWERING,
/// <summary>
/// The model extracts text from images or scanned documents.
/// </summary>
@@ -137,6 +137,11 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
/// Nothing is reported from here: this route answers with the embedding models alone, and what
/// is reported replaces everything an instance said before. The windows of the chat models
/// would go missing the moment somebody opens the embedding settings.
///
/// Nothing is filtered either, for the same reason. The route is the statement: OpenRouter
/// serves these to embed with, which is more than a name can say. Asking the registry on top
/// could only drop a model whose name we do not recognize -- and where the two disagree, the
/// answer is a rule in Models/, not a model missing from this list.
/// </remarks>
/// <param name="apiKeyProvisional">An API key which is not stored yet.</param>
/// <param name="token">The cancellation token to use.</param>
@@ -156,4 +161,4 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
},
token: token);
}
}
}
@@ -97,12 +97,16 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
switch (host)
{
case Host.LLAMA_CPP:
return await this.LoadLlamaCppTextModels(["embed"], [], apiKeyProvisional, token);
return await this.LoadLlamaCppTextModels(apiKeyProvisional, token);
case Host.LM_STUDIO:
case Host.OLLAMA:
case Host.VLLM:
return await this.LoadModels( SecretStoreType.LLM_PROVIDER, ["embed"], [], apiKeyProvisional, token);
var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, apiKeyProvisional, token);
return result with
{
Models = [..result.Models.Where(model => model.IsChatModel(this.Provider))]
};
}
return ModelLoadResult.FromModels([]);
@@ -129,14 +133,18 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
case Host.LM_STUDIO:
case Host.OLLAMA:
case Host.VLLM:
return await this.LoadModels( SecretStoreType.EMBEDDING_PROVIDER, [], ["embed"], apiKeyProvisional, token);
var result = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, apiKeyProvisional, token);
return result with
{
Models = [..result.Models.Where(model => model.IsEmbeddingModel(this.Provider))]
};
}
return ModelLoadResult.FromModels([]);
}
catch(Exception e)
{
LOGGER.LogError($"Failed to load text models from self-hosted provider: {e.Message}");
LOGGER.LogError($"Failed to load embedding models from self-hosted provider: {e.Message}");
return ModelLoadResult.Failure(ModelLoadFailureReason.UNKNOWN, e.Message);
}
}
@@ -154,10 +162,22 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
new Provider.Model("loaded-model", TB("Model as configured by whisper.cpp")),
]);
//
// These two answer the models endpoint with everything they serve, and nothing in
// that answer says which of them listens. Asking what each model is made for is the
// only thing standing between this list and every chat and embedding model of the
// installation, which is what it used to hold. An engine running no speech model at
// all therefore offers nothing here, and says so, rather than offering models which
// would fail the moment audio reaches them.
//
case Host.OLLAMA:
case Host.VLLM:
return await this.LoadModels(SecretStoreType.TRANSCRIPTION_PROVIDER, [], [], apiKeyProvisional, token);
var result = await this.LoadModels(SecretStoreType.TRANSCRIPTION_PROVIDER, apiKeyProvisional, token);
return result with
{
Models = [..result.Models.Where(model => model.IsTranscriptionModel(this.Provider))]
};
default:
return ModelLoadResult.FromModels([]);
}
@@ -171,7 +191,21 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
#endregion
private async Task<ModelLoadResult> LoadModels(SecretStoreType storeType, string[] ignorePhrases, string[] filterPhrases, string? apiKeyProvisional, CancellationToken token)
/// <summary>
/// Everything the engine lists, in the order it listed it.
/// </summary>
/// <remarks>
/// What kind of model each of these is stays unanswered here. It used to be answered right in
/// this method, by looking for the word "embed" in the name: the text models were the ones
/// without it, the embedding models the ones with it. That reading lost bge-m3 and all-minilm,
/// which say what they are through another word, and handed them to the chat list instead. The
/// callers ask the shared model kind detection now, the way every other provider does.
/// </remarks>
/// <param name="storeType">Which key to send along.</param>
/// <param name="apiKeyProvisional">A key from a dialog which has not stored it yet.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The models the engine named, unsorted and unfiltered.</returns>
private async Task<ModelLoadResult> LoadModels(SecretStoreType storeType, string? apiKeyProvisional, CancellationToken token)
{
var secretKey = await this.GetModelLoadingSecretKey(storeType, apiKeyProvisional, isTryingSecret: true);
@@ -211,10 +245,8 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
//
ListedModels.Shared.Report(this.ConfiguredProviderId, ListingsOf(models));
return SuccessfulModelLoadResult(models.
Where(model => !string.IsNullOrWhiteSpace(model.Id) &&
!ignorePhrases.Any(ignorePhrase => model.Id.Contains(ignorePhrase, StringComparison.InvariantCulture)) &&
filterPhrases.All( filter => model.Id.Contains(filter, StringComparison.InvariantCulture)))
return SuccessfulModelLoadResult(models
.Where(model => !string.IsNullOrWhiteSpace(model.Id))
.Select(n => new Provider.Model(n.Id, null)));
}
catch (Exception e) when (this.IsTimeoutException(e, token))
@@ -229,7 +261,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
if (host is not Host.LLAMA_CPP || !chatModel.IsSystemModel)
return chatModel;
var modelLoadResult = await this.LoadLlamaCppTextModels(["embed"], [], null, token);
var modelLoadResult = await this.LoadLlamaCppTextModels(null, token);
if (!modelLoadResult.Success)
return chatModel;
@@ -266,7 +298,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
return chatModel;
}
private async Task<ModelLoadResult> LoadLlamaCppTextModels(string[] ignorePhrases, string[] filterPhrases, string? apiKeyProvisional, CancellationToken token)
private async Task<ModelLoadResult> LoadLlamaCppTextModels(string? apiKeyProvisional, CancellationToken token)
{
var secretKey = await this.GetModelLoadingSecretKey(SecretStoreType.LLM_PROVIDER, apiKeyProvisional, true);
@@ -298,7 +330,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
return LlamaCppLegacyModelResult();
var models = responseModels
.Where(model => IsMatchingLlamaCppTextModel(model, ignorePhrases, filterPhrases))
.Where(this.IsMatchingLlamaCppTextModel)
.Select(model => new Provider.Model(model.Id, null))
.ToList();
@@ -330,15 +362,23 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
/// <returns>One listing per model, which says nothing for the models the engine was silent about.</returns>
private static IEnumerable<ModelListing> ListingsOf(IEnumerable<Model> models) => models.Select(model => ModelListing.For(model.Id, model.ContextWindowTokens));
private static bool IsMatchingLlamaCppTextModel(Model model, string[] ignorePhrases, string[] filterPhrases)
/// <summary>
/// Whether this is a model somebody can chat with, as far as llama.cpp and the rules say.
/// </summary>
/// <remarks>
/// Two sources, and both have to agree. What a model is made for comes from the shared rules,
/// the same answer the other engines get. What the running build of it puts out comes from
/// llama.cpp itself, which states the modalities on this route: an engine serving a model that
/// answers in something other than text knows that before any rule about the name could.
/// </remarks>
/// <param name="model">The model as llama.cpp listed it.</param>
/// <returns>True when both agree that it answers a chat in text.</returns>
private bool IsMatchingLlamaCppTextModel(Model model)
{
if (string.IsNullOrWhiteSpace(model.Id))
return false;
if (ignorePhrases.Any(ignorePhrase => model.Id.Contains(ignorePhrase, StringComparison.InvariantCultureIgnoreCase)))
return false;
if (!filterPhrases.All(filter => model.Id.Contains(filter, StringComparison.InvariantCultureIgnoreCase)))
if (!new Provider.Model(model.Id, null).IsChatModel(this.Provider))
return false;
var outputModalities = model.Architecture?.OutputModalities;
+15 -10
View File
@@ -76,7 +76,7 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, new Uri("https://
/// <inheritdoc />
public override async Task<ModelLoadResult> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, ["grok-"], apiKeyProvisional, token);
var result = await this.LoadModels(SecretStoreType.LLM_PROVIDER, apiKeyProvisional, token);
return result with
{
//
@@ -108,19 +108,24 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, new Uri("https://
#endregion
private Task<ModelLoadResult> LoadModels(SecretStoreType storeType, string[] prefixes, string? apiKeyProvisional, CancellationToken token)
/// <summary>
/// Reads the xAI catalog, whole.
/// </summary>
/// <remarks>
/// Every name in it begins with "grok", which is why the prefix this used to filter by never
/// took anything away -- and why it said nothing either. What it did carry was Grok 2, appended
/// to every answer whether xAI still served it or not. It does not: the catalog has moved on to
/// Grok 4, and an entry nobody can talk to is worse than one missing from the list.
///
/// What the catalog does hold besides the chat models is five names which draw or film. The
/// caller asks the registry about those.
/// </remarks>
private Task<ModelLoadResult> LoadModels(SecretStoreType storeType, string? apiKeyProvisional, CancellationToken token)
{
return this.LoadModelsResponse<ModelsResponse>(
storeType,
"models",
modelResponse => modelResponse.Data.Where(model => prefixes.Any(prefix => model.Id.StartsWith(prefix, StringComparison.InvariantCulture)))
.Concat([
new Model
{
Id = "grok-2-latest",
DisplayName = "Grok 2.0 (latest)",
}
]),
modelResponse => modelResponse.Data,
apiKeyProvisional, token: token);
}
}