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 09:59:37 +02:00
committed by GitHub
parent 730edba87a
commit 08045c2aac
17 changed files with 1326 additions and 163 deletions
+52 -25
View File
@@ -3650,7 +3650,7 @@ Write out the current server config as YAML to stdout.`,
Description: "The base URL of the OpenAI API.",
Flag: "aibridge-openai-base-url",
Env: "CODER_AIBRIDGE_OPENAI_BASE_URL",
Value: &c.AI.BridgeConfig.OpenAI.BaseURL,
Value: &c.AI.BridgeConfig.LegacyOpenAI.BaseURL,
Default: "https://api.openai.com/v1/",
Group: &deploymentGroupAIBridge,
YAML: "openai_base_url",
@@ -3660,7 +3660,7 @@ Write out the current server config as YAML to stdout.`,
Description: "The key to authenticate against the OpenAI API.",
Flag: "aibridge-openai-key",
Env: "CODER_AIBRIDGE_OPENAI_KEY",
Value: &c.AI.BridgeConfig.OpenAI.Key,
Value: &c.AI.BridgeConfig.LegacyOpenAI.Key,
Default: "",
Group: &deploymentGroupAIBridge,
Annotations: serpent.Annotations{}.Mark(annotationSecretKey, "true"),
@@ -3670,7 +3670,7 @@ Write out the current server config as YAML to stdout.`,
Description: "The base URL of the Anthropic API.",
Flag: "aibridge-anthropic-base-url",
Env: "CODER_AIBRIDGE_ANTHROPIC_BASE_URL",
Value: &c.AI.BridgeConfig.Anthropic.BaseURL,
Value: &c.AI.BridgeConfig.LegacyAnthropic.BaseURL,
Default: "https://api.anthropic.com/",
Group: &deploymentGroupAIBridge,
YAML: "anthropic_base_url",
@@ -3680,7 +3680,7 @@ Write out the current server config as YAML to stdout.`,
Description: "The key to authenticate against the Anthropic API.",
Flag: "aibridge-anthropic-key",
Env: "CODER_AIBRIDGE_ANTHROPIC_KEY",
Value: &c.AI.BridgeConfig.Anthropic.Key,
Value: &c.AI.BridgeConfig.LegacyAnthropic.Key,
Default: "",
Group: &deploymentGroupAIBridge,
Annotations: serpent.Annotations{}.Mark(annotationSecretKey, "true"),
@@ -3691,7 +3691,7 @@ Write out the current server config as YAML to stdout.`,
"over CODER_AIBRIDGE_BEDROCK_REGION.",
Flag: "aibridge-bedrock-base-url",
Env: "CODER_AIBRIDGE_BEDROCK_BASE_URL",
Value: &c.AI.BridgeConfig.Bedrock.BaseURL,
Value: &c.AI.BridgeConfig.LegacyBedrock.BaseURL,
Default: "",
Group: &deploymentGroupAIBridge,
YAML: "bedrock_base_url",
@@ -3702,7 +3702,7 @@ Write out the current server config as YAML to stdout.`,
"'https://bedrock-runtime.<region>.amazonaws.com'.",
Flag: "aibridge-bedrock-region",
Env: "CODER_AIBRIDGE_BEDROCK_REGION",
Value: &c.AI.BridgeConfig.Bedrock.Region,
Value: &c.AI.BridgeConfig.LegacyBedrock.Region,
Default: "",
Group: &deploymentGroupAIBridge,
YAML: "bedrock_region",
@@ -3712,7 +3712,7 @@ Write out the current server config as YAML to stdout.`,
Description: "The access key to authenticate against the AWS Bedrock API.",
Flag: "aibridge-bedrock-access-key",
Env: "CODER_AIBRIDGE_BEDROCK_ACCESS_KEY",
Value: &c.AI.BridgeConfig.Bedrock.AccessKey,
Value: &c.AI.BridgeConfig.LegacyBedrock.AccessKey,
Default: "",
Group: &deploymentGroupAIBridge,
Annotations: serpent.Annotations{}.Mark(annotationSecretKey, "true"),
@@ -3722,7 +3722,7 @@ Write out the current server config as YAML to stdout.`,
Description: "The access key secret to use with the access key to authenticate against the AWS Bedrock API.",
Flag: "aibridge-bedrock-access-key-secret",
Env: "CODER_AIBRIDGE_BEDROCK_ACCESS_KEY_SECRET",
Value: &c.AI.BridgeConfig.Bedrock.AccessKeySecret,
Value: &c.AI.BridgeConfig.LegacyBedrock.AccessKeySecret,
Default: "",
Group: &deploymentGroupAIBridge,
Annotations: serpent.Annotations{}.Mark(annotationSecretKey, "true"),
@@ -3732,7 +3732,7 @@ Write out the current server config as YAML to stdout.`,
Description: "The model to use when making requests to the AWS Bedrock API.",
Flag: "aibridge-bedrock-model",
Env: "CODER_AIBRIDGE_BEDROCK_MODEL",
Value: &c.AI.BridgeConfig.Bedrock.Model,
Value: &c.AI.BridgeConfig.LegacyBedrock.Model,
Default: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", // See https://docs.claude.com/en/api/claude-on-amazon-bedrock#accessing-bedrock.
Group: &deploymentGroupAIBridge,
YAML: "bedrock_model",
@@ -3742,7 +3742,7 @@ Write out the current server config as YAML to stdout.`,
Description: "The small fast model to use when making requests to the AWS Bedrock API. Claude Code uses Haiku-class models to perform background tasks. See https://docs.claude.com/en/docs/claude-code/settings#environment-variables.",
Flag: "aibridge-bedrock-small-fastmodel",
Env: "CODER_AIBRIDGE_BEDROCK_SMALL_FAST_MODEL",
Value: &c.AI.BridgeConfig.Bedrock.SmallFastModel,
Value: &c.AI.BridgeConfig.LegacyBedrock.SmallFastModel,
Default: "global.anthropic.claude-haiku-4-5-20251001-v1:0", // See https://docs.claude.com/en/api/claude-on-amazon-bedrock#accessing-bedrock.
Group: &deploymentGroupAIBridge,
YAML: "bedrock_small_fast_model",
@@ -3940,17 +3940,15 @@ Write out the current server config as YAML to stdout.`,
YAML: "key_file",
},
{
Name: "AI Bridge Proxy Domain Allowlist",
Description: "Comma-separated list of AI provider domains for which HTTPS traffic will be decrypted and routed through AI Bridge. " +
"Requests to other domains will be tunneled directly without decryption. " +
"Supported domains: api.anthropic.com, api.openai.com, api.individual.githubcopilot.com, api.business.githubcopilot.com, api.enterprise.githubcopilot.com, chatgpt.com.",
Flag: "aibridge-proxy-domain-allowlist",
Env: "CODER_AIBRIDGE_PROXY_DOMAIN_ALLOWLIST",
Value: &c.AI.BridgeProxyConfig.DomainAllowlist,
Default: "api.anthropic.com,api.openai.com,api.individual.githubcopilot.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,chatgpt.com",
Hidden: true,
Group: &deploymentGroupAIBridgeProxy,
YAML: "domain_allowlist",
Name: "AI Bridge Proxy Domain Allowlist",
Description: "Deprecated: This value is now derived automatically from the configured AI Bridge providers' base URLs. Setting this value has no effect. This option will be removed in a future release.",
Flag: "aibridge-proxy-domain-allowlist",
Env: "CODER_AIBRIDGE_PROXY_DOMAIN_ALLOWLIST",
Value: &c.AI.BridgeProxyConfig.DomainAllowlist,
Default: "",
Hidden: true,
Group: &deploymentGroupAIBridgeProxy,
YAML: "domain_allowlist",
},
{
Name: "AI Bridge Proxy Upstream Proxy",
@@ -4047,10 +4045,16 @@ Write out the current server config as YAML to stdout.`,
}
type AIBridgeConfig struct {
Enabled serpent.Bool `json:"enabled" typescript:",notnull"`
OpenAI AIBridgeOpenAIConfig `json:"openai" typescript:",notnull"`
Anthropic AIBridgeAnthropicConfig `json:"anthropic" typescript:",notnull"`
Bedrock AIBridgeBedrockConfig `json:"bedrock" typescript:",notnull"`
Enabled serpent.Bool `json:"enabled" typescript:",notnull"`
// Deprecated: Use Providers with indexed CODER_AIBRIDGE_PROVIDER_<N>_* env vars instead.
LegacyOpenAI AIBridgeOpenAIConfig `json:"openai" typescript:",notnull"`
// Deprecated: Use Providers with indexed CODER_AIBRIDGE_PROVIDER_<N>_* env vars instead.
LegacyAnthropic AIBridgeAnthropicConfig `json:"anthropic" typescript:",notnull"`
// Deprecated: Use Providers with indexed CODER_AIBRIDGE_PROVIDER_<N>_* env vars instead.
LegacyBedrock AIBridgeBedrockConfig `json:"bedrock" typescript:",notnull"`
// Providers holds provider instances populated from CODER_AIBRIDGE_PROVIDER_<N>_<KEY>
// env vars and/or the deprecated LegacyOpenAI/LegacyAnthropic/LegacyBedrock fields above.
Providers []AIBridgeProviderConfig `json:"providers,omitempty"`
// Deprecated: Injected MCP in AI Bridge is deprecated and will be removed in a future release.
InjectCoderMCPTools serpent.Bool `json:"inject_coder_mcp_tools" typescript:",notnull"`
Retention serpent.Duration `json:"retention" typescript:",notnull"`
@@ -4086,6 +4090,29 @@ type AIBridgeBedrockConfig struct {
SmallFastModel serpent.String `json:"small_fast_model" typescript:",notnull"`
}
// AIBridgeProviderConfig represents a single AI Bridge provider instance,
// parsed from CODER_AIBRIDGE_PROVIDER_<N>_<KEY> environment variables.
// This follows the same indexed pattern as ExternalAuthConfig.
type AIBridgeProviderConfig struct {
// Type is the provider type: "openai", "anthropic", or "copilot".
Type string `json:"type"`
// Name is the unique instance identifier used for routing.
// Defaults to Type if not provided.
Name string `json:"name"`
// Key is the API key for authenticating with the upstream provider.
Key string `json:"-"`
// BaseURL is the base URL of the upstream provider API.
BaseURL string `json:"base_url"`
// Bedrock fields (only applicable when Type == "anthropic").
BedrockBaseURL string `json:"-"`
BedrockRegion string `json:"bedrock_region,omitempty"`
BedrockAccessKey string `json:"-"`
BedrockAccessKeySecret string `json:"-"`
BedrockModel string `json:"bedrock_model,omitempty"`
BedrockSmallFastModel string `json:"bedrock_small_fast_model,omitempty"`
}
type AIBridgeProxyConfig struct {
Enabled serpent.Bool `json:"enabled" typescript:",notnull"`
ListenAddr serpent.String `json:"listen_addr" typescript:",notnull"`