Skip to content

Commit 6647f2a

Browse files
author
gluck
committed
restore PR #15, dropped by a force push of local main
25c8211 (the copilot endpoints.api / direct token mode and the form channel migration) landed on origin/main while this branch was building the distribution work, and a force push of the local main replaced it. It was never in this line of history, so nothing here superseded it: merging the original commit back keeps its identity, and its hash, exactly as PR #15 recorded them.
2 parents b660a76 + 25c8211 commit 6647f2a

5 files changed

Lines changed: 92 additions & 36 deletions

File tree

‎internal/provider/copilot/copilot.go‎

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
const (
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 {
5556
func 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

281280
func 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.
368368
type 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

376379
func 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+
380407
func (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)

‎internal/provider/copilot/reg.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
type Config struct {
1818
APIKey string `toml:"api_key"`
1919
EnterpriseDomain string `toml:"enterprise_domain,omitempty"`
20+
TokenMode string `toml:"token_mode,omitempty"`
21+
BaseURL string `toml:"base_url,omitempty"`
2022
}
2123

2224
func loadConfig(loaded *config.Loaded) Config {
@@ -62,7 +64,7 @@ func buildFromContext(ctx provider.BuildContext) (provider.Provider, error) {
6264
}
6365
return ctx.Backend.OpenTranslation(aria, "copilot-responses")
6466
}
65-
p, err := New(knobs, ctx.Resolver, cfg.EnterpriseDomain, messagesCacheOpen, responsesCacheOpen)
67+
p, err := New(knobs, ctx.Resolver, cfg, messagesCacheOpen, responsesCacheOpen)
6668
if err != nil {
6769
return nil, err
6870
}

‎internal/provider/copilot/responses.go‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,14 @@ func newResponsesProvider(
6262
maxTokens: knobs.MaxTokens,
6363
machineID: uuid.NewString(),
6464
limits: map[string]responseContextLimits{},
65-
baseURL: func(token string) string { return baseURLFromToken(token, enterpriseDomain) },
65+
baseURL: func(token string) string {
66+
if b, ok := tokenSrc.(interface{ BaseURL() string }); ok {
67+
if u := b.BaseURL(); u != "" {
68+
return u
69+
}
70+
}
71+
return baseURLFromToken(token, enterpriseDomain)
72+
},
6673
dial: dialResponses,
6774
}
6875
}

‎internal/provider/copilot/responses_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,7 @@ func TestCopilotSeparatesMessagesAndResponsesCaches(t *testing.T) {
874874
p, err := New(
875875
provider.Knobs{Model: "gpt-5.6-terra"},
876876
&staticResponseTokenSource{token: "test-token"},
877-
"",
877+
Config{},
878878
func(string) (store.Log[[]json.RawMessage], error) { return messages, nil },
879879
func(string) (store.Log[[]json.RawMessage], error) { return responses, nil },
880880
)

‎internal/store/schema.go‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,11 @@ func ensureSchema(root string, trunks *xwal.Store) error {
169169
}
170170

171171
// checkGeneration is the version half, split out so it can run BEFORE figwal
172-
// opens the store. A generation-1 store names its form channel "form",
173-
// and figwal's manifest is authoritative for channel shape: so it refuses the
174-
// open itself, with "no reducer \"form\" registered", which explains
175-
// nothing to the person holding the store. This must speak first.
172+
// opens the store. A generation-1 store names its form channel
173+
// chanFormLegacy, and figwal's manifest is authoritative for channel shape:
174+
// so it refuses the open itself, with "no reducer \"jsonmerge\" registered
175+
// for channel \"chalkboard\"", which explains nothing to the person holding
176+
// the store. This must speak first.
176177
func checkGeneration(f schemaFile, root string, trunks *xwal.Store) error {
177178
if f.StoreVersion > storeVersion {
178179
return fmt.Errorf(
@@ -324,21 +325,26 @@ func migrateGenerations(root string, from int) error {
324325
return nil
325326
}
326327

328+
// chanFormLegacy is the generation-1 name of the form channel. It is a
329+
// constant so that renaming chanForm cannot silently rewrite the old name a
330+
// migration exists to recognise.
331+
const chanFormLegacy = "chalkboard"
332+
327333
// migrateFormChannel is generation 1 -> 2: the form's channel directory was
328-
// called "form". Renaming it is figwal's job (the manifest is its
334+
// called chanFormLegacy. Renaming it is figwal's job (the manifest is its
329335
// format); the sidecar's channel key is ours.
330336
func migrateFormChannel(root string) error {
331-
if _, err := os.Stat(filepath.Join(root, "form")); err != nil {
337+
if _, err := os.Stat(filepath.Join(root, chanFormLegacy)); err != nil {
332338
if !os.IsNotExist(err) {
333339
return err
334340
}
335341
// Nothing under the old name. Either this store never had one, or a
336342
// previous run moved it and died before the sidecar was stamped, and
337343
// RenameChannel repairs the manifest for that second case.
338-
if rerr := xwal.RenameChannel(root, "form", chanForm); rerr != nil && !isNoSuchChannel(rerr) {
344+
if rerr := xwal.RenameChannel(root, chanFormLegacy, chanForm); rerr != nil && !isNoSuchChannel(rerr) {
339345
return rerr
340346
}
341-
} else if err := xwal.RenameChannel(root, "form", chanForm); err != nil {
347+
} else if err := xwal.RenameChannel(root, chanFormLegacy, chanForm); err != nil {
342348
return err
343349
}
344350
f, err := readSchema(root)
@@ -348,8 +354,8 @@ func migrateFormChannel(root string) error {
348354
if f.Channels == nil {
349355
return nil
350356
}
351-
if v, ok := f.Channels["form"]; ok {
352-
delete(f.Channels, "form")
357+
if v, ok := f.Channels[chanFormLegacy]; ok {
358+
delete(f.Channels, chanFormLegacy)
353359
if _, taken := f.Channels[chanForm]; !taken {
354360
f.Channels[chanForm] = v
355361
}

0 commit comments

Comments
 (0)