feat: add provider interface and registry

This commit is contained in:
leonardotrapani
2026-01-31 20:34:33 +01:00
parent 5a4d99538f
commit 933bb6fc32
8 changed files with 392 additions and 1 deletions
+41
View File
@@ -0,0 +1,41 @@
package provider
// ElevenLabsProvider implements Provider for ElevenLabs services (transcription only)
type ElevenLabsProvider struct{}
func (p *ElevenLabsProvider) Name() string {
return "elevenlabs"
}
func (p *ElevenLabsProvider) RequiresAPIKey() bool {
return true
}
func (p *ElevenLabsProvider) ValidateAPIKey(key string) bool {
// ElevenLabs API keys don't have a consistent prefix, just check non-empty
return len(key) > 0
}
func (p *ElevenLabsProvider) SupportsTranscription() bool {
return true
}
func (p *ElevenLabsProvider) SupportsLLM() bool {
return false
}
func (p *ElevenLabsProvider) DefaultTranscriptionModel() string {
return "scribe_v1"
}
func (p *ElevenLabsProvider) DefaultLLMModel() string {
return ""
}
func (p *ElevenLabsProvider) TranscriptionModels() []string {
return []string{"scribe_v1", "scribe_v2"}
}
func (p *ElevenLabsProvider) LLMModels() []string {
return nil
}
+42
View File
@@ -0,0 +1,42 @@
package provider
import "strings"
// GroqProvider implements Provider for Groq services
type GroqProvider struct{}
func (p *GroqProvider) Name() string {
return "groq"
}
func (p *GroqProvider) RequiresAPIKey() bool {
return true
}
func (p *GroqProvider) ValidateAPIKey(key string) bool {
return strings.HasPrefix(key, "gsk_")
}
func (p *GroqProvider) SupportsTranscription() bool {
return true
}
func (p *GroqProvider) SupportsLLM() bool {
return true
}
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"}
}
+41
View File
@@ -0,0 +1,41 @@
package provider
// MistralProvider implements Provider for Mistral services (transcription only)
type MistralProvider struct{}
func (p *MistralProvider) Name() string {
return "mistral"
}
func (p *MistralProvider) RequiresAPIKey() bool {
return true
}
func (p *MistralProvider) ValidateAPIKey(key string) bool {
// Mistral API keys don't have a consistent prefix, just check non-empty
return len(key) > 0
}
func (p *MistralProvider) SupportsTranscription() bool {
return true
}
func (p *MistralProvider) SupportsLLM() bool {
return false
}
func (p *MistralProvider) DefaultTranscriptionModel() string {
return "voxtral-mini-latest"
}
func (p *MistralProvider) DefaultLLMModel() string {
return ""
}
func (p *MistralProvider) TranscriptionModels() []string {
return []string{"voxtral-mini-latest", "voxtral-mini-2507"}
}
func (p *MistralProvider) LLMModels() []string {
return nil
}
+42
View File
@@ -0,0 +1,42 @@
package provider
import "strings"
// OpenAIProvider implements Provider for OpenAI services
type OpenAIProvider struct{}
func (p *OpenAIProvider) Name() string {
return "openai"
}
func (p *OpenAIProvider) RequiresAPIKey() bool {
return true
}
func (p *OpenAIProvider) ValidateAPIKey(key string) bool {
return strings.HasPrefix(key, "sk-")
}
func (p *OpenAIProvider) SupportsTranscription() bool {
return true
}
func (p *OpenAIProvider) SupportsLLM() bool {
return true
}
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"}
}
+69
View File
@@ -0,0 +1,69 @@
package provider
// 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
}
// ProviderConfig holds configuration for a single provider
type ProviderConfig struct {
APIKey string `toml:"api_key"`
}
var registry = make(map[string]Provider)
func init() {
Register(&OpenAIProvider{})
Register(&GroqProvider{})
Register(&MistralProvider{})
Register(&ElevenLabsProvider{})
}
// Register adds a provider to the registry
func Register(p Provider) {
registry[p.Name()] = p
}
// GetProvider returns a provider by name, or nil if not found
func GetProvider(name string) Provider {
return registry[name]
}
// ListProviders returns all registered provider names
func ListProviders() []string {
names := make([]string, 0, len(registry))
for name := range registry {
names = append(names, name)
}
return names
}
// ListProvidersWithTranscription returns providers that support transcription
func ListProvidersWithTranscription() []string {
var names []string
for name, p := range registry {
if p.SupportsTranscription() {
names = append(names, name)
}
}
return names
}
// ListProvidersWithLLM returns providers that support LLM
func ListProvidersWithLLM() []string {
var names []string
for name, p := range registry {
if p.SupportsLLM() {
names = append(names, name)
}
}
return names
}
+139
View File
@@ -0,0 +1,139 @@
package provider
import (
"slices"
"testing"
)
func TestProviderInterface(t *testing.T) {
providers := []struct {
name string
hasTranscription bool
hasLLM 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", ""},
}
for _, tc := range providers {
t.Run(tc.name, func(t *testing.T) {
p := GetProvider(tc.name)
if p == nil {
t.Fatalf("GetProvider(%q) returned nil", tc.name)
}
if p.Name() != tc.name {
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)
}
if p.SupportsLLM() != tc.hasLLM {
t.Errorf("SupportsLLM() = %v, want %v", p.SupportsLLM(), tc.hasLLM)
}
if p.DefaultTranscriptionModel() != tc.defaultTransModel {
t.Errorf("DefaultTranscriptionModel() = %q, want %q", p.DefaultTranscriptionModel(), tc.defaultTransModel)
}
if p.DefaultLLMModel() != tc.defaultLLMModel {
t.Errorf("DefaultLLMModel() = %q, want %q", p.DefaultLLMModel(), tc.defaultLLMModel)
}
if !p.RequiresAPIKey() {
t.Error("RequiresAPIKey() should be true for all providers")
}
if tc.hasTranscription && len(p.TranscriptionModels()) == 0 {
t.Error("TranscriptionModels() should not be empty for transcription provider")
}
if tc.hasLLM && len(p.LLMModels()) == 0 {
t.Error("LLMModels() should not be empty for LLM provider")
}
})
}
}
func TestGetProviderNotFound(t *testing.T) {
p := GetProvider("nonexistent")
if p != nil {
t.Errorf("GetProvider(nonexistent) should return nil, got %v", p)
}
}
func TestListProviders(t *testing.T) {
providers := ListProviders()
expected := []string{"openai", "groq", "mistral", "elevenlabs"}
for _, name := range expected {
if !slices.Contains(providers, name) {
t.Errorf("ListProviders() missing %q", name)
}
}
}
func TestListProvidersWithTranscription(t *testing.T) {
providers := ListProvidersWithTranscription()
// All providers support transcription
expected := []string{"openai", "groq", "mistral", "elevenlabs"}
for _, name := range expected {
if !slices.Contains(providers, name) {
t.Errorf("ListProvidersWithTranscription() missing %q", name)
}
}
}
func TestListProvidersWithLLM(t *testing.T) {
providers := ListProvidersWithLLM()
expected := []string{"openai", "groq"}
for _, name := range expected {
if !slices.Contains(providers, name) {
t.Errorf("ListProvidersWithLLM() missing %q", name)
}
}
// Mistral and ElevenLabs should NOT be in the list
notExpected := []string{"mistral", "elevenlabs"}
for _, name := range notExpected {
if slices.Contains(providers, name) {
t.Errorf("ListProvidersWithLLM() should not include %q", name)
}
}
}
func TestValidateAPIKey(t *testing.T) {
tests := []struct {
provider string
key string
valid bool
}{
{"openai", "sk-abc123", true},
{"openai", "invalid", false},
{"openai", "", false},
{"groq", "gsk_abc123", true},
{"groq", "invalid", false},
{"groq", "", false},
{"mistral", "any-non-empty", true},
{"mistral", "", false},
{"elevenlabs", "any-non-empty", true},
{"elevenlabs", "", false},
}
for _, tc := range tests {
t.Run(tc.provider+"_"+tc.key, func(t *testing.T) {
p := GetProvider(tc.provider)
if p.ValidateAPIKey(tc.key) != tc.valid {
t.Errorf("ValidateAPIKey(%q) = %v, want %v", tc.key, !tc.valid, tc.valid)
}
})
}
}
+17
View File
@@ -0,0 +1,17 @@
# Ralph Progress Log
Started: Sat Jan 31 08:30:51 PM CET 2026
---
## Task 1: Create Provider interface and registry - COMPLETE
Created internal/provider package with:
- Provider interface with all required methods
- ProviderConfig struct for API key storage
- 4 provider implementations: OpenAI, Groq, Mistral, ElevenLabs
- Registry with GetProvider(), ListProviders(), ListProvidersWithLLM(), ListProvidersWithTranscription()
- Comprehensive tests (all passing)
Key decisions:
- OpenAI and Groq support both transcription + LLM
- Mistral and ElevenLabs are transcription-only
- ValidateAPIKey checks prefix for OpenAI (sk-) and Groq (gsk_), accepts any non-empty for others
+1 -1
View File
@@ -22,7 +22,7 @@
"Capability methods return correct values", "Capability methods return correct values",
"Typecheck passes" "Typecheck passes"
], ],
"passes": false "passes": true
}, },
{ {
"title": "Refactor config to unified provider structure", "title": "Refactor config to unified provider structure",