mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
_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>
81 lines
2.9 KiB
Go
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)]
|
|
}
|
|
}
|