feat: list OpenRouter's transcription models, which its plain catalogue hides
Nightly Build / build (push) Successful in 7m33s
Nightly Build / build (push) Successful in 7m33s
Adding a transcribe model on OpenRouter logged "provider 'OpenRouter' does not support transcription model listing" and dropped the user into typing a model id by hand — `list_transcribe_models` was never implemented for it, so the trait default answered None. OpenRouter does serve the catalogue: it is the same `/models` envelope under `output_modalities=transcription`. The filter is not an optimisation — those models carry `architecture.modality = "audio->transcription"` and are absent from the unfiltered listing, so nothing else surfaces them. `fetch_openai_models` therefore takes an optional raw query string; plain OpenAI has no filters, but a gateway hosting several service kinds needs to say which catalogue it wants. Transcription itself already worked: OpenRouter accepts the OpenAI-style multipart body that `OpenAiAudioTranscriber` sends, so only the listing was missing. The feed says nothing about per-model languages, hence the empty `languages` — the hint stays the user's to set.
This commit is contained in:
@@ -48,13 +48,22 @@ pub(crate) fn extra_with_reasoning(
|
|||||||
/// the `data` envelope so each provider can map and enrich them with its own
|
/// the `data` envelope so each provider can map and enrich them with its own
|
||||||
/// heuristics. `api_key` is sent as a bearer token when present (local
|
/// heuristics. `api_key` is sent as a bearer token when present (local
|
||||||
/// providers pass `None`); `who` is the display name used in error messages.
|
/// providers pass `None`); `who` is the display name used in error messages.
|
||||||
|
///
|
||||||
|
/// `query` appends a raw query string (no leading `?`). Plain OpenAI has no
|
||||||
|
/// filters, but a gateway hosting several service kinds needs one to say which
|
||||||
|
/// catalogue it wants — OpenRouter's STT models are absent from the unfiltered
|
||||||
|
/// listing and only appear under `output_modalities=transcription`.
|
||||||
pub(crate) async fn fetch_openai_models(
|
pub(crate) async fn fetch_openai_models(
|
||||||
http: &reqwest::Client,
|
http: &reqwest::Client,
|
||||||
base_url: &str,
|
base_url: &str,
|
||||||
api_key: Option<&str>,
|
api_key: Option<&str>,
|
||||||
|
query: Option<&str>,
|
||||||
who: &str,
|
who: &str,
|
||||||
) -> Result<Vec<serde_json::Value>> {
|
) -> Result<Vec<serde_json::Value>> {
|
||||||
let url = format!("{}/models", base_url.trim_end_matches('/'));
|
let url = match query {
|
||||||
|
Some(q) => format!("{}/models?{q}", base_url.trim_end_matches('/')),
|
||||||
|
None => format!("{}/models", base_url.trim_end_matches('/')),
|
||||||
|
};
|
||||||
let mut req = http.get(&url);
|
let mut req = http.get(&url);
|
||||||
if let Some(key) = api_key {
|
if let Some(key) = api_key {
|
||||||
req = req.bearer_auth(key);
|
req = req.bearer_auth(key);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::image_generate::ImageGenerateModelRecord;
|
|||||||
use crate::image_generate::openrouter_image::OpenRouterImageGenerator;
|
use crate::image_generate::openrouter_image::OpenRouterImageGenerator;
|
||||||
use crate::llm::{LlmModelRecord, LlmProviderRecord};
|
use crate::llm::{LlmModelRecord, LlmProviderRecord};
|
||||||
use crate::llm::providers::{RemoteLlmModelInfo, build_openai_llm, fetch_openai_models};
|
use crate::llm::providers::{RemoteLlmModelInfo, build_openai_llm, fetch_openai_models};
|
||||||
use crate::transcribe::TranscribeModelRecord;
|
use crate::transcribe::{RemoteTranscribeModelInfo, TranscribeModelRecord};
|
||||||
use crate::transcribe::openai_audio::OpenAiAudioTranscriber;
|
use crate::transcribe::openai_audio::OpenAiAudioTranscriber;
|
||||||
use crate::tts::TtsModelRecord;
|
use crate::tts::TtsModelRecord;
|
||||||
use crate::tts::openai_tts::OpenAiTtsSynthesiser;
|
use crate::tts::openai_tts::OpenAiTtsSynthesiser;
|
||||||
@@ -47,7 +47,7 @@ impl OpenRouterProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_catalog(&self, api_key: &str) -> Result<Vec<RemoteLlmModelInfo>> {
|
async fn fetch_catalog(&self, api_key: &str) -> Result<Vec<RemoteLlmModelInfo>> {
|
||||||
let raw = fetch_openai_models(&self.http, "https://openrouter.ai/api/v1", Some(api_key), "OpenRouter").await?;
|
let raw = fetch_openai_models(&self.http, "https://openrouter.ai/api/v1", Some(api_key), None, "OpenRouter").await?;
|
||||||
|
|
||||||
let models = raw
|
let models = raw
|
||||||
.iter()
|
.iter()
|
||||||
@@ -97,6 +97,35 @@ impl OpenRouterProvider {
|
|||||||
|
|
||||||
Ok(models)
|
Ok(models)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// OpenRouter's STT catalogue is the same `/models` envelope, filtered by
|
||||||
|
/// output modality. The filter is not an optimisation: these models carry
|
||||||
|
/// `architecture.modality = "audio->transcription"` and are **absent** from
|
||||||
|
/// the unfiltered listing, so nothing but this query surfaces them.
|
||||||
|
///
|
||||||
|
/// The feed says nothing about which languages each model covers (every
|
||||||
|
/// entry here is multilingual or auto-detecting anyway), hence the empty
|
||||||
|
/// `languages` — the per-model hint stays the user's to set.
|
||||||
|
async fn fetch_transcribe_catalog(&self, api_key: &str) -> Result<Vec<RemoteTranscribeModelInfo>> {
|
||||||
|
let raw = fetch_openai_models(
|
||||||
|
&self.http, "https://openrouter.ai/api/v1", Some(api_key),
|
||||||
|
Some("output_modalities=transcription"), "OpenRouter",
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
Ok(raw
|
||||||
|
.iter()
|
||||||
|
.filter_map(|m| {
|
||||||
|
let id = m["id"].as_str()?.to_string();
|
||||||
|
let name = m["name"].as_str().unwrap_or(&id).to_string();
|
||||||
|
Some(RemoteTranscribeModelInfo {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description: m["description"].as_str().map(String::from),
|
||||||
|
languages: Vec::new(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@@ -113,6 +142,12 @@ impl ApiProvider for OpenRouterProvider {
|
|||||||
Ok(Some(self.fetch_catalog(api_key).await?))
|
Ok(Some(self.fetch_catalog(api_key).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_transcribe_models(&self, record: &LlmProviderRecord) -> Result<Option<Vec<RemoteTranscribeModelInfo>>> {
|
||||||
|
let api_key = record.api_key.as_deref()
|
||||||
|
.ok_or_else(|| anyhow!("provider '{}': api_key required for openrouter model listing", record.name))?;
|
||||||
|
Ok(Some(self.fetch_transcribe_catalog(api_key).await?))
|
||||||
|
}
|
||||||
|
|
||||||
fn reasoning_mode(&self, _model_id: &str, capabilities: &[String]) -> Option<ReasoningMode> {
|
fn reasoning_mode(&self, _model_id: &str, capabilities: &[String]) -> Option<ReasoningMode> {
|
||||||
// Fallback for stored/manually-added models with no catalog descriptor.
|
// Fallback for stored/manually-added models with no catalog descriptor.
|
||||||
// The precise per-model set comes from `parse_reasoning` in the catalog;
|
// The precise per-model set comes from `parse_reasoning` in the catalog;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ impl RequestyProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_catalog(&self, api_key: &str) -> Result<Vec<RemoteLlmModelInfo>> {
|
async fn fetch_catalog(&self, api_key: &str) -> Result<Vec<RemoteLlmModelInfo>> {
|
||||||
let raw = fetch_openai_models(self.http(), BASE_URL, Some(api_key), "Requesty").await?;
|
let raw = fetch_openai_models(self.http(), BASE_URL, Some(api_key), None, "Requesty").await?;
|
||||||
Ok(raw.iter().filter_map(map_model).collect())
|
Ok(raw.iter().filter_map(map_model).collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user