@@ -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.
@@ -655,13 +663,13 @@ func (s *Server) authMiddleware(host string, ctx *goproxy.ProxyCtx) (*goproxy.Co
655663 provider := s .loadProviderRouter ().providerFromHost (ctx .Req .URL .Hostname ())
656664 // A concurrent Reload can swap the router between CONNECT matching
657665 // and provider lookup, so treat a missing mapping as a runtime miss.
658- if provider == "" {
666+ if provider . name == "" {
659667 logger .Warn (s .ctx , "rejecting CONNECT request with no provider mapping" )
660668 return goproxy .RejectConnect , host
661669 }
662670
663671 logger = logger .With (
664- slog .F ("provider" , provider ),
672+ slog .F ("provider" , provider . name ),
665673 )
666674
667675 proxyAuth := ctx .Req .Header .Get ("Proxy-Authorization" )
@@ -685,7 +693,7 @@ func (s *Server) authMiddleware(host string, ctx *goproxy.ProxyCtx) (*goproxy.Co
685693 ctx .UserData = & requestContext {
686694 ConnectSessionID : connectSessionID ,
687695 CoderToken : coderToken ,
688- Provider : provider ,
696+ Provider : provider . name ,
689697 }
690698
691699 logger .Debug (s .ctx , "request CONNECT authenticated" )
@@ -936,14 +944,14 @@ func (s *Server) handleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*http.
936944 }
937945 }
938946 liveProvider := s .loadProviderRouter ().providerFromHost (host )
939- if liveProvider == "" || liveProvider != reqCtx .Provider {
947+ if liveProvider . name == "" || liveProvider . name != reqCtx .Provider {
940948 s .logger .Warn (s .ctx , "provider mapping changed or removed since CONNECT, passing through" ,
941949 slog .F ("connect_id" , reqCtx .ConnectSessionID .String ()),
942950 slog .F ("host" , req .Host ),
943951 slog .F ("method" , req .Method ),
944952 slog .F ("path" , originalPath ),
945953 slog .F ("connect_provider" , reqCtx .Provider ),
946- slog .F ("live_provider" , liveProvider ),
954+ slog .F ("live_provider" , liveProvider . name ),
947955 )
948956 return req , nil
949957 }
@@ -992,7 +1000,8 @@ func (s *Server) handleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*http.
9921000 req .URL = parsedGatewayTargetURL
9931001 req .Host = parsedGatewayTargetURL .Host
9941002
995- injectBYOKHeaderIfNeeded (req .Header , reqCtx .CoderToken )
1003+ // Prepare Coder authentication for centralized and BYOK requests.
1004+ prepareAIGatewayAuth (req .Header , reqCtx .CoderToken , liveProvider .providerType )
9961005
9971006 // Set request ID header to correlate requests between aibridgeproxyd and aibridged.
9981007 req .Header .Set (agplaibridge .HeaderCoderRequestID , reqCtx .RequestID .String ())
@@ -1019,24 +1028,34 @@ func (s *Server) handleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*http.
10191028 return req , nil
10201029}
10211030
1022- // injectBYOKHeaderIfNeeded sets HeaderCoderToken when the
1023- // Authorization header carries a bearer token that differs from the
1024- // Coder token, indicating the client is using its own LLM
1025- // credentials. Clients that can set custom headers
1026- // do this themselves; this handles clients that cannot.
1027- //
1028- // In centralized mode, Authorization carries the Coder token
1029- // itself, so aibridged discovers it via ExtractAuthToken
1030- // without any extra header.
1031- func injectBYOKHeaderIfNeeded (header http.Header , coderToken string ) {
1032- // Don’t overwrite the header if it’s already set.
1033- if header .Get (agplaibridge .HeaderCoderToken ) != "" {
1031+ // prepareAIGatewayAuth prepares the Coder authentication headers for AI
1032+ // Gateway. Copilot is always BYOK, while other providers may use centralized
1033+ // or BYOK authentication.
1034+ func prepareAIGatewayAuth (headers http.Header , coderToken , providerType string ) {
1035+ // Copilot is always BYOK, even when a route does not include a provider
1036+ // credential (e.g., /_ping). Prevent the Coder token from being forwarded
1037+ // to Copilot as a provider credential.
1038+ if providerType == aibridgeconfig .ProviderCopilot {
1039+ headers .Set (agplaibridge .HeaderCoderToken , coderToken )
1040+
1041+ if extractCoderTokenFromBearerAuth (headers .Get ("Authorization" )) == coderToken {
1042+ headers .Del ("Authorization" )
1043+ }
1044+ if strings .TrimSpace (headers .Get ("X-Api-Key" )) == coderToken {
1045+ headers .Del ("X-Api-Key" )
1046+ }
1047+ return
1048+ }
1049+
1050+ // For other providers, only add the Coder token when a separate provider
1051+ // credential indicates BYOK.
1052+ if headers .Get (agplaibridge .HeaderCoderToken ) != "" {
10341053 return
10351054 }
10361055
1037- bearer := extractCoderTokenFromBearerAuth (header .Get ("Authorization" ))
1056+ bearer := extractCoderTokenFromBearerAuth (headers .Get ("Authorization" ))
10381057 if bearer != "" && bearer != coderToken {
1039- header .Set (agplaibridge .HeaderCoderToken , coderToken )
1058+ headers .Set (agplaibridge .HeaderCoderToken , coderToken )
10401059 }
10411060}
10421061
0 commit comments