diff --git a/cmd/root.go b/cmd/root.go index ac6002f6..5fa32862 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,6 +12,7 @@ import ( "github.com/github/gh-models/cmd/generate" "github.com/github/gh-models/cmd/list" "github.com/github/gh-models/cmd/run" + "github.com/github/gh-models/cmd/usage" "github.com/github/gh-models/cmd/view" "github.com/github/gh-models/internal/azuremodels" "github.com/github/gh-models/pkg/command" @@ -54,11 +55,12 @@ func NewRootCommand() *cobra.Command { } } - cfg := command.NewConfigWithTerminal(terminal, client) + cfg := command.NewConfigWithTerminal(terminal, client, token) cmd.AddCommand(eval.NewEvalCommand(cfg)) cmd.AddCommand(list.NewListCommand(cfg)) cmd.AddCommand(run.NewRunCommand(cfg)) + cmd.AddCommand(usage.NewUsageCommand(cfg)) cmd.AddCommand(view.NewViewCommand(cfg)) cmd.AddCommand(generate.NewGenerateCommand(cfg)) diff --git a/cmd/root_test.go b/cmd/root_test.go index 0dd07ec4..300060d7 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -22,6 +22,7 @@ func TestRoot(t *testing.T) { require.Regexp(t, regexp.MustCompile(`eval\s+Evaluate prompts using test data and evaluators`), output) require.Regexp(t, regexp.MustCompile(`list\s+List available models`), output) require.Regexp(t, regexp.MustCompile(`run\s+Run inference with the specified model`), output) + require.Regexp(t, regexp.MustCompile(`usage\s+Show premium request usage and costs`), output) require.Regexp(t, regexp.MustCompile(`view\s+View details about a model`), output) require.Regexp(t, regexp.MustCompile(`generate\s+Generate tests and evaluations for prompts`), output) }) diff --git a/cmd/usage/usage.go b/cmd/usage/usage.go new file mode 100644 index 00000000..7479f6e4 --- /dev/null +++ b/cmd/usage/usage.go @@ -0,0 +1,336 @@ +// Package usage provides a gh command to show GitHub Models and Copilot usage information. +package usage + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "sort" + "time" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/go-gh/v2/pkg/tableprinter" + "github.com/github/gh-models/pkg/command" + "github.com/mgutz/ansi" + "github.com/spf13/cobra" +) + +const defaultGitHubAPIBase = "https://api.github.com" + +var ( + headerColor = ansi.ColorFunc("white+du") + greenColor = ansi.ColorFunc("green") + yellowColor = ansi.ColorFunc("yellow") +) + +// usageOptions holds configuration for the usage command, allowing dependency injection for testing. +type usageOptions struct { + apiBase string + httpClient *http.Client +} + +// premiumRequestUsageResponse represents the API response for premium request usage. +type premiumRequestUsageResponse struct { + TimePeriod timePeriod `json:"timePeriod"` + User string `json:"user"` + UsageItems []usageItem `json:"usageItems"` +} + +type timePeriod struct { + Year int `json:"year"` + Month int `json:"month"` + Day int `json:"day,omitempty"` +} + +type usageItem struct { + Product string `json:"product"` + SKU string `json:"sku"` + Model string `json:"model"` + UnitType string `json:"unitType"` + PricePerUnit float64 `json:"pricePerUnit"` + GrossQuantity float64 `json:"grossQuantity"` + GrossAmount float64 `json:"grossAmount"` + DiscountQuantity float64 `json:"discountQuantity"` + DiscountAmount float64 `json:"discountAmount"` + NetQuantity float64 `json:"netQuantity"` + NetAmount float64 `json:"netAmount"` +} + +func defaultOptions() *usageOptions { + return &usageOptions{ + apiBase: defaultGitHubAPIBase, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +// NewUsageCommand returns a new command to show model usage information. +func NewUsageCommand(cfg *command.Config) *cobra.Command { + return newUsageCommand(cfg, nil) +} + +// newUsageCommand is the internal constructor that accepts options for testing. +func newUsageCommand(cfg *command.Config, opts *usageOptions) *cobra.Command { + if opts == nil { + opts = defaultOptions() + } + + var ( + flagMonth int + flagYear int + flagDay int + flagToday bool + ) + + cmd := &cobra.Command{ + Use: "usage", + Short: "Show premium request usage and costs", + Long: heredoc.Docf(` + Display premium request usage statistics for GitHub Models and Copilot. + + Shows a breakdown of requests by model, with gross and net costs. + By default, shows usage for the current billing period (month). + + Use %[1]s--today%[1]s to see only today's usage, or %[1]s--month%[1]s and + %[1]s--year%[1]s to query a specific billing period. + + Requires the %[1]suser%[1]s scope on your GitHub token. If you get a 404 error, + run: %[1]sgh auth refresh -h github.com -s user%[1]s + `, "`"), + Example: heredoc.Doc(` + # Show current month's usage + $ gh models usage + + # Show today's usage + $ gh models usage --today + + # Show usage for a specific month + $ gh models usage --year 2026 --month 2 + + # Show usage for a specific day + $ gh models usage --year 2026 --month 3 --day 15 + `), + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + token := cfg.Token + if token == "" { + return fmt.Errorf("no GitHub token found. Please run 'gh auth login' to authenticate") + } + + ctx := cmd.Context() + + // Resolve time period + now := time.Now().UTC() + year := flagYear + month := flagMonth + day := flagDay + + if flagToday { + year = now.Year() + month = int(now.Month()) + day = now.Day() + } + + if year == 0 { + year = now.Year() + } + if month == 0 { + month = int(now.Month()) + } + + // Validate date arguments to fail fast with a clear message + // instead of letting the API return a confusing error. + if year < 1 { + return fmt.Errorf("invalid value for --year: %d (must be >= 1)", year) + } + if month < 1 || month > 12 { + return fmt.Errorf("invalid value for --month: %d (must be between 1 and 12)", month) + } + // day == 0 means "not specified" and is allowed; validate only if non-zero. + if day < 0 || day > 31 { + return fmt.Errorf("invalid value for --day: %d (must be between 1 and 31)", day) + } + + // Get username + username, err := getUsername(ctx, opts, token) + if err != nil { + return fmt.Errorf("failed to get username: %w", err) + } + + // Build query params + query := fmt.Sprintf("?year=%d&month=%d", year, month) + if day > 0 { + query += fmt.Sprintf("&day=%d", day) + } + + // Fetch usage + data, err := fetchPremiumRequestUsage(ctx, opts, token, username, query) + if err != nil { + return err + } + + // Format period string + periodStr := fmt.Sprintf("%d-%02d", data.TimePeriod.Year, data.TimePeriod.Month) + if data.TimePeriod.Day > 0 { + periodStr += fmt.Sprintf("-%02d", data.TimePeriod.Day) + } + + if len(data.UsageItems) == 0 { + cfg.WriteToOut(fmt.Sprintf("\nNo usage found for %s\n\n", periodStr)) + return nil + } + + // Sort by gross amount descending + items := data.UsageItems + sort.Slice(items, func(i, j int) bool { + return items[i].GrossAmount > items[j].GrossAmount + }) + + // Calculate totals across the rows that will actually be rendered + // (matching the GrossQuantity > 0 filter applied below) so the + // printed totals agree with the visible rows. + var totalReqs, totalGross, totalNet float64 + for _, item := range items { + if item.GrossQuantity == 0 { + continue + } + totalReqs += item.GrossQuantity + totalGross += item.GrossAmount + totalNet += item.NetAmount + } + + // Print header + if cfg.IsTerminalOutput { + cfg.WriteToOut("\n") + if flagToday { + cfg.WriteToOut(fmt.Sprintf("Premium request usage for %s (%s, today)\n", username, periodStr)) + } else { + cfg.WriteToOut(fmt.Sprintf("Premium request usage for %s (%s)\n", username, periodStr)) + } + cfg.WriteToOut("\n") + } + + // Print table + printer := cfg.NewTablePrinter() + + printer.AddHeader([]string{"PRODUCT", "MODEL", "REQUESTS", "GROSS", "NET"}, tableprinter.WithColor(headerColor)) + printer.EndRow() + + for _, item := range items { + if item.GrossQuantity == 0 { + continue + } + printer.AddField(item.Product) + printer.AddField(item.Model) + printer.AddField(fmt.Sprintf("%.1f", item.GrossQuantity)) + printer.AddField(fmt.Sprintf("$%.2f", item.GrossAmount)) + printer.AddField(fmt.Sprintf("$%.2f", item.NetAmount)) + printer.EndRow() + } + + if err := printer.Render(); err != nil { + return err + } + + // Print summary + if cfg.IsTerminalOutput { + cfg.WriteToOut("\n") + cfg.WriteToOut(fmt.Sprintf("Total: %.0f requests, $%.2f gross, $%.2f net\n", totalReqs, totalGross, totalNet)) + + if totalGross > 0 && totalNet == 0 { + cfg.WriteToOut(greenColor("All usage included in your plan (100% discount)") + "\n") + } else if totalGross > 0 && totalNet > 0 { + pct := (totalNet / totalGross) * 100 + cfg.WriteToOut(yellowColor(fmt.Sprintf("Net cost: $%.2f (%.0f%% of gross)", totalNet, pct)) + "\n") + } else if totalNet > 0 { + // Defensive: avoid divide-by-zero / Inf / NaN if the API + // reports net cost without a gross amount. + cfg.WriteToOut(yellowColor(fmt.Sprintf("Net cost: $%.2f (gross amount unavailable; percentage not shown)", totalNet)) + "\n") + } + cfg.WriteToOut("\n") + } + + return nil + }, + } + + cmd.Flags().IntVar(&flagYear, "year", 0, "Filter by year (default: current year)") + cmd.Flags().IntVar(&flagMonth, "month", 0, "Filter by month (default: current month)") + cmd.Flags().IntVar(&flagDay, "day", 0, "Filter by specific day") + cmd.Flags().BoolVar(&flagToday, "today", false, "Show only today's usage") + + // --today is shorthand for the current Y/M/D, so it conflicts with + // any explicit date flag; refuse the combination instead of silently + // overriding the user's input. + cmd.MarkFlagsMutuallyExclusive("today", "year") + cmd.MarkFlagsMutuallyExclusive("today", "month") + cmd.MarkFlagsMutuallyExclusive("today", "day") + + return cmd +} + +func getUsername(ctx context.Context, opts *usageOptions, token string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, opts.apiBase+"/user", nil) + if err != nil { + return "", err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("X-GitHub-Api-Version", "2022-11-28") + + resp, err := opts.httpClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to get user info: HTTP %d", resp.StatusCode) + } + + var user struct { + Login string `json:"login"` + } + if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { + return "", err + } + return user.Login, nil +} + +func fetchPremiumRequestUsage(ctx context.Context, opts *usageOptions, token, username, query string) (*premiumRequestUsageResponse, error) { + url := fmt.Sprintf("%s/users/%s/settings/billing/premium_request/usage%s", opts.apiBase, username, query) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("X-GitHub-Api-Version", "2022-11-28") + + resp, err := opts.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("usage data not available (HTTP 404). You may need the 'user' scope.\nRun: gh auth refresh -h github.com -s user\n\nResponse: %s", string(body)) + } + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("failed to fetch usage data: HTTP %d\n%s", resp.StatusCode, string(body)) + } + + var data premiumRequestUsageResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, fmt.Errorf("failed to parse usage response: %w", err) + } + + return &data, nil +} diff --git a/cmd/usage/usage_test.go b/cmd/usage/usage_test.go new file mode 100644 index 00000000..48399331 --- /dev/null +++ b/cmd/usage/usage_test.go @@ -0,0 +1,354 @@ +package usage + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/github/gh-models/internal/azuremodels" + "github.com/github/gh-models/pkg/command" + "github.com/stretchr/testify/require" +) + +func newTestServer(t *testing.T, userResp map[string]string, usageResp *premiumRequestUsageResponse, usageStatus int) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Match exact paths so tests fail loudly if the command starts + // hitting an unexpected endpoint, instead of silently returning + // canned responses for any matching prefix/suffix. + switch { + case r.URL.Path == "/user": + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(userResp) + case r.URL.Path == "/users/testuser/settings/billing/premium_request/usage": + w.Header().Set("Content-Type", "application/json") + if usageStatus != 0 && usageStatus != http.StatusOK { + w.WriteHeader(usageStatus) + _, _ = w.Write([]byte(`{"message":"Not Found"}`)) + return + } + _ = json.NewEncoder(w).Encode(usageResp) + default: + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message":"unknown path: ` + r.URL.Path + `"}`)) + } + })) +} + +func newTestOpts(serverURL string) *usageOptions { + return &usageOptions{ + apiBase: serverURL, + httpClient: http.DefaultClient, + } +} + +func TestUsageCommand(t *testing.T) { + t.Run("shows usage table", func(t *testing.T) { + usageResp := &premiumRequestUsageResponse{ + TimePeriod: timePeriod{Year: 2026, Month: 3}, + User: "testuser", + UsageItems: []usageItem{ + { + Product: "Copilot", + Model: "Claude Opus 4.6", + GrossQuantity: 100, + GrossAmount: 4.0, + NetAmount: 0.0, + }, + { + Product: "Copilot", + Model: "GPT-5.2", + GrossQuantity: 50, + GrossAmount: 2.0, + NetAmount: 0.0, + }, + }, + } + + server := newTestServer(t, map[string]string{"login": "testuser"}, usageResp, 0) + defer server.Close() + + buf := new(bytes.Buffer) + client := azuremodels.NewMockClient() + cfg := command.NewConfig(buf, buf, client, true, 120) + cfg.Token = "test-token" + + cmd := newUsageCommand(cfg, newTestOpts(server.URL)) + cmd.SetArgs([]string{"--year", "2026", "--month", "3"}) + + _, err := cmd.ExecuteC() + require.NoError(t, err) + + output := buf.String() + require.Contains(t, output, "testuser") + require.Contains(t, output, "2026-03") + require.Contains(t, output, "Claude Opus 4.6") + require.Contains(t, output, "GPT-5.2") + require.Contains(t, output, "100.0") + require.Contains(t, output, "$4.00") + require.Contains(t, output, "Total: 150 requests") + require.Contains(t, output, "100% discount") + }) + + t.Run("shows net cost when applicable", func(t *testing.T) { + usageResp := &premiumRequestUsageResponse{ + TimePeriod: timePeriod{Year: 2026, Month: 3}, + User: "testuser", + UsageItems: []usageItem{ + { + Product: "Copilot", + Model: "Claude Opus 4.6", + GrossQuantity: 100, + GrossAmount: 4.0, + NetAmount: 2.0, + }, + }, + } + + server := newTestServer(t, map[string]string{"login": "testuser"}, usageResp, 0) + defer server.Close() + + buf := new(bytes.Buffer) + client := azuremodels.NewMockClient() + cfg := command.NewConfig(buf, buf, client, true, 120) + cfg.Token = "test-token" + + cmd := newUsageCommand(cfg, newTestOpts(server.URL)) + cmd.SetArgs([]string{"--year", "2026", "--month", "3"}) + + _, err := cmd.ExecuteC() + require.NoError(t, err) + + output := buf.String() + require.Contains(t, output, "Net cost: $2.00") + }) + + t.Run("shows no usage message when empty", func(t *testing.T) { + usageResp := &premiumRequestUsageResponse{ + TimePeriod: timePeriod{Year: 2026, Month: 1}, + User: "testuser", + UsageItems: []usageItem{}, + } + + server := newTestServer(t, map[string]string{"login": "testuser"}, usageResp, 0) + defer server.Close() + + buf := new(bytes.Buffer) + client := azuremodels.NewMockClient() + cfg := command.NewConfig(buf, buf, client, true, 120) + cfg.Token = "test-token" + + cmd := newUsageCommand(cfg, newTestOpts(server.URL)) + cmd.SetArgs([]string{"--year", "2026", "--month", "1"}) + + _, err := cmd.ExecuteC() + require.NoError(t, err) + + output := buf.String() + require.Contains(t, output, "No usage found") + }) + + t.Run("errors without token", func(t *testing.T) { + buf := new(bytes.Buffer) + client := azuremodels.NewMockClient() + cfg := command.NewConfig(buf, buf, client, true, 120) + cfg.Token = "" + + cmd := newUsageCommand(cfg, nil) + cmd.SetArgs([]string{}) + + _, err := cmd.ExecuteC() + require.Error(t, err) + require.Contains(t, err.Error(), "no GitHub token found") + }) + + t.Run("404 returns helpful scope error", func(t *testing.T) { + server := newTestServer(t, map[string]string{"login": "testuser"}, nil, http.StatusNotFound) + defer server.Close() + + buf := new(bytes.Buffer) + client := azuremodels.NewMockClient() + cfg := command.NewConfig(buf, buf, client, true, 120) + cfg.Token = "test-token" + + cmd := newUsageCommand(cfg, newTestOpts(server.URL)) + cmd.SetArgs([]string{"--year", "2026", "--month", "3"}) + + _, err := cmd.ExecuteC() + require.Error(t, err) + require.Contains(t, err.Error(), "HTTP 404") + require.Contains(t, err.Error(), "gh auth refresh") + }) + + t.Run("--today flag works", func(t *testing.T) { + usageResp := &premiumRequestUsageResponse{ + TimePeriod: timePeriod{Year: 2026, Month: 3, Day: 29}, + User: "testuser", + UsageItems: []usageItem{ + { + Product: "Copilot", + Model: "Claude Opus 4.6", + GrossQuantity: 5, + GrossAmount: 0.20, + NetAmount: 0.0, + }, + }, + } + + server := newTestServer(t, map[string]string{"login": "testuser"}, usageResp, 0) + defer server.Close() + + buf := new(bytes.Buffer) + client := azuremodels.NewMockClient() + cfg := command.NewConfig(buf, buf, client, true, 120) + cfg.Token = "test-token" + + cmd := newUsageCommand(cfg, newTestOpts(server.URL)) + cmd.SetArgs([]string{"--today"}) + + _, err := cmd.ExecuteC() + require.NoError(t, err) + + output := buf.String() + require.Contains(t, output, "today") + }) + + t.Run("--help prints usage info", func(t *testing.T) { + outBuf := new(bytes.Buffer) + errBuf := new(bytes.Buffer) + cmd := NewUsageCommand(nil) + cmd.SetOut(outBuf) + cmd.SetErr(errBuf) + cmd.SetArgs([]string{"--help"}) + + err := cmd.Help() + + require.NoError(t, err) + require.Contains(t, outBuf.String(), "Display premium request usage statistics") + require.Empty(t, errBuf.String()) + }) + + t.Run("sorts by gross amount descending", func(t *testing.T) { + usageResp := &premiumRequestUsageResponse{ + TimePeriod: timePeriod{Year: 2026, Month: 3}, + User: "testuser", + UsageItems: []usageItem{ + { + Product: "Copilot", + Model: "GPT-5.2", + GrossQuantity: 10, + GrossAmount: 0.40, + NetAmount: 0.0, + }, + { + Product: "Copilot", + Model: "Claude Opus 4.6", + GrossQuantity: 100, + GrossAmount: 4.0, + NetAmount: 0.0, + }, + }, + } + + server := newTestServer(t, map[string]string{"login": "testuser"}, usageResp, 0) + defer server.Close() + + buf := new(bytes.Buffer) + client := azuremodels.NewMockClient() + cfg := command.NewConfig(buf, buf, client, true, 120) + cfg.Token = "test-token" + + cmd := newUsageCommand(cfg, newTestOpts(server.URL)) + cmd.SetArgs([]string{"--year", "2026", "--month", "3"}) + + _, err := cmd.ExecuteC() + require.NoError(t, err) + + output := buf.String() + opusIdx := bytes.Index([]byte(output), []byte("Claude Opus")) + gptIdx := bytes.Index([]byte(output), []byte("GPT-5.2")) + require.GreaterOrEqual(t, opusIdx, 0, "Claude Opus should be present in the output") + require.GreaterOrEqual(t, gptIdx, 0, "GPT-5.2 should be present in the output") + require.True(t, opusIdx < gptIdx, "Claude Opus 4.6 should appear before GPT-5.2 (sorted by gross amount)") + }) + + t.Run("net cost with zero gross does not produce NaN/Inf", func(t *testing.T) { + usageResp := &premiumRequestUsageResponse{ + TimePeriod: timePeriod{Year: 2026, Month: 3}, + User: "testuser", + UsageItems: []usageItem{ + { + Product: "Copilot", + Model: "Edge case model", + GrossQuantity: 1, + GrossAmount: 0.0, + NetAmount: 1.50, + }, + }, + } + + server := newTestServer(t, map[string]string{"login": "testuser"}, usageResp, 0) + defer server.Close() + + buf := new(bytes.Buffer) + client := azuremodels.NewMockClient() + cfg := command.NewConfig(buf, buf, client, true, 120) + cfg.Token = "test-token" + + cmd := newUsageCommand(cfg, newTestOpts(server.URL)) + cmd.SetArgs([]string{"--year", "2026", "--month", "3"}) + + _, err := cmd.ExecuteC() + require.NoError(t, err) + + output := buf.String() + require.NotContains(t, output, "NaN") + require.NotContains(t, output, "+Inf") + require.NotContains(t, output, "-Inf") + require.Contains(t, output, "gross amount unavailable") + }) + + t.Run("--today is mutually exclusive with date flags", func(t *testing.T) { + buf := new(bytes.Buffer) + client := azuremodels.NewMockClient() + cfg := command.NewConfig(buf, buf, client, true, 120) + cfg.Token = "test-token" + + cmd := newUsageCommand(cfg, nil) + cmd.SetArgs([]string{"--today", "--year", "2026"}) + + _, err := cmd.ExecuteC() + require.Error(t, err) + require.Contains(t, err.Error(), "none of the others can be") + }) + + t.Run("rejects invalid month", func(t *testing.T) { + buf := new(bytes.Buffer) + client := azuremodels.NewMockClient() + cfg := command.NewConfig(buf, buf, client, true, 120) + cfg.Token = "test-token" + + cmd := newUsageCommand(cfg, nil) + cmd.SetArgs([]string{"--year", "2026", "--month", "13"}) + + _, err := cmd.ExecuteC() + require.Error(t, err) + require.Contains(t, err.Error(), "invalid value for --month") + }) + + t.Run("rejects invalid day", func(t *testing.T) { + buf := new(bytes.Buffer) + client := azuremodels.NewMockClient() + cfg := command.NewConfig(buf, buf, client, true, 120) + cfg.Token = "test-token" + + cmd := newUsageCommand(cfg, nil) + cmd.SetArgs([]string{"--year", "2026", "--month", "3", "--day", "99"}) + + _, err := cmd.ExecuteC() + require.Error(t, err) + require.Contains(t, err.Error(), "invalid value for --day") + }) +} diff --git a/pkg/command/config.go b/pkg/command/config.go index 36296b44..c446b1eb 100644 --- a/pkg/command/config.go +++ b/pkg/command/config.go @@ -18,6 +18,8 @@ type Config struct { ErrOut io.Writer // Client is the client for interacting with the models service. Client azuremodels.Client + // Token is the GitHub authentication token. + Token string // IsTerminalOutput is true if the output should be formatted for a terminal. IsTerminalOutput bool // TerminalWidth is the width of the terminal. @@ -29,13 +31,19 @@ func NewConfig(out, errOut io.Writer, client azuremodels.Client, isTerminalOutpu return &Config{Out: out, ErrOut: errOut, Client: client, IsTerminalOutput: isTerminalOutput, TerminalWidth: width} } +// NewConfigWithToken returns a new command configuration with a GitHub token. +func NewConfigWithToken(out, errOut io.Writer, client azuremodels.Client, token string, isTerminalOutput bool, width int) *Config { + return &Config{Out: out, ErrOut: errOut, Client: client, Token: token, IsTerminalOutput: isTerminalOutput, TerminalWidth: width} +} + // NewConfigWithTerminal returns a new command configuration using the given terminal. -func NewConfigWithTerminal(terminal term.Term, client azuremodels.Client) *Config { +func NewConfigWithTerminal(terminal term.Term, client azuremodels.Client, token string) *Config { width, _, _ := terminal.Size() return &Config{ Out: terminal.Out(), ErrOut: terminal.ErrOut(), Client: client, + Token: token, IsTerminalOutput: terminal.IsTerminalOutput(), TerminalWidth: width, }