From c23ac60b8fa46b273ed61813e9f7a5206e66d1d6 Mon Sep 17 00:00:00 2001 From: leonardotrapani Date: Sun, 1 Feb 2026 00:33:20 +0100 Subject: [PATCH] refactor provider interface to return models with metadata - replaced old TranscriptionModels/LLMModels/SupportsX methods with Models() []Model - added DefaultModel(t ModelType), IsLocal() to interface - added helpers: GetModel, ModelsOfType, FindModelByID, ModelsForLanguage, ValidateModelLanguage - all providers now return full Model metadata with SupportedLanguages, AdapterType, Endpoint - groq distil-whisper-large-v3-en marked as english-only - updated TUI to use new interface --- internal/provider/elevenlabs.go | 51 +++++--- internal/provider/groq.go | 107 +++++++++++++--- internal/provider/mistral.go | 51 +++++--- internal/provider/openai.go | 96 ++++++++++++--- internal/provider/provider.go | 114 +++++++++++++++-- internal/provider/provider_test.go | 157 +++++++++++++++++++++--- internal/tui/configure_llm.go | 2 +- internal/tui/configure_transcription.go | 2 +- progress.txt | 14 +++ tasks/prd.jsonc | 2 +- 10 files changed, 498 insertions(+), 98 deletions(-) diff --git a/internal/provider/elevenlabs.go b/internal/provider/elevenlabs.go index a36a173..fe7aed8 100644 --- a/internal/provider/elevenlabs.go +++ b/internal/provider/elevenlabs.go @@ -1,5 +1,7 @@ package provider +import "github.com/leonardotrapani/hyprvoice/internal/language" + // ElevenLabsProvider implements Provider for ElevenLabs services (transcription only) type ElevenLabsProvider struct{} @@ -16,26 +18,43 @@ func (p *ElevenLabsProvider) ValidateAPIKey(key string) bool { return len(key) > 0 } -func (p *ElevenLabsProvider) SupportsTranscription() bool { - return true -} - -func (p *ElevenLabsProvider) SupportsLLM() bool { +func (p *ElevenLabsProvider) IsLocal() bool { return false } -func (p *ElevenLabsProvider) DefaultTranscriptionModel() string { - return "scribe_v1" +func (p *ElevenLabsProvider) Models() []Model { + allLangs := language.AllLanguageCodes() + + return []Model{ + { + ID: "scribe_v1", + Name: "Scribe v1", + Description: "99 languages, best accuracy", + Type: Transcription, + Streaming: false, + Local: false, + AdapterType: "elevenlabs", + SupportedLanguages: allLangs, + Endpoint: &EndpointConfig{BaseURL: "https://api.elevenlabs.io", Path: "/v1/speech-to-text"}, + }, + { + ID: "scribe_v2", + Name: "Scribe v2", + Description: "Lower latency, real-time optimized", + Type: Transcription, + Streaming: false, + Local: false, + AdapterType: "elevenlabs", + SupportedLanguages: allLangs, + Endpoint: &EndpointConfig{BaseURL: "https://api.elevenlabs.io", Path: "/v1/speech-to-text"}, + }, + } } -func (p *ElevenLabsProvider) DefaultLLMModel() string { +func (p *ElevenLabsProvider) DefaultModel(t ModelType) string { + switch t { + case Transcription: + return "scribe_v1" + } return "" } - -func (p *ElevenLabsProvider) TranscriptionModels() []string { - return []string{"scribe_v1", "scribe_v2"} -} - -func (p *ElevenLabsProvider) LLMModels() []string { - return nil -} diff --git a/internal/provider/groq.go b/internal/provider/groq.go index 77cc4c6..8d5ef38 100644 --- a/internal/provider/groq.go +++ b/internal/provider/groq.go @@ -1,6 +1,10 @@ package provider -import "strings" +import ( + "strings" + + "github.com/leonardotrapani/hyprvoice/internal/language" +) // GroqProvider implements Provider for Groq services type GroqProvider struct{} @@ -17,26 +21,91 @@ func (p *GroqProvider) ValidateAPIKey(key string) bool { return strings.HasPrefix(key, "gsk_") } -func (p *GroqProvider) SupportsTranscription() bool { - return true +func (p *GroqProvider) IsLocal() bool { + return false } -func (p *GroqProvider) SupportsLLM() bool { - return true +func (p *GroqProvider) Models() []Model { + allLangs := language.AllLanguageCodes() + + return []Model{ + // transcription models + { + ID: "whisper-large-v3", + Name: "Whisper Large v3", + Description: "Full Whisper v3 model, best accuracy", + Type: Transcription, + Streaming: false, + Local: false, + AdapterType: "openai", + SupportedLanguages: allLangs, + Endpoint: &EndpointConfig{BaseURL: "https://api.groq.com/openai", Path: "/v1/audio/transcriptions"}, + }, + { + ID: "whisper-large-v3-turbo", + Name: "Whisper Large v3 Turbo", + Description: "Faster Whisper v3 with slightly lower accuracy", + Type: Transcription, + Streaming: false, + Local: false, + AdapterType: "openai", + SupportedLanguages: allLangs, + Endpoint: &EndpointConfig{BaseURL: "https://api.groq.com/openai", Path: "/v1/audio/transcriptions"}, + }, + { + ID: "distil-whisper-large-v3-en", + Name: "Distil Whisper Large v3 EN", + Description: "English-only, fastest option", + Type: Transcription, + Streaming: false, + Local: false, + AdapterType: "openai", + SupportedLanguages: []string{"en"}, + Endpoint: &EndpointConfig{BaseURL: "https://api.groq.com/openai", Path: "/v1/audio/transcriptions"}, + }, + // LLM models + { + ID: "llama-3.3-70b-versatile", + Name: "Llama 3.3 70B Versatile", + Description: "Most capable Llama model", + Type: LLM, + Streaming: false, + Local: false, + AdapterType: "openai", + SupportedLanguages: allLangs, + Endpoint: &EndpointConfig{BaseURL: "https://api.groq.com/openai", Path: "/v1/chat/completions"}, + }, + { + ID: "llama-3.1-8b-instant", + Name: "Llama 3.1 8B Instant", + Description: "Fast and efficient", + Type: LLM, + Streaming: false, + Local: false, + AdapterType: "openai", + SupportedLanguages: allLangs, + Endpoint: &EndpointConfig{BaseURL: "https://api.groq.com/openai", Path: "/v1/chat/completions"}, + }, + { + ID: "mixtral-8x7b-32768", + Name: "Mixtral 8x7B", + Description: "Mixture of experts model", + Type: LLM, + Streaming: false, + Local: false, + AdapterType: "openai", + SupportedLanguages: allLangs, + Endpoint: &EndpointConfig{BaseURL: "https://api.groq.com/openai", Path: "/v1/chat/completions"}, + }, + } } -func (p *GroqProvider) DefaultTranscriptionModel() string { - return "whisper-large-v3-turbo" -} - -func (p *GroqProvider) DefaultLLMModel() string { - return "llama-3.3-70b-versatile" -} - -func (p *GroqProvider) TranscriptionModels() []string { - return []string{"whisper-large-v3", "whisper-large-v3-turbo"} -} - -func (p *GroqProvider) LLMModels() []string { - return []string{"llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768"} +func (p *GroqProvider) DefaultModel(t ModelType) string { + switch t { + case Transcription: + return "whisper-large-v3-turbo" + case LLM: + return "llama-3.3-70b-versatile" + } + return "" } diff --git a/internal/provider/mistral.go b/internal/provider/mistral.go index c080701..9bd28b8 100644 --- a/internal/provider/mistral.go +++ b/internal/provider/mistral.go @@ -1,5 +1,7 @@ package provider +import "github.com/leonardotrapani/hyprvoice/internal/language" + // MistralProvider implements Provider for Mistral services (transcription only) type MistralProvider struct{} @@ -16,26 +18,43 @@ func (p *MistralProvider) ValidateAPIKey(key string) bool { return len(key) > 0 } -func (p *MistralProvider) SupportsTranscription() bool { - return true -} - -func (p *MistralProvider) SupportsLLM() bool { +func (p *MistralProvider) IsLocal() bool { return false } -func (p *MistralProvider) DefaultTranscriptionModel() string { - return "voxtral-mini-latest" +func (p *MistralProvider) Models() []Model { + allLangs := language.AllLanguageCodes() + + return []Model{ + { + ID: "voxtral-mini-latest", + Name: "Voxtral Mini Latest", + Description: "Latest Voxtral model, best for most uses", + Type: Transcription, + Streaming: false, + Local: false, + AdapterType: "openai", + SupportedLanguages: allLangs, + Endpoint: &EndpointConfig{BaseURL: "https://api.mistral.ai", Path: "/v1/audio/transcriptions"}, + }, + { + ID: "voxtral-mini-2507", + Name: "Voxtral Mini 2507", + Description: "Stable Voxtral version from July 2025", + Type: Transcription, + Streaming: false, + Local: false, + AdapterType: "openai", + SupportedLanguages: allLangs, + Endpoint: &EndpointConfig{BaseURL: "https://api.mistral.ai", Path: "/v1/audio/transcriptions"}, + }, + } } -func (p *MistralProvider) DefaultLLMModel() string { +func (p *MistralProvider) DefaultModel(t ModelType) string { + switch t { + case Transcription: + return "voxtral-mini-latest" + } return "" } - -func (p *MistralProvider) TranscriptionModels() []string { - return []string{"voxtral-mini-latest", "voxtral-mini-2507"} -} - -func (p *MistralProvider) LLMModels() []string { - return nil -} diff --git a/internal/provider/openai.go b/internal/provider/openai.go index 318ba30..941d887 100644 --- a/internal/provider/openai.go +++ b/internal/provider/openai.go @@ -1,6 +1,10 @@ package provider -import "strings" +import ( + "strings" + + "github.com/leonardotrapani/hyprvoice/internal/language" +) // OpenAIProvider implements Provider for OpenAI services type OpenAIProvider struct{} @@ -17,26 +21,80 @@ func (p *OpenAIProvider) ValidateAPIKey(key string) bool { return strings.HasPrefix(key, "sk-") } -func (p *OpenAIProvider) SupportsTranscription() bool { - return true +func (p *OpenAIProvider) IsLocal() bool { + return false } -func (p *OpenAIProvider) SupportsLLM() bool { - return true +func (p *OpenAIProvider) Models() []Model { + allLangs := language.AllLanguageCodes() + + return []Model{ + // transcription models + { + ID: "whisper-1", + Name: "Whisper 1", + Description: "OpenAI's production speech-to-text model", + Type: Transcription, + Streaming: false, + Local: false, + AdapterType: "openai", + SupportedLanguages: allLangs, + Endpoint: &EndpointConfig{BaseURL: "https://api.openai.com", Path: "/v1/audio/transcriptions"}, + }, + // LLM models + { + ID: "gpt-4o-mini", + Name: "GPT-4o Mini", + Description: "Fast and affordable GPT-4 variant", + Type: LLM, + Streaming: false, + Local: false, + AdapterType: "openai", + SupportedLanguages: allLangs, + Endpoint: &EndpointConfig{BaseURL: "https://api.openai.com", Path: "/v1/chat/completions"}, + }, + { + ID: "gpt-4o", + Name: "GPT-4o", + Description: "Most capable GPT-4 model", + Type: LLM, + Streaming: false, + Local: false, + AdapterType: "openai", + SupportedLanguages: allLangs, + Endpoint: &EndpointConfig{BaseURL: "https://api.openai.com", Path: "/v1/chat/completions"}, + }, + { + ID: "gpt-4-turbo", + Name: "GPT-4 Turbo", + Description: "Faster GPT-4 with large context window", + Type: LLM, + Streaming: false, + Local: false, + AdapterType: "openai", + SupportedLanguages: allLangs, + Endpoint: &EndpointConfig{BaseURL: "https://api.openai.com", Path: "/v1/chat/completions"}, + }, + { + ID: "gpt-3.5-turbo", + Name: "GPT-3.5 Turbo", + Description: "Fast and cost-effective", + Type: LLM, + Streaming: false, + Local: false, + AdapterType: "openai", + SupportedLanguages: allLangs, + Endpoint: &EndpointConfig{BaseURL: "https://api.openai.com", Path: "/v1/chat/completions"}, + }, + } } -func (p *OpenAIProvider) DefaultTranscriptionModel() string { - return "whisper-1" -} - -func (p *OpenAIProvider) DefaultLLMModel() string { - return "gpt-4o-mini" -} - -func (p *OpenAIProvider) TranscriptionModels() []string { - return []string{"whisper-1"} -} - -func (p *OpenAIProvider) LLMModels() []string { - return []string{"gpt-4o-mini", "gpt-4o", "gpt-4-turbo", "gpt-3.5-turbo"} +func (p *OpenAIProvider) DefaultModel(t ModelType) string { + switch t { + case Transcription: + return "whisper-1" + case LLM: + return "gpt-4o-mini" + } + return "" } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 8c5d243..4f65f4a 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1,16 +1,19 @@ package provider +import ( + "errors" + "fmt" + "strings" +) + // Provider defines the interface for a transcription/LLM service provider type Provider interface { Name() string RequiresAPIKey() bool ValidateAPIKey(key string) bool - SupportsTranscription() bool - SupportsLLM() bool - DefaultTranscriptionModel() string - DefaultLLMModel() string - TranscriptionModels() []string - LLMModels() []string + IsLocal() bool + Models() []Model + DefaultModel(t ModelType) string } // ProviderConfig holds configuration for a single provider @@ -50,7 +53,7 @@ func ListProviders() []string { func ListProvidersWithTranscription() []string { var names []string for name, p := range registry { - if p.SupportsTranscription() { + if hasModelsOfType(p, Transcription) { names = append(names, name) } } @@ -61,9 +64,104 @@ func ListProvidersWithTranscription() []string { func ListProvidersWithLLM() []string { var names []string for name, p := range registry { - if p.SupportsLLM() { + if hasModelsOfType(p, LLM) { names = append(names, name) } } return names } + +// hasModelsOfType returns true if provider has any models of the given type +func hasModelsOfType(p Provider, t ModelType) bool { + for _, m := range p.Models() { + if m.Type == t { + return true + } + } + return false +} + +// GetModel returns a model from a specific provider, or error if not found +func GetModel(providerName, modelID string) (*Model, error) { + p := GetProvider(providerName) + if p == nil { + return nil, fmt.Errorf("unknown provider: %s", providerName) + } + + for _, m := range p.Models() { + if m.ID == modelID { + return &m, nil + } + } + return nil, fmt.Errorf("model %s not found in provider %s", modelID, providerName) +} + +// ModelsOfType returns all models of the given type from a provider +func ModelsOfType(p Provider, t ModelType) []Model { + var result []Model + for _, m := range p.Models() { + if m.Type == t { + result = append(result, m) + } + } + return result +} + +// FindModelByID searches all providers for a model with the given ID +func FindModelByID(modelID string) (*Model, Provider, error) { + for _, p := range registry { + for _, m := range p.Models() { + if m.ID == modelID { + return &m, p, nil + } + } + } + return nil, nil, fmt.Errorf("model %s not found in any provider", modelID) +} + +// ModelsForLanguage returns models from a provider that support the given language +func ModelsForLanguage(p Provider, t ModelType, langCode string) []Model { + var result []Model + for _, m := range p.Models() { + if m.Type == t && m.SupportsLanguage(langCode) { + result = append(result, m) + } + } + return result +} + +// ValidateModelLanguage checks if a model supports the given language. +// Returns error with list of supported languages if not supported. +// Returns nil if langCode is "" (auto) - auto is always supported. +func ValidateModelLanguage(providerName, modelID, langCode string) error { + if langCode == "" { + return nil // auto always supported + } + + model, err := GetModel(providerName, modelID) + if err != nil { + return err + } + + if model.SupportsLanguage(langCode) { + return nil + } + + // truncate supported languages list for error message + supported := model.SupportedLanguages + if len(supported) > 10 { + supported = append(supported[:10], "...") + } + + return fmt.Errorf( + "model %s does not support language '%s'. Supported: %s", + modelID, + langCode, + strings.Join(supported, ", "), + ) +} + +var ( + ErrProviderNotFound = errors.New("provider not found") + ErrModelNotFound = errors.New("model not found") +) diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 8a69af7..58ef292 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -10,13 +10,14 @@ func TestProviderInterface(t *testing.T) { name string hasTranscription bool hasLLM bool + isLocal bool defaultTransModel string defaultLLMModel string }{ - {"openai", true, true, "whisper-1", "gpt-4o-mini"}, - {"groq", true, true, "whisper-large-v3-turbo", "llama-3.3-70b-versatile"}, - {"mistral", true, false, "voxtral-mini-latest", ""}, - {"elevenlabs", true, false, "scribe_v1", ""}, + {"openai", true, true, false, "whisper-1", "gpt-4o-mini"}, + {"groq", true, true, false, "whisper-large-v3-turbo", "llama-3.3-70b-versatile"}, + {"mistral", true, false, false, "voxtral-mini-latest", ""}, + {"elevenlabs", true, false, false, "scribe_v1", ""}, } for _, tc := range providers { @@ -30,32 +31,38 @@ func TestProviderInterface(t *testing.T) { t.Errorf("Name() = %q, want %q", p.Name(), tc.name) } - if p.SupportsTranscription() != tc.hasTranscription { - t.Errorf("SupportsTranscription() = %v, want %v", p.SupportsTranscription(), tc.hasTranscription) + hasTranscription := len(ModelsOfType(p, Transcription)) > 0 + if hasTranscription != tc.hasTranscription { + t.Errorf("hasTranscription = %v, want %v", hasTranscription, tc.hasTranscription) } - if p.SupportsLLM() != tc.hasLLM { - t.Errorf("SupportsLLM() = %v, want %v", p.SupportsLLM(), tc.hasLLM) + hasLLM := len(ModelsOfType(p, LLM)) > 0 + if hasLLM != tc.hasLLM { + t.Errorf("hasLLM = %v, want %v", hasLLM, tc.hasLLM) } - if p.DefaultTranscriptionModel() != tc.defaultTransModel { - t.Errorf("DefaultTranscriptionModel() = %q, want %q", p.DefaultTranscriptionModel(), tc.defaultTransModel) + if p.IsLocal() != tc.isLocal { + t.Errorf("IsLocal() = %v, want %v", p.IsLocal(), tc.isLocal) } - if p.DefaultLLMModel() != tc.defaultLLMModel { - t.Errorf("DefaultLLMModel() = %q, want %q", p.DefaultLLMModel(), tc.defaultLLMModel) + if p.DefaultModel(Transcription) != tc.defaultTransModel { + t.Errorf("DefaultModel(Transcription) = %q, want %q", p.DefaultModel(Transcription), tc.defaultTransModel) + } + + if p.DefaultModel(LLM) != tc.defaultLLMModel { + t.Errorf("DefaultModel(LLM) = %q, want %q", p.DefaultModel(LLM), tc.defaultLLMModel) } if !p.RequiresAPIKey() { - t.Error("RequiresAPIKey() should be true for all providers") + t.Error("RequiresAPIKey() should be true for all cloud providers") } - if tc.hasTranscription && len(p.TranscriptionModels()) == 0 { - t.Error("TranscriptionModels() should not be empty for transcription provider") + if tc.hasTranscription && len(ModelsOfType(p, Transcription)) == 0 { + t.Error("should have transcription models") } - if tc.hasLLM && len(p.LLMModels()) == 0 { - t.Error("LLMModels() should not be empty for LLM provider") + if tc.hasLLM && len(ModelsOfType(p, LLM)) == 0 { + t.Error("should have LLM models") } }) } @@ -137,3 +144,119 @@ func TestValidateAPIKey(t *testing.T) { }) } } + +func TestGetModel(t *testing.T) { + // valid provider and model + m, err := GetModel("openai", "whisper-1") + if err != nil { + t.Errorf("GetModel('openai', 'whisper-1') unexpected error: %v", err) + } + if m == nil { + t.Fatal("GetModel returned nil model") + } + if m.ID != "whisper-1" { + t.Errorf("GetModel returned model with ID %q, want 'whisper-1'", m.ID) + } + + // unknown provider + _, err = GetModel("nonexistent", "whisper-1") + if err == nil { + t.Error("GetModel('nonexistent', ...) should return error") + } + + // unknown model + _, err = GetModel("openai", "nonexistent") + if err == nil { + t.Error("GetModel('openai', 'nonexistent') should return error") + } +} + +func TestModelsOfType(t *testing.T) { + p := GetProvider("openai") + trans := ModelsOfType(p, Transcription) + llm := ModelsOfType(p, LLM) + + if len(trans) != 1 { + t.Errorf("ModelsOfType(Transcription) = %d, want 1", len(trans)) + } + if len(llm) != 4 { + t.Errorf("ModelsOfType(LLM) = %d, want 4", len(llm)) + } +} + +func TestFindModelByID(t *testing.T) { + // find model that exists + m, p, err := FindModelByID("whisper-1") + if err != nil { + t.Errorf("FindModelByID('whisper-1') unexpected error: %v", err) + } + if m == nil || p == nil { + t.Fatal("FindModelByID returned nil") + } + if m.ID != "whisper-1" { + t.Errorf("FindModelByID returned model %q, want 'whisper-1'", m.ID) + } + if p.Name() != "openai" { + t.Errorf("FindModelByID returned provider %q, want 'openai'", p.Name()) + } + + // model not found + _, _, err = FindModelByID("nonexistent") + if err == nil { + t.Error("FindModelByID('nonexistent') should return error") + } +} + +func TestModelsForLanguage(t *testing.T) { + groq := GetProvider("groq") + + // en should include all models (distil supports en) + enModels := ModelsForLanguage(groq, Transcription, "en") + if len(enModels) != 3 { + t.Errorf("ModelsForLanguage('en') = %d, want 3", len(enModels)) + } + + // es should exclude distil-whisper-large-v3-en + esModels := ModelsForLanguage(groq, Transcription, "es") + if len(esModels) != 2 { + t.Errorf("ModelsForLanguage('es') = %d, want 2 (distil excluded)", len(esModels)) + } + + // auto ("") should include all models + autoModels := ModelsForLanguage(groq, Transcription, "") + if len(autoModels) != 3 { + t.Errorf("ModelsForLanguage('') = %d, want 3 (auto returns all)", len(autoModels)) + } +} + +func TestValidateModelLanguage(t *testing.T) { + // valid language for multilingual model + err := ValidateModelLanguage("groq", "whisper-large-v3", "es") + if err != nil { + t.Errorf("ValidateModelLanguage(whisper-large-v3, 'es') unexpected error: %v", err) + } + + // invalid language for English-only model + err = ValidateModelLanguage("groq", "distil-whisper-large-v3-en", "es") + if err == nil { + t.Error("ValidateModelLanguage(distil-whisper, 'es') should return error") + } + + // auto always passes + err = ValidateModelLanguage("groq", "distil-whisper-large-v3-en", "") + if err != nil { + t.Errorf("ValidateModelLanguage(distil-whisper, '') should pass (auto): %v", err) + } + + // unknown provider + err = ValidateModelLanguage("nonexistent", "whisper-1", "en") + if err == nil { + t.Error("ValidateModelLanguage with unknown provider should return error") + } + + // unknown model + err = ValidateModelLanguage("openai", "nonexistent", "en") + if err == nil { + t.Error("ValidateModelLanguage with unknown model should return error") + } +} diff --git a/internal/tui/configure_llm.go b/internal/tui/configure_llm.go index 5000ce3..c274da0 100644 --- a/internal/tui/configure_llm.go +++ b/internal/tui/configure_llm.go @@ -13,7 +13,7 @@ func editLLM(cfg *config.Config, configuredProviders []string) ([]string, error) var llmProviders []string for _, name := range configuredProviders { p := provider.GetProvider(name) - if p != nil && p.SupportsLLM() { + if p != nil && len(provider.ModelsOfType(p, provider.LLM)) > 0 { llmProviders = append(llmProviders, name) } } diff --git a/internal/tui/configure_transcription.go b/internal/tui/configure_transcription.go index dd99539..e1fdb99 100644 --- a/internal/tui/configure_transcription.go +++ b/internal/tui/configure_transcription.go @@ -13,7 +13,7 @@ func editTranscription(cfg *config.Config, configuredProviders []string) ([]stri var transcriptionOptions []huh.Option[string] for _, name := range configuredProviders { p := provider.GetProvider(name) - if p != nil && p.SupportsTranscription() { + if p != nil && len(provider.ModelsOfType(p, provider.Transcription)) > 0 { switch name { case "openai": transcriptionOptions = append(transcriptionOptions, diff --git a/progress.txt b/progress.txt index f2a5b3d..a8776ca 100644 --- a/progress.txt +++ b/progress.txt @@ -26,3 +26,17 @@ Started: Sun Feb 1 12:22:47 AM CET 2026 - Helper methods: NeedsDownload(), IsStreaming(), SupportsLanguage(code), SupportsAllLanguages() - SupportsLanguage("") always returns true (auto always allowed) - All tests passing, typecheck passes + +### Task 4: Refactor Provider interface to return Models +- Updated `internal/provider/provider.go` Provider interface +- Replaced old methods with: Models() []Model, DefaultModel(t ModelType) string, IsLocal() bool +- Added package-level helpers: + - GetModel(providerName, modelID string) (*Model, error) + - ModelsOfType(p Provider, t ModelType) []Model + - FindModelByID(modelID string) (*Model, Provider, error) + - ModelsForLanguage(p Provider, t ModelType, langCode string) []Model + - ValidateModelLanguage(providerName, modelID, langCode string) error +- Updated all providers (openai, groq, mistral, elevenlabs) with full Model metadata +- Updated TUI files to use ModelsOfType instead of old SupportsTranscription/SupportsLLM +- Added comprehensive tests for all new helper functions +- All tests passing, typecheck passes diff --git a/tasks/prd.jsonc b/tasks/prd.jsonc index 48b153d..9516ae2 100644 --- a/tasks/prd.jsonc +++ b/tasks/prd.jsonc @@ -110,7 +110,7 @@ "ValidateModelLanguage returns nil for '' (auto) on any model", "Typecheck passes" ], - "passes": false + "passes": true }, { "title": "Define BatchAdapter and StreamingAdapter interfaces",