@@ -28,6 +28,7 @@ import (
2828const (
2929 providerName = "copilot"
3030 defaultBaseURL = "https://api.individual.githubcopilot.com"
31+ directBaseURL = "https://api.enterprise.githubcopilot.com"
3132 copilotAPIVersion = "2026-06-01"
3233)
3334
@@ -55,14 +56,14 @@ type Copilot struct {
5556func New (
5657 knobs provider.Knobs ,
5758 githubToken auth.TokenResolver ,
58- enterpriseDomain string ,
59+ cfg Config ,
5960 messagesCacheOpen func (string ) (store.Log [[]json.RawMessage ], error ),
6061 responsesCacheOpen func (string ) (store.Log [[]json.RawMessage ], error ),
6162) (* Copilot , error ) {
6263 if githubToken == nil {
6364 return nil , fmt .Errorf ("copilot: nil token resolver (need GitHub access token)" )
6465 }
65- tokenSrc := NewCopilotTokenSource (githubToken , enterpriseDomain )
66+ tokenSrc := newTokenSource (githubToken , cfg )
6667
6768 inner , err := anthropicsdk .New (knobs , tokenSrc , messagesCacheOpen )
6869 if err != nil {
@@ -79,7 +80,7 @@ func New(
7980
8081 return & Copilot {
8182 inner : inner ,
82- responses : newResponsesProvider (knobs , tokenSrc , enterpriseDomain , responsesCacheOpen ),
83+ responses : newResponsesProvider (knobs , tokenSrc , cfg . EnterpriseDomain , responsesCacheOpen ),
8384 tokenSrc : tokenSrc ,
8485 model : knobs .Model ,
8586 catalog : map [string ]catalogModel {},
@@ -96,7 +97,7 @@ func copilotRequestOptions(tokenSrc *CopilotTokenSource) []option.RequestOption
9697 if err != nil {
9798 return nil , err
9899 }
99- baseURL := baseURLFromToken ( token , tokenSrc .domain )
100+ baseURL := tokenSrc .BaseURL ( )
100101 // Rewrite the URL to the Copilot endpoint
101102 req .URL .Scheme = "https"
102103 req .URL .Host = baseURL [len ("https://" ):]
@@ -271,11 +272,9 @@ func (c *Copilot) routeForModel(ctx context.Context, model string) (modelRoute,
271272 if route := routeForCatalogModel (entry ); route != modelRouteUnknown {
272273 return route , nil
273274 }
275+ return modelRouteUnknown , fmt .Errorf ("copilot: model %q has no supported direct transport" , model )
274276 }
275- if isAnthropicModel (model ) {
276- return modelRouteMessages , nil
277- }
278- return modelRouteUnknown , fmt .Errorf ("copilot: model %q has no supported direct transport" , model )
277+ return modelRouteUnknown , fmt .Errorf ("copilot: model %q is not in this Copilot catalog" , model )
279278}
280279
281280func routeForCatalogModel (model catalogModel ) modelRoute {
@@ -304,7 +303,7 @@ func (c *Copilot) fetchCatalog(ctx context.Context) ([]catalogModel, error) {
304303 if err != nil {
305304 return nil , fmt .Errorf ("copilot models: resolve token: %w" , err )
306305 }
307- baseURL := baseURLFromToken ( token , c .tokenSrc .domain )
306+ baseURL := c .tokenSrc .BaseURL ( )
308307 req , err := http .NewRequestWithContext (ctx , "GET" , baseURL + "/models" , nil )
309308 if err != nil {
310309 return nil , err
@@ -364,35 +363,74 @@ func baseURLFromToken(token, enterpriseDomain string) string {
364363}
365364
366365// CopilotTokenSource exchanges a GitHub access token for a short-lived
367- // Copilot session token, caching it until near expiry.
366+ // Copilot session token, caching it until near expiry. In direct mode the
367+ // GitHub token is presented to the API unchanged, as the Copilot CLI does.
368368type CopilotTokenSource struct {
369- github auth.TokenResolver
370- domain string
371- mu sync.Mutex
372- token string
373- expiresAt time.Time
369+ github auth.TokenResolver
370+ domain string
371+ direct bool
372+ baseOverride string
373+ mu sync.Mutex
374+ token string
375+ apiBase string
376+ expiresAt time.Time
374377}
375378
376379func NewCopilotTokenSource (github auth.TokenResolver , enterpriseDomain string ) * CopilotTokenSource {
377380 return & CopilotTokenSource {github : github , domain : enterpriseDomain }
378381}
379382
383+ func newTokenSource (github auth.TokenResolver , cfg Config ) * CopilotTokenSource {
384+ return & CopilotTokenSource {
385+ github : github ,
386+ domain : cfg .EnterpriseDomain ,
387+ direct : strings .EqualFold (strings .TrimSpace (cfg .TokenMode ), "direct" ),
388+ baseOverride : strings .TrimRight (strings .TrimSpace (cfg .BaseURL ), "/" ),
389+ }
390+ }
391+
392+ // BaseURL reports the API host for the currently resolved credential. An
393+ // explicit override wins, then the endpoints.api the exchange handed back,
394+ // then the legacy derivation from the token body.
395+ func (s * CopilotTokenSource ) BaseURL () string {
396+ s .mu .Lock ()
397+ defer s .mu .Unlock ()
398+ if s .baseOverride != "" {
399+ return s .baseOverride
400+ }
401+ if s .apiBase != "" {
402+ return s .apiBase
403+ }
404+ return baseURLFromToken (s .token , s .domain )
405+ }
406+
380407func (s * CopilotTokenSource ) Resolve () (string , error ) {
381408 s .mu .Lock ()
382409 defer s .mu .Unlock ()
410+ if s .direct {
411+ githubToken , err := s .github .Resolve ()
412+ if err != nil {
413+ return "" , fmt .Errorf ("copilot: resolve github token: %w" , err )
414+ }
415+ if s .apiBase == "" {
416+ s .apiBase = directBaseURL
417+ }
418+ return githubToken , nil
419+ }
383420 if s .token != "" && time .Now ().Before (s .expiresAt ) {
384421 return s .token , nil
385422 }
386423 githubToken , err := s .github .Resolve ()
387424 if err != nil {
388425 return "" , fmt .Errorf ("copilot: resolve github token: %w" , err )
389426 }
390- tok , exp , err := exchangeCopilotToken (githubToken , s .domain )
427+ tok , exp , api , err := exchangeCopilotToken (githubToken , s .domain )
391428 if err != nil {
392429 return "" , err
393430 }
394431 s .token = tok
395432 s .expiresAt = exp
433+ s .apiBase = api
396434 return tok , nil
397435}
398436
@@ -406,15 +444,15 @@ func (s *CopilotTokenSource) Invalidate(token string) error {
406444 return s .github .Invalidate ("" )
407445}
408446
409- func exchangeCopilotToken (githubAccessToken , enterpriseDomain string ) (token string , expiresAt time.Time , err error ) {
447+ func exchangeCopilotToken (githubAccessToken , enterpriseDomain string ) (token string , expiresAt time.Time , apiBase string , err error ) {
410448 domain := enterpriseDomain
411449 if domain == "" {
412450 domain = "github.com"
413451 }
414452 tokenURL := fmt .Sprintf ("https://api.%s/copilot_internal/v2/token" , domain )
415453 req , err := http .NewRequest ("GET" , tokenURL , nil )
416454 if err != nil {
417- return "" , time.Time {}, err
455+ return "" , time.Time {}, "" , err
418456 }
419457 req .Header .Set ("Authorization" , "Bearer " + githubAccessToken )
420458 for k , v := range copilotStaticHeaders {
@@ -425,25 +463,28 @@ func exchangeCopilotToken(githubAccessToken, enterpriseDomain string) (token str
425463 }
426464 resp , err := (& http.Client {Timeout : 15 * time .Second }).Do (req )
427465 if err != nil {
428- return "" , time.Time {}, fmt .Errorf ("copilot token exchange: %w" , err )
466+ return "" , time.Time {}, "" , fmt .Errorf ("copilot token exchange: %w" , err )
429467 }
430468 defer resp .Body .Close ()
431469 if resp .StatusCode != 200 {
432470 body , _ := io .ReadAll (resp .Body )
433- return "" , time.Time {}, fmt .Errorf ("copilot token exchange %d: %s" , resp .StatusCode , body )
471+ return "" , time.Time {}, "" , fmt .Errorf ("copilot token exchange %d: %s" , resp .StatusCode , body )
434472 }
435473 var parsed struct {
436474 Token string `json:"token"`
437475 ExpiresAt int64 `json:"expires_at"`
476+ Endpoints struct {
477+ API string `json:"api"`
478+ } `json:"endpoints"`
438479 }
439480 if err := json .NewDecoder (resp .Body ).Decode (& parsed ); err != nil {
440- return "" , time.Time {}, fmt .Errorf ("copilot token parse: %w" , err )
481+ return "" , time.Time {}, "" , fmt .Errorf ("copilot token parse: %w" , err )
441482 }
442483 if parsed .Token == "" {
443- return "" , time.Time {}, fmt .Errorf ("copilot token exchange returned empty token" )
484+ return "" , time.Time {}, "" , fmt .Errorf ("copilot token exchange returned empty token" )
444485 }
445486 exp := time .Unix (parsed .ExpiresAt , 0 ).Add (- 5 * time .Minute )
446- return parsed .Token , exp , nil
487+ return parsed .Token , exp , strings . TrimRight ( parsed . Endpoints . API , "/" ), nil
447488}
448489
449490// debug helper (kept; guarded by env var)
0 commit comments