Align usage dashboard with CCSwitch
Filter request logs to model API calls and add request, provider, model, and settings tabs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f729910082
commit
272f604274
8 files changed
+444
-64
No files matched your search
@@ -680,6 +680,60 @@ func TestApplyAndRestore(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponsesRequestsAreLogged(t *testing.T) {
|
||||
env := newCodexTestEnv(t)
|
||||
resp := env.post(t, "/v1/responses", responsesBody("gpt-5-codex", false), env.store.Config().RelayToken)
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
|
||||
status := env.get(t, "/api/status")
|
||||
stats := status["stats"].(map[string]any)
|
||||
if stats["requests_total"].(float64) != 1 {
|
||||
t.Errorf("requests_total = %v, want 1", stats["requests_total"])
|
||||
}
|
||||
logs := env.get(t, "/api/logs")["items"].([]any)
|
||||
if len(logs) != 1 {
|
||||
t.Fatalf("request logs = %v, want exactly one Responses request", logs)
|
||||
}
|
||||
if logs[0].(map[string]any)["path"] != "/v1/responses" {
|
||||
t.Errorf("logged path = %v, want /v1/responses", logs[0].(map[string]any)["path"])
|
||||
}
|
||||
if logs[0].(map[string]any)["app"] != "Codex" || logs[0].(map[string]any)["provider"] != "codex-up" {
|
||||
t.Errorf("log app/provider = %v/%v, want Codex/codex-up", logs[0].(map[string]any)["app"], logs[0].(map[string]any)["provider"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagementRequestsAreExcludedFromLogsAndStats(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
for _, path := range []string{"/api/status", "/api/logs", "/api/providers?target=claude", "/api/settings"} {
|
||||
resp, err := http.Get(env.gateway.URL + path)
|
||||
if err != nil {
|
||||
t.Fatalf("get %s: %v", path, err)
|
||||
}
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("GET %s status = %d, want 200", path, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
resp := env.post(t, "/v1/messages/count_tokens", messagesBody("m", false), env.store.Config().RelayToken)
|
||||
resp.Body.Close()
|
||||
|
||||
status := env.get(t, "/api/status")
|
||||
stats := status["stats"].(map[string]any)
|
||||
if stats["requests_total"].(float64) != 0 {
|
||||
t.Errorf("requests_total = %v, want 0 after management requests", stats["requests_total"])
|
||||
}
|
||||
logs := env.get(t, "/api/logs")["items"].([]any)
|
||||
if len(logs) != 0 {
|
||||
t.Errorf("request logs = %v, want no management requests", logs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusAndLogs(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
resp := env.post(t, "/v1/messages", messagesBody("m", false), env.store.Config().RelayToken)
|
||||
@@ -701,8 +755,8 @@ func TestStatusAndLogs(t *testing.T) {
|
||||
t.Errorf("status = %v", status)
|
||||
}
|
||||
stats := status["stats"].(map[string]any)
|
||||
if stats["requests_total"].(float64) < 1 {
|
||||
t.Errorf("stats = %v", stats)
|
||||
if stats["requests_total"].(float64) != 1 {
|
||||
t.Errorf("stats = %v, want exactly the one model request", stats)
|
||||
}
|
||||
active := status["active_claude_provider"].(map[string]any)
|
||||
if active["name"] != "test" {
|
||||
@@ -725,6 +779,9 @@ func TestStatusAndLogs(t *testing.T) {
|
||||
if messageLog["model"] != "ds-chat" {
|
||||
t.Errorf("log model = %v, want the resolved model", messageLog["model"])
|
||||
}
|
||||
if messageLog["app"] != "Claude Code" || messageLog["provider"] != "test" {
|
||||
t.Errorf("log app/provider = %v/%v, want Claude Code/test", messageLog["app"], messageLog["provider"])
|
||||
}
|
||||
if messageLog["status"].(float64) != 200 {
|
||||
t.Errorf("log status = %v", messageLog["status"])
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
rec := recordFrom(r)
|
||||
rec.update(func(e *LogEntry) {
|
||||
e.Provider = provider.Name
|
||||
e.Model = model
|
||||
e.InputTokens = anthropic.EstimateTokens(&req)
|
||||
})
|
||||
|
||||
@@ -38,6 +38,7 @@ func (s *Server) handleResponses(w http.ResponseWriter, r *http.Request) {
|
||||
rec := recordFrom(r)
|
||||
if provider.Protocol == config.ProtocolOpenAIResponses {
|
||||
rec.update(func(e *LogEntry) {
|
||||
e.Provider = provider.Name
|
||||
e.Model = model
|
||||
e.InputTokens = (len(body) + 3) / 4
|
||||
})
|
||||
@@ -57,6 +58,7 @@ func (s *Server) handleResponses(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
rec.update(func(e *LogEntry) {
|
||||
e.Provider = provider.Name
|
||||
e.Model = model
|
||||
e.InputTokens = responses.EstimateTokens(chatReq)
|
||||
})
|
||||
|
||||
@@ -35,6 +35,8 @@ type LogEntry struct {
|
||||
Time time.Time `json:"time"`
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
App string `json:"app,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Status int `json:"status"`
|
||||
DurationMS int64 `json:"duration_ms"`
|
||||
@@ -71,9 +73,9 @@ func (s *Server) Port() int { return s.port }
|
||||
// Handler returns the full HTTP handler.
|
||||
func (s *Server) Handler() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("POST /v1/messages", s.withAuth(s.handleMessages))
|
||||
mux.Handle("POST /v1/messages", s.withLogging(s.withAuth(s.handleMessages)))
|
||||
mux.HandleFunc("POST /v1/messages/count_tokens", s.withAuth(s.handleCountTokens))
|
||||
mux.HandleFunc("POST /v1/responses", s.withAuth(s.handleResponses))
|
||||
mux.Handle("POST /v1/responses", s.withLogging(s.withAuth(s.handleResponses)))
|
||||
mux.HandleFunc("GET /api/status", s.handleStatus)
|
||||
mux.HandleFunc("POST /api/fetch-models", s.handleFetchModels)
|
||||
mux.HandleFunc("GET /api/providers", s.handleListProviders)
|
||||
@@ -86,7 +88,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("GET /api/settings", s.handleGetSettings)
|
||||
mux.HandleFunc("PUT /api/settings", s.handleUpdateSettings)
|
||||
mux.HandleFunc("GET /api/logs", s.handleLogs)
|
||||
return s.withLogging(mux)
|
||||
return mux
|
||||
}
|
||||
|
||||
// withAuth protects the Anthropic-compatible endpoints with the local
|
||||
@@ -139,6 +141,17 @@ func (rec *record) update(fn func(*LogEntry)) {
|
||||
fn(&rec.entry)
|
||||
}
|
||||
|
||||
func requestApp(path string) string {
|
||||
switch path {
|
||||
case "/v1/messages":
|
||||
return "Claude Code"
|
||||
case "/v1/responses":
|
||||
return "Codex"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) withLogging(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
@@ -146,6 +159,7 @@ func (s *Server) withLogging(next http.Handler) http.Handler {
|
||||
Time: start,
|
||||
Method: r.Method,
|
||||
Path: r.URL.Path,
|
||||
App: requestApp(r.URL.Path),
|
||||
}}
|
||||
ww := &statusWriter{ResponseWriter: w, status: http.StatusOK}
|
||||
next.ServeHTTP(ww, r.WithContext(context.WithValue(r.Context(), recordKey{}, rec)))
|
||||
|
||||
Reference in new issue
Block a user