From db85032b086fc9e33f895cc1415dd94d70aeb351 Mon Sep 17 00:00:00 2001 From: YG Park Date: Wed, 19 Aug 2026 13:01:01 +0900 Subject: [PATCH] feat(usage): add Copilot to prism usage The combined table gains a Copilot row between Claude and OpenCode, showing the monthly premium-requests allowance (unlimited pools are not reported by the server), and `prism copilot usage` works standalone. Pace projects against the calendar-month window the server reports. Verified: go test ./... pass; local build confirmed against prod (individual plan, 45.3% used, pace projected) and output approved. Co-Authored-By: Claude Opus 5 --- internal/cli/run.go | 18 +++++++++++++++++- internal/cli/run_test.go | 26 ++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/internal/cli/run.go b/internal/cli/run.go index 7e122ec..00bd101 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -37,6 +37,13 @@ var fetchAnthropicUsage = func(ctx context.Context, options commonOptions) (api. } return client.Usage(ctx, "anthropic") } +var fetchCopilotUsage = func(ctx context.Context, options commonOptions) (api.ProviderUsage, error) { + client, err := prismClient(ctx, options) + if err != nil { + return api.ProviderUsage{}, err + } + return client.Usage(ctx, "copilot") +} type commonOptions struct { profile string @@ -108,6 +115,8 @@ func Run( usage, err = fetchOpenCodeGoUsage(ctx) } else if providerName == "anthropic" { usage, err = fetchAnthropicUsage(ctx, options) + } else if providerName == "copilot" { + usage, err = fetchCopilotUsage(ctx, options) } else { usage, err = fetchChatGPTUsage(ctx, options) } @@ -188,6 +197,7 @@ func runCombinedUsage(ctx context.Context, args []string, output io.Writer) erro } chatGPTResults := make(chan usageResult, 1) anthropicResults := make(chan usageResult, 1) + copilotResults := make(chan usageResult, 1) openCodeResults := make(chan usageResult, 1) go func() { usage, fetchErr := fetchChatGPTUsage(ctx, options) @@ -197,6 +207,10 @@ func runCombinedUsage(ctx context.Context, args []string, output io.Writer) erro usage, fetchErr := fetchAnthropicUsage(ctx, options) anthropicResults <- usageResult{usage: usage, err: fetchErr} }() + go func() { + usage, fetchErr := fetchCopilotUsage(ctx, options) + copilotResults <- usageResult{usage: usage, err: fetchErr} + }() go func() { usage, fetchErr := fetchOpenCodeGoUsage(ctx) openCodeResults <- usageResult{usage: usage, err: fetchErr} @@ -208,6 +222,7 @@ func runCombinedUsage(ctx context.Context, args []string, output io.Writer) erro }{ {name: "ChatGPT", result: <-chatGPTResults}, {name: "Claude", result: <-anthropicResults}, + {name: "Copilot", result: <-copilotResults}, {name: "OpenCode", result: <-openCodeResults}, } succeeded := 0 @@ -235,7 +250,7 @@ func runCombinedUsage(ctx context.Context, args []string, output io.Writer) erro func validateCommand(provider string, command string, positionals []string, options commonOptions) error { switch command { case "usage": - if provider != "chatgpt" && provider != "anthropic" && provider != "opencode-go" { + if provider != "chatgpt" && provider != "anthropic" && provider != "copilot" && provider != "opencode-go" { return fmt.Errorf("%s usage is not supported", provider) } if len(positionals) != 0 { @@ -749,6 +764,7 @@ Usage: prism codex enable|disable|status prism usage [--profile ] prism chatgpt usage [--profile ] + prism copilot usage [--profile ] prism opencode-go usage prism chatgpt auth login [--profile ] prism anthropic auth login [--profile ] diff --git a/internal/cli/run_test.go b/internal/cli/run_test.go index 72ce87e..a723e12 100644 --- a/internal/cli/run_test.go +++ b/internal/cli/run_test.go @@ -38,10 +38,12 @@ func TestHelpDocumentsSupportedCommandsWithoutInternalDetails(t *testing.T) { func TestCombinedUsageShowsChatGPTAnthropicAndOpenCodeGo(t *testing.T) { originalChatGPT := fetchChatGPTUsage originalAnthropic := fetchAnthropicUsage + originalCopilot := fetchCopilotUsage originalOpenCode := fetchOpenCodeGoUsage defer func() { fetchChatGPTUsage = originalChatGPT fetchAnthropicUsage = originalAnthropic + fetchCopilotUsage = originalCopilot fetchOpenCodeGoUsage = originalOpenCode }() plan := "pro" @@ -63,6 +65,16 @@ func TestCombinedUsageShowsChatGPTAnthropicAndOpenCodeGo(t *testing.T) { Limits: []api.UsageLimit{{Name: "default", Window: "5h", UsedPercent: 20, RemainingPercent: 80}}, }}}, nil } + copilotPlan := "individual" + fetchCopilotUsage = func(_ context.Context, options commonOptions) (api.ProviderUsage, error) { + if options.profile != "work-admin" || !options.profileSet { + t.Fatalf("Copilot options = %+v", options) + } + return api.ProviderUsage{Provider: "copilot", Accounts: []api.UsageAccount{{ + Name: "example-user", Plan: &copilotPlan, + Limits: []api.UsageLimit{{Name: "premium requests", Window: "monthly", UsedPercent: 45.3, RemainingPercent: 54.7}}, + }}}, nil + } fetchOpenCodeGoUsage = func(context.Context) (api.ProviderUsage, error) { return api.ProviderUsage{Provider: "opencode-go", Accounts: []api.UsageAccount{{ Name: "-", Limits: []api.UsageLimit{{Name: "rolling", Window: "5h", UsedPercent: 2, RemainingPercent: 98}}, @@ -77,7 +89,7 @@ func TestCombinedUsageShowsChatGPTAnthropicAndOpenCodeGo(t *testing.T) { t.Fatalf("ChatGPT options = %+v", chatGPTOptions) } text := output.String() - if strings.Count(text, "┌") != 1 || !strings.Contains(text, "│ PROVIDER │ ACCOUNT") || !strings.Contains(text, "ChatGPT") || !strings.Contains(text, "Claude") || !strings.Contains(text, "OpenCode") || !strings.Contains(text, "Max 20x") || !strings.Contains(text, "person@example.com") || strings.Contains(text, "OpenCode workspace") { + if strings.Count(text, "┌") != 1 || !strings.Contains(text, "│ PROVIDER │ ACCOUNT") || !strings.Contains(text, "ChatGPT") || !strings.Contains(text, "Claude") || !strings.Contains(text, "Copilot") || !strings.Contains(text, "OpenCode") || !strings.Contains(text, "Max 20x") || !strings.Contains(text, "person@example.com") || !strings.Contains(text, "premium requests") || strings.Contains(text, "OpenCode workspace") { t.Fatalf("output = %q", text) } } @@ -85,10 +97,12 @@ func TestCombinedUsageShowsChatGPTAnthropicAndOpenCodeGo(t *testing.T) { func TestCombinedUsageKeepsPartialResults(t *testing.T) { originalChatGPT := fetchChatGPTUsage originalAnthropic := fetchAnthropicUsage + originalCopilot := fetchCopilotUsage originalOpenCode := fetchOpenCodeGoUsage defer func() { fetchChatGPTUsage = originalChatGPT fetchAnthropicUsage = originalAnthropic + fetchCopilotUsage = originalCopilot fetchOpenCodeGoUsage = originalOpenCode }() fetchChatGPTUsage = func(context.Context, commonOptions) (api.ProviderUsage, error) { @@ -97,6 +111,9 @@ func TestCombinedUsageKeepsPartialResults(t *testing.T) { fetchAnthropicUsage = func(context.Context, commonOptions) (api.ProviderUsage, error) { return api.ProviderUsage{}, errors.New("Anthropic login unavailable") } + fetchCopilotUsage = func(context.Context, commonOptions) (api.ProviderUsage, error) { + return api.ProviderUsage{}, errors.New("Copilot login unavailable") + } fetchOpenCodeGoUsage = func(context.Context) (api.ProviderUsage, error) { return api.ProviderUsage{Provider: "opencode-go", Accounts: []api.UsageAccount{{ Name: "-", Limits: []api.UsageLimit{{Name: "weekly", Window: "7d"}}, @@ -115,10 +132,12 @@ func TestCombinedUsageKeepsPartialResults(t *testing.T) { func TestCombinedUsageFailsOnlyWhenEveryProviderFails(t *testing.T) { originalChatGPT := fetchChatGPTUsage originalAnthropic := fetchAnthropicUsage + originalCopilot := fetchCopilotUsage originalOpenCode := fetchOpenCodeGoUsage defer func() { fetchChatGPTUsage = originalChatGPT fetchAnthropicUsage = originalAnthropic + fetchCopilotUsage = originalCopilot fetchOpenCodeGoUsage = originalOpenCode }() fetchChatGPTUsage = func(context.Context, commonOptions) (api.ProviderUsage, error) { @@ -127,13 +146,16 @@ func TestCombinedUsageFailsOnlyWhenEveryProviderFails(t *testing.T) { fetchAnthropicUsage = func(context.Context, commonOptions) (api.ProviderUsage, error) { return api.ProviderUsage{}, errors.New("unavailable") } + fetchCopilotUsage = func(context.Context, commonOptions) (api.ProviderUsage, error) { + return api.ProviderUsage{}, errors.New("unavailable") + } fetchOpenCodeGoUsage = func(context.Context) (api.ProviderUsage, error) { return api.ProviderUsage{}, errors.New("unavailable") } var output bytes.Buffer err := Run(context.Background(), []string{"usage"}, &output, &bytes.Buffer{}, "test") - if err == nil || err.Error() != "usage is unavailable for ChatGPT, Claude, and OpenCode" { + if err == nil || err.Error() != "usage is unavailable for ChatGPT, Claude, Copilot, and OpenCode" { t.Fatalf("error = %v", err) } }