Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion internal/provider/copilot.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package provider

import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"runtime"
"strings"
"time"

Expand Down Expand Up @@ -58,6 +60,39 @@ func normalizeCopilotModel(model string) string {
return m
}

func (c *CopilotProvider) exchangeGitHubToken(ctx context.Context, githubToken string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com/copilot_internal/v2/token", nil)
if err != nil {
return "", fmt.Errorf("failed creating token request: %w", err)
}
req.Header.Set("Authorization", "Token "+githubToken)
req.Header.Set("User-Agent", "lazycommit/1.0")

resp, err := c.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("failed exchanging token: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var body struct {
Message string `json:"message"`
}
Comment on lines +77 to +79

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better code clarity and maintainability, consider replacing this anonymous struct with a named struct defined at the package level, for example copilotErrorResponse. This makes the code easier to read and allows for reuse.

_ = json.NewDecoder(resp.Body).Decode(&body)
return "", fmt.Errorf("token exchange failed: %d %s", resp.StatusCode, body.Message)
}
Comment on lines +76 to +82

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error from json.NewDecoder(...).Decode(...) is currently ignored. If the response body for a non-OK status is not valid JSON or is empty, body.Message will be empty, and the error message will be less informative. It's better to handle this case and provide a more generic error message if the specific error from GitHub can't be parsed.

if resp.StatusCode != http.StatusOK {
		var body struct {
			Message string `json:"message"`
		}
		if err := json.NewDecoder(resp.Body).Decode(&body); err == nil && body.Message != "" {
			return "", fmt.Errorf("token exchange failed: %d %s", resp.StatusCode, body.Message)
		}
		return "", fmt.Errorf("token exchange failed with status code: %d", resp.StatusCode)
	}

var tr struct {
Token string `json:"token"`
ExpiresAt int64 `json:"expires_at"`
}
Comment on lines +83 to +86

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better code clarity and maintainability, consider replacing this anonymous struct with a named struct defined at the package level, for example copilotTokenResponse. This makes the code easier to read and allows for reuse.

if err := json.NewDecoder(resp.Body).Decode(&tr); err != nil {
return "", fmt.Errorf("failed decoding token response: %w", err)
}
if tr.Token == "" {
return "", fmt.Errorf("empty copilot bearer token")
}
return tr.Token, nil
}

func (c *CopilotProvider) getGitHubToken() string {
if c.apiKey != "" {
return c.apiKey
Expand Down Expand Up @@ -88,7 +123,19 @@ func (c *CopilotProvider) GenerateCommitMessages(ctx context.Context, diff strin
return nil, fmt.Errorf("GitHub token is required for Copilot provider")
}

bearer := githubToken
var bearer string
var err error

// On Windows, use the token directly; on other platforms, exchange it for a Copilot token

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment explains what the code does, but it would be very helpful for future maintenance to also explain why this OS-specific logic is necessary. Why does Windows use the token directly while other platforms need to exchange it?

if runtime.GOOS == "windows" {
bearer = githubToken
} else {
bearer, err = c.exchangeGitHubToken(ctx, githubToken)
if err != nil {
return nil, err
}
}


client := openai.NewClient(
option.WithBaseURL(c.endpoint),
Expand Down