diff --git a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs index 2a63180b..410efac4 100644 --- a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs +++ b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs @@ -93,6 +93,16 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, new Uri( } /// + /// + /// 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. + /// public override Task GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default) { // Source: https://docs.fireworks.ai/api-reference/audio-transcriptions#param-model diff --git a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs index 54b32e57..3b77344a 100644 --- a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs +++ b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs @@ -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 /// @@ -123,13 +129,27 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("ht } /// - public override Task GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default) + /// + /// 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. + /// + public override async Task 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 diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index 5464a4e0..17e65b98 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -124,13 +124,16 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U } /// - public override Task GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default) + public override async Task 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 @@ -145,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); } -} +} \ No newline at end of file diff --git a/app/Tests/Models/Corpus/ModelKindCorpus.cs b/app/Tests/Models/Corpus/ModelKindCorpus.cs index ac73ea69..e91d27bf 100644 --- a/app/Tests/Models/Corpus/ModelKindCorpus.cs +++ b/app/Tests/Models/Corpus/ModelKindCorpus.cs @@ -122,6 +122,13 @@ public static class ModelKindCorpus new(SELF_HOSTED, "wav2vec2-large-xlsr-53", TRANSCRIPTION), new(MISTRAL, "voxtral-mini-latest", TRANSCRIPTION), + // The rest of what Mistral actually serves, off its own catalog. The app offered the one + // name above alone, because that is the one the documentation names; these three were there + // the whole time. Both sizes come as a rolling name and as a dated snapshot. + new(MISTRAL, "voxtral-small-latest", TRANSCRIPTION), + new(MISTRAL, "voxtral-mini-2602", TRANSCRIPTION), + new(MISTRAL, "voxtral-small-2507", TRANSCRIPTION), + // NVIDIA's other speech line, once plain and once as the hub names it. The second one is // what makes the rule a segment worth keeping: the organization comes off before any rule // sees the name, so what is left has to carry the word on its own. @@ -154,6 +161,11 @@ public static class ModelKindCorpus new(SELF_HOSTED, "xtts-v2", SPEECH_SYNTHESIS), new(ALIBABA_CLOUD, "qwen-tts", SPEECH_SYNTHESIS), + + // The Voxtral which speaks instead of listening. It stands here to hold the other half of + // Mistral's transcription list: the catalog is asked now, so what is not a transcription + // model has to be kept out by what it is, not by the list having been short. + new(MISTRAL, "voxtral-mini-tts-latest", SPEECH_SYNTHESIS), ]; /// @@ -177,6 +189,12 @@ public static class ModelKindCorpus new(ALIBABA_CLOUD, "qwen-tts-realtime", REALTIME), new(ALIBABA_CLOUD, "qwen3-asr-flash-realtime", REALTIME), + // The other two of Mistral's Voxtral line. The second one carries "transcribe" and stays + // out of the transcription list all the same: whatever it does, it does over a connection + // the app cannot open, and that is what the rank on the realtime rule is for. + new(MISTRAL, "voxtral-mini-realtime-latest", REALTIME), + new(MISTRAL, "voxtral-mini-transcribe-realtime-2602", REALTIME), + // // Google's whole two-way line, taken off its catalog rather than written from memory. It // uses the other word for the thing OpenAI calls realtime, so none of these was recognized