mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
**feat(auth): add AuthIndex for diagnostics and ensure usage recording**
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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{},
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 ©Auth
|
return ©Auth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user