feat: support multiple keys per AI Bridge provider (#24683)

## Description

Adds support for configuring multiple API keys per AI Bridge provider. This PR introduces the configuration parsing and validation only; wiring the key pools into the aibridge providers will happen in upstream PRs.

## Changes

Providers now accept a comma-separated list of keys via the `KEYS` env var (or a single key via the existing `KEY` var). The two are mutually exclusive. Bedrock follows the same pattern with `BEDROCK_ACCESS_KEYS` / `BEDROCK_ACCESS_KEY_SECRETS`, with an additional validation that the two slices have matching lengths.

Key validation at startup checks for empty values, duplicates, and a maximum of 5 keys per provider. 

Related to: https://github.com/coder/internal/issues/1445

> [!NOTE]
> Initially generated by Coder Agents, modified and reviewed by @ssncferreira
This commit is contained in:
Susana Ferreira
2026-04-30 09:19:32 +01:00
committed by GitHub
parent 123c8dfc02
commit 101a4082dd
5 changed files with 239 additions and 47 deletions
+20 -5
View File
@@ -110,12 +110,18 @@ func buildProviders(cfg codersdk.AIBridgeConfig) ([]aibridge.Provider, error) {
if name == "" {
name = p.Type
}
// Currently, only the first key is used, if any.
// TODO(ssncferreira): pass a keypool.Pool instead.
var key string
if len(p.Keys) > 0 {
key = p.Keys[0]
}
switch p.Type {
case aibridge.ProviderOpenAI:
providers = append(providers, aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{
Name: name,
BaseURL: p.BaseURL,
Key: p.Key,
Key: key,
APIDumpDir: p.DumpDir,
CircuitBreaker: cbConfig,
SendActorHeaders: cfg.SendActorHeaders.Value(),
@@ -124,7 +130,7 @@ func buildProviders(cfg codersdk.AIBridgeConfig) ([]aibridge.Provider, error) {
providers = append(providers, aibridge.NewAnthropicProvider(aibridge.AnthropicConfig{
Name: name,
BaseURL: p.BaseURL,
Key: p.Key,
Key: key,
APIDumpDir: p.DumpDir,
CircuitBreaker: cbConfig,
SendActorHeaders: cfg.SendActorHeaders.Value(),
@@ -148,14 +154,23 @@ func buildProviders(cfg codersdk.AIBridgeConfig) ([]aibridge.Provider, error) {
// AIBridgeProviderConfig into an aibridge AWSBedrockConfig.
// Returns nil if no Bedrock fields are set.
func bedrockConfigFromProvider(p codersdk.AIBridgeProviderConfig) *aibridge.AWSBedrockConfig {
if p.BedrockRegion == "" && p.BedrockBaseURL == "" && p.BedrockAccessKey == "" && p.BedrockAccessKeySecret == "" {
if p.BedrockRegion == "" && p.BedrockBaseURL == "" && len(p.BedrockAccessKeys) == 0 && len(p.BedrockAccessKeySecrets) == 0 {
return nil
}
// Currently, only the first key pair is used, if any.
// TODO(ssncferreira): pass a keypool.Pool instead.
var accessKey, accessKeySecret string
if len(p.BedrockAccessKeys) > 0 {
accessKey = p.BedrockAccessKeys[0]
}
if len(p.BedrockAccessKeySecrets) > 0 {
accessKeySecret = p.BedrockAccessKeySecrets[0]
}
return &aibridge.AWSBedrockConfig{
BaseURL: p.BedrockBaseURL,
Region: p.BedrockRegion,
AccessKey: p.BedrockAccessKey,
AccessKeySecret: p.BedrockAccessKeySecret,
AccessKey: accessKey,
AccessKeySecret: accessKeySecret,
Model: p.BedrockModel,
SmallFastModel: p.BedrockSmallFastModel,
}
+11 -11
View File
@@ -46,13 +46,13 @@ func TestBuildProviders(t *testing.T) {
{
Type: aibridge.ProviderAnthropic,
Name: "anthropic-zdr",
Key: "sk-zdr",
Keys: []string{"sk-zdr"},
DumpDir: "/tmp/anthropic-dump",
},
{
Type: aibridge.ProviderOpenAI,
Name: "openai-azure",
Key: "sk-azure",
Keys: []string{"sk-azure"},
BaseURL: "https://azure.openai.com",
DumpDir: "/tmp/openai-dump",
},
@@ -72,7 +72,7 @@ func TestBuildProviders(t *testing.T) {
t.Parallel()
cfg := codersdk.AIBridgeConfig{
Providers: []codersdk.AIBridgeProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI, Key: "sk-indexed"},
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI, Keys: []string{"sk-indexed"}},
},
}
cfg.LegacyOpenAI.Key = serpent.String("sk-legacy")
@@ -86,7 +86,7 @@ func TestBuildProviders(t *testing.T) {
t.Parallel()
cfg := codersdk.AIBridgeConfig{
Providers: []codersdk.AIBridgeProviderConfig{
{Type: aibridge.ProviderAnthropic, Name: aibridge.ProviderAnthropic, Key: "sk-indexed"},
{Type: aibridge.ProviderAnthropic, Name: aibridge.ProviderAnthropic, Keys: []string{"sk-indexed"}},
},
}
cfg.LegacyAnthropic.Key = serpent.String("sk-legacy")
@@ -100,7 +100,7 @@ func TestBuildProviders(t *testing.T) {
t.Parallel()
cfg := codersdk.AIBridgeConfig{
Providers: []codersdk.AIBridgeProviderConfig{
{Type: aibridge.ProviderAnthropic, Name: "anthropic-zdr", Key: "sk-zdr"},
{Type: aibridge.ProviderAnthropic, Name: "anthropic-zdr", Keys: []string{"sk-zdr"}},
},
}
cfg.LegacyOpenAI.Key = serpent.String("sk-openai")
@@ -220,9 +220,9 @@ func TestDomainsFromProviders(t *testing.T) {
providers, err := buildProviders(codersdk.AIBridgeConfig{
Providers: []codersdk.AIBridgeProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: "openai", Key: "k"},
{Type: aibridge.ProviderAnthropic, Name: "anthropic", Key: "k"},
{Type: aibridge.ProviderOpenAI, Name: "custom", Key: "k", BaseURL: "https://custom-llm.example.com:8443/api"},
{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)
@@ -244,8 +244,8 @@ func TestDomainsFromProviders(t *testing.T) {
providers, err := buildProviders(codersdk.AIBridgeConfig{
Providers: []codersdk.AIBridgeProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: "first", Key: "k", BaseURL: "https://api.example.com/v1"},
{Type: aibridge.ProviderOpenAI, Name: "second", Key: "k", BaseURL: "https://api.example.com/v2"},
{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)
@@ -269,7 +269,7 @@ func TestDomainsFromProviders(t *testing.T) {
providers, err := buildProviders(codersdk.AIBridgeConfig{
Providers: []codersdk.AIBridgeProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: "provider", Key: "k", BaseURL: "https://API.Example.COM/v1"},
{Type: aibridge.ProviderOpenAI, Name: "provider", Keys: []string{"k"}, BaseURL: "https://API.Example.COM/v1"},
},
})
require.NoError(t, err)