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
|
package provider
|
||||||
|
|
||||||
|
import "github.com/leonardotrapani/hyprvoice/internal/language"
|
||||||
|
|
||||||
// ElevenLabsProvider implements Provider for ElevenLabs services (transcription only)
|
// ElevenLabsProvider implements Provider for ElevenLabs services (transcription only)
|
||||||
type ElevenLabsProvider struct{}
|
type ElevenLabsProvider struct{}
|
||||||
|
|
||||||
@@ -16,26 +18,43 @@ func (p *ElevenLabsProvider) ValidateAPIKey(key string) bool {
|
|||||||
return len(key) > 0
|
return len(key) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ElevenLabsProvider) SupportsTranscription() bool {
|
func (p *ElevenLabsProvider) IsLocal() bool {
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ElevenLabsProvider) SupportsLLM() bool {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ElevenLabsProvider) DefaultTranscriptionModel() string {
|
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) DefaultModel(t ModelType) string {
|
||||||
|
switch t {
|
||||||
|
case Transcription:
|
||||||
return "scribe_v1"
|
return "scribe_v1"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ElevenLabsProvider) DefaultLLMModel() string {
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ElevenLabsProvider) TranscriptionModels() []string {
|
|
||||||
return []string{"scribe_v1", "scribe_v2"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ElevenLabsProvider) LLMModels() []string {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
+85
-16
@@ -1,6 +1,10 @@
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/leonardotrapani/hyprvoice/internal/language"
|
||||||
|
)
|
||||||
|
|
||||||
// GroqProvider implements Provider for Groq services
|
// GroqProvider implements Provider for Groq services
|
||||||
type GroqProvider struct{}
|
type GroqProvider struct{}
|
||||||
@@ -17,26 +21,91 @@ func (p *GroqProvider) ValidateAPIKey(key string) bool {
|
|||||||
return strings.HasPrefix(key, "gsk_")
|
return strings.HasPrefix(key, "gsk_")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *GroqProvider) SupportsTranscription() bool {
|
func (p *GroqProvider) IsLocal() bool {
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *GroqProvider) SupportsLLM() bool {
|
func (p *GroqProvider) Models() []Model {
|
||||||
return true
|
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"
|
return "whisper-large-v3-turbo"
|
||||||
}
|
case LLM:
|
||||||
|
|
||||||
func (p *GroqProvider) DefaultLLMModel() string {
|
|
||||||
return "llama-3.3-70b-versatile"
|
return "llama-3.3-70b-versatile"
|
||||||
}
|
}
|
||||||
|
return ""
|
||||||
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"}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
|
import "github.com/leonardotrapani/hyprvoice/internal/language"
|
||||||
|
|
||||||
// MistralProvider implements Provider for Mistral services (transcription only)
|
// MistralProvider implements Provider for Mistral services (transcription only)
|
||||||
type MistralProvider struct{}
|
type MistralProvider struct{}
|
||||||
|
|
||||||
@@ -16,26 +18,43 @@ func (p *MistralProvider) ValidateAPIKey(key string) bool {
|
|||||||
return len(key) > 0
|
return len(key) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MistralProvider) SupportsTranscription() bool {
|
func (p *MistralProvider) IsLocal() bool {
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *MistralProvider) SupportsLLM() bool {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MistralProvider) DefaultTranscriptionModel() string {
|
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) DefaultModel(t ModelType) string {
|
||||||
|
switch t {
|
||||||
|
case Transcription:
|
||||||
return "voxtral-mini-latest"
|
return "voxtral-mini-latest"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MistralProvider) DefaultLLMModel() string {
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MistralProvider) TranscriptionModels() []string {
|
|
||||||
return []string{"voxtral-mini-latest", "voxtral-mini-2507"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *MistralProvider) LLMModels() []string {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
+74
-16
@@ -1,6 +1,10 @@
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/leonardotrapani/hyprvoice/internal/language"
|
||||||
|
)
|
||||||
|
|
||||||
// OpenAIProvider implements Provider for OpenAI services
|
// OpenAIProvider implements Provider for OpenAI services
|
||||||
type OpenAIProvider struct{}
|
type OpenAIProvider struct{}
|
||||||
@@ -17,26 +21,80 @@ func (p *OpenAIProvider) ValidateAPIKey(key string) bool {
|
|||||||
return strings.HasPrefix(key, "sk-")
|
return strings.HasPrefix(key, "sk-")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *OpenAIProvider) SupportsTranscription() bool {
|
func (p *OpenAIProvider) IsLocal() bool {
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *OpenAIProvider) SupportsLLM() bool {
|
func (p *OpenAIProvider) Models() []Model {
|
||||||
return true
|
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"
|
return "whisper-1"
|
||||||
}
|
case LLM:
|
||||||
|
|
||||||
func (p *OpenAIProvider) DefaultLLMModel() string {
|
|
||||||
return "gpt-4o-mini"
|
return "gpt-4o-mini"
|
||||||
}
|
}
|
||||||
|
return ""
|
||||||
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"}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// Provider defines the interface for a transcription/LLM service provider
|
// Provider defines the interface for a transcription/LLM service provider
|
||||||
type Provider interface {
|
type Provider interface {
|
||||||
Name() string
|
Name() string
|
||||||
RequiresAPIKey() bool
|
RequiresAPIKey() bool
|
||||||
ValidateAPIKey(key string) bool
|
ValidateAPIKey(key string) bool
|
||||||
SupportsTranscription() bool
|
IsLocal() bool
|
||||||
SupportsLLM() bool
|
Models() []Model
|
||||||
DefaultTranscriptionModel() string
|
DefaultModel(t ModelType) string
|
||||||
DefaultLLMModel() string
|
|
||||||
TranscriptionModels() []string
|
|
||||||
LLMModels() []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProviderConfig holds configuration for a single provider
|
// ProviderConfig holds configuration for a single provider
|
||||||
@@ -50,7 +53,7 @@ func ListProviders() []string {
|
|||||||
func ListProvidersWithTranscription() []string {
|
func ListProvidersWithTranscription() []string {
|
||||||
var names []string
|
var names []string
|
||||||
for name, p := range registry {
|
for name, p := range registry {
|
||||||
if p.SupportsTranscription() {
|
if hasModelsOfType(p, Transcription) {
|
||||||
names = append(names, name)
|
names = append(names, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,9 +64,104 @@ func ListProvidersWithTranscription() []string {
|
|||||||
func ListProvidersWithLLM() []string {
|
func ListProvidersWithLLM() []string {
|
||||||
var names []string
|
var names []string
|
||||||
for name, p := range registry {
|
for name, p := range registry {
|
||||||
if p.SupportsLLM() {
|
if hasModelsOfType(p, LLM) {
|
||||||
names = append(names, name)
|
names = append(names, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return names
|
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
|
name string
|
||||||
hasTranscription bool
|
hasTranscription bool
|
||||||
hasLLM bool
|
hasLLM bool
|
||||||
|
isLocal bool
|
||||||
defaultTransModel string
|
defaultTransModel string
|
||||||
defaultLLMModel string
|
defaultLLMModel string
|
||||||
}{
|
}{
|
||||||
{"openai", true, true, "whisper-1", "gpt-4o-mini"},
|
{"openai", true, true, false, "whisper-1", "gpt-4o-mini"},
|
||||||
{"groq", true, true, "whisper-large-v3-turbo", "llama-3.3-70b-versatile"},
|
{"groq", true, true, false, "whisper-large-v3-turbo", "llama-3.3-70b-versatile"},
|
||||||
{"mistral", true, false, "voxtral-mini-latest", ""},
|
{"mistral", true, false, false, "voxtral-mini-latest", ""},
|
||||||
{"elevenlabs", true, false, "scribe_v1", ""},
|
{"elevenlabs", true, false, false, "scribe_v1", ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range providers {
|
for _, tc := range providers {
|
||||||
@@ -30,32 +31,38 @@ func TestProviderInterface(t *testing.T) {
|
|||||||
t.Errorf("Name() = %q, want %q", p.Name(), tc.name)
|
t.Errorf("Name() = %q, want %q", p.Name(), tc.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.SupportsTranscription() != tc.hasTranscription {
|
hasTranscription := len(ModelsOfType(p, Transcription)) > 0
|
||||||
t.Errorf("SupportsTranscription() = %v, want %v", p.SupportsTranscription(), tc.hasTranscription)
|
if hasTranscription != tc.hasTranscription {
|
||||||
|
t.Errorf("hasTranscription = %v, want %v", hasTranscription, tc.hasTranscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.SupportsLLM() != tc.hasLLM {
|
hasLLM := len(ModelsOfType(p, LLM)) > 0
|
||||||
t.Errorf("SupportsLLM() = %v, want %v", p.SupportsLLM(), tc.hasLLM)
|
if hasLLM != tc.hasLLM {
|
||||||
|
t.Errorf("hasLLM = %v, want %v", hasLLM, tc.hasLLM)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.DefaultTranscriptionModel() != tc.defaultTransModel {
|
if p.IsLocal() != tc.isLocal {
|
||||||
t.Errorf("DefaultTranscriptionModel() = %q, want %q", p.DefaultTranscriptionModel(), tc.defaultTransModel)
|
t.Errorf("IsLocal() = %v, want %v", p.IsLocal(), tc.isLocal)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.DefaultLLMModel() != tc.defaultLLMModel {
|
if p.DefaultModel(Transcription) != tc.defaultTransModel {
|
||||||
t.Errorf("DefaultLLMModel() = %q, want %q", p.DefaultLLMModel(), tc.defaultLLMModel)
|
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() {
|
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 {
|
if tc.hasTranscription && len(ModelsOfType(p, Transcription)) == 0 {
|
||||||
t.Error("TranscriptionModels() should not be empty for transcription provider")
|
t.Error("should have transcription models")
|
||||||
}
|
}
|
||||||
|
|
||||||
if tc.hasLLM && len(p.LLMModels()) == 0 {
|
if tc.hasLLM && len(ModelsOfType(p, LLM)) == 0 {
|
||||||
t.Error("LLMModels() should not be empty for LLM provider")
|
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
|
var llmProviders []string
|
||||||
for _, name := range configuredProviders {
|
for _, name := range configuredProviders {
|
||||||
p := provider.GetProvider(name)
|
p := provider.GetProvider(name)
|
||||||
if p != nil && p.SupportsLLM() {
|
if p != nil && len(provider.ModelsOfType(p, provider.LLM)) > 0 {
|
||||||
llmProviders = append(llmProviders, name)
|
llmProviders = append(llmProviders, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func editTranscription(cfg *config.Config, configuredProviders []string) ([]stri
|
|||||||
var transcriptionOptions []huh.Option[string]
|
var transcriptionOptions []huh.Option[string]
|
||||||
for _, name := range configuredProviders {
|
for _, name := range configuredProviders {
|
||||||
p := provider.GetProvider(name)
|
p := provider.GetProvider(name)
|
||||||
if p != nil && p.SupportsTranscription() {
|
if p != nil && len(provider.ModelsOfType(p, provider.Transcription)) > 0 {
|
||||||
switch name {
|
switch name {
|
||||||
case "openai":
|
case "openai":
|
||||||
transcriptionOptions = append(transcriptionOptions,
|
transcriptionOptions = append(transcriptionOptions,
|
||||||
|
|||||||
@@ -26,3 +26,17 @@ Started: Sun Feb 1 12:22:47 AM CET 2026
|
|||||||
- Helper methods: NeedsDownload(), IsStreaming(), SupportsLanguage(code), SupportsAllLanguages()
|
- Helper methods: NeedsDownload(), IsStreaming(), SupportsLanguage(code), SupportsAllLanguages()
|
||||||
- SupportsLanguage("") always returns true (auto always allowed)
|
- SupportsLanguage("") always returns true (auto always allowed)
|
||||||
- All tests passing, typecheck passes
|
- 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",
|
"ValidateModelLanguage returns nil for '' (auto) on any model",
|
||||||
"Typecheck passes"
|
"Typecheck passes"
|
||||||
],
|
],
|
||||||
"passes": false
|
"passes": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Define BatchAdapter and StreamingAdapter interfaces",
|
"title": "Define BatchAdapter and StreamingAdapter interfaces",
|
||||||
|
|||||||
Reference in New Issue
Block a user