**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 {
return nil
}
auth.EnsureIndex()
runtimeOnly := isRuntimeOnlyAuth(auth)
if runtimeOnly && (auth.Disabled || auth.Status == coreauth.StatusDisabled) {
return nil
@@ -306,6 +307,7 @@ func (h *Handler) buildAuthFileEntry(auth *coreauth.Auth) gin.H {
}
entry := gin.H{
"id": auth.ID,
"auth_index": auth.Index,
"name": name,
"type": 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)
reporter.publish(ctx, parseOpenAIUsage(data))
// Ensure usage is recorded even if upstream omits usage metadata.
reporter.ensurePublished(ctx)
var param any
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)
out <- cliproxyexecutor.StreamChunk{Err: errScan}
}
// Guarantee a usage record exists even if the stream never emitted usage data.
reporter.ensurePublished(ctx)
}()
return stream, nil

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
baseauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth"
@@ -14,6 +15,8 @@ import (
type Auth struct {
// ID uniquely identifies the auth record across restarts.
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 string `json:"provider"`
// 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 any `json:"-"`
indexAssigned bool `json:"-"`
}
// QuotaState contains limiter tracking data for a credential.
@@ -87,6 +92,12 @@ type ModelState struct {
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.
func (a *Auth) Clone() *Auth {
if a == nil {
@@ -115,6 +126,20 @@ func (a *Auth) Clone() *Auth {
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.
func (m *ModelState) Clone() *ModelState {
if m == nil {

View File

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