Files
new-api-custom/pkg/perf_metrics/outcome.go
T

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
}