feat: add provider interface and registry
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -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"}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user