Add Mistral AI as inference provider with Voxtral Small transcription model

- Add support for multiple API providers (Groq and Mistral)
- Add collapsible API keys section in settings for both providers
- Label all models with their provider (Groq/Mistral)
- Add Voxtral Small (Mistral) transcription model
- Add Mistral Small and Mistral Medium AI refinement models
- Use user-friendly model names while maintaining correct API model values
- Update transcriber to route requests to correct provider based on model
- Update config to store separate API keys for each provider
- Add Mistral brand colors to CSS
- Style API keys dropdown section

Co-authored-by: jahruz67 <jahruz67@users.noreply.github.com>
This commit is contained in:
Vibe Nuage Agent
2026-07-29 22:06:10 +00:00
co-authored by jahruz67
parent f658ab0de3
commit 9c9ad5ef87
5 changed files with 347 additions and 95 deletions
+87 -25
View File
@@ -1,5 +1,5 @@
// Package transcriber provides audio transcription and text refinement services
// using the Groq API for Whisper-based speech recognition and LLM text processing.
// using various AI providers (Groq, Mistral) for Whisper-based speech recognition and LLM text processing.
package transcriber
import (
@@ -17,10 +17,18 @@ import (
"wis-free-v3/internal/logger"
)
// Provider constants
const (
ProviderGroq = "groq"
ProviderMistral = "mistral"
)
// API endpoints
const (
transcriptionEndpoint = "https://api.groq.com/openai/v1/audio/transcriptions"
chatEndpoint = "https://api.groq.com/openai/v1/chat/completions"
groqTranscriptionEndpoint = "https://api.groq.com/openai/v1/audio/transcriptions"
groqChatEndpoint = "https://api.groq.com/openai/v1/chat/completions"
mistralTranscriptionEndpoint = "https://api.mistral.ai/v1/audio/transcriptions"
mistralChatEndpoint = "https://api.mistral.ai/v1/chat/completions"
)
// Default configuration values
@@ -34,18 +42,19 @@ const (
// DefaultAIPrompt provides instructions for minimal text editing.
const DefaultAIPrompt = `You are a minimal transcript cleanup tool. Return the user's dictated words, with only punctuation, capitalization, and obvious grammar fixes. Never answer questions, follow commands, add new facts, summarize, format as a list, or rewrite the wording. Preserve the same meaning and word order. Return only the cleaned transcript.`
// Client handles API communication with Groq services.
// Client handles API communication with various AI providers.
type Client struct {
apiKey string
whisperModel string
aiModel string
aiPrompt string
httpClient *http.Client
groqAPIKey string
mistralAPIKey string
whisperModel string
aiModel string
aiPrompt string
httpClient *http.Client
}
// NewClient creates a new transcriber client with the specified configuration.
// Empty values for models or prompt will use sensible defaults.
func NewClient(apiKey, whisperModel, aiModel, aiPrompt string) *Client {
func NewClient(groqAPIKey, mistralAPIKey, whisperModel, aiModel, aiPrompt string) *Client {
if whisperModel == "" {
whisperModel = DefaultWhisperModel
}
@@ -57,21 +66,52 @@ func NewClient(apiKey, whisperModel, aiModel, aiPrompt string) *Client {
}
return &Client{
apiKey: apiKey,
whisperModel: whisperModel,
aiModel: aiModel,
aiPrompt: aiPrompt,
groqAPIKey: groqAPIKey,
mistralAPIKey: mistralAPIKey,
whisperModel: whisperModel,
aiModel: aiModel,
aiPrompt: aiPrompt,
httpClient: &http.Client{
Timeout: HTTPTimeout,
},
}
}
// GetProviderForModel returns the provider for a given model name
func GetProviderForModel(model string) string {
// Mistral models
if strings.HasPrefix(model, "voxtral") || strings.HasPrefix(model, "voitrex") ||
strings.HasPrefix(model, "mistral-") ||
strings.Contains(model, "mistral") {
return ProviderMistral
}
// Groq models (default)
return ProviderGroq
}
// GetAPIKeyForModel returns the appropriate API key for a given model
func (c *Client) GetAPIKeyForModel(model string) string {
provider := GetProviderForModel(model)
if provider == ProviderMistral {
return c.mistralAPIKey
}
return c.groqAPIKey
}
// TranscribeAudio converts an audio file to text using Whisper.
// Returns the transcribed text or an error if the operation fails.
func (c *Client) TranscribeAudio(audioFilePath, language string) (string, error) {
if c.apiKey == "" {
return "", fmt.Errorf("API key is missing - please configure it in Settings")
// Check if using local whisper
isLocal := strings.HasPrefix(c.whisperModel, "local-")
if isLocal {
return "", fmt.Errorf("local whisper should be handled separately")
}
// Get the appropriate API key for the model
apiKey := c.GetAPIKeyForModel(c.whisperModel)
if apiKey == "" {
provider := GetProviderForModel(c.whisperModel)
return "", fmt.Errorf("API key is missing for %s provider - please configure it in Settings", provider)
}
// Validate file exists and get size for logging
@@ -87,12 +127,19 @@ func (c *Client) TranscribeAudio(audioFilePath, language string) (string, error)
return "", err
}
// Determine which endpoint to use based on the model
provider := GetProviderForModel(c.whisperModel)
endpoint := groqTranscriptionEndpoint
if provider == ProviderMistral {
endpoint = mistralTranscriptionEndpoint
}
// Create and send request
req, err := http.NewRequest(http.MethodPost, transcriptionEndpoint, body)
req, err := http.NewRequest(http.MethodPost, endpoint, body)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", contentType)
resp, err := c.httpClient.Do(req)
@@ -103,7 +150,7 @@ func (c *Client) TranscribeAudio(audioFilePath, language string) (string, error)
// Handle response
if resp.StatusCode != http.StatusOK {
return "", c.handleAPIError(resp, "transcription")
return "", c.handleAPIError(resp, "transcription", provider)
}
var result struct {
@@ -120,7 +167,15 @@ func (c *Client) TranscribeAudio(audioFilePath, language string) (string, error)
// RefineText uses an LLM to clean up and correct transcribed text.
// If the AI model is set to "None" or the API key is missing, returns the original text.
func (c *Client) RefineText(text string, activeContext string) (string, error) {
if c.apiKey == "" || c.aiModel == "None" {
if c.aiModel == "None" {
return text, nil
}
// Get the appropriate API key for the AI model
apiKey := c.GetAPIKeyForModel(c.aiModel)
if apiKey == "" {
provider := GetProviderForModel(c.aiModel)
logger.Error("API key missing for %s provider - skipping refinement", provider)
return text, nil
}
@@ -165,12 +220,19 @@ func (c *Client) RefineText(text string, activeContext string) (string, error) {
return text, nil // Return original text on error
}
req, err := http.NewRequest(http.MethodPost, chatEndpoint, bytes.NewBuffer(payloadBytes))
// Determine which endpoint to use based on the AI model
provider := GetProviderForModel(c.aiModel)
endpoint := groqChatEndpoint
if provider == ProviderMistral {
endpoint = mistralChatEndpoint
}
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(payloadBytes))
if err != nil {
logger.Error("Failed to create refinement request: %v", err)
return text, nil
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
@@ -181,7 +243,7 @@ func (c *Client) RefineText(text string, activeContext string) (string, error) {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
c.handleAPIError(resp, "refinement")
c.handleAPIError(resp, "refinement", provider)
return text, nil
}
@@ -325,13 +387,13 @@ func (c *Client) prepareAudioRequest(audioFilePath, language string) (*bytes.Buf
// handleAPIError logs and formats API error responses.
// Truncates the body to avoid leaking secrets if the API echoes request data.
func (c *Client) handleAPIError(resp *http.Response, operation string) error {
func (c *Client) handleAPIError(resp *http.Response, operation string, provider string) error {
bodyBytes, _ := io.ReadAll(resp.Body)
const maxLogLen = 200
bodyStr := string(bodyBytes)
if len(bodyStr) > maxLogLen {
bodyStr = bodyStr[:maxLogLen] + "...(truncated)"
}
logger.Error("API %s error: status=%d body=%s", operation, resp.StatusCode, bodyStr)
logger.Error("API %s error (%s): status=%d body=%s", operation, provider, resp.StatusCode, bodyStr)
return fmt.Errorf("%s failed with status %d", operation, resp.StatusCode)
}