fa4a48d5e7
* chore: remove orphan executor tracing file and deprecated ResolveScript wrappers - Delete internal/agentctl/tracing/executor.go (7 TraceExecutor* funcs) - Delete internal/orchestrator/executor/script_template.go (deprecated wrapper) - Delete scriptengine.ResolveScript convenience function Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove orphan setter methods across healthpoll, jira, linear, slack, updates - Delete Poller.SetInterval from integrations/healthpoll (no callers) - Delete Poller.SetIssueTickHook from jira and linear (no callers) - Delete Trigger.SetInterval and Trigger.SetMatchHook from slack (no callers) - Delete Service.SetPollInterval from system/updates (no callers) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove orphan wrapper functions and accessor method - Delete updates.FetchLatestRelease shim (wraps FetchLatestReleaseFrom, no callers) - Delete usage.NewClaudeUsageClient and NewCodexUsageClient shims (no callers) - Delete database.Service.Path() accessor (no callers) - Update ErrGitHubRateLimited godoc to reference FetchLatestReleaseFrom Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove unwired handleListExecutors MCP handler The sibling profile handlers (list/create/update/delete) are registered via RegisterFunc in handlers.go. handleListExecutors never was — dead flow. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove stale hook-field comments after setter deletion SetIssueTickHook (jira, linear) and SetMatchHook (slack) were removed in the deadcode cleanup. Their struct fields still claimed "tests use this" — no test or caller can set these fields now. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove orphan hook fields and fire methods after setter deletion issueTickHook (jira/linear) and matchHook (slack) are unexported and never set after their public setters were removed; fireIssueTickHook / fireHook were permanent no-ops. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
107 lines
2.8 KiB
Go
107 lines
2.8 KiB
Go
package usage
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
const codexUsageURL = "https://chatgpt.com/backend-api/wham/usage"
|
|
|
|
// CodexUsageClient fetches utilization from the OpenAI Codex usage API.
|
|
// It reads the bearer token from ~/.config/codex/auth.json and inspects
|
|
// response headers for utilization percentages. Fail-open: if the token is
|
|
// missing or the request fails, nil is returned (no utilization data).
|
|
type CodexUsageClient struct {
|
|
authPath string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// NewCodexUsageClientWithPath creates a client with an explicit auth path (for tests).
|
|
func NewCodexUsageClientWithPath(authPath string) *CodexUsageClient {
|
|
return &CodexUsageClient{
|
|
authPath: authPath,
|
|
httpClient: &http.Client{Timeout: 10 * time.Second},
|
|
}
|
|
}
|
|
|
|
// AuthPath returns the path this client reads credentials from.
|
|
func (c *CodexUsageClient) AuthPath() string {
|
|
return c.authPath
|
|
}
|
|
|
|
type codexAuthJSON struct {
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
// FetchUsage implements ProviderUsageClient.
|
|
// Returns nil, nil if the auth file is missing or the token cannot be read —
|
|
// this is the fail-open path for Codex.
|
|
func (c *CodexUsageClient) FetchUsage(ctx context.Context) (*ProviderUsage, error) {
|
|
token, err := c.readToken()
|
|
if err != nil {
|
|
// Fail-open: no Codex auth available.
|
|
return nil, nil //nolint:nilerr
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, codexUsageURL, nil)
|
|
if err != nil {
|
|
return nil, nil //nolint:nilerr
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, nil //nolint:nilerr
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, nil
|
|
}
|
|
|
|
primary := parseHeaderPercent(resp.Header.Get("x-codex-primary-used-percent"))
|
|
secondary := parseHeaderPercent(resp.Header.Get("x-codex-secondary-used-percent"))
|
|
|
|
now := time.Now()
|
|
windows := []UtilizationWindow{
|
|
{Label: "primary", UtilizationPct: primary, ResetAt: now.Add(24 * time.Hour)},
|
|
{Label: "secondary", UtilizationPct: secondary, ResetAt: now.Add(7 * 24 * time.Hour)},
|
|
}
|
|
return &ProviderUsage{
|
|
Provider: "openai",
|
|
Windows: windows,
|
|
FetchedAt: now,
|
|
}, nil
|
|
}
|
|
|
|
func (c *CodexUsageClient) readToken() (string, error) {
|
|
data, err := os.ReadFile(c.authPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("read %s: %w", c.authPath, err)
|
|
}
|
|
var auth codexAuthJSON
|
|
if err := json.Unmarshal(data, &auth); err != nil {
|
|
return "", fmt.Errorf("parse %s: %w", c.authPath, err)
|
|
}
|
|
if auth.Token == "" {
|
|
return "", fmt.Errorf("empty token in %s", c.authPath)
|
|
}
|
|
return auth.Token, nil
|
|
}
|
|
|
|
func parseHeaderPercent(v string) float64 {
|
|
if v == "" {
|
|
return 0
|
|
}
|
|
f, err := strconv.ParseFloat(v, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return f
|
|
}
|