129 lines
4.9 KiB
Go
129 lines
4.9 KiB
Go
package perfmetrics
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
|
"github.com/QuantumNous/new-api/relaykit/types"
|
|
)
|
|
|
|
type Outcome string
|
|
|
|
const (
|
|
OutcomeSuccess Outcome = "success"
|
|
OutcomeFailure Outcome = "failure"
|
|
OutcomeIgnored Outcome = "ignored"
|
|
)
|
|
|
|
// ClassifyRelayOutcome decides whether one finished relay counts as a health
|
|
// sample. Business rejections and cancellations during generation are not
|
|
// samples; a stream already completed normally without errors remains eligible
|
|
// after its context is canceled. Classification is independent of retries,
|
|
// channel disabling and billing.
|
|
func ClassifyRelayOutcome(ctx context.Context, info *relaycommon.RelayInfo, apiErr *types.NewAPIError) Outcome {
|
|
if info == nil || info.PerformanceBusinessRejection {
|
|
return OutcomeIgnored
|
|
}
|
|
stream := info.StreamStatus.OutcomeSnapshot()
|
|
completedNormally := info.IsStream && info.StreamStatus != nil && apiErr == nil &&
|
|
stream.Response == relaycommon.ResponseOutcomeCompleted &&
|
|
(stream.EndReason == relaycommon.StreamEndReasonEOF || stream.EndReason == relaycommon.StreamEndReasonDone) &&
|
|
!stream.HasErrors && info.StreamStatus.EndError == nil
|
|
if ctx != nil && ctx.Err() == context.Canceled && !completedNormally {
|
|
return OutcomeIgnored
|
|
}
|
|
if apiErr != nil && errors.Is(apiErr, context.Canceled) {
|
|
return OutcomeIgnored
|
|
}
|
|
if stream.Response == relaycommon.ResponseOutcomeFailed {
|
|
return classifyFailure(false, stream.ErrorCode, stream.ErrorType, stream.ErrorStatus)
|
|
}
|
|
if apiErr != nil {
|
|
root := rootAPIError(apiErr)
|
|
local := root.GetErrorType() == types.ErrorTypeNewAPIError
|
|
return classifyFailure(local, string(root.GetErrorCode()), root.ToOpenAIError().Type, root.StatusCode)
|
|
}
|
|
deadlineExceeded := info.StreamStatus != nil && errors.Is(info.StreamStatus.EndError, context.DeadlineExceeded)
|
|
if stream.Response == relaycommon.ResponseOutcomeCancelled || stream.EndReason == relaycommon.StreamEndReasonPingFail {
|
|
return OutcomeIgnored
|
|
}
|
|
if stream.EndReason == relaycommon.StreamEndReasonClientGone && !deadlineExceeded {
|
|
return OutcomeIgnored
|
|
}
|
|
if stream.Response == relaycommon.ResponseOutcomeIncomplete {
|
|
switch stream.IncompleteReason {
|
|
case "max_output_tokens", "max_tokens":
|
|
return OutcomeSuccess
|
|
case "content_filter", "safety", "content_policy_violation":
|
|
return OutcomeIgnored
|
|
default:
|
|
return OutcomeFailure
|
|
}
|
|
}
|
|
if stream.HasErrors || deadlineExceeded {
|
|
return OutcomeFailure
|
|
}
|
|
switch stream.EndReason {
|
|
case relaycommon.StreamEndReasonTimeout, relaycommon.StreamEndReasonScannerErr, relaycommon.StreamEndReasonPanic:
|
|
return OutcomeFailure
|
|
}
|
|
if stream.ExpectsTerminal && stream.Response == relaycommon.ResponseOutcomeUnknown && stream.EndReason != relaycommon.StreamEndReasonDone {
|
|
return OutcomeFailure
|
|
}
|
|
return OutcomeSuccess
|
|
}
|
|
|
|
// rootAPIError follows host wrappers back to the error that describes what
|
|
// actually happened, e.g. an upstream credential error re-wrapped as a local
|
|
// invalid request.
|
|
func rootAPIError(apiErr *types.NewAPIError) *types.NewAPIError {
|
|
for {
|
|
var inner *types.NewAPIError
|
|
if !errors.As(apiErr.Unwrap(), &inner) || inner == apiErr {
|
|
return apiErr
|
|
}
|
|
apiErr = inner
|
|
}
|
|
}
|
|
|
|
func classifyFailure(local bool, code, errorType string, status int) Outcome {
|
|
code, errorType = strings.ToLower(code), strings.ToLower(errorType)
|
|
if local {
|
|
if strings.HasPrefix(code, "violation_fee.") {
|
|
return OutcomeIgnored
|
|
}
|
|
switch types.ErrorCode(code) {
|
|
case types.ErrorCodeInvalidRequest, types.ErrorCodeSensitiveWordsDetected, types.ErrorCodeReadRequestBodyFailed,
|
|
types.ErrorCodeConvertRequestFailed, types.ErrorCodeAccessDenied, types.ErrorCodeBadRequestBody,
|
|
types.ErrorCodeInsufficientUserQuota, types.ErrorCodePreConsumeTokenQuotaFailed, types.ErrorCodePromptBlocked:
|
|
return OutcomeIgnored
|
|
}
|
|
return OutcomeFailure
|
|
}
|
|
// Specific codes take precedence over broad protocol types, because
|
|
// providers also report invalid credentials as invalid_request_error.
|
|
for _, value := range []string{code, errorType} {
|
|
switch value {
|
|
case "invalid_api_key", "api_key_invalid", "api_key_expired", "api_key_service_blocked", "invalid_authentication",
|
|
"authentication_error", "unauthenticated", "permission_denied", "permission_error", "access_denied",
|
|
"insufficient_quota", "quota_exceeded", "resource_exhausted", "rate_limit_exceeded", "rate_limit_error",
|
|
"overloaded_error", "server_error", "internal_error", "service_unavailable", "model_not_found",
|
|
"insufficient_user_quota", "pre_consume_token_quota_failed":
|
|
return OutcomeFailure
|
|
case "context_length_exceeded", "invalid_request", "invalid_request_error", "invalid_argument",
|
|
"sensitive_words_detected", "prompt_blocked", "content_filter", "content_policy_violation", "safety":
|
|
return OutcomeIgnored
|
|
}
|
|
if strings.HasPrefix(value, "violation_fee.") {
|
|
return OutcomeIgnored
|
|
}
|
|
}
|
|
switch status {
|
|
case 400, 405, 409, 413, 415, 422:
|
|
return OutcomeIgnored
|
|
}
|
|
return OutcomeFailure
|
|
}
|