Files
coder/enterprise/cli/aibridgeproxyd.go
T
Danny Kopping 08045c2aac 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>
2026-04-15 07:59:37 +00:00

81 lines
2.9 KiB
Go

//go:build !slim
package cli
import (
"context"
"net/url"
"strings"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/xerrors"
"github.com/coder/aibridge"
"github.com/coder/coder/v2/enterprise/aibridgeproxyd"
"github.com/coder/coder/v2/enterprise/coderd"
)
func newAIBridgeProxyDaemon(coderAPI *coderd.API, providers []aibridge.Provider) (*aibridgeproxyd.Server, error) {
ctx := context.Background()
coderAPI.Logger.Debug(ctx, "starting in-memory aibridgeproxy daemon")
logger := coderAPI.Logger.Named("aibridgeproxyd")
domains, providerFromHost := domainsFromProviders(providers)
reg := prometheus.WrapRegistererWithPrefix("coder_aibridgeproxyd_", coderAPI.PrometheusRegistry)
metrics := aibridgeproxyd.NewMetrics(reg)
srv, err := aibridgeproxyd.New(ctx, logger, aibridgeproxyd.Options{
ListenAddr: coderAPI.DeploymentValues.AI.BridgeProxyConfig.ListenAddr.String(),
TLSCertFile: coderAPI.DeploymentValues.AI.BridgeProxyConfig.TLSCertFile.String(),
TLSKeyFile: coderAPI.DeploymentValues.AI.BridgeProxyConfig.TLSKeyFile.String(),
CoderAccessURL: coderAPI.AccessURL.String(),
MITMCertFile: coderAPI.DeploymentValues.AI.BridgeProxyConfig.MITMCertFile.String(),
MITMKeyFile: coderAPI.DeploymentValues.AI.BridgeProxyConfig.MITMKeyFile.String(),
DomainAllowlist: domains,
AIBridgeProviderFromHost: providerFromHost,
UpstreamProxy: coderAPI.DeploymentValues.AI.BridgeProxyConfig.UpstreamProxy.String(),
UpstreamProxyCA: coderAPI.DeploymentValues.AI.BridgeProxyConfig.UpstreamProxyCA.String(),
AllowedPrivateCIDRs: coderAPI.DeploymentValues.AI.BridgeProxyConfig.AllowedPrivateCIDRs.Value(),
Metrics: metrics,
})
if err != nil {
return nil, xerrors.Errorf("failed to start in-memory aibridgeproxy daemon: %w", err)
}
return srv, nil
}
// domainsFromProviders extracts distinct hostnames from providers' base
// URLs and builds a host-to-provider-name mapping function. The returned
// domain list is suitable for use as DomainAllowlist and the mapping
// function is suitable for use as AIBridgeProviderFromHost.
func domainsFromProviders(providers []aibridge.Provider) ([]string, func(string) string) {
hostToProvider := make(map[string]string, len(providers))
var domains []string
for _, p := range providers {
raw := p.BaseURL()
if raw == "" {
continue
}
u, err := url.Parse(raw)
if err != nil || u.Hostname() == "" {
continue
}
host := strings.ToLower(u.Hostname())
if _, exists := hostToProvider[host]; exists {
// First provider wins; duplicates are expected when
// multiple providers share a base URL host (e.g. two
// OpenAI providers using the same proxy).
continue
}
hostToProvider[host] = p.Name()
domains = append(domains, host)
}
return domains, func(host string) string {
return hostToProvider[strings.ToLower(host)]
}
}