summaryrefslogtreecommitdiff
path: root/internal/llm
diff options
context:
space:
mode:
Diffstat (limited to 'internal/llm')
-rw-r--r--internal/llm/anthropic.go4
-rw-r--r--internal/llm/ollama.go4
-rw-r--r--internal/llm/openai.go4
-rw-r--r--internal/llm/openrouter.go4
-rw-r--r--internal/llm/provider.go29
-rw-r--r--internal/llm/test_helpers_test.go12
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 }