Ask the endpoint for transcription models before falling back to the documented list

This commit is contained in:
Thorsten Sommer 2026-09-19 17:34:34 +02:00
parent 9595c6a19f
commit a4485ce618
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
4 changed files with 64 additions and 13 deletions

View File

@ -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

View File

@ -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

View File

@ -124,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

View File

@ -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),
];
/// <summary>
@ -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