Skip to content
Merged
Show file tree
Hide file tree
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
18 changes: 17 additions & 1 deletion internal/cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand All @@ -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}
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -749,6 +764,7 @@ Usage:
prism codex enable|disable|status
prism usage [--profile <name>]
prism chatgpt usage [--profile <name>]
prism copilot usage [--profile <name>]
prism opencode-go usage
prism chatgpt auth login [--profile <name>]
prism anthropic auth login [--profile <name>]
Expand Down
26 changes: 24 additions & 2 deletions internal/cli/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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}},
Expand All @@ -77,18 +89,20 @@ 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)
}
}

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) {
Expand All @@ -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"}},
Expand All @@ -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) {
Expand All @@ -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)
}
}
Expand Down
Loading