@@ -28,6 +28,7 @@ import (
2828 "golang.org/x/xerrors"
2929
3030 "cdr.dev/slog/v3"
31+ aibridgeconfig "github.com/coder/coder/v2/aibridge/config"
3132 agplaibridge "github.com/coder/coder/v2/coderd/aibridge"
3233)
3334
@@ -132,7 +133,7 @@ type Server struct {
132133 // refreshProviders fetches the live provider snapshot on Reload.
133134 // Nil disables hot-reload.
134135 refreshProviders RefreshProvidersFunc
135- // providerRouter holds the live (mitmHosts, nameByHost) pair .
136+ // providerRouter holds the live routing snapshot .
136137 providerRouter atomic.Pointer [providerRouter ]
137138 // allowedPorts is the port allowlist for CONNECT requests. Fixed at
138139 // construction; not reloadable.
@@ -149,19 +150,26 @@ type Server struct {
149150 metrics * Metrics
150151}
151152
153+ type routedProvider struct {
154+ name string
155+ providerType string
156+ }
157+
152158// providerRouter keeps CONNECT matching and provider lookup in sync.
153159type providerRouter struct {
154- mitmHosts []string // host:port set the goproxy condition matches against.
155- nameByHost map [string ]string // lowercase hostname -> provider name .
160+ mitmHosts []string // host:port set the goproxy condition matches against.
161+ providerByHost map [string ]routedProvider // lowercase hostname -> provider.
156162}
157163
158164// emptyProviderRouter is used before the first Reload (or when the
159165// operator deconfigures every provider) so handlers can safely call
160166// loadProviderRouter without a nil check.
161- var emptyProviderRouter = & providerRouter {nameByHost : map [string ]string {}}
167+ var emptyProviderRouter = & providerRouter {
168+ providerByHost : map [string ]routedProvider {},
169+ }
162170
163- func (r * providerRouter ) providerFromHost (host string ) string {
164- return r .nameByHost [strings .ToLower (host )]
171+ func (r * providerRouter ) providerFromHost (host string ) routedProvider {
172+ return r .providerByHost [strings .ToLower (host )]
165173}
166174
167175// requestContext holds metadata propagated through the proxy request/response chain.
@@ -651,13 +659,13 @@ func (s *Server) authMiddleware(host string, ctx *goproxy.ProxyCtx) (*goproxy.Co
651659 provider := s .loadProviderRouter ().providerFromHost (ctx .Req .URL .Hostname ())
652660 // A concurrent Reload can swap the router between CONNECT matching
653661 // and provider lookup, so treat a missing mapping as a runtime miss.
654- if provider == "" {
662+ if provider . name == "" {
655663 logger .Warn (s .ctx , "rejecting CONNECT request with no provider mapping" )
656664 return goproxy .RejectConnect , host
657665 }
658666
659667 logger = logger .With (
660- slog .F ("provider" , provider ),
668+ slog .F ("provider" , provider . name ),
661669 )
662670
663671 proxyAuth := ctx .Req .Header .Get ("Proxy-Authorization" )
@@ -681,7 +689,7 @@ func (s *Server) authMiddleware(host string, ctx *goproxy.ProxyCtx) (*goproxy.Co
681689 ctx .UserData = & requestContext {
682690 ConnectSessionID : connectSessionID ,
683691 CoderToken : coderToken ,
684- Provider : provider ,
692+ Provider : provider . name ,
685693 }
686694
687695 logger .Debug (s .ctx , "request CONNECT authenticated" )
@@ -932,14 +940,14 @@ func (s *Server) handleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*http.
932940 }
933941 }
934942 liveProvider := s .loadProviderRouter ().providerFromHost (host )
935- if liveProvider == "" || liveProvider != reqCtx .Provider {
943+ if liveProvider . name == "" || liveProvider . name != reqCtx .Provider {
936944 s .logger .Warn (s .ctx , "provider mapping changed or removed since CONNECT, passing through" ,
937945 slog .F ("connect_id" , reqCtx .ConnectSessionID .String ()),
938946 slog .F ("host" , req .Host ),
939947 slog .F ("method" , req .Method ),
940948 slog .F ("path" , originalPath ),
941949 slog .F ("connect_provider" , reqCtx .Provider ),
942- slog .F ("live_provider" , liveProvider ),
950+ slog .F ("live_provider" , liveProvider . name ),
943951 )
944952 return req , nil
945953 }
@@ -988,7 +996,8 @@ func (s *Server) handleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*http.
988996 req .URL = aiBridgeParsedURL
989997 req .Host = aiBridgeParsedURL .Host
990998
991- injectBYOKHeaderIfNeeded (req .Header , reqCtx .CoderToken )
999+ // Prepare Coder authentication for centralized and BYOK requests.
1000+ prepareAIGatewayAuth (req .Header , reqCtx .CoderToken , liveProvider .providerType )
9921001
9931002 // Set request ID header to correlate requests between aibridgeproxyd and aibridged.
9941003 req .Header .Set (agplaibridge .HeaderCoderRequestID , reqCtx .RequestID .String ())
@@ -1015,24 +1024,34 @@ func (s *Server) handleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*http.
10151024 return req , nil
10161025}
10171026
1018- // injectBYOKHeaderIfNeeded sets HeaderCoderToken when the
1019- // Authorization header carries a bearer token that differs from the
1020- // Coder token, indicating the client is using its own LLM
1021- // credentials. Clients that can set custom headers
1022- // do this themselves; this handles clients that cannot.
1023- //
1024- // In centralized mode, Authorization carries the Coder token
1025- // itself, so aibridged discovers it via ExtractAuthToken
1026- // without any extra header.
1027- func injectBYOKHeaderIfNeeded (header http.Header , coderToken string ) {
1028- // Don’t overwrite the header if it’s already set.
1029- if header .Get (agplaibridge .HeaderCoderToken ) != "" {
1027+ // prepareAIGatewayAuth prepares the Coder authentication headers for AI
1028+ // Gateway. Copilot is always BYOK, while other providers may use centralized
1029+ // or BYOK authentication.
1030+ func prepareAIGatewayAuth (headers http.Header , coderToken , providerType string ) {
1031+ // Copilot is always BYOK, even when a route does not include a provider
1032+ // credential (e.g., /_ping). Prevent the Coder token from being forwarded
1033+ // to Copilot as a provider credential.
1034+ if providerType == aibridgeconfig .ProviderCopilot {
1035+ headers .Set (agplaibridge .HeaderCoderToken , coderToken )
1036+
1037+ if extractCoderTokenFromBearerAuth (headers .Get ("Authorization" )) == coderToken {
1038+ headers .Del ("Authorization" )
1039+ }
1040+ if strings .TrimSpace (headers .Get ("X-Api-Key" )) == coderToken {
1041+ headers .Del ("X-Api-Key" )
1042+ }
1043+ return
1044+ }
1045+
1046+ // For other providers, only add the Coder token when a separate provider
1047+ // credential indicates BYOK.
1048+ if headers .Get (agplaibridge .HeaderCoderToken ) != "" {
10301049 return
10311050 }
10321051
1033- bearer := extractCoderTokenFromBearerAuth (header .Get ("Authorization" ))
1052+ bearer := extractCoderTokenFromBearerAuth (headers .Get ("Authorization" ))
10341053 if bearer != "" && bearer != coderToken {
1035- header .Set (agplaibridge .HeaderCoderToken , coderToken )
1054+ headers .Set (agplaibridge .HeaderCoderToken , coderToken )
10361055 }
10371056}
10381057
0 commit comments