1372 lines · 38.8 KB · Go
RawDownload
package helps import (	"bytes"	"context"	"errors"	"fmt"	"io"	"net/http"	"reflect"	"strings"	"sync"	"time" 	"github.com/gin-gonic/gin"	"github.com/router-for-me/CLIProxyAPI/v7/internal/clienterror"	internallogging "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"	"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"	cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"	cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"	"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage"	"github.com/tidwall/gjson"	"github.com/tidwall/sjson") type UsageReporter struct {	provider            string	baseURL             string	executorType        string	model               string	alias               string	authID              string	authIndex           string	authMu              sync.RWMutex	accessTokenHash     string	authType            string	apiKey              string	sessionID           string	parentSessionID     string	source              string	reasoning           string	serviceTier         string	generate            bool	stream              bool	requestedAt         time.Time	ttftMu              sync.RWMutex	ttft                time.Duration	firstPacketDuration time.Duration	firstPacketSet      bool	ttftStart           time.Time	ttftSet             bool	once                sync.Once} type usageExecutor interface {	Identifier() string} func NewExecutorUsageReporter(ctx context.Context, executor usageExecutor, model string, auth *cliproxyauth.Auth) *UsageReporter {	provider := ""	if executor != nil {		provider = executor.Identifier()	}	reporter := NewUsageReporter(ctx, provider, model, auth)	reporter.executorType = ExecutorTypeName(executor)	return reporter} func NewUsageReporter(ctx context.Context, provider, model string, auth *cliproxyauth.Auth) *UsageReporter {	apiKey := APIKeyFromContext(ctx)	alias := usage.RequestedModelAliasFromContext(ctx)	if alias == "" {		alias = model	}	sessionID := ""	parentSessionID := ""	clientMeta := internallogging.GetClientRequestMetadata(ctx)	if clientMeta.SessionID != "" {		sessionID = clientMeta.SessionID		parentSessionID = clientMeta.ParentSessionID		if sessionID == parentSessionID || !isHierarchyParent(sessionID, parentSessionID) {			parentSessionID = ""		}	}	baseURL := ""	if auth != nil {		if auth.Attributes != nil {			baseURL = strings.TrimSpace(auth.Attributes["base_url"])		}		if baseURL == "" && auth.Metadata != nil {			if v, ok := auth.Metadata["base_url"].(string); ok {				baseURL = strings.TrimSpace(v)			}		}	}	reporter := &UsageReporter{		provider:        provider,		baseURL:         baseURL,		model:           model,		alias:           strings.TrimSpace(alias),		requestedAt:     time.Now(),		apiKey:          apiKey,		sessionID:       sessionID,		parentSessionID: parentSessionID,		source:          resolveUsageSource(auth, apiKey),		authType:        resolveUsageAuthType(auth),		reasoning:       usage.ReasoningEffortFromContext(ctx),		serviceTier:     usage.ServiceTierFromContext(ctx),		generate:        usage.GenerateFromContext(ctx),		stream:          usage.StreamFromContext(ctx),	}	if auth != nil {		reporter.authID = auth.ID		reporter.authIndex = auth.EnsureIndex()		reporter.accessTokenHash = authAccessTokenSHA256(auth)	}	return reporter} // SetStream records whether the request was executed in streaming mode.func (r *UsageReporter) SetStream(stream bool) {	if r == nil {		return	}	r.stream = stream} // SetSessionHierarchy sets the explicit session and parent session identifiers.// Callers should invoke this method before Publish or EnsurePublished on the request thread.func (r *UsageReporter) SetSessionHierarchy(sessionID, parentSessionID string) {	if r == nil {		return	}	r.sessionID = strings.TrimSpace(sessionID)	r.parentSessionID = strings.TrimSpace(parentSessionID)	if r.sessionID == "" || r.sessionID == r.parentSessionID || !isHierarchyParent(r.sessionID, r.parentSessionID) {		r.parentSessionID = ""	}} func isHierarchyParent(primary, parent string) bool {	if parent == "" || primary == "" || primary == parent {		return false	}	if strings.Contains(primary, ":agent:") {		return true	}	idx1 := strings.Index(primary, ":")	idx2 := strings.Index(parent, ":")	if idx1 > 0 && idx2 > 0 && primary[:idx1] == parent[:idx2] {		return true	}	if idx1 == -1 && idx2 == -1 {		return true	}	return false} // UpdateAccessTokenFingerprint records the token version actually used upstream.func (r *UsageReporter) UpdateAccessTokenFingerprint(auth *cliproxyauth.Auth) {	if r == nil {		return	}	r.authMu.Lock()	r.accessTokenHash = authAccessTokenSHA256(auth)	r.authMu.Unlock()} func (r *UsageReporter) accessTokenFingerprint() string {	if r == nil {		return ""	}	r.authMu.RLock()	defer r.authMu.RUnlock()	return r.accessTokenHash} func ExecutorTypeName(executor any) string {	if executor == nil {		return ""	}	executorType := reflect.TypeOf(executor)	for executorType.Kind() == reflect.Pointer {		executorType = executorType.Elem()	}	return strings.TrimSpace(executorType.Name())} func (r *UsageReporter) Publish(ctx context.Context, detail usage.Detail) {	r.publishWithOutcome(ctx, detail, false, usage.Failure{})} func (r *UsageReporter) PublishAdditionalModel(ctx context.Context, model string, detail usage.Detail) {	record, ok := r.buildAdditionalModelRecord(model, detail)	if !ok {		return	}	r.publishRecord(ctx, record)} func (r *UsageReporter) SetTranslatedReasoningEffort(payload []byte, format string) {	if r == nil {		return	}	r.reasoning = thinking.ExtractTranslatedReasoningEffort(payload, format)} func (r *UsageReporter) TrackHTTPClient(client *http.Client) *http.Client {	return r.trackHTTPClient(client, false)} // TrackHTTPClientRoundTripOnly records the TTFT start time upon sending the request// and captures first-packet arrival fallback on initial body reads, while keeping// effective TTFT unset. This allows protocol-aware streaming executors (like Codex SSE)// to mark effective TTFT explicitly upon receiving substantive token events, while// preserving first-packet fallback metrics for non-2xx error bodies or keepalive streams.func (r *UsageReporter) TrackHTTPClientRoundTripOnly(client *http.Client) *http.Client {	return r.trackHTTPClient(client, true)} func (r *UsageReporter) trackHTTPClient(client *http.Client, packetOnly bool) *http.Client {	if r == nil || client == nil {		return client	}	tracked := *client	transport := tracked.Transport	if transport == nil {		transport = http.DefaultTransport	}	tracked.Transport = usageTTFTRoundTripper{		base:       transport,		reporter:   r,		packetOnly: packetOnly,	}	return &tracked} func (r *UsageReporter) ObserveResponse(resp *http.Response) {	if r == nil || resp == nil || resp.Body == nil {		return	}	r.StartResponseTTFT()	resp.Body = &usageTTFTReadCloser{		ReadCloser: resp.Body,		mark: func() {			r.MarkFirstResponseByte()		},	}} func (r *UsageReporter) ObserveResponsePacketOnly(resp *http.Response) {	if r == nil || resp == nil || resp.Body == nil {		return	}	r.StartResponseTTFT()	resp.Body = &usageTTFTReadCloser{		ReadCloser: resp.Body,		mark: func() {			r.RecordFirstPacket()		},	}} func (r *UsageReporter) StartResponseTTFT() {	if r == nil {		return	}	r.ttftMu.Lock()	if !r.ttftSet && r.ttftStart.IsZero() {		r.ttftStart = time.Now()	}	r.ttftMu.Unlock()} func (r *UsageReporter) IsTTFTSet() bool {	if r == nil {		return false	}	r.ttftMu.RLock()	defer r.ttftMu.RUnlock()	return r.ttftSet} func (r *UsageReporter) IsFirstPacketSet() bool {	if r == nil {		return false	}	r.ttftMu.RLock()	defer r.ttftMu.RUnlock()	return r.firstPacketSet} // RecordFirstPacket records the arrival time of the first packet/chunk from upstream as a fallback.func (r *UsageReporter) RecordFirstPacket() {	r.ObserveTokenEvent(false)} // ObserveTokenEvent records the first packet fallback time on the first frame,// and if isToken is true, records the effective TTFT. It uses a fast-path// read check to return immediately with zero write lock contention once TTFT is set// or when subsequent non-token metadata frames arrive after the first packet.func (r *UsageReporter) ObserveTokenEvent(isToken bool) {	if r == nil {		return	}	r.ttftMu.RLock()	if r.ttftSet {		r.ttftMu.RUnlock()		return	}	start := r.ttftStart	alreadyRecordedPacket := r.firstPacketSet	r.ttftMu.RUnlock() 	if start.IsZero() {		return	} 	if !isToken && alreadyRecordedPacket {		return	} 	r.ttftMu.Lock()	defer r.ttftMu.Unlock()	if r.ttftSet {		return	}	if !r.firstPacketSet {		r.firstPacketDuration = time.Since(start)		r.firstPacketSet = true	}	if isToken {		r.ttft = time.Since(start)		r.ttftSet = true		r.ttftStart = time.Time{}	}} func (r *UsageReporter) MarkFirstResponseByte() {	if r == nil {		return	}	r.ttftMu.Lock()	if r.ttftSet {		r.ttftMu.Unlock()		return	}	start := r.ttftStart	r.ttftStart = time.Time{}	r.ttftMu.Unlock()	if start.IsZero() {		return	}	r.setTTFT(time.Since(start))} func (r *UsageReporter) buildAdditionalModelRecord(model string, detail usage.Detail) (usage.Record, bool) {	if r == nil {		return usage.Record{}, false	}	model = strings.TrimSpace(model)	if model == "" {		return usage.Record{}, false	}	detail = normalizeUsageDetailTotal(detail, r.provider, r.executorType)	if !hasNonZeroTokenUsage(detail) {		return usage.Record{}, false	}	return r.buildRecordForModel(model, detail, false, usage.Failure{}), true} func (r *UsageReporter) PublishFailure(ctx context.Context, errs ...error) {	r.publishWithOutcome(ctx, usage.Detail{}, true, failFromErrors(errs...))} func (r *UsageReporter) PublishFailureWithDetail(ctx context.Context, detail usage.Detail, errs ...error) {	r.publishWithOutcome(ctx, detail, true, failFromErrors(errs...))} func (r *UsageReporter) TrackFailure(ctx context.Context, errPtr *error) {	if r == nil || errPtr == nil {		return	}	if *errPtr != nil {		r.PublishFailure(ctx, *errPtr)	}} func (r *UsageReporter) publishWithOutcome(ctx context.Context, detail usage.Detail, failed bool, fail usage.Failure) {	if r == nil {		return	}	detail = normalizeUsageDetailTotal(detail, r.provider, r.executorType)	r.once.Do(func() {		r.publishRecord(ctx, r.buildRecord(detail, failed, fail))	})} func normalizeUsageDetailTotal(detail usage.Detail, provider, executorType string) usage.Detail {	return usage.EnsureTokenBreakdownForProvider(detail, provider, executorType)} func hasNonZeroTokenUsage(detail usage.Detail) bool {	return detail.InputTokens != 0 ||		detail.OutputTokens != 0 ||		detail.ReasoningTokens != 0 ||		detail.CachedTokens != 0 ||		detail.CacheReadTokens != 0 ||		detail.CacheCreationTokens != 0 ||		detail.TotalTokens != 0 ||		detail.TokenBreakdown.TotalTokens != 0} // ensurePublished guarantees that a usage record is emitted exactly once.// It is safe to call multiple times; only the first call wins due to once.Do.// This is used to ensure request counting even when upstream responses do not// include any usage fields (tokens), especially for streaming paths.func (r *UsageReporter) EnsurePublished(ctx context.Context) {	if r == nil {		return	}	r.once.Do(func() {		r.publishRecord(ctx, r.buildRecord(usage.Detail{}, false, usage.Failure{}))	})} func (r *UsageReporter) publishRecord(ctx context.Context, record usage.Record) {	record.ResponseHeaders = internallogging.GetResponseHeaders(ctx)	usage.PublishRecord(ctx, record)} func (r *UsageReporter) buildRecord(detail usage.Detail, failed bool, failures ...usage.Failure) usage.Record {	var fail usage.Failure	if len(failures) > 0 {		fail = failures[0]	}	if r == nil {		return usage.Record{Detail: detail, Failed: failed, Fail: fail, Generate: usage.GenerateFlag(true)}	}	return r.buildRecordForModel(r.model, detail, failed, fail)} func (r *UsageReporter) buildRecordForModel(model string, detail usage.Detail, failed bool, fail usage.Failure) usage.Record {	if r == nil {		return usage.Record{Model: model, Detail: detail, Failed: failed, Fail: fail, Generate: usage.GenerateFlag(true)}	}	return usage.Record{		Provider:            r.provider,		BaseURL:             r.baseURL,		ExecutorType:        r.executorType,		Model:               model,		Alias:               r.alias,		Source:              r.source,		APIKey:              r.apiKey,		SessionID:           r.sessionID,		ParentSessionID:     r.parentSessionID,		AuthID:              r.authID,		AuthIndex:           r.authIndex,		AccessTokenSHA256:   r.accessTokenFingerprint(),		AuthType:            r.authType,		ReasoningEffort:     r.reasoning,		ServiceTier:         r.serviceTier,		ResponseServiceTier: strings.TrimSpace(detail.ResponseServiceTier),		Generate:            usage.GenerateFlag(r.generate),		Stream:              r.stream,		RequestedAt:         r.requestedAt,		Latency:             r.latency(),		TTFT:                r.ttftDuration(),		Failed:              failed,		Fail:                fail,		Detail:              detail,	}} func failFromErrors(errs ...error) usage.Failure {	for _, err := range errs {		if err == nil {			continue		}		body := strings.TrimSpace(err.Error())		type responseBodyProvider interface {			ResponseBody() []byte		}		var responseErr responseBodyProvider		if errors.As(err, &responseErr) && responseErr != nil {			if responseBody := responseErr.ResponseBody(); len(responseBody) > 0 {				body = string(responseBody)			}		}		return usage.Failure{			Body:       body,			StatusCode: clienterror.HTTPStatusFromError(err),		}	}	return usage.Failure{}} func (r *UsageReporter) latency() time.Duration {	if r == nil || r.requestedAt.IsZero() {		return 0

Lines 1–500 of 1372.