diff options
Diffstat (limited to 'internal/llm')
| -rw-r--r-- | internal/llm/anthropic.go | 4 | ||||
| -rw-r--r-- | internal/llm/ollama.go | 4 | ||||
| -rw-r--r-- | internal/llm/openai.go | 4 | ||||
| -rw-r--r-- | internal/llm/openrouter.go | 4 | ||||
| -rw-r--r-- | internal/llm/provider.go | 29 | ||||
| -rw-r--r-- | internal/llm/test_helpers_test.go | 12 |
6 files changed, 31 insertions, 26 deletions
diff --git a/internal/llm/anthropic.go b/internal/llm/anthropic.go index 7da72b3..17c40ad 100644 --- a/internal/llm/anthropic.go +++ b/internal/llm/anthropic.go @@ -85,10 +85,6 @@ var ( _ Streamer = (*anthropicClient)(nil) ) -func init() { - RegisterProvider("anthropic", anthropicProviderFactory) -} - func anthropicProviderFactory(cfg Config, keys ProviderKeys) (Client, error) { if strings.TrimSpace(keys.AnthropicAPIKey) == "" { return nil, missingAPIKeyError("anthropic", "ANTHROPIC_API_KEY", "HEXAI_ANTHROPIC_API_KEY") diff --git a/internal/llm/ollama.go b/internal/llm/ollama.go index e212466..b2cecfa 100644 --- a/internal/llm/ollama.go +++ b/internal/llm/ollama.go @@ -45,10 +45,6 @@ type ollamaChatResponse struct { Error string `json:"error,omitempty"` } -func init() { - RegisterProvider("ollama", ollamaProviderFactory) -} - func ollamaProviderFactory(cfg Config, _ ProviderKeys) (Client, error) { return newOllamaWithTimeout( cfg.OllamaBaseURL, diff --git a/internal/llm/openai.go b/internal/llm/openai.go index cf18d9b..a119fe7 100644 --- a/internal/llm/openai.go +++ b/internal/llm/openai.go @@ -78,10 +78,6 @@ type oaStreamChunk struct { } `json:"error,omitempty"` } -func init() { - RegisterProvider("openai", openAIProviderFactory) -} - func openAIProviderFactory(cfg Config, keys ProviderKeys) (Client, error) { if strings.TrimSpace(keys.OpenAIAPIKey) == "" { return nil, missingAPIKeyError("openai", "OPENAI_API_KEY", "HEXAI_OPENAI_API_KEY") diff --git a/internal/llm/openrouter.go b/internal/llm/openrouter.go index 451e9ad..aa8a4d4 100644 --- a/internal/llm/openrouter.go +++ b/internal/llm/openrouter.go @@ -27,10 +27,6 @@ var ( _ Streamer = openRouterClient{} ) -func init() { - RegisterProvider("openrouter", openRouterProviderFactory) -} - func openRouterProviderFactory(cfg Config, keys ProviderKeys) (Client, error) { if strings.TrimSpace(keys.OpenRouterAPIKey) == "" { return nil, missingAPIKeyError("openrouter", "OPENROUTER_API_KEY", "HEXAI_OPENROUTER_API_KEY") diff --git a/internal/llm/provider.go b/internal/llm/provider.go index 3c72181..6c0c04b 100644 --- a/internal/llm/provider.go +++ b/internal/llm/provider.go @@ -103,20 +103,17 @@ type ProviderKeys struct { // ProviderFactory builds an LLM client for a named provider. type ProviderFactory func(cfg Config, keys ProviderKeys) (Client, error) -// providerRegistry is a package-level singleton populated by init() calls in -// each provider file (anthropic.go, openai.go, etc.). It must be a -// package-level var — rather than a constructor argument — because Go's -// init() mechanism runs before any application code, and the alternative -// (an explicit RegisterAll() in main) would require every binary that uses -// the llm package to manually enumerate all providers. The RWMutex makes the -// map safe for the rare case where RegisterProvider is called from a test -// goroutine after init() has completed. +// providerRegistry is a package-level singleton populated via RegisterAllProviders. +// Callers (binaries and tests) must call RegisterAllProviders before creating any +// clients. The RWMutex makes the map safe for concurrent reads once populated. var ( - providerRegistryMu sync.RWMutex - providerRegistry = map[string]ProviderFactory{} + providerRegistryMu sync.RWMutex + providerRegistry = map[string]ProviderFactory{} + registerProvidersOnce sync.Once ) // RegisterProvider registers a provider factory by normalized name. +// Panics on empty name, nil factory, or duplicate registration. func RegisterProvider(name string, factory ProviderFactory) { normalized := normalizeProvider(name) if normalized == "" { @@ -133,6 +130,18 @@ func RegisterProvider(name string, factory ProviderFactory) { providerRegistry[normalized] = factory } +// RegisterAllProviders registers all built-in LLM providers (anthropic, openai, +// openrouter, ollama). It is safe to call from multiple entry points because the +// actual registration runs only once via sync.Once. +func RegisterAllProviders() { + registerProvidersOnce.Do(func() { + RegisterProvider("anthropic", anthropicProviderFactory) + RegisterProvider("openai", openAIProviderFactory) + RegisterProvider("openrouter", openRouterProviderFactory) + RegisterProvider("ollama", ollamaProviderFactory) + }) +} + // NewFromConfig creates an LLM client using only the supplied configuration. // The OpenAI API key is supplied separately and may be read from the environment // by the caller; other environment-based configuration is not used. diff --git a/internal/llm/test_helpers_test.go b/internal/llm/test_helpers_test.go index 051747a..b6553bf 100644 --- a/internal/llm/test_helpers_test.go +++ b/internal/llm/test_helpers_test.go @@ -1,3 +1,15 @@ package llm +import ( + "os" + "testing" +) + +// TestMain registers all built-in providers before any test runs, mirroring +// the explicit registration that happens in production binaries. +func TestMain(m *testing.M) { + RegisterAllProviders() + os.Exit(m.Run()) +} + func f64p(v float64) *float64 { return &v } |
