Skip to content
Open
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
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ go build -o ddollar ./src
## πŸš€ Usage

```bash
# Validate your token setup first
ddollar --validate

# Run any long-running CLI
ddollar claude --continue
ddollar python train_model.py
Expand Down Expand Up @@ -150,9 +153,48 @@ See [docs/TOR_INTEGRATION.md](docs/TOR_INTEGRATION.md) for:

---

## πŸ” Validate Your Setup

Before running long sessions, validate your tokens:

```bash
ddollar --validate
```

**Example output**:
```
πŸ” Validating tokens...

[1/3] Testing Anthropic token...
βœ“ Valid
Requests: 4850/5000 remaining (3.0% used)
Tokens: 95234/100000 remaining (4.8% used)
Reset: 52m 18s

[2/3] Testing Anthropic token...
βœ“ Valid
Requests: 5000/5000 remaining (0.0% used)
Tokens: 100000/100000 remaining (0.0% used)

[3/3] Testing OpenAI token...
βœ— FAILED: HTTP 401: authentication failed or invalid token

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Summary: 2 valid, 1 invalid, 3 total
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```

This tests each token with a minimal API call and shows:
- βœ“ Token validity
- Current rate limit usage
- Time until rate limits reset

---

## πŸ› Troubleshooting

- **"No tokens found"** β†’ Set `ANTHROPIC_API_KEY` (etc) in shell
- **Token validation fails** β†’ Run `ddollar --validate` to test each token
- **Process won't rotate** β†’ Tool must support `--continue` flag
- **Limit hit before rotation** β†’ Tokens hitting limits faster than 60s check interval

Expand Down
Binary file added ddollar
Binary file not shown.
43 changes: 43 additions & 0 deletions src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/drawohara/ddollar/src/supervisor"
"github.com/drawohara/ddollar/src/tokens"
"github.com/drawohara/ddollar/src/validator"
)

const version = "0.2.0"
Expand All @@ -21,6 +22,8 @@ func main() {
fmt.Printf("ddollar %s\n", version)
case "help", "--help", "-h":
printUsage()
case "validate", "--validate":
validateTokens()
default:
// Everything else is a command to supervise
superviseCommand(os.Args[1:])
Expand All @@ -32,14 +35,17 @@ func printUsage() {

Usage:
ddollar [--interactive] <command> [args...]
ddollar --validate # Test all tokens

Examples:
ddollar claude --continue # All-night AI sessions
ddollar python train_model.py # Long-running scripts
ddollar --interactive node agent.js # Prompt on limit hit
ddollar --validate # Validate token config

Flags:
--interactive, -i Prompt user when limit hit (default: auto-rotate)
--validate Test all tokens and show rate limit status
--help, -h Show this help
--version, -v Show version

Expand Down Expand Up @@ -104,3 +110,40 @@ func superviseCommand(args []string) {
os.Exit(1)
}
}

func validateTokens() {
// Discover tokens
fmt.Println("Discovering API tokens...")
discovered := tokens.Discover()

if len(discovered) == 0 {
fmt.Println("ERROR: No API tokens found in environment.")
fmt.Println("\nSet one or more:")
for _, p := range tokens.SupportedProviders {
for _, envVar := range p.EnvVars {
fmt.Printf(" export %s=your-token-here\n", envVar)
}
}
os.Exit(1)
}

// Create token pool
pool := tokens.NewPool()
for _, pt := range discovered {
if err := pool.AddProvider(pt.Provider, pt.Tokens); err != nil {
fmt.Printf("Warning: Failed to add provider %s: %v\n", pt.Provider.Name, err)
continue
}
}

if pool.ProviderCount() == 0 {
fmt.Println("ERROR: No providers configured")
os.Exit(1)
}

// Run validation
if err := validator.Validate(pool); err != nil {
fmt.Printf("\n%v\n", err)
os.Exit(1)
}
}
12 changes: 11 additions & 1 deletion src/supervisor/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ func (m *Monitor) Watch(token *tokens.Token, statusChan chan *RateLimitStatus) {
}
}

// CheckLimitsPublic is a public wrapper for checkLimits (used by validator)
func (m *Monitor) CheckLimitsPublic(token *tokens.Token) (*RateLimitStatus, error) {
return m.checkLimits(token)
}

// checkLimits makes a minimal API call to check rate limit headers
func (m *Monitor) checkLimits(token *tokens.Token) (*RateLimitStatus, error) {
var resp *http.Response
Expand All @@ -81,6 +86,11 @@ func (m *Monitor) checkLimits(token *tokens.Token) (*RateLimitStatus, error) {
}
defer resp.Body.Close()

// Check for HTTP errors (authentication failures, etc)
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("HTTP %d: authentication failed or invalid token", resp.StatusCode)
}

// Parse provider-specific headers
status := &RateLimitStatus{Provider: token.Provider.Name}

Expand All @@ -97,7 +107,7 @@ func (m *Monitor) checkLimits(token *tokens.Token) (*RateLimitStatus, error) {
func (m *Monitor) checkAnthropic(token *tokens.Token) (*http.Response, error) {
// Minimal request: 1 token response
reqBody := map[string]interface{}{
"model": "claude-3-5-sonnet-20241022",
"model": "claude-3-5-sonnet-20240620",
"max_tokens": 1,
"messages": []map[string]string{
{"role": "user", "content": "."},
Expand Down
111 changes: 111 additions & 0 deletions src/validator/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package validator

import (
"fmt"
"time"

"github.com/drawohara/ddollar/src/supervisor"
"github.com/drawohara/ddollar/src/tokens"
)

// Validate tests all tokens by making a minimal API call to each
func Validate(pool *tokens.Pool) error {
fmt.Println("\nπŸ” Validating tokens...\n")

totalTokens := pool.TotalTokenCount()
validTokens := 0
invalidTokens := 0

// Create a monitor for making API calls
monitor := supervisor.NewMonitor(60*time.Second, 0.95)

// Test each token
for i := 0; i < totalTokens; i++ {
token := pool.CurrentToken()
if token == nil {
break
}

fmt.Printf("[%d/%d] Testing %s token...\n", i+1, totalTokens, token.Provider.Name)

// Make a test API call
status, err := testToken(monitor, token)

if err != nil {
fmt.Printf(" βœ— FAILED: %v\n\n", err)
invalidTokens++
} else {
fmt.Printf(" βœ“ Valid\n")

// Only show rate limit details if we got data
if status.RequestsLimit > 0 || status.TokensLimit > 0 {
if status.RequestsLimit > 0 {
fmt.Printf(" Requests: %d/%d remaining (%.1f%% used)\n",
status.RequestsRemaining, status.RequestsLimit, status.RequestsPercentUsed())
}
if status.TokensLimit > 0 {
fmt.Printf(" Tokens: %d/%d remaining (%.1f%% used)\n",
status.TokensRemaining, status.TokensLimit, status.TokensPercentUsed())
}
if !status.ResetTime.IsZero() {
fmt.Printf(" Reset: %s\n", formatDuration(status.TimeUntilReset()))
}
} else {
fmt.Printf(" Rate limits: Will be monitored during supervision\n")
}

fmt.Println()
validTokens++
}

// Move to next token
pool.Next()
}

// Summary
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
fmt.Printf("Summary: %d valid, %d invalid, %d total\n", validTokens, invalidTokens, totalTokens)
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")

if invalidTokens > 0 {
return fmt.Errorf("\n⚠️ %d token(s) failed validation", invalidTokens)
}

fmt.Println("\nβœ“ All tokens validated successfully!")
return nil
}

// testToken makes a minimal API call to verify the token works
func testToken(monitor *supervisor.Monitor, token *tokens.Token) (*supervisor.RateLimitStatus, error) {
// Use the existing checkLimits method from monitor
// This makes a minimal API call and parses rate limit headers
status, err := monitor.CheckLimitsPublic(token)
if err != nil {
return nil, err
}

// Check for authentication errors or invalid tokens
// Note: Some providers may not return rate limit headers on all endpoints
// We consider the token valid if we got a successful response

return status, nil
}

// formatDuration formats a duration in human-readable form
func formatDuration(d time.Duration) string {
d = d.Round(time.Second)

h := d / time.Hour
d -= h * time.Hour
m := d / time.Minute
d -= m * time.Minute
s := d / time.Second

if h > 0 {
return fmt.Sprintf("%dh %dm", h, m)
}
if m > 0 {
return fmt.Sprintf("%dm %ds", m, s)
}
return fmt.Sprintf("%ds", s)
}