refactor: load AI providers from the database at startup (#25672)

Replace the env-based `BuildProviders` with a DB-backed loader. The database is now the single source of truth for runtime provider configuration; env config arrives via `SeedAIProvidersFromEnv` (run at boot) and `BuildProviders` reads it back as `aibridge.Provider` instances. `cli/server.go` and `enterprise/cli/server.go` both call the same path, so aibridged and aibridgeproxyd see the same provider set.

Per-provider `DumpDir` is replaced by a top-level `CODER_AI_GATEWAY_DUMP_DIR` base; each provider's effective dump path is `<base>/<provider name>`.
This commit is contained in:
Danny Kopping
2026-05-26 15:57:01 +02:00
committed by GitHub
parent dfd7ca3b98
commit 282ab7de34
19 changed files with 570 additions and 258 deletions
+12 -24
View File
@@ -6,11 +6,8 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/aibridge"
agplcli "github.com/coder/coder/v2/cli"
"github.com/coder/coder/v2/codersdk"
)
func TestDomainsFromProviders(t *testing.T) {
@@ -19,14 +16,11 @@ func TestDomainsFromProviders(t *testing.T) {
t.Run("ExtractsHostnames", func(t *testing.T) {
t.Parallel()
providers, err := agplcli.BuildProviders(codersdk.AIBridgeConfig{
Providers: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: "openai", Keys: []string{"k"}},
{Type: aibridge.ProviderAnthropic, Name: "anthropic", Keys: []string{"k"}},
{Type: aibridge.ProviderOpenAI, Name: "custom", Keys: []string{"k"}, BaseURL: "https://custom-llm.example.com:8443/api"},
},
})
require.NoError(t, err)
providers := []aibridge.Provider{
aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{Name: "openai", BaseURL: "https://api.openai.com/v1/"}),
aibridge.NewAnthropicProvider(aibridge.AnthropicConfig{Name: "anthropic", BaseURL: "https://api.anthropic.com/"}, nil),
aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{Name: "custom", BaseURL: "https://custom-llm.example.com:8443/api"}),
}
domains, mapping := domainsFromProviders(providers)
@@ -43,13 +37,10 @@ func TestDomainsFromProviders(t *testing.T) {
t.Run("DeduplicatesSameHost", func(t *testing.T) {
t.Parallel()
providers, err := agplcli.BuildProviders(codersdk.AIBridgeConfig{
Providers: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: "first", Keys: []string{"k"}, BaseURL: "https://api.example.com/v1"},
{Type: aibridge.ProviderOpenAI, Name: "second", Keys: []string{"k"}, BaseURL: "https://api.example.com/v2"},
},
})
require.NoError(t, err)
providers := []aibridge.Provider{
aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{Name: "first", BaseURL: "https://api.example.com/v1"}),
aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{Name: "second", BaseURL: "https://api.example.com/v2"}),
}
domains, mapping := domainsFromProviders(providers)
@@ -68,12 +59,9 @@ func TestDomainsFromProviders(t *testing.T) {
t.Run("CaseInsensitive", func(t *testing.T) {
t.Parallel()
providers, err := agplcli.BuildProviders(codersdk.AIBridgeConfig{
Providers: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: "provider", Keys: []string{"k"}, BaseURL: "https://API.Example.COM/v1"},
},
})
require.NoError(t, err)
providers := []aibridge.Provider{
aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{Name: "provider", BaseURL: "https://API.Example.COM/v1"}),
}
domains, mapping := domainsFromProviders(providers)
+1 -1
View File
@@ -167,7 +167,7 @@ func (r *RootCmd) Server(_ func()) *serpent.Command {
// in-memory roundtripper regardless of license); only the proxy
// daemon remains enterprise-gated by config.
if options.DeploymentValues.AI.BridgeProxyConfig.Enabled.Value() {
providers, err := agplcli.BuildProviders(options.DeploymentValues.AI.BridgeConfig)
providers, err := agplcli.BuildProviders(ctx, options.Database, options.DeploymentValues.AI.BridgeConfig, options.Logger.Named("aibridge.providers"))
if err != nil {
return nil, nil, xerrors.Errorf("build AI providers: %w", err)
}
+6
View File
@@ -114,6 +114,12 @@ AI GATEWAY OPTIONS:
with AI budgets. "highest" selects the group with the largest spend
limit, and is currently the only supported value.
--ai-gateway-dump-dir string, $CODER_AI_GATEWAY_DUMP_DIR
Base directory for dumping AI Bridge request/response pairs to disk
for debugging. When set, each provider writes under a subdirectory
named after the provider. Sensitive headers are redacted. Leave empty
to disable.
--ai-gateway-allow-byok bool, $CODER_AI_GATEWAY_ALLOW_BYOK (default: true)
Allow users to provide their own LLM API keys or subscriptions. When
disabled, only centralized key authentication is permitted.