new-api/middleware/muse_request_context.go
zizi 67c173cbcb Preserve Muse relay tags without hijacking new-api request ids
Muse now forwards business context alongside each per-user token request, so new-api needs to ingest those tags for relay selection, billing audit, and log correlation without breaking its own internal request-id semantics.

This adds a dedicated Muse request-context middleware, carries Muse tags into RelayInfo, and appends them into consume/error log payloads. Muse request ids are stored as separate business metadata instead of overwriting the internal request id used for pre-consume idempotency.

Constraint: The relay layer must keep working for both /v1 and /v1beta request paths
Constraint: Client-supplied Muse request ids cannot replace the internal request id used by new-api billing/idempotency logic
Rejected: Overwriting common.RequestIdKey with X-Request-Id | mixes external business ids with internal idempotency ids
Rejected: Adding new log table columns in phase 1 | this task only needs relay/log propagation, so other JSON is the minimal compatible path
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: Keep Muse business identifiers in dedicated context keys and log other fields; do not reuse internal request-id slots for external correlation ids
Tested: go test ./middleware ./relay/common ./controller -run MuseRequestContext -count=1
Tested: go test ./middleware ./relay/common ./controller -count=1
Not-tested: Live relay call from Muse into a running new-api instance
2026-04-17 17:14:41 +08:00

114 lines
2.7 KiB
Go

package middleware
import (
"fmt"
"strings"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/gin-gonic/gin"
)
const (
museHeaderUserID = "X-Muse-User-Id"
museHeaderWorkID = "X-Muse-Work-Id"
museHeaderRequest = "X-Request-Id"
museHeaderTraceID = "X-Trace-Id"
museHeaderScene = "X-Muse-Scene"
jsonContentType = "application/json"
)
type museMetadataEnvelope struct {
Metadata map[string]any `json:"metadata"`
}
func MuseRequestContext() gin.HandlerFunc {
return func(c *gin.Context) {
metadata := extractMuseMetadata(c)
museUserID := firstNonBlank(
c.GetHeader(museHeaderUserID),
stringifyMetadata(metadata["muse_user_id"]),
)
museWorkID := firstNonBlank(
c.GetHeader(museHeaderWorkID),
stringifyMetadata(metadata["work_id"]),
stringifyMetadata(metadata["muse_work_id"]),
)
museScene := firstNonBlank(
c.GetHeader(museHeaderScene),
stringifyMetadata(metadata["scene"]),
)
requestID := firstNonBlank(
c.GetHeader(museHeaderRequest),
stringifyMetadata(metadata["request_id"]),
)
traceID := firstNonBlank(
c.GetHeader(museHeaderTraceID),
stringifyMetadata(metadata["trace_id"]),
)
if museUserID != "" {
common.SetContextKey(c, constant.ContextKeyMuseUserID, museUserID)
}
if museWorkID != "" {
common.SetContextKey(c, constant.ContextKeyMuseWorkID, museWorkID)
}
if requestID != "" {
common.SetContextKey(c, constant.ContextKeyMuseRequestID, requestID)
}
if museScene != "" {
common.SetContextKey(c, constant.ContextKeyMuseScene, museScene)
}
if traceID != "" {
common.SetContextKey(c, constant.ContextKeyMuseTraceID, traceID)
}
c.Next()
}
}
func extractMuseMetadata(c *gin.Context) map[string]any {
if c == nil || c.Request == nil {
return nil
}
if !strings.Contains(strings.ToLower(c.GetHeader("Content-Type")), jsonContentType) {
return nil
}
if !needsMuseMetadataRead(c) {
return nil
}
var envelope museMetadataEnvelope
if err := common.UnmarshalBodyReusable(c, &envelope); err != nil {
return nil
}
return envelope.Metadata
}
func needsMuseMetadataRead(c *gin.Context) bool {
return c.GetHeader(museHeaderUserID) == "" ||
c.GetHeader(museHeaderWorkID) == "" ||
c.GetHeader(museHeaderRequest) == "" ||
c.GetHeader(museHeaderTraceID) == "" ||
c.GetHeader(museHeaderScene) == ""
}
func firstNonBlank(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func stringifyMetadata(value any) string {
switch v := value.(type) {
case nil:
return ""
case string:
return strings.TrimSpace(v)
default:
return strings.TrimSpace(fmt.Sprintf("%v", v))
}
}