feat: sync deployed v1.0.0-rc.38-custom.42
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
This commit is contained in:
1 parent
aa6c3f50f4
commit
ceddfd3d6b
828 files changed
+122253
-13702
No files matched your search
@@ -0,0 +1,194 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
"github.com/QuantumNous/new-api/relaykit/types"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const requestPolicyContextKey = "request_policy_state"
|
||||
|
||||
type PolicyDecision struct {
|
||||
Action string `json:"action"`
|
||||
Reason string `json:"reason"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type PolicyEvent struct {
|
||||
Sequence int `json:"sequence,omitempty"`
|
||||
Candidates []model.SelectionCandidate `json:"candidates,omitempty"`
|
||||
CandidateTotal int `json:"candidate_total,omitempty"`
|
||||
Attempt int `json:"attempt"`
|
||||
ChannelID int `json:"channel_id,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
Rule string `json:"rule,omitempty"`
|
||||
Status int `json:"status,omitempty"`
|
||||
ErrorCode string `json:"error_code,omitempty"`
|
||||
ErrorSource string `json:"error_source,omitempty"`
|
||||
ElapsedMS int64 `json:"elapsed_ms"`
|
||||
Decision PolicyDecision `json:"decision"`
|
||||
Health string `json:"health,omitempty"`
|
||||
}
|
||||
|
||||
// RequestPolicyState records how one request was routed so administrators can
|
||||
// read the decision flow in the log details. Channel selection and the retry
|
||||
// decision stay in the relay flows; this state only records them.
|
||||
type RequestPolicyState struct {
|
||||
FinalLogged bool
|
||||
StartedAt time.Time
|
||||
Attempts int
|
||||
SelectedGroup string
|
||||
SessionMode string
|
||||
SessionModeSource string
|
||||
RuleName string
|
||||
Successful bool
|
||||
OutcomeRecorded bool
|
||||
mu sync.Mutex
|
||||
events []PolicyEvent
|
||||
candidateCount int
|
||||
}
|
||||
|
||||
func RequestPolicy(c *gin.Context) *RequestPolicyState {
|
||||
if c != nil {
|
||||
if value, ok := c.Get(requestPolicyContextKey); ok {
|
||||
return value.(*RequestPolicyState)
|
||||
}
|
||||
}
|
||||
state := &RequestPolicyState{StartedAt: time.Now()}
|
||||
if c != nil {
|
||||
c.Set(requestPolicyContextKey, state)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func (s *RequestPolicyState) AddEvent(event PolicyEvent) {
|
||||
if event.Attempt == 0 {
|
||||
event.Attempt = s.Attempts
|
||||
}
|
||||
event.ElapsedMS = time.Since(s.StartedAt).Milliseconds()
|
||||
if event.Group == "" {
|
||||
event.Group = s.SelectedGroup
|
||||
}
|
||||
if event.Rule == "" {
|
||||
event.Rule = s.RuleName
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if len(s.events) < 512 {
|
||||
event.Sequence = len(s.events) + 1
|
||||
s.events = append(s.events, event)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RequestPolicyState) Events() []PolicyEvent {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return slices.Clone(s.events)
|
||||
}
|
||||
|
||||
func (s *RequestPolicyState) BeginAttempt(channel *model.Channel, group string) {
|
||||
s.candidateCount = 0
|
||||
if s.SelectedGroup != "" && group != "" && group != s.SelectedGroup {
|
||||
s.AddEvent(PolicyEvent{Attempt: s.Attempts + 1, Group: group, Decision: PolicyDecision{Action: "switch", Reason: "fallback_group_switch", Source: "routing"}})
|
||||
}
|
||||
s.Attempts++
|
||||
s.Successful = false
|
||||
s.OutcomeRecorded = false
|
||||
s.SelectedGroup = group
|
||||
s.AddEvent(PolicyEvent{ChannelID: channel.Id, Decision: PolicyDecision{Action: "attempt", Reason: "channel_selected", Source: "routing"}})
|
||||
}
|
||||
|
||||
// RecordPolicyFailure appends the failed attempt and the retry decision made
|
||||
// for it. The health entry mirrors the check in ProcessChannelError, which
|
||||
// performs the actual disable.
|
||||
func RecordPolicyFailure(c *gin.Context, channelID int, err *types.NewAPIError, decision PolicyDecision) {
|
||||
if c == nil || err == nil {
|
||||
return
|
||||
}
|
||||
source := "upstream"
|
||||
switch {
|
||||
case types.IsChannelError(err):
|
||||
source = "channel"
|
||||
case err.GetErrorCode() == types.ErrorCodeDoRequestFailed:
|
||||
source = "transport"
|
||||
case err.GetErrorType() == types.ErrorTypeNewAPIError:
|
||||
source = "local"
|
||||
}
|
||||
state := RequestPolicy(c)
|
||||
event := PolicyEvent{ChannelID: channelID, Status: err.StatusCode, ErrorCode: string(err.GetErrorCode()), ErrorSource: source, Decision: PolicyDecision{Action: "failure", Reason: "upstream_failure", Source: source}}
|
||||
if source == "local" {
|
||||
event.Decision.Reason = "local_rejection"
|
||||
}
|
||||
state.AddEvent(event)
|
||||
event.Decision, event.Health = decision, "unchanged"
|
||||
if err.GetErrorCode() != relaycommon.ResponseGuardErrorCode && source != "local" && c.GetBool("auto_ban") && ShouldDisableChannel(err) {
|
||||
event.Health = "channel_disable_requested"
|
||||
if common.GetContextKeyBool(c, constant.ContextKeyChannelIsMultiKey) {
|
||||
event.Health = "key_disable_requested"
|
||||
}
|
||||
}
|
||||
state.AddEvent(event)
|
||||
}
|
||||
|
||||
func MarkRequestPolicySuccess(c *gin.Context, stream *relaycommon.StreamStatus) {
|
||||
state := RequestPolicy(c)
|
||||
if state.OutcomeRecorded {
|
||||
return
|
||||
}
|
||||
state.OutcomeRecorded = true
|
||||
state.Successful = stream == nil || stream.IsNormalEnd() && !stream.HasErrors() && (stream.ResponseOutcome() == "" || stream.ResponseOutcome() == "completed")
|
||||
decision := PolicyDecision{Action: "success", Reason: "request_completed", Source: "upstream"}
|
||||
if !state.Successful {
|
||||
decision = PolicyDecision{Action: "stop", Reason: "stream_not_successful", Source: "system"}
|
||||
}
|
||||
channelID := 0
|
||||
if c != nil {
|
||||
channelID = c.GetInt("channel_id")
|
||||
}
|
||||
state.AddEvent(PolicyEvent{ChannelID: channelID, Decision: decision})
|
||||
}
|
||||
|
||||
// Rules explicitly inherit the global default or override it. Rules without a
|
||||
// mode retain their legacy retry behavior until an administrator changes them.
|
||||
func EffectiveSessionMode(setting *operation_setting.ChannelAffinitySetting, rule operation_setting.ChannelAffinityRule) (mode, source string) {
|
||||
if rule.SessionMode == "inherit" {
|
||||
if setting.SessionMode != "" {
|
||||
return setting.SessionMode, "global"
|
||||
}
|
||||
return "prefer", "global"
|
||||
}
|
||||
if rule.SessionMode != "" {
|
||||
return rule.SessionMode, "session_rule"
|
||||
}
|
||||
if rule.SkipRetryOnFailure {
|
||||
return "strict", "session_rule"
|
||||
}
|
||||
return "prefer", "session_rule"
|
||||
}
|
||||
|
||||
// RecordRequestPolicyTermination appends the final decision after routing has
|
||||
// stopped. It never writes a log row itself: the per-channel error log written
|
||||
// by ProcessChannelError already carries the decision record, so a second row
|
||||
// here would duplicate it.
|
||||
func RecordRequestPolicyTermination(c *gin.Context, apiErr *types.NewAPIError) {
|
||||
if c == nil || apiErr == nil {
|
||||
return
|
||||
}
|
||||
state := RequestPolicy(c)
|
||||
if state.FinalLogged {
|
||||
return
|
||||
}
|
||||
state.FinalLogged = true
|
||||
events := state.Events()
|
||||
if len(events) == 0 || events[len(events)-1].Decision.Action != "stop" {
|
||||
state.AddEvent(PolicyEvent{ChannelID: c.GetInt("channel_id"), Status: apiErr.StatusCode, ErrorCode: string(apiErr.GetErrorCode()), Decision: PolicyDecision{Action: "stop", Reason: "request_failed", Source: "system"}, Health: "unchanged"})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user