feat: configure multiple AI Bridge providers of the same type (#23948)

_Disclaimer: produced mostly by Claude Opus 4.6 following detailed
planning._

## Summary
- Support multiple instances of the same AI Bridge provider type via
indexed env vars (`CODER_AIBRIDGE_PROVIDER_<N>_<KEY>`), following the
`CODER_EXTERNAL_AUTH_<N>_<KEY>` pattern
- Existing single-provider env vars (`CODER_AIBRIDGE_OPENAI_KEY`, etc.)
continue to work unchanged
- Setting both a legacy env var and an indexed provider with the same
name errors at startup to prevent silent misconfiguration
- Mark legacy provider fields (`OpenAI`, `Anthropic`, `Bedrock`) as
deprecated in `AIBridgeConfig` in favor of `Providers`
  ## Example
```sh
CODER_AIBRIDGE_PROVIDER_0_TYPE=anthropic
CODER_AIBRIDGE_PROVIDER_0_NAME=anthropic-corp
CODER_AIBRIDGE_PROVIDER_0_KEY=sk-ant-corp-xxx

CODER_AIBRIDGE_PROVIDER_0_BASE_URL=https://llm-proxy.internal.example.com/anthropic

CODER_AIBRIDGE_PROVIDER_1_TYPE=anthropic
CODER_AIBRIDGE_PROVIDER_1_NAME=anthropic-direct
  CODER_AIBRIDGE_PROVIDER_1_KEY=sk-ant-direct-yyy         
  ```
  Each instance is routed by name:
- /api/v2/aibridge/**anthropic-corp**/v1/messages
- /api/v2/aibridge/**anthropic-direct**/v1/messages
Closes
[AIGOV-157](https://linear.app/codercom/issue/AIGOV-157/spike-to-understand-if-there-is-a-simple-way-to-handle-multi-api-key)

---------

Signed-off-by: Danny Kopping <danny@coder.com>
This commit is contained in:
Danny Kopping
2026-04-15 07:59:37 +00:00
committed by GitHub
parent 730edba87a
commit 08045c2aac
17 changed files with 1326 additions and 163 deletions
+8 -8
View File
@@ -1361,7 +1361,7 @@ func TestListChatModels(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
values := chatDeploymentValues(t)
values.AI.BridgeConfig.OpenAI.Key = serpent.String("deployment-openai-key")
values.AI.BridgeConfig.LegacyOpenAI.Key = serpent.String("deployment-openai-key")
client := newChatClientWithDeploymentValues(t, values)
_ = coderdtest.CreateFirstUser(t, client.Client)
@@ -1749,7 +1749,7 @@ func TestListChatProviders(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
values := chatDeploymentValues(t)
values.AI.BridgeConfig.OpenAI.Key = serpent.String("deployment-openai-key")
values.AI.BridgeConfig.LegacyOpenAI.Key = serpent.String("deployment-openai-key")
client := newChatClientWithDeploymentValues(t, values)
_ = coderdtest.CreateFirstUser(t, client.Client)
@@ -1903,7 +1903,7 @@ func TestCreateChatProvider(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
values := chatDeploymentValues(t)
values.AI.BridgeConfig.OpenAI.Key = serpent.String("deployment-openai-key")
values.AI.BridgeConfig.LegacyOpenAI.Key = serpent.String("deployment-openai-key")
client := newChatClientWithDeploymentValues(t, values)
_ = coderdtest.CreateFirstUser(t, client.Client)
@@ -2105,7 +2105,7 @@ func TestUpdateChatProvider(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
values := chatDeploymentValues(t)
values.AI.BridgeConfig.OpenAI.Key = serpent.String("deployment-openai-key")
values.AI.BridgeConfig.LegacyOpenAI.Key = serpent.String("deployment-openai-key")
client := newChatClientWithDeploymentValues(t, values)
_ = coderdtest.CreateFirstUser(t, client.Client)
@@ -2341,9 +2341,9 @@ func TestChatProviderAPIKeysFromDeploymentValues(t *testing.T) {
t.Parallel()
values := chatDeploymentValues(t)
values.AI.BridgeConfig.OpenAI.Key = serpent.String("deployment-openai-key")
values.AI.BridgeConfig.Anthropic.Key = serpent.String("deployment-anthropic-key")
values.AI.BridgeConfig.OpenAI.BaseURL = serpent.String("https://custom-openai.example.com")
values.AI.BridgeConfig.LegacyOpenAI.Key = serpent.String("deployment-openai-key")
values.AI.BridgeConfig.LegacyAnthropic.Key = serpent.String("deployment-anthropic-key")
values.AI.BridgeConfig.LegacyOpenAI.BaseURL = serpent.String("https://custom-openai.example.com")
keys := coderd.ChatProviderAPIKeysFromDeploymentValues(values)
require.Equal(t, chatprovider.ProviderAPIKeys{}, keys)
@@ -2546,7 +2546,7 @@ func TestUserChatProviderConfigs(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
values := chatDeploymentValues(t)
values.AI.BridgeConfig.OpenAI.Key = serpent.String("deployment-openai-key")
values.AI.BridgeConfig.LegacyOpenAI.Key = serpent.String("deployment-openai-key")
client := newChatClientWithDeploymentValues(t, values)
_ = coderdtest.CreateFirstUser(t, client.Client)