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
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
+86
-17
@@ -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 {
|
||||
func (p *GroqProvider) DefaultModel(t ModelType) string {
|
||||
switch t {
|
||||
case Transcription:
|
||||
return "whisper-large-v3-turbo"
|
||||
}
|
||||
|
||||
func (p *GroqProvider) DefaultLLMModel() string {
|
||||
case LLM:
|
||||
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"}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+75
-17
@@ -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 {
|
||||
func (p *OpenAIProvider) DefaultModel(t ModelType) string {
|
||||
switch t {
|
||||
case Transcription:
|
||||
return "whisper-1"
|
||||
}
|
||||
|
||||
func (p *OpenAIProvider) DefaultLLMModel() string {
|
||||
case LLM:
|
||||
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"}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@
|
||||
"ValidateModelLanguage returns nil for '' (auto) on any model",
|
||||
"Typecheck passes"
|
||||
],
|
||||
"passes": false
|
||||
"passes": true
|
||||
},
|
||||
{
|
||||
"title": "Define BatchAdapter and StreamingAdapter interfaces",
|
||||
|
||||
Reference in New Issue
Block a user