Skip to content

Commit 21f0eb5

Browse files
MO2k4JanDeDobbeleer
authored andcommitted
feat(claude): add cost sub-fields and rate limits
1 parent 6bd24b0 commit 21f0eb5

File tree

3 files changed

+224
-6
lines changed

3 files changed

+224
-6
lines changed

src/segments/claude.go

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type Claude struct {
1616

1717
// ClaudeData represents the parsed Claude JSON data
1818
type ClaudeData struct {
19+
RateLimits *ClaudeRateLimits `json:"rate_limits"`
1920
Model ClaudeModel `json:"model"`
2021
Workspace ClaudeWorkspace `json:"workspace"`
2122
SessionID string `json:"session_id"`
@@ -36,10 +37,35 @@ type ClaudeWorkspace struct {
3637
GitWorktree string `json:"git_worktree"`
3738
}
3839

40+
// DurationMS is a duration in milliseconds that formats as "Xm Ys".
41+
type DurationMS int64
42+
43+
func (d DurationMS) String() string {
44+
totalSeconds := int64(d) / 1000
45+
minutes := totalSeconds / 60
46+
seconds := totalSeconds % 60
47+
return fmt.Sprintf("%dm %ds", minutes, seconds)
48+
}
49+
3950
// ClaudeCost represents cost and duration information
4051
type ClaudeCost struct {
41-
TotalCostUSD float64 `json:"total_cost_usd"`
42-
TotalDurationMS int64 `json:"total_duration_ms"`
52+
TotalCostUSD float64 `json:"total_cost_usd"`
53+
TotalDurationMS DurationMS `json:"total_duration_ms"`
54+
TotalAPIDurationMS DurationMS `json:"total_api_duration_ms"`
55+
TotalLinesAdded int `json:"total_lines_added"`
56+
TotalLinesRemoved int `json:"total_lines_removed"`
57+
}
58+
59+
// ClaudeRateLimitWindow represents a single rate limit time window.
60+
type ClaudeRateLimitWindow struct {
61+
UsedPercentage *float64 `json:"used_percentage"`
62+
ResetsAt *int64 `json:"resets_at"`
63+
}
64+
65+
// ClaudeRateLimits represents rate limit information across time windows.
66+
type ClaudeRateLimits struct {
67+
FiveHour *ClaudeRateLimitWindow `json:"five_hour"`
68+
SevenDay *ClaudeRateLimitWindow `json:"seven_day"`
4369
}
4470

4571
// ClaudeContextWindow represents token usage information
@@ -145,6 +171,49 @@ func (c *Claude) FormattedCost() string {
145171
return fmt.Sprintf("$%.2f", c.Cost.TotalCostUSD)
146172
}
147173

174+
// FormattedDuration returns total session duration as "Xm Ys".
175+
func (c *Claude) FormattedDuration() string {
176+
return c.Cost.TotalDurationMS.String()
177+
}
178+
179+
// FormattedAPIDuration returns API wait time as "Xm Ys".
180+
func (c *Claude) FormattedAPIDuration() string {
181+
return c.Cost.TotalAPIDurationMS.String()
182+
}
183+
184+
// rateLimitPercentage extracts a percentage from a rate limit window with nil-safety.
185+
func rateLimitPercentage(limits *ClaudeRateLimits, window func(*ClaudeRateLimits) *ClaudeRateLimitWindow) text.Percentage {
186+
if limits == nil {
187+
return 0
188+
}
189+
190+
w := window(limits)
191+
if w == nil || w.UsedPercentage == nil {
192+
return 0
193+
}
194+
195+
percent := int(*w.UsedPercentage + 0.5)
196+
if percent > 100 {
197+
return 100
198+
}
199+
200+
return text.Percentage(percent)
201+
}
202+
203+
// FiveHourUsage returns the 5-hour rolling window rate limit usage as a Percentage.
204+
func (c *Claude) FiveHourUsage() text.Percentage {
205+
return rateLimitPercentage(c.RateLimits, func(r *ClaudeRateLimits) *ClaudeRateLimitWindow {
206+
return r.FiveHour
207+
})
208+
}
209+
210+
// SevenDayUsage returns the 7-day window rate limit usage as a Percentage.
211+
func (c *Claude) SevenDayUsage() text.Percentage {
212+
return rateLimitPercentage(c.RateLimits, func(r *ClaudeRateLimits) *ClaudeRateLimitWindow {
213+
return r.SevenDay
214+
})
215+
}
216+
148217
// FormattedTokens returns a human-readable string of current context tokens.
149218
// Uses CurrentUsage (which represents actual context and resets on compact/clear)
150219
// with fallback to total tokens for backwards compatibility.

src/segments/claude_test.go

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@ func TestClaudeSegment(t *testing.T) {
3939
GitWorktree: "/repo/project/.worktrees/feature",
4040
},
4141
Cost: ClaudeCost{
42-
TotalCostUSD: 0.01,
43-
TotalDurationMS: 45000,
42+
TotalCostUSD: 0.01,
43+
TotalDurationMS: 45000,
44+
TotalAPIDurationMS: 30000,
45+
TotalLinesAdded: 156,
46+
TotalLinesRemoved: 23,
4447
},
4548
ContextWindow: ClaudeContextWindow{
4649
TotalInputTokens: 15234,
@@ -51,6 +54,16 @@ func TestClaudeSegment(t *testing.T) {
5154
OutputTokens: 1200,
5255
},
5356
},
57+
RateLimits: &ClaudeRateLimits{
58+
FiveHour: &ClaudeRateLimitWindow{
59+
UsedPercentage: new(24.5),
60+
ResetsAt: new(int64(1711180800)),
61+
},
62+
SevenDay: &ClaudeRateLimitWindow{
63+
UsedPercentage: new(45.0),
64+
ResetsAt: new(int64(1711612800)),
65+
},
66+
},
5467
},
5568
ExpectedEnabled: true,
5669
ExpectedModel: "Opus",
@@ -317,6 +330,43 @@ func TestClaudeFormattedCost(t *testing.T) {
317330
}
318331
}
319332

333+
func TestClaudeFormattedDuration(t *testing.T) {
334+
cases := []struct {
335+
Case string
336+
Expected string
337+
MS DurationMS
338+
}{
339+
{Case: "Zero", MS: 0, Expected: "0m 0s"},
340+
{Case: "Seconds only", MS: 45000, Expected: "0m 45s"},
341+
{Case: "Minutes and seconds", MS: 125000, Expected: "2m 5s"},
342+
{Case: "Exact minute", MS: 60000, Expected: "1m 0s"},
343+
}
344+
345+
for _, tc := range cases {
346+
claude := &Claude{}
347+
claude.Cost.TotalDurationMS = tc.MS
348+
assert.Equal(t, tc.Expected, claude.FormattedDuration(), tc.Case)
349+
}
350+
}
351+
352+
func TestClaudeFormattedAPIDuration(t *testing.T) {
353+
cases := []struct {
354+
Case string
355+
Expected string
356+
MS DurationMS
357+
}{
358+
{Case: "Zero", MS: 0, Expected: "0m 0s"},
359+
{Case: "Seconds only", MS: 30000, Expected: "0m 30s"},
360+
{Case: "Minutes and seconds", MS: 90000, Expected: "1m 30s"},
361+
}
362+
363+
for _, tc := range cases {
364+
claude := &Claude{}
365+
claude.Cost.TotalAPIDurationMS = tc.MS
366+
assert.Equal(t, tc.Expected, claude.FormattedAPIDuration(), tc.Case)
367+
}
368+
}
369+
320370
func TestClaudeFormattedTokens(t *testing.T) {
321371
cases := []struct {
322372
Case string
@@ -415,3 +465,79 @@ func TestClaudeFormattedTokens(t *testing.T) {
415465
assert.Equal(t, tc.ExpectedFormat, formatted, tc.Case)
416466
}
417467
}
468+
469+
func TestClaudeRateLimitUsage(t *testing.T) {
470+
cases := []struct {
471+
RateLimits *ClaudeRateLimits
472+
Case string
473+
ExpectedFive text.Percentage
474+
ExpectedSeven text.Percentage
475+
}{
476+
{
477+
Case: "Nil RateLimits",
478+
RateLimits: nil,
479+
ExpectedFive: 0,
480+
ExpectedSeven: 0,
481+
},
482+
{
483+
Case: "Nil FiveHour window",
484+
RateLimits: &ClaudeRateLimits{
485+
SevenDay: &ClaudeRateLimitWindow{UsedPercentage: new(50.0)},
486+
},
487+
ExpectedFive: 0,
488+
ExpectedSeven: 50,
489+
},
490+
{
491+
Case: "Nil SevenDay window",
492+
RateLimits: &ClaudeRateLimits{
493+
FiveHour: &ClaudeRateLimitWindow{UsedPercentage: new(25.0)},
494+
},
495+
ExpectedFive: 25,
496+
ExpectedSeven: 0,
497+
},
498+
{
499+
Case: "Nil UsedPercentage",
500+
RateLimits: &ClaudeRateLimits{
501+
FiveHour: &ClaudeRateLimitWindow{UsedPercentage: nil},
502+
SevenDay: &ClaudeRateLimitWindow{UsedPercentage: nil},
503+
},
504+
ExpectedFive: 0,
505+
ExpectedSeven: 0,
506+
},
507+
{
508+
Case: "Valid percentages",
509+
RateLimits: &ClaudeRateLimits{
510+
FiveHour: &ClaudeRateLimitWindow{UsedPercentage: new(42.7)},
511+
SevenDay: &ClaudeRateLimitWindow{UsedPercentage: new(75.3)},
512+
},
513+
ExpectedFive: 43,
514+
ExpectedSeven: 75,
515+
},
516+
{
517+
Case: "Value over 100 capped",
518+
RateLimits: &ClaudeRateLimits{
519+
FiveHour: &ClaudeRateLimitWindow{UsedPercentage: new(150.0)},
520+
SevenDay: &ClaudeRateLimitWindow{UsedPercentage: new(200.0)},
521+
},
522+
ExpectedFive: 100,
523+
ExpectedSeven: 100,
524+
},
525+
{
526+
Case: "Zero percentages",
527+
RateLimits: &ClaudeRateLimits{
528+
FiveHour: &ClaudeRateLimitWindow{UsedPercentage: new(0.0)},
529+
SevenDay: &ClaudeRateLimitWindow{UsedPercentage: new(0.0)},
530+
},
531+
ExpectedFive: 0,
532+
ExpectedSeven: 0,
533+
},
534+
}
535+
536+
for _, tc := range cases {
537+
claude := &Claude{}
538+
claude.RateLimits = tc.RateLimits
539+
540+
assert.Equal(t, tc.ExpectedFive, claude.FiveHourUsage(), tc.Case+" (FiveHour)")
541+
assert.Equal(t, tc.ExpectedSeven, claude.SevenDayUsage(), tc.Case+" (SevenDay)")
542+
}
543+
}

website/docs/segments/cli/claude.mdx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ import Config from "@site/src/components/Config.js";
5151
| `.TokenUsagePercent` | `Percentage` | Percentage of context window used (0-100) |
5252
| `.FormattedCost` | `string` | Formatted cost string (e.g., "$0.15" or "$0.0012") |
5353
| `.FormattedTokens` | `string` | Human-readable token count (e.g., "1.2K", "15.3M") |
54+
| `.FormattedDuration` | `string` | Total session duration (e.g., "2m 5s") |
55+
| `.FormattedAPIDuration` | `string` | API wait time (e.g., "0m 45s") |
56+
| `.FiveHourUsage` | `Percentage` | 5-hour rolling rate limit usage (0-100) |
57+
| `.SevenDayUsage` | `Percentage` | 7-day rate limit usage (0-100) |
5458

5559
#### Model Properties
5660

@@ -71,8 +75,11 @@ import Config from "@site/src/components/Config.js";
7175

7276
| Name | Type | Description |
7377
| ------------------ | --------- | -------------------------------------- |
74-
| `.TotalCostUSD` | `float64` | Total cost in USD |
75-
| `.TotalDurationMS` | `int64` | Total session duration in milliseconds |
78+
| `.TotalCostUSD` | `float64` | Total cost in USD |
79+
| `.TotalDurationMS` | `DurationMS` | Total session duration in milliseconds (formats as "Xm Ys") |
80+
| `.TotalAPIDurationMS` | `DurationMS` | Time spent waiting for API responses (formats as "Xm Ys") |
81+
| `.TotalLinesAdded` | `int` | Lines of code added in the session |
82+
| `.TotalLinesRemoved` | `int` | Lines of code removed in the session |
7683

7784
#### ContextWindow Properties
7885

@@ -90,6 +97,22 @@ import Config from "@site/src/components/Config.js";
9097
| `.InputTokens` | `int` | Input tokens for the current message |
9198
| `.OutputTokens` | `int` | Output tokens for the current message |
9299

100+
#### RateLimits Properties
101+
102+
Available when Claude Code provides rate limit data (Pro/Max subscribers). Access via `.RateLimits`.
103+
104+
| Name | Type | Description |
105+
| ----------- | ----------------- | ------------------------ |
106+
| `.FiveHour` | `RateLimitWindow` | 5-hour rolling window |
107+
| `.SevenDay` | `RateLimitWindow` | 7-day rolling window |
108+
109+
#### RateLimitWindow Properties
110+
111+
| Name | Type | Description |
112+
| ----------------- | ---------- | ---------------------------------------- |
113+
| `.UsedPercentage` | `*float64` | Usage percentage (0-100), nil if unknown |
114+
| `.ResetsAt` | `*int64` | Unix epoch seconds when window resets |
115+
93116
### Percentage Methods
94117

95118
The `TokenUsagePercent` property is a `Percentage` type that provides additional functionality:

0 commit comments

Comments
 (0)