mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-19 14:24:45 +08:00
feat(service): record upstream model across all gateway paths
Propagate UpstreamModel through ForwardResult and OpenAIForwardResult in Anthropic direct, API-key passthrough, Bedrock, and OpenAI gateway flows. Extract optionalNonEqualStringPtr and optionalTrimmedStringPtr into usage_log_helpers.go. Store upstream_model only when it differs from the requested model. Also introduces anthropicPassthroughForwardInput struct to reduce parameter count.
This commit is contained in:
@@ -491,6 +491,7 @@ type ForwardResult struct {
|
|||||||
RequestID string
|
RequestID string
|
||||||
Usage ClaudeUsage
|
Usage ClaudeUsage
|
||||||
Model string
|
Model string
|
||||||
|
UpstreamModel string // Actual upstream model after mapping (empty = no mapping)
|
||||||
Stream bool
|
Stream bool
|
||||||
Duration time.Duration
|
Duration time.Duration
|
||||||
FirstTokenMs *int // 首字时间(流式请求)
|
FirstTokenMs *int // 首字时间(流式请求)
|
||||||
@@ -3989,7 +3990,13 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
|||||||
passthroughModel = mappedModel
|
passthroughModel = mappedModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return s.forwardAnthropicAPIKeyPassthrough(ctx, c, account, passthroughBody, passthroughModel, parsed.Stream, startTime)
|
return s.forwardAnthropicAPIKeyPassthroughWithInput(ctx, c, account, anthropicPassthroughForwardInput{
|
||||||
|
Body: passthroughBody,
|
||||||
|
RequestModel: passthroughModel,
|
||||||
|
OriginalModel: parsed.Model,
|
||||||
|
RequestStream: parsed.Stream,
|
||||||
|
StartTime: startTime,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if account != nil && account.IsBedrock() {
|
if account != nil && account.IsBedrock() {
|
||||||
@@ -4513,6 +4520,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
|||||||
RequestID: resp.Header.Get("x-request-id"),
|
RequestID: resp.Header.Get("x-request-id"),
|
||||||
Usage: *usage,
|
Usage: *usage,
|
||||||
Model: originalModel, // 使用原始模型用于计费和日志
|
Model: originalModel, // 使用原始模型用于计费和日志
|
||||||
|
UpstreamModel: mappedModel,
|
||||||
Stream: reqStream,
|
Stream: reqStream,
|
||||||
Duration: time.Since(startTime),
|
Duration: time.Since(startTime),
|
||||||
FirstTokenMs: firstTokenMs,
|
FirstTokenMs: firstTokenMs,
|
||||||
@@ -4520,14 +4528,38 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type anthropicPassthroughForwardInput struct {
|
||||||
|
Body []byte
|
||||||
|
RequestModel string
|
||||||
|
OriginalModel string
|
||||||
|
RequestStream bool
|
||||||
|
StartTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
func (s *GatewayService) forwardAnthropicAPIKeyPassthrough(
|
func (s *GatewayService) forwardAnthropicAPIKeyPassthrough(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
c *gin.Context,
|
c *gin.Context,
|
||||||
account *Account,
|
account *Account,
|
||||||
body []byte,
|
body []byte,
|
||||||
reqModel string,
|
reqModel string,
|
||||||
|
originalModel string,
|
||||||
reqStream bool,
|
reqStream bool,
|
||||||
startTime time.Time,
|
startTime time.Time,
|
||||||
|
) (*ForwardResult, error) {
|
||||||
|
return s.forwardAnthropicAPIKeyPassthroughWithInput(ctx, c, account, anthropicPassthroughForwardInput{
|
||||||
|
Body: body,
|
||||||
|
RequestModel: reqModel,
|
||||||
|
OriginalModel: originalModel,
|
||||||
|
RequestStream: reqStream,
|
||||||
|
StartTime: startTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GatewayService) forwardAnthropicAPIKeyPassthroughWithInput(
|
||||||
|
ctx context.Context,
|
||||||
|
c *gin.Context,
|
||||||
|
account *Account,
|
||||||
|
input anthropicPassthroughForwardInput,
|
||||||
) (*ForwardResult, error) {
|
) (*ForwardResult, error) {
|
||||||
token, tokenType, err := s.GetAccessToken(ctx, account)
|
token, tokenType, err := s.GetAccessToken(ctx, account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -4543,19 +4575,19 @@ func (s *GatewayService) forwardAnthropicAPIKeyPassthrough(
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.LegacyPrintf("service.gateway", "[Anthropic 自动透传] 命中 API Key 透传分支: account=%d name=%s model=%s stream=%v",
|
logger.LegacyPrintf("service.gateway", "[Anthropic 自动透传] 命中 API Key 透传分支: account=%d name=%s model=%s stream=%v",
|
||||||
account.ID, account.Name, reqModel, reqStream)
|
account.ID, account.Name, input.RequestModel, input.RequestStream)
|
||||||
|
|
||||||
if c != nil {
|
if c != nil {
|
||||||
c.Set("anthropic_passthrough", true)
|
c.Set("anthropic_passthrough", true)
|
||||||
}
|
}
|
||||||
// 重试间复用同一请求体,避免每次 string(body) 产生额外分配。
|
// 重试间复用同一请求体,避免每次 string(body) 产生额外分配。
|
||||||
setOpsUpstreamRequestBody(c, body)
|
setOpsUpstreamRequestBody(c, input.Body)
|
||||||
|
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
retryStart := time.Now()
|
retryStart := time.Now()
|
||||||
for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
|
for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
|
||||||
upstreamCtx, releaseUpstreamCtx := detachStreamUpstreamContext(ctx, reqStream)
|
upstreamCtx, releaseUpstreamCtx := detachStreamUpstreamContext(ctx, input.RequestStream)
|
||||||
upstreamReq, err := s.buildUpstreamRequestAnthropicAPIKeyPassthrough(upstreamCtx, c, account, body, token)
|
upstreamReq, err := s.buildUpstreamRequestAnthropicAPIKeyPassthrough(upstreamCtx, c, account, input.Body, token)
|
||||||
releaseUpstreamCtx()
|
releaseUpstreamCtx()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -4713,8 +4745,8 @@ func (s *GatewayService) forwardAnthropicAPIKeyPassthrough(
|
|||||||
var usage *ClaudeUsage
|
var usage *ClaudeUsage
|
||||||
var firstTokenMs *int
|
var firstTokenMs *int
|
||||||
var clientDisconnect bool
|
var clientDisconnect bool
|
||||||
if reqStream {
|
if input.RequestStream {
|
||||||
streamResult, err := s.handleStreamingResponseAnthropicAPIKeyPassthrough(ctx, resp, c, account, startTime, reqModel)
|
streamResult, err := s.handleStreamingResponseAnthropicAPIKeyPassthrough(ctx, resp, c, account, input.StartTime, input.RequestModel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -4734,9 +4766,10 @@ func (s *GatewayService) forwardAnthropicAPIKeyPassthrough(
|
|||||||
return &ForwardResult{
|
return &ForwardResult{
|
||||||
RequestID: resp.Header.Get("x-request-id"),
|
RequestID: resp.Header.Get("x-request-id"),
|
||||||
Usage: *usage,
|
Usage: *usage,
|
||||||
Model: reqModel,
|
Model: input.OriginalModel,
|
||||||
Stream: reqStream,
|
UpstreamModel: input.RequestModel,
|
||||||
Duration: time.Since(startTime),
|
Stream: input.RequestStream,
|
||||||
|
Duration: time.Since(input.StartTime),
|
||||||
FirstTokenMs: firstTokenMs,
|
FirstTokenMs: firstTokenMs,
|
||||||
ClientDisconnect: clientDisconnect,
|
ClientDisconnect: clientDisconnect,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -5241,6 +5274,7 @@ func (s *GatewayService) forwardBedrock(
|
|||||||
RequestID: resp.Header.Get("x-amzn-requestid"),
|
RequestID: resp.Header.Get("x-amzn-requestid"),
|
||||||
Usage: *usage,
|
Usage: *usage,
|
||||||
Model: reqModel,
|
Model: reqModel,
|
||||||
|
UpstreamModel: mappedModel,
|
||||||
Stream: reqStream,
|
Stream: reqStream,
|
||||||
Duration: time.Since(startTime),
|
Duration: time.Since(startTime),
|
||||||
FirstTokenMs: firstTokenMs,
|
FirstTokenMs: firstTokenMs,
|
||||||
@@ -7529,6 +7563,7 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu
|
|||||||
AccountID: account.ID,
|
AccountID: account.ID,
|
||||||
RequestID: requestID,
|
RequestID: requestID,
|
||||||
Model: result.Model,
|
Model: result.Model,
|
||||||
|
UpstreamModel: optionalNonEqualStringPtr(result.UpstreamModel, result.Model),
|
||||||
ReasoningEffort: result.ReasoningEffort,
|
ReasoningEffort: result.ReasoningEffort,
|
||||||
InboundEndpoint: optionalTrimmedStringPtr(input.InboundEndpoint),
|
InboundEndpoint: optionalTrimmedStringPtr(input.InboundEndpoint),
|
||||||
UpstreamEndpoint: optionalTrimmedStringPtr(input.UpstreamEndpoint),
|
UpstreamEndpoint: optionalTrimmedStringPtr(input.UpstreamEndpoint),
|
||||||
@@ -7710,6 +7745,7 @@ func (s *GatewayService) RecordUsageWithLongContext(ctx context.Context, input *
|
|||||||
AccountID: account.ID,
|
AccountID: account.ID,
|
||||||
RequestID: requestID,
|
RequestID: requestID,
|
||||||
Model: result.Model,
|
Model: result.Model,
|
||||||
|
UpstreamModel: optionalNonEqualStringPtr(result.UpstreamModel, result.Model),
|
||||||
ReasoningEffort: result.ReasoningEffort,
|
ReasoningEffort: result.ReasoningEffort,
|
||||||
InboundEndpoint: optionalTrimmedStringPtr(input.InboundEndpoint),
|
InboundEndpoint: optionalTrimmedStringPtr(input.InboundEndpoint),
|
||||||
UpstreamEndpoint: optionalTrimmedStringPtr(input.UpstreamEndpoint),
|
UpstreamEndpoint: optionalTrimmedStringPtr(input.UpstreamEndpoint),
|
||||||
|
|||||||
@@ -281,6 +281,7 @@ func (s *OpenAIGatewayService) handleChatBufferedStreamingResponse(
|
|||||||
Usage: usage,
|
Usage: usage,
|
||||||
Model: originalModel,
|
Model: originalModel,
|
||||||
BillingModel: mappedModel,
|
BillingModel: mappedModel,
|
||||||
|
UpstreamModel: mappedModel,
|
||||||
Stream: false,
|
Stream: false,
|
||||||
Duration: time.Since(startTime),
|
Duration: time.Since(startTime),
|
||||||
}, nil
|
}, nil
|
||||||
@@ -328,6 +329,7 @@ func (s *OpenAIGatewayService) handleChatStreamingResponse(
|
|||||||
Usage: usage,
|
Usage: usage,
|
||||||
Model: originalModel,
|
Model: originalModel,
|
||||||
BillingModel: mappedModel,
|
BillingModel: mappedModel,
|
||||||
|
UpstreamModel: mappedModel,
|
||||||
Stream: true,
|
Stream: true,
|
||||||
Duration: time.Since(startTime),
|
Duration: time.Since(startTime),
|
||||||
FirstTokenMs: firstTokenMs,
|
FirstTokenMs: firstTokenMs,
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ func (s *OpenAIGatewayService) handleAnthropicBufferedStreamingResponse(
|
|||||||
Usage: usage,
|
Usage: usage,
|
||||||
Model: originalModel,
|
Model: originalModel,
|
||||||
BillingModel: mappedModel,
|
BillingModel: mappedModel,
|
||||||
|
UpstreamModel: mappedModel,
|
||||||
Stream: false,
|
Stream: false,
|
||||||
Duration: time.Since(startTime),
|
Duration: time.Since(startTime),
|
||||||
}, nil
|
}, nil
|
||||||
@@ -351,6 +352,7 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse(
|
|||||||
Usage: usage,
|
Usage: usage,
|
||||||
Model: originalModel,
|
Model: originalModel,
|
||||||
BillingModel: mappedModel,
|
BillingModel: mappedModel,
|
||||||
|
UpstreamModel: mappedModel,
|
||||||
Stream: true,
|
Stream: true,
|
||||||
Duration: time.Since(startTime),
|
Duration: time.Since(startTime),
|
||||||
FirstTokenMs: firstTokenMs,
|
FirstTokenMs: firstTokenMs,
|
||||||
|
|||||||
@@ -216,6 +216,9 @@ type OpenAIForwardResult struct {
|
|||||||
// This is set by the Anthropic Messages conversion path where
|
// This is set by the Anthropic Messages conversion path where
|
||||||
// the mapped upstream model differs from the client-facing model.
|
// the mapped upstream model differs from the client-facing model.
|
||||||
BillingModel string
|
BillingModel string
|
||||||
|
// UpstreamModel is the actual model sent to the upstream provider after mapping.
|
||||||
|
// Empty when no mapping was applied (requested model was used as-is).
|
||||||
|
UpstreamModel string
|
||||||
// ServiceTier records the OpenAI Responses API service tier, e.g. "priority" / "flex".
|
// ServiceTier records the OpenAI Responses API service tier, e.g. "priority" / "flex".
|
||||||
// Nil means the request did not specify a recognized tier.
|
// Nil means the request did not specify a recognized tier.
|
||||||
ServiceTier *string
|
ServiceTier *string
|
||||||
@@ -2128,6 +2131,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
|
|||||||
firstTokenMs,
|
firstTokenMs,
|
||||||
wsAttempts,
|
wsAttempts,
|
||||||
)
|
)
|
||||||
|
wsResult.UpstreamModel = mappedModel
|
||||||
return wsResult, nil
|
return wsResult, nil
|
||||||
}
|
}
|
||||||
s.writeOpenAIWSFallbackErrorResponse(c, account, wsErr)
|
s.writeOpenAIWSFallbackErrorResponse(c, account, wsErr)
|
||||||
@@ -2263,6 +2267,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
|
|||||||
RequestID: resp.Header.Get("x-request-id"),
|
RequestID: resp.Header.Get("x-request-id"),
|
||||||
Usage: *usage,
|
Usage: *usage,
|
||||||
Model: originalModel,
|
Model: originalModel,
|
||||||
|
UpstreamModel: mappedModel,
|
||||||
ServiceTier: serviceTier,
|
ServiceTier: serviceTier,
|
||||||
ReasoningEffort: reasoningEffort,
|
ReasoningEffort: reasoningEffort,
|
||||||
Stream: reqStream,
|
Stream: reqStream,
|
||||||
@@ -4134,7 +4139,8 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
|
|||||||
APIKeyID: apiKey.ID,
|
APIKeyID: apiKey.ID,
|
||||||
AccountID: account.ID,
|
AccountID: account.ID,
|
||||||
RequestID: requestID,
|
RequestID: requestID,
|
||||||
Model: billingModel,
|
Model: result.Model,
|
||||||
|
UpstreamModel: optionalNonEqualStringPtr(result.UpstreamModel, result.Model),
|
||||||
ServiceTier: result.ServiceTier,
|
ServiceTier: result.ServiceTier,
|
||||||
ReasoningEffort: result.ReasoningEffort,
|
ReasoningEffort: result.ReasoningEffort,
|
||||||
InboundEndpoint: optionalTrimmedStringPtr(input.InboundEndpoint),
|
InboundEndpoint: optionalTrimmedStringPtr(input.InboundEndpoint),
|
||||||
@@ -4700,11 +4706,3 @@ func normalizeOpenAIReasoningEffort(raw string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func optionalTrimmedStringPtr(raw string) *string {
|
|
||||||
trimmed := strings.TrimSpace(raw)
|
|
||||||
if trimmed == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &trimmed
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ type UsageLog struct {
|
|||||||
AccountID int64
|
AccountID int64
|
||||||
RequestID string
|
RequestID string
|
||||||
Model string
|
Model string
|
||||||
|
// UpstreamModel is the actual model sent to the upstream provider after mapping.
|
||||||
|
// Nil means no mapping was applied (requested model was used as-is).
|
||||||
|
UpstreamModel *string
|
||||||
// ServiceTier records the OpenAI service tier used for billing, e.g. "priority" / "flex".
|
// ServiceTier records the OpenAI service tier used for billing, e.g. "priority" / "flex".
|
||||||
ServiceTier *string
|
ServiceTier *string
|
||||||
// ReasoningEffort is the request's reasoning effort level.
|
// ReasoningEffort is the request's reasoning effort level.
|
||||||
|
|||||||
21
backend/internal/service/usage_log_helpers.go
Normal file
21
backend/internal/service/usage_log_helpers.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func optionalTrimmedStringPtr(raw string) *string {
|
||||||
|
trimmed := strings.TrimSpace(raw)
|
||||||
|
if trimmed == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
// optionalNonEqualStringPtr returns a pointer to value if it is non-empty and
|
||||||
|
// differs from compare; otherwise nil. Used to store upstream_model only when
|
||||||
|
// it differs from the requested model.
|
||||||
|
func optionalNonEqualStringPtr(value, compare string) *string {
|
||||||
|
if value == "" || value == compare {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &value
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user