Merge pull request #10 from jahruz67/vibe/mistral-provider-4e9fb5

Add Mistral AI as inference provider with Voxtral Small transcription model
This commit is contained in:
jahruz67
2026-07-29 15:09:59 -07:00
committed by GitHub
5 changed files with 347 additions and 95 deletions
+32 -12
View File
@@ -344,8 +344,12 @@ func (a *App) processRecording(recordingPath string) {
isLocal := strings.HasPrefix(a.config.WhisperModel, "local-")
// IDIOT-PROOFING: Validate API key before attempting cloud transcription
if !isLocal && !strings.HasPrefix(a.config.APIKey, "gsk_") {
err = fmt.Errorf("invalid API key - must start with gsk_")
if !isLocal {
// Get the appropriate API key for the whisper model
apiKey := a.config.GetAPIKey(transcriber.GetProviderForModel(a.config.WhisperModel))
if apiKey == "" {
err = fmt.Errorf("API key is missing for the selected provider")
}
} else if isLocal {
// Use local whisper.cpp
if a.whisperManager == nil {
@@ -424,16 +428,25 @@ func (a *App) processRecording(recordingPath string) {
// GetSettings returns the current configuration
func (a *App) GetSettings() map[string]interface{} {
conf := make(map[string]interface{})
// Mask the API key: only reveal the last 4 characters so the user can verify
// Mask the API keys: only reveal the last 4 characters so the user can verify
// which key is configured without exposing the full secret to the frontend.
if a.config.APIKey != "" {
key := a.config.APIKey
if a.config.GroqAPIKey != "" {
key := a.config.GroqAPIKey
if len(key) > 4 {
key = "****" + key[len(key)-4:]
}
conf["api_key"] = key
conf["groq_api_key"] = key
} else {
conf["api_key"] = ""
conf["groq_api_key"] = ""
}
if a.config.MistralAPIKey != "" {
key := a.config.MistralAPIKey
if len(key) > 4 {
key = "****" + key[len(key)-4:]
}
conf["mistral_api_key"] = key
} else {
conf["mistral_api_key"] = ""
}
conf["shortcut"] = a.config.Shortcut
conf["whisper_model"] = a.config.WhisperModel
@@ -459,12 +472,18 @@ func (a *App) GetSettings() map[string]interface{} {
// SaveSettings updates the configuration
func (a *App) SaveSettings(settings map[string]interface{}) string {
if val, ok := settings["api_key"].(string); ok {
if val, ok := settings["groq_api_key"].(string); ok {
// Only update the API key if it's not the masked value returned by GetSettings.
// GetSettings masks the key as "****abcd" so the frontend can show the last 4 chars.
// If the user didn't change it and sent back the masked value, preserve the real key.
if !strings.HasPrefix(val, "****") {
a.config.APIKey = val
a.config.GroqAPIKey = val
}
}
if val, ok := settings["mistral_api_key"].(string); ok {
// Only update the API key if it's not the masked value returned by GetSettings.
if !strings.HasPrefix(val, "****") {
a.config.MistralAPIKey = val
}
}
if val, ok := settings["shortcut"].(string); ok {
@@ -520,10 +539,10 @@ func (a *App) SaveSettings(settings map[string]interface{}) string {
}
// Re-init transcriber with new settings.
// Note: a.config.APIKey is already updated by the api_key field above.
// Note: a.config.GroqAPIKey is already updated by the api_key field above.
// Since GetSettings masks the key, we only update if it's not still the masked value.
a.transcriber = transcriber.NewClient(
a.config.APIKey,
a.config.GroqAPIKey,
a.config.WhisperModel,
a.config.AIModel,
a.config.AIPrompt,
@@ -613,7 +632,8 @@ func (a *App) startupHeadless() {
// Initialize transcriber
a.transcriber = transcriber.NewClient(
a.config.APIKey,
a.config.GroqAPIKey,
a.config.MistralAPIKey,
a.config.WhisperModel,
a.config.AIModel,
a.config.AIPrompt,
+104 -39
View File
@@ -24,19 +24,48 @@
<div class="section-group-label">Authentication</div>
<!-- API Key -->
<!-- API Keys Dropdown Section -->
<div class="section">
<label>Groq API Key</label>
<div class="form-control">
<div class="flex-row">
<div class="input-wrapper">
<input type="password" id="apiKey" placeholder="gsk_...">
<button class="eye-btn" onclick="toggleApiKey()" id="eyeBtn">👁️</button>
<details class="api-keys-details">
<summary class="api-keys-summary">
<span>API Keys</span>
<span id="apiKeysStatus" style="color: var(--text-2); font-size: 12px; margin-left: 8px;">
<span id="groqKeyStatus">No Groq key</span> | <span id="mistralKeyStatus">No Mistral key</span>
</span>
</summary>
<div class="api-keys-content">
<!-- Groq API Key -->
<div class="api-key-section">
<label style="margin-top: 12px; margin-bottom: 8px; display: block;">Groq API Key</label>
<div class="form-control">
<div class="flex-row">
<div class="input-wrapper">
<input type="password" id="groqApiKey" placeholder="gsk_...">
<button class="eye-btn" onclick="toggleGroqApiKey()" id="groqEyeBtn">👁️</button>
</div>
<button onclick="saveGroqApiKey()">Save</button>
</div>
</div>
<p class="hint">Get your free key at <a href="#" onclick="window.runtime.BrowserOpenURL('https://console.groq.com/keys'); return false;" style="color: var(--accent);">console.groq.com/keys</a></p>
</div>
<!-- Mistral API Key -->
<div class="api-key-section">
<label style="margin-top: 12px; margin-bottom: 8px; display: block;">Mistral API Key</label>
<div class="form-control">
<div class="flex-row">
<div class="input-wrapper">
<input type="password" id="mistralApiKey" placeholder="mx_...">
<button class="eye-btn" onclick="toggleMistralApiKey()" id="mistralEyeBtn">👁️</button>
</div>
<button onclick="saveMistralApiKey()">Save</button>
</div>
</div>
<p class="hint">Get your key at <a href="#" onclick="window.runtime.BrowserOpenURL('https://console.mistral.ai/api-keys'); return false;" style="color: var(--accent);">console.mistral.ai/api-keys</a></p>
</div>
<button onclick="saveApiKey()">Save</button>
</div>
</div>
<p class="hint">Get your free key at <a href="#" onclick="window.runtime.BrowserOpenURL('https://console.groq.com/keys'); return false;" style="color: var(--accent);">console.groq.com/keys</a></p>
</details>
</div>
<!-- Global Hotkey (hidden on Linux because Linux uses the system shortcut command below) -->
@@ -74,8 +103,8 @@
<button onclick="copyLinuxPressCommand()">Copy</button>
</div>
</div>
<p class="hint" style="margin-top: 10px;">GNOME: Settings Keyboard Custom Shortcuts Add a shortcut with the copied command.</p>
<p class="hint">KDE: System Settings Shortcuts Command/URL Add a shortcut with the copied command.</p>
<p class="hint" style="margin-top: 10px;">GNOME: Settings  Keyboard  Custom Shortcuts  Add a shortcut with the copied command.</p>
<p class="hint">KDE: System Settings  Shortcuts  Command/URL  Add a shortcut with the copied command.</p>
</details>
</div>
@@ -93,8 +122,9 @@
<div class="section">
<label>Whisper Model</label>
<select id="whisperModel" onchange="saveWhisperModel()" class="form-control">
<option value="whisper-large-v3-turbo">whisper-large-v3-turbo (Recommended)</option>
<option value="whisper-large-v3">whisper-large-v3</option>
<option value="whisper-large-v3-turbo">Whisper Large v3 Turbo (Groq)</option>
<option value="whisper-large-v3">Whisper Large v3 (Groq)</option>
<option value="voxtral-small-latest">Voxtral Small (Mistral)</option>
</select>
</div>
@@ -116,12 +146,14 @@
<label>AI Model</label>
<select id="aiModel" onchange="saveAiModel()" class="form-control">
<option value="None">None — skip refinement</option>
<option value="openai/gpt-oss-120b-high">openai/gpt-oss-120b (high reasoning)</option>
<option value="openai/gpt-oss-120b">openai/gpt-oss-120b (low reasoning)</option>
<option value="openai/gpt-oss-20b-high">openai/gpt-oss-20b (high reasoning)</option>
<option value="openai/gpt-oss-20b">openai/gpt-oss-20b (low reasoning)</option>
<option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile</option>
<option value="llama-3.1-8b-instant">llama-3.1-8b-instant (faster)</option>
<option value="openai/gpt-oss-120b-high">GPT OSS 120B High (Groq)</option>
<option value="openai/gpt-oss-120b">GPT OSS 120B (Groq)</option>
<option value="openai/gpt-oss-20b-high">GPT OSS 20B High (Groq)</option>
<option value="openai/gpt-oss-20b">GPT OSS 20B (Groq)</option>
<option value="llama-3.3-70b-versatile">Llama 3.3 70B Versatile (Groq)</option>
<option value="llama-3.1-8b-instant">Llama 3.1 8B Instant (Groq)</option>
<option value="mistral-small-latest">Mistral Small (Mistral)</option>
<option value="mistral-medium-latest">Mistral Medium (Mistral)</option>
</select>
</div>
@@ -189,7 +221,7 @@
<div id="whisperInstalled" style="display: none;">
<div style="background: var(--green-dim); border: 1px solid rgba(61, 186, 110, 0.25); border-radius: 7px; padding: 13px 16px; margin-bottom: 14px;">
<div style="color: var(--green); font-weight: 500; font-size: 13px;"> Offline Whisper installed</div>
<div style="color: var(--green); font-weight: 500; font-size: 13px;"> Offline Whisper installed</div>
<div style="font-size: 12px; color: var(--text-2); margin-top: 4px;">
Model: <span id="installedModel"></span>
</div>
@@ -208,18 +240,56 @@
</div>
<script type="module">
let apiKeyVisible = false;
let groqApiKeyVisible = false;
let mistralApiKeyVisible = false;
let linuxShortcutMode = false;
// Toggle API key visibility
window.toggleApiKey = function () {
const input = document.getElementById('apiKey');
const btn = document.getElementById('eyeBtn');
apiKeyVisible = !apiKeyVisible;
input.type = apiKeyVisible ? 'text' : 'password';
btn.textContent = apiKeyVisible ? '🙈' : '👁️';
// Toggle Groq API key visibility
window.toggleGroqApiKey = function () {
const input = document.getElementById('groqApiKey');
const btn = document.getElementById('groqEyeBtn');
groqApiKeyVisible = !groqApiKeyVisible;
input.type = groqApiKeyVisible ? 'text' : 'password';
btn.textContent = groqApiKeyVisible ? '👁️' : '👁️';
};
// Toggle Mistral API key visibility
window.toggleMistralApiKey = function () {
const input = document.getElementById('mistralApiKey');
const btn = document.getElementById('mistralEyeBtn');
mistralApiKeyVisible = !mistralApiKeyVisible;
input.type = mistralApiKeyVisible ? 'text' : 'password';
btn.textContent = mistralApiKeyVisible ? '👁️' : '👁️';
};
// Save Groq API key
window.saveGroqApiKey = async function () {
const key = document.getElementById('groqApiKey').value.trim();
await window.go.main.App.SaveSettings({ groq_api_key: key });
showSaveStatus();
updateApiKeyStatus();
};
// Save Mistral API key
window.saveMistralApiKey = async function () {
const key = document.getElementById('mistralApiKey').value.trim();
await window.go.main.App.SaveSettings({ mistral_api_key: key });
showSaveStatus();
updateApiKeyStatus();
};
// Update API key status display
function updateApiKeyStatus() {
const groqKey = document.getElementById('groqApiKey').value;
const mistralKey = document.getElementById('mistralApiKey').value;
const groqStatus = document.getElementById('groqKeyStatus');
const mistralStatus = document.getElementById('mistralKeyStatus');
groqStatus.textContent = groqKey ? (groqKey.length > 4 ? 'Groq key set' : 'Groq key') : 'No Groq key';
mistralStatus.textContent = mistralKey ? (mistralKey.length > 4 ? 'Mistral key set' : 'Mistral key') : 'No Mistral key';
}
// Record shortcut
window.recordShortcut = function () {
if (linuxShortcutMode) {
@@ -397,7 +467,8 @@
try {
const settings = await window.go.main.App.GetSettings();
document.getElementById('apiKey').value = settings.api_key || '';
document.getElementById('groqApiKey').value = settings.groq_api_key || '';
document.getElementById('mistralApiKey').value = settings.mistral_api_key || '';
const shortcutInput = document.getElementById('shortcutInput');
if (shortcutInput) {
shortcutInput.value = settings.shortcut || 'alt+z';
@@ -434,6 +505,7 @@
document.getElementById('appVersion').textContent = ver === 'dev' ? 'dev' : ('v' + ver);
renderHistoryList(settings.history || []);
updateApiKeyStatus();
} catch (err) {
console.error("Failed to load settings:", err);
}
@@ -457,13 +529,6 @@
}
}
// Save functions
window.saveApiKey = async function () {
const key = document.getElementById('apiKey').value.trim();
await window.go.main.App.SaveSettings({ api_key: key });
showSaveStatus();
};
function showSaveStatus(message = 'Saved') {
const el = document.getElementById('saveStatus');
el.textContent = message;
@@ -540,7 +605,7 @@
statusEl.style.color = 'var(--green)';
statusEl.style.border = '1px solid rgba(61, 186, 110, 0.2)';
} else {
statusEl.innerHTML = '● Offline install local Whisper for transcription';
statusEl.innerHTML = '● Offline \u2014 install local Whisper for transcription';
statusEl.style.background = 'var(--red-dim)';
statusEl.style.color = 'var(--red)';
statusEl.style.border = '1px solid rgba(224, 82, 82, 0.2)';
@@ -580,7 +645,7 @@
// Add local option to whisper dropdown
const localOption = document.createElement('option');
localOption.value = 'local-' + info.model;
localOption.textContent = ' Local ' + info.model + ' (offline)';
localOption.textContent = ' Local \u2014 ' + info.model + ' (offline)';
localOption.style.fontWeight = 'bold';
whisperSelect.insertBefore(localOption, whisperSelect.firstChild);
} else {
+97 -16
View File
@@ -18,6 +18,8 @@
--red-dim: rgba(224, 82, 82, 0.12);
--radius: 10px;
--radius-sm: 7px;
--mistral: #8a5cf0;
--mistral-dim: rgba(138, 92, 240, 0.15);
color-scheme: dark;
}
@@ -35,14 +37,14 @@ body {
-webkit-font-smoothing: antialiased;
}
/* ── Layout ── */
/* Layout */
.container {
max-width: 680px;
margin: 0 auto;
padding: 32px 28px 60px;
}
/* ── Header ── */
/* Header */
h1 {
font-size: 20px;
font-weight: 600;
@@ -64,7 +66,7 @@ h1 {
flex-wrap: nowrap;
}
/* ── Section cards ── */
/* Section cards */
.section {
background: var(--surface);
border: 1px solid var(--border);
@@ -78,7 +80,7 @@ h1 {
border-color: var(--border-hover);
}
/* ── Labels ── */
/* Labels */
label {
display: block;
font-size: 13px;
@@ -96,7 +98,7 @@ label {
line-height: 1.5;
}
/* ── Inputs ── */
/* Inputs */
input[type="text"],
input[type="password"],
select,
@@ -157,12 +159,12 @@ input[type="checkbox"] {
flex-shrink: 0;
}
/* ── Form control width ── */
/* Form control width */
.form-control {
width: 100%;
}
/* ── Input wrapper (for eye-btn) ── */
/* Input wrapper (for eye-btn) */
.input-wrapper {
position: relative;
flex: 1;
@@ -197,7 +199,7 @@ input[type="checkbox"] {
box-shadow: none !important;
}
/* ── Buttons ── */
/* Buttons */
button {
background: var(--accent);
color: #fff;
@@ -254,7 +256,7 @@ button:active {
opacity: 1 !important;
}
/* ── Save status toast ── */
/* Save status toast */
.save-status {
position: fixed;
bottom: 24px;
@@ -279,7 +281,7 @@ button:active {
opacity: 1;
}
/* ── History ── */
/* History */
.history-list {
max-height: 240px;
overflow-y: auto;
@@ -308,7 +310,7 @@ button:active {
display: block;
}
/* ── Inline status badges (online/offline) ── */
/* Inline status badges (online/offline) */
#connectionStatus {
border-radius: var(--radius-sm) !important;
font-size: 13px !important;
@@ -333,16 +335,95 @@ button:active {
background: var(--accent-hover) !important;
}
/* ── Offline Whisper section separator ── */
/* Offline Whisper section separator */
#offlineWhisperSection {
border-top: 1px solid var(--border) !important;
margin-top: 20px !important;
padding-top: 20px !important;
}
/* API Keys Dropdown Section */
.api-keys-details {
color: var(--text);
}
.api-keys-summary {
cursor: pointer;
font-weight: 500;
font-size: 13px;
color: var(--text);
padding: 2px 0;
user-select: none;
list-style: none;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
/* ── Collapsible fallback details ── */
.api-keys-summary::-webkit-details-marker {
display: none;
}
.api-keys-summary::after {
content: '\u25b6';
font-size: 10px;
color: var(--text-3);
transition: transform 0.15s ease;
display: inline-block;
margin-left: 8px;
}
.api-keys-details[open] > .api-keys-summary::after {
transform: rotate(90deg);
}
.api-keys-summary:hover {
color: var(--text);
}
.api-keys-content {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.api-key-section {
margin-bottom: 12px;
}
.api-key-section:last-child {
margin-bottom: 0;
}
/* Provider badges for model options */
select option[data-provider]::before {
content: attr(data-provider);
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
margin-right: 8px;
opacity: 0.8;
}
select option[data-provider="groq"]::before {
background: var(--accent-dim);
color: var(--accent);
}
select option[data-provider="mistral"]::before {
background: var(--mistral-dim);
color: var(--mistral);
}
select option[data-provider="local"]::before {
background: var(--green-dim);
color: var(--green);
}
/* Collapsible fallback details */
.fallback-details {
color: var(--text);
}
@@ -365,7 +446,7 @@ button:active {
}
.fallback-summary::before {
content: '';
content: '\u25b6';
font-size: 10px;
color: var(--text-3);
transition: transform 0.15s ease;
@@ -380,7 +461,7 @@ button:active {
color: var(--text);
}
/* ── Section group header divider ── */
/* Section group header divider */
.section-group-label {
font-size: 11px;
font-weight: 600;
@@ -390,7 +471,7 @@ button:active {
margin: 22px 0 8px;
}
/* ── Scrollbar ── */
/* Scrollbar */
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
+26 -2
View File
@@ -31,7 +31,8 @@ type HistoryItem struct {
// Config represents the complete application configuration.
type Config struct {
Version int `json:"version"`
APIKey string `json:"api_key"`
GroqAPIKey string `json:"groq_api_key"`
MistralAPIKey string `json:"mistral_api_key"`
Shortcut string `json:"shortcut"`
WhisperModel string `json:"whisper_model"`
AIModel string `json:"ai_model"`
@@ -50,7 +51,8 @@ var saveMu sync.Mutex
// DefaultConfig returns a new configuration with sensible default values.
func DefaultConfig() *Config {
return &Config{
APIKey: "",
GroqAPIKey: "",
MistralAPIKey: "",
Shortcut: DefaultShortcut,
WhisperModel: DefaultWhisperModel,
AIModel: DefaultAIModel,
@@ -197,3 +199,25 @@ func (c *Config) AddHistoryItem(text, timestamp string) {
func (c *Config) ClearHistory() {
c.History = []HistoryItem{}
}
// GetAPIKey returns the API key for a specific provider
func (c *Config) GetAPIKey(provider string) string {
switch provider {
case "groq":
return c.GroqAPIKey
case "mistral":
return c.MistralAPIKey
default:
return ""
}
}
// SetAPIKey sets the API key for a specific provider
func (c *Config) SetAPIKey(provider, key string) {
switch provider {
case "groq":
c.GroqAPIKey = key
case "mistral":
c.MistralAPIKey = key
}
}
+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)
}