**feat(auth): add AuthIndex for diagnostics and ensure usage recording**

This commit is contained in:
Luis Pater
2025-11-19 22:02:40 +08:00
parent 9acfbcc2a0
commit cc3cf09c00
7 changed files with 41 additions and 0 deletions

View File

@@ -292,6 +292,7 @@ func (h *Handler) buildAuthFileEntry(auth *coreauth.Auth) gin.H {
if auth == nil { if auth == nil {
return nil return nil
} }
auth.EnsureIndex()
runtimeOnly := isRuntimeOnlyAuth(auth) runtimeOnly := isRuntimeOnlyAuth(auth)
if runtimeOnly && (auth.Disabled || auth.Status == coreauth.StatusDisabled) { if runtimeOnly && (auth.Disabled || auth.Status == coreauth.StatusDisabled) {
return nil return nil
@@ -306,6 +307,7 @@ func (h *Handler) buildAuthFileEntry(auth *coreauth.Auth) gin.H {
} }
entry := gin.H{ entry := gin.H{
"id": auth.ID, "id": auth.ID,
"auth_index": auth.Index,
"name": name, "name": name,
"type": strings.TrimSpace(auth.Provider), "type": strings.TrimSpace(auth.Provider),
"provider": strings.TrimSpace(auth.Provider), "provider": strings.TrimSpace(auth.Provider),

View File

@@ -112,6 +112,8 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
} }
appendAPIResponseChunk(ctx, e.cfg, data) appendAPIResponseChunk(ctx, e.cfg, data)
reporter.publish(ctx, parseOpenAIUsage(data)) reporter.publish(ctx, parseOpenAIUsage(data))
// Ensure usage is recorded even if upstream omits usage metadata.
reporter.ensurePublished(ctx)
var param any var param any
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, &param) out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, &param)
@@ -217,6 +219,8 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
reporter.publishFailure(ctx) reporter.publishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: errScan} out <- cliproxyexecutor.StreamChunk{Err: errScan}
} }
// Guarantee a usage record exists even if the stream never emitted usage data.
reporter.ensurePublished(ctx)
}() }()
return stream, nil return stream, nil

View File

@@ -18,6 +18,7 @@ type usageReporter struct {
provider string provider string
model string model string
authID string authID string
authIndex uint64
apiKey string apiKey string
source string source string
requestedAt time.Time requestedAt time.Time
@@ -35,6 +36,7 @@ func newUsageReporter(ctx context.Context, provider, model string, auth *cliprox
} }
if auth != nil { if auth != nil {
reporter.authID = auth.ID reporter.authID = auth.ID
reporter.authIndex = auth.Index
} }
return reporter return reporter
} }
@@ -76,6 +78,7 @@ func (r *usageReporter) publishWithOutcome(ctx context.Context, detail usage.Det
Source: r.source, Source: r.source,
APIKey: r.apiKey, APIKey: r.apiKey,
AuthID: r.authID, AuthID: r.authID,
AuthIndex: r.authIndex,
RequestedAt: r.requestedAt, RequestedAt: r.requestedAt,
Failed: failed, Failed: failed,
Detail: detail, Detail: detail,
@@ -98,6 +101,7 @@ func (r *usageReporter) ensurePublished(ctx context.Context) {
Source: r.source, Source: r.source,
APIKey: r.apiKey, APIKey: r.apiKey,
AuthID: r.authID, AuthID: r.authID,
AuthIndex: r.authIndex,
RequestedAt: r.requestedAt, RequestedAt: r.requestedAt,
Failed: false, Failed: false,
Detail: usage.Detail{}, Detail: usage.Detail{},

View File

@@ -90,6 +90,7 @@ type modelStats struct {
type RequestDetail struct { type RequestDetail struct {
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
Source string `json:"source"` Source string `json:"source"`
AuthIndex uint64 `json:"auth_index"`
Tokens TokenStats `json:"tokens"` Tokens TokenStats `json:"tokens"`
Failed bool `json:"failed"` Failed bool `json:"failed"`
} }
@@ -197,6 +198,7 @@ func (s *RequestStatistics) Record(ctx context.Context, record coreusage.Record)
s.updateAPIStats(stats, modelName, RequestDetail{ s.updateAPIStats(stats, modelName, RequestDetail{
Timestamp: timestamp, Timestamp: timestamp,
Source: record.Source, Source: record.Source,
AuthIndex: record.AuthIndex,
Tokens: detail, Tokens: detail,
Failed: failed, Failed: failed,
}) })

View File

@@ -169,6 +169,7 @@ func (m *Manager) Register(ctx context.Context, auth *Auth) (*Auth, error) {
if auth == nil { if auth == nil {
return nil, nil return nil, nil
} }
auth.EnsureIndex()
if auth.ID == "" { if auth.ID == "" {
auth.ID = uuid.NewString() auth.ID = uuid.NewString()
} }
@@ -185,6 +186,7 @@ func (m *Manager) Update(ctx context.Context, auth *Auth) (*Auth, error) {
if auth == nil || auth.ID == "" { if auth == nil || auth.ID == "" {
return nil, nil return nil, nil
} }
auth.EnsureIndex()
m.mu.Lock() m.mu.Lock()
m.auths[auth.ID] = auth.Clone() m.auths[auth.ID] = auth.Clone()
m.mu.Unlock() m.mu.Unlock()
@@ -209,6 +211,7 @@ func (m *Manager) Load(ctx context.Context) error {
if auth == nil || auth.ID == "" { if auth == nil || auth.ID == "" {
continue continue
} }
auth.EnsureIndex()
m.auths[auth.ID] = auth.Clone() m.auths[auth.ID] = auth.Clone()
} }
return nil return nil

View File

@@ -5,6 +5,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
baseauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth" baseauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth"
@@ -14,6 +15,8 @@ import (
type Auth struct { type Auth struct {
// ID uniquely identifies the auth record across restarts. // ID uniquely identifies the auth record across restarts.
ID string `json:"id"` ID string `json:"id"`
// Index is a monotonically increasing runtime identifier used for diagnostics.
Index uint64 `json:"-"`
// Provider is the upstream provider key (e.g. "gemini", "claude"). // Provider is the upstream provider key (e.g. "gemini", "claude").
Provider string `json:"provider"` Provider string `json:"provider"`
// FileName stores the relative or absolute path of the backing auth file. // FileName stores the relative or absolute path of the backing auth file.
@@ -55,6 +58,8 @@ type Auth struct {
// Runtime carries non-serialisable data used during execution (in-memory only). // Runtime carries non-serialisable data used during execution (in-memory only).
Runtime any `json:"-"` Runtime any `json:"-"`
indexAssigned bool `json:"-"`
} }
// QuotaState contains limiter tracking data for a credential. // QuotaState contains limiter tracking data for a credential.
@@ -87,6 +92,12 @@ type ModelState struct {
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
var authIndexCounter atomic.Uint64
func nextAuthIndex() uint64 {
return authIndexCounter.Add(1) - 1
}
// Clone shallow copies the Auth structure, duplicating maps to avoid accidental mutation. // Clone shallow copies the Auth structure, duplicating maps to avoid accidental mutation.
func (a *Auth) Clone() *Auth { func (a *Auth) Clone() *Auth {
if a == nil { if a == nil {
@@ -115,6 +126,20 @@ func (a *Auth) Clone() *Auth {
return &copyAuth return &copyAuth
} }
// EnsureIndex returns the global index, assigning one if it was not set yet.
func (a *Auth) EnsureIndex() uint64 {
if a == nil {
return 0
}
if a.indexAssigned {
return a.Index
}
idx := nextAuthIndex()
a.Index = idx
a.indexAssigned = true
return idx
}
// Clone duplicates a model state including nested error details. // Clone duplicates a model state including nested error details.
func (m *ModelState) Clone() *ModelState { func (m *ModelState) Clone() *ModelState {
if m == nil { if m == nil {

View File

@@ -14,6 +14,7 @@ type Record struct {
Model string Model string
APIKey string APIKey string
AuthID string AuthID string
AuthIndex uint64
Source string Source string
RequestedAt time.Time RequestedAt time.Time
Failed bool Failed bool