mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-09-01 09:09:19 +00:00
feat(transcription): add Chat Completions API provider for audio transcription (#1130)
* feat(transcription): add Chat Completions API provider for audio transcription
The existing transcription pipeline only supports the OpenAI Whisper API
(/v1/audio/transcriptions with multipart upload). Providers like OpenRouter
expose audio transcription through the Chat Completions API instead, using
base64-encoded audio in the `input_audio` content type.
Add `ChatCompletionsTranscriptionProvider` that sends audio as base64 in
a chat completion request and extracts the transcript from the response.
Compatible with OpenRouter, OpenAI GPT-4o-audio, and any provider that
supports audio input via Chat Completions.
Config changes:
- TRANSCRIPTION_PROVIDER=chat_completions selects the new provider
- TRANSCRIPTION_API_KEY overrides provider-specific keys
- LLM_API_KEY used as fallback for chat_completions provider
- Default model per provider (whisper-1 for openai, gemini-2.0-flash for
chat_completions)
* style: address review feedback — formatting, idiomatic patterns
- Fix rustfmt formatting for provider constructor chain
- Use or_else for resolve_api_key priority chain (Gemini review)
- Use trim_end_matches('/') instead of while loop (Gemini review)
---------
Co-authored-by: SMKRV <[email protected]>
This commit is contained in:
+64
-13
@@ -9,11 +9,15 @@ use crate::settings::Settings;
|
||||
pub struct TranscriptionConfig {
|
||||
/// Whether audio transcription is enabled.
|
||||
pub enabled: bool,
|
||||
/// Provider: "openai" (default).
|
||||
/// Provider: "openai" (default) or "chat_completions".
|
||||
pub provider: String,
|
||||
/// OpenAI API key (reuses OPENAI_API_KEY).
|
||||
pub openai_api_key: Option<SecretString>,
|
||||
/// Model to use (default: "whisper-1").
|
||||
/// Explicit transcription API key (overrides provider-specific keys).
|
||||
pub api_key: Option<SecretString>,
|
||||
/// LLM API key (reuses LLM_API_KEY, used as fallback for chat_completions).
|
||||
pub llm_api_key: Option<SecretString>,
|
||||
/// Model to use (default depends on provider).
|
||||
pub model: String,
|
||||
/// Base URL override for the transcription API.
|
||||
pub base_url: Option<String>,
|
||||
@@ -25,6 +29,8 @@ impl Default for TranscriptionConfig {
|
||||
enabled: false,
|
||||
provider: "openai".to_string(),
|
||||
openai_api_key: None,
|
||||
api_key: None,
|
||||
llm_api_key: None,
|
||||
model: "whisper-1".to_string(),
|
||||
base_url: None,
|
||||
}
|
||||
@@ -42,8 +48,15 @@ impl TranscriptionConfig {
|
||||
optional_env("TRANSCRIPTION_PROVIDER")?.unwrap_or_else(|| "openai".to_string());
|
||||
|
||||
let openai_api_key = optional_env("OPENAI_API_KEY")?.map(SecretString::from);
|
||||
let api_key = optional_env("TRANSCRIPTION_API_KEY")?.map(SecretString::from);
|
||||
let llm_api_key = optional_env("LLM_API_KEY")?.map(SecretString::from);
|
||||
|
||||
let model = optional_env("TRANSCRIPTION_MODEL")?.unwrap_or_else(|| "whisper-1".to_string());
|
||||
let default_model = match provider.as_str() {
|
||||
"chat_completions" => "google/gemini-2.0-flash-001",
|
||||
_ => "whisper-1",
|
||||
};
|
||||
let model =
|
||||
optional_env("TRANSCRIPTION_MODEL")?.unwrap_or_else(|| default_model.to_string());
|
||||
|
||||
let base_url = optional_env("TRANSCRIPTION_BASE_URL")?;
|
||||
|
||||
@@ -51,29 +64,67 @@ impl TranscriptionConfig {
|
||||
enabled,
|
||||
provider,
|
||||
openai_api_key,
|
||||
api_key,
|
||||
llm_api_key,
|
||||
model,
|
||||
base_url,
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve the API key for the configured provider.
|
||||
///
|
||||
/// Priority: `TRANSCRIPTION_API_KEY` > provider-specific key.
|
||||
fn resolve_api_key(&self) -> Option<&SecretString> {
|
||||
self.api_key
|
||||
.as_ref()
|
||||
.or_else(|| match self.provider.as_str() {
|
||||
"chat_completions" => self.llm_api_key.as_ref().or(self.openai_api_key.as_ref()),
|
||||
_ => self.openai_api_key.as_ref(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create the transcription provider if enabled and configured.
|
||||
pub fn create_provider(&self) -> Option<Box<dyn crate::transcription::TranscriptionProvider>> {
|
||||
if !self.enabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Currently only OpenAI Whisper is supported; more providers can be
|
||||
// added here with a match on self.provider.
|
||||
let api_key = self.openai_api_key.as_ref()?;
|
||||
tracing::info!(model = %self.model, "Audio transcription enabled via OpenAI Whisper");
|
||||
let api_key = self.resolve_api_key()?;
|
||||
|
||||
let mut provider = crate::transcription::OpenAiWhisperProvider::new(api_key.clone())
|
||||
.with_model(&self.model);
|
||||
match self.provider.as_str() {
|
||||
"chat_completions" => {
|
||||
tracing::info!(
|
||||
model = %self.model,
|
||||
"Audio transcription enabled via Chat Completions API"
|
||||
);
|
||||
|
||||
if let Some(ref base_url) = self.base_url {
|
||||
provider = provider.with_base_url(base_url);
|
||||
let mut provider = crate::transcription::ChatCompletionsTranscriptionProvider::new(
|
||||
api_key.clone(),
|
||||
)
|
||||
.with_model(&self.model);
|
||||
|
||||
if let Some(ref base_url) = self.base_url {
|
||||
provider = provider.with_base_url(base_url);
|
||||
}
|
||||
|
||||
Some(Box::new(provider))
|
||||
}
|
||||
_ => {
|
||||
tracing::info!(
|
||||
model = %self.model,
|
||||
"Audio transcription enabled via OpenAI Whisper"
|
||||
);
|
||||
|
||||
let mut provider =
|
||||
crate::transcription::OpenAiWhisperProvider::new(api_key.clone())
|
||||
.with_model(&self.model);
|
||||
|
||||
if let Some(ref base_url) = self.base_url {
|
||||
provider = provider.with_base_url(base_url);
|
||||
}
|
||||
|
||||
Some(Box::new(provider))
|
||||
}
|
||||
}
|
||||
|
||||
Some(Box::new(provider))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user