use crate::config::{Config, Provider};
pub(super) fn resolve_provider(cli_provider: Option<Provider>, config: &Config) -> Provider {
if let Some(p) = cli_provider {
return p;
}
config
.transcription
.as_ref()
.map(|t| t.default_provider)
.unwrap_or(Provider::Mistral)
}
pub(super) fn resolve_model(
cli_model: Option<&str>,
config: &Config,
provider: Provider,
realtime: bool,
) -> String {
if let Some(m) = cli_model {
return m.to_string();
}
match provider {
Provider::Mistral => config
.providers
.mistral
.as_ref()
.map(|c| c.model.clone())
.unwrap_or_else(|| "voxtral-mini-2507".to_string()),
Provider::OpenAI => {
if realtime {
config
.providers
.openai
.as_ref()
.map(|c| c.realtime_model.clone())
.unwrap_or_else(|| "gpt-live-transcribe".to_string())
} else {
config
.providers
.openai
.as_ref()
.map(|c| c.model.clone())
.unwrap_or_else(|| "gpt-transcribe".to_string())
}
}
Provider::Parakeet => config
.providers
.parakeet
.as_ref()
.map(|c| c.resolved_model_name())
.unwrap_or_else(|| "parakeet-tdt-0.6b-v3-int8".to_string()),
}
}
const MISTRAL_TRANSCRIPTION_MODELS: &[&str] = &["voxtral-mini-2507", "voxtral-mini-2602"];
const PARAKEET_TRANSCRIPTION_MODELS: &[&str] = &["parakeet-tdt-0.6b-v3-int8"];
const MISTRAL_REALTIME_MODELS: &[&str] = &["voxtral-mini-transcribe-realtime-2602"];
fn add_known_realtime_models(out: &mut Vec<(Provider, String, bool)>, provider: Provider) {
let models = match provider {
Provider::OpenAI => crate::transcription::openai::OPENAI_REALTIME_MODELS,
Provider::Mistral => MISTRAL_REALTIME_MODELS,
Provider::Parakeet => &[],
};
for m in models {
out.push((provider, (*m).to_string(), true));
}
}
pub(super) fn build_retry_candidates(
config: &Config,
cli_provider: Option<Provider>,
cli_model: Option<&str>,
) -> Vec<(Provider, String, bool)> {
let mut out: Vec<(Provider, String, bool)> = Vec::new();
if let (Some(provider), Some(model)) = (cli_provider, cli_model) {
let streaming = provider == Provider::OpenAI
&& crate::transcription::openai::known_model_mode(model)
== Some(crate::transcription::openai::OpenAITranscriptionMode::Realtime);
out.push((provider, model.to_string(), streaming));
}
match cli_provider {
Some(provider) => {
add_known_models_with_streaming(&mut out, provider);
add_known_realtime_models(&mut out, provider);
}
None => {
add_known_models_with_streaming(&mut out, Provider::OpenAI);
add_known_models_with_streaming(&mut out, Provider::Mistral);
add_known_realtime_models(&mut out, Provider::OpenAI);
add_known_realtime_models(&mut out, Provider::Mistral);
out.push((
Provider::OpenAI,
resolve_model(None, config, Provider::OpenAI, false),
false,
));
out.push((
Provider::Mistral,
resolve_model(None, config, Provider::Mistral, false),
false,
));
if config.providers.parakeet.is_some() {
add_known_models_with_streaming(&mut out, Provider::Parakeet);
out.push((
Provider::Parakeet,
resolve_model(None, config, Provider::Parakeet, false),
false,
));
}
}
}
out.sort_by(|a, b| {
a.0.to_string()
.cmp(&b.0.to_string())
.then_with(|| a.1.cmp(&b.1))
.then_with(|| a.2.cmp(&b.2))
});
out.dedup();
out
}
fn add_known_models_with_streaming(out: &mut Vec<(Provider, String, bool)>, provider: Provider) {
let models = match provider {
Provider::OpenAI => crate::transcription::openai::OPENAI_BATCH_MODELS,
Provider::Mistral => MISTRAL_TRANSCRIPTION_MODELS,
Provider::Parakeet => PARAKEET_TRANSCRIPTION_MODELS,
};
for m in models {
out.push((provider, (*m).to_string(), false));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{Config, ParakeetConfig, ParakeetVariant, ProvidersConfig};
use std::path::PathBuf;
fn config_with_parakeet(parakeet: Option<ParakeetConfig>) -> Config {
Config {
output_dir: PathBuf::from("/tmp/talk-rs-test"),
providers: ProvidersConfig {
mistral: None,
openai: None,
parakeet,
kokoro: None,
},
indicators: None,
transcription: None,
speak: None,
paste: None,
audio: None,
recording: None,
}
}
fn parakeet_section() -> ParakeetConfig {
ParakeetConfig {
variant: ParakeetVariant::Int8,
model_dir: None,
num_threads: 2,
model: None,
}
}
#[test]
fn no_filter_omits_parakeet_when_unconfigured() {
let config = config_with_parakeet(None);
let candidates = build_retry_candidates(&config, None, None);
assert!(
!candidates.iter().any(|(p, _, _)| *p == Provider::Parakeet),
"Parakeet must not be an auto-candidate when unconfigured; got: {:?}",
candidates
);
}
#[test]
fn no_filter_includes_parakeet_when_configured() {
let config = config_with_parakeet(Some(parakeet_section()));
let candidates = build_retry_candidates(&config, None, None);
assert!(
candidates.iter().any(|(p, _, _)| *p == Provider::Parakeet),
"Parakeet should be an auto-candidate when configured; got: {:?}",
candidates
);
assert!(
!candidates
.iter()
.any(|(p, _, streaming)| *p == Provider::Parakeet && *streaming),
"Parakeet has no realtime mode; must not appear as streaming"
);
}
#[test]
fn explicit_provider_parakeet_yields_candidates_without_config() {
let config = config_with_parakeet(None);
let candidates = build_retry_candidates(&config, Some(Provider::Parakeet), None);
assert!(
candidates.iter().any(|(p, _, _)| *p == Provider::Parakeet),
"explicit --provider parakeet must yield Parakeet candidates; got: {:?}",
candidates
);
}
#[test]
fn openai_defaults_and_known_catalogs_are_consistent() {
let config = config_with_parakeet(None);
assert_eq!(
resolve_model(None, &config, Provider::OpenAI, false),
"gpt-transcribe"
);
assert_eq!(
resolve_model(None, &config, Provider::OpenAI, true),
"gpt-live-transcribe"
);
let candidates = build_retry_candidates(&config, Some(Provider::OpenAI), None);
for model in [
"gpt-transcribe",
"whisper-1",
"gpt-4o-transcribe",
"gpt-4o-mini-transcribe",
] {
assert!(candidates.contains(&(Provider::OpenAI, model.to_string(), false)));
}
for model in ["gpt-live-transcribe", "gpt-realtime-whisper"] {
assert!(candidates.contains(&(Provider::OpenAI, model.to_string(), true)));
}
}
#[test]
fn explicit_openai_realtime_model_is_only_classified_realtime() {
let config = config_with_parakeet(None);
for model in crate::transcription::openai::OPENAI_REALTIME_MODELS {
let candidates = build_retry_candidates(&config, Some(Provider::OpenAI), Some(model));
assert!(candidates.contains(&(Provider::OpenAI, (*model).to_string(), true)));
assert!(!candidates.contains(&(Provider::OpenAI, (*model).to_string(), false)));
}
}
#[test]
fn explicit_openai_batch_model_is_only_classified_batch() {
let config = config_with_parakeet(None);
for model in crate::transcription::openai::OPENAI_BATCH_MODELS {
let candidates = build_retry_candidates(&config, Some(Provider::OpenAI), Some(model));
assert!(candidates.contains(&(Provider::OpenAI, (*model).to_string(), false)));
assert!(!candidates.contains(&(Provider::OpenAI, (*model).to_string(), true)));
}
}
}