package auth import ( "encoding/json" "errors" "fmt" "net/http" "strconv" "strings" "time" "unicode/utf8" "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executionregistry" ) const ( maxHomeConcurrencyTupleFieldLength = 256 asciiWhitespace = " \t\r\n\v\f" ) var ErrMalformedHomeConcurrencyTuple = errors.New("malformed Home concurrency tuple") // HomeConcurrencyBusyError is a trusted, Home-originated concurrency admission failure. type HomeConcurrencyBusyError struct { cause *Error retryAfter time.Duration } // NewHomeConcurrencyBusyError creates a typed Home concurrency busy error. func NewHomeConcurrencyBusyError(message string, retryAfter time.Duration) error { message = strings.TrimSpace(message) if message == "" { message = "credential concurrency limit exceeded" } return newHomeConcurrencyBusyError(&Error{ Code: "credential_concurrency_exceeded", Message: message, Retryable: true, HTTPStatus: http.StatusTooManyRequests, }, retryAfter) } func newHomeConcurrencyBusyError(cause *Error, retryAfter time.Duration) *HomeConcurrencyBusyError { return &HomeConcurrencyBusyError{cause: cause, retryAfter: retryAfter} } func (e *HomeConcurrencyBusyError) Error() string { if e == nil || e.cause == nil { return "" } return e.cause.Error() } // Unwrap preserves the Home error's code, retryability, and status for errors.As callers. func (e *HomeConcurrencyBusyError) Unwrap() error { if e == nil { return nil } return e.cause } func (e *HomeConcurrencyBusyError) StatusCode() int { if e == nil || e.cause == nil { return 0 } return e.cause.StatusCode() } func (e *HomeConcurrencyBusyError) RetryAfter() *time.Duration { if e == nil || e.retryAfter <= 0 { return nil } value := e.retryAfter return &value } func (e *HomeConcurrencyBusyError) SafeResponseHeaders() http.Header { if e == nil { return nil } return safeRetryAfterHeader(e.retryAfter) } type homeConcurrencyTuple struct { Accounted bool `json:"accounted"` CredentialID string `json:"credential_id"` Model string `json:"model"` } func validateAccountedHomeConcurrencyTuple(tuple homeConcurrencyTuple) error { model, validModel := validCanonicalHomeConcurrencyModelKey(tuple.Model) if !tuple.Accounted || !validHomeConcurrencyTupleField(tuple.CredentialID) || !validModel || tuple.Model != model { return ErrMalformedHomeConcurrencyTuple } return nil } // canonicalHomeConcurrencyModelKey removes recognized reasoning suffixes from a Home limiter model key. func canonicalHomeConcurrencyModelKey(model string) string { if !utf8.ValidString(model) { return "" } trimmed := strings.ToLower(strings.Trim(model, asciiWhitespace)) if !strings.HasSuffix(trimmed, ")") { return trimmed } open := strings.LastIndexByte(trimmed, '(') if open < 0 { return trimmed } suffix := trimmed[open+1 : len(trimmed)-1] if !recognizedHomeConcurrencySuffix(suffix) { return trimmed } base := strings.Trim(trimmed[:open], asciiWhitespace) if base == "" { return trimmed } return base } func validCanonicalHomeConcurrencyModelKey(model string) (string, bool) { key := canonicalHomeConcurrencyModelKey(model) return key, key != "" && utf8.ValidString(key) && len(key) <= maxHomeConcurrencyTupleFieldLength } func recognizedHomeConcurrencySuffix(value string) bool { if value == "-1" { return true } switch strings.ToLower(value) { case "none", "auto", "minimal", "low", "medium", "high", "xhigh", "max": return true } if value == "" || len(value) > 10 { return false } var parsed int64 for index := 0; index < len(value); index++ { if value[index] < '0' || value[index] > '9' { return false } parsed = parsed*10 + int64(value[index]-'0') if parsed > 2_147_483_647 { return false } } return true } func validHomeConcurrencyTupleField(value string) bool { return value != "" && utf8.ValidString(value) && strings.TrimSpace(value) == value && len(value) <= maxHomeConcurrencyTupleFieldLength } func installHomeConcurrencyScope(registry *executionregistry.Registry, pending *executionregistry.PendingDispatch, tuple homeConcurrencyTuple, base executionregistry.ScopeSpec) (*executionregistry.Scope, error) { if registry == nil || pending == nil { return nil, executionregistry.ErrInvalidPendingDispatch } if !tuple.Accounted { base.Accounted = false return registry.Install(pending, base) } if errValidate := validateAccountedHomeConcurrencyTuple(tuple); errValidate != nil { return nil, errValidate } base.CredentialID = tuple.CredentialID base.Model = tuple.Model base.Accounted = true return registry.Install(pending, base) } type homeDispatchConcurrencyEnvelope struct { Tuple homeConcurrencyTuple Present bool } func decodeHomeDispatchConcurrencyEnvelope(raw []byte) (homeDispatchConcurrencyEnvelope, error) { if !utf8.Valid(raw) { return homeDispatchConcurrencyEnvelope{}, errors.New("Home response is not valid UTF-8") } var fields map[string]json.RawMessage if errUnmarshal := json.Unmarshal(raw, &fields); errUnmarshal != nil || fields == nil { return homeDispatchConcurrencyEnvelope{}, errors.New("Home response is not a JSON object") } envelope := homeDispatchConcurrencyEnvelope{} rawTuple, present := fields["concurrency"] if !present { return envelope, nil } envelope.Present = true if errUnmarshal := json.Unmarshal(rawTuple, &envelope.Tuple); errUnmarshal != nil { return envelope, errUnmarshal } if errValidate := validateAccountedHomeConcurrencyTuple(envelope.Tuple); errValidate != nil { return envelope, errValidate } return envelope, nil } func canonicalHomeDispatchModel(responseModel, requestedModel string) string { if model := strings.TrimSpace(responseModel); model != "" { return model } return requestedModel } func decodeHomeDispatchError(raw []byte) error { var fields map[string]json.RawMessage if errUnmarshal := json.Unmarshal(raw, &fields); errUnmarshal != nil || fields == nil { return nil } rawError, present := fields["error"] if !present { return nil } var detail *homeErrorDetail if errUnmarshal := json.Unmarshal(rawError, &detail); errUnmarshal != nil || detail == nil { return &Error{Code: "invalid_auth", Message: "home returned malformed error payload", HTTPStatus: http.StatusBadGateway} } code := strings.TrimSpace(detail.Type) if code == "" { code = strings.TrimSpace(detail.Code) } if code == "" { return &Error{Code: "invalid_auth", Message: "home returned malformed error payload", HTTPStatus: http.StatusBadGateway} } message := strings.TrimSpace(detail.Message) if message == "" { message = "home returned error" } result := &Error{Code: code, Message: message, Retryable: detail.Retryable, HTTPStatus: http.StatusBadGateway} switch strings.ToLower(code) { case "model_not_found": result.HTTPStatus = http.StatusNotFound case "model_cooldown": result.HTTPStatus = http.StatusTooManyRequests cooldownErr := &homeDispatchRetryAfterError{cause: result} if detail.RetryAfterMS > 0 { cooldownErr.retryAfter = time.Duration(detail.RetryAfterMS) * time.Millisecond } if detail.RequestRetry != nil && *detail.RequestRetry >= 0 { cooldownErr.requestRetry = *detail.RequestRetry cooldownErr.hasRequestRetry = true } return cooldownErr case "authentication_error", "unauthorized", "no_credentials", "invalid_credential": result.HTTPStatus = http.StatusUnauthorized case "credential_concurrency_exceeded", "credential_model_concurrency_exceeded": result.HTTPStatus = http.StatusTooManyRequests return newHomeConcurrencyBusyError(result, time.Duration(detail.RetryAfterMS)*time.Millisecond) case "auth_not_found", "auth_unavailable", "refresh_temporarily_unavailable", "home_unavailable", "concurrency_protocol_required", "concurrency_tracker_unavailable", "concurrency_node_unavailable": result.HTTPStatus = http.StatusServiceUnavailable } return result } func invalidHomeConcurrencyResponse(message string) error { return &Error{Code: "invalid_home_concurrency", Message: message, HTTPStatus: http.StatusBadGateway} } func verifyAccountedHomeConcurrencyIdentity(tuple homeConcurrencyTuple, auth *Auth, authIndex string) error { if !tuple.Accounted { return nil } if auth == nil || auth.ID != tuple.CredentialID || authIndex != tuple.CredentialID { return invalidHomeConcurrencyResponse("Home concurrency identity does not match dispatched auth") } return nil } // SafeResponseHeaders returns trusted response headers only for concrete // Home-generated retry errors. func SafeResponseHeaders(err error) http.Header { var busy *HomeConcurrencyBusyError if errors.As(err, &busy) && busy != nil { return busy.SafeResponseHeaders() } var exhausted *homeRetryRoundExhaustedError if errors.As(err, &exhausted) && exhausted != nil { retryAfter := exhausted.RetryAfter() if retryAfter == nil { return nil } return safeRetryAfterHeader(*retryAfter) } var cooldown *homeDispatchRetryAfterError if !errors.As(err, &cooldown) || cooldown == nil { return nil } retryAfter := cooldown.RetryAfter() if retryAfter == nil { return nil } return safeRetryAfterHeader(*retryAfter) } func safeRetryAfterHeader(retryAfter time.Duration) http.Header { if retryAfter <= 0 { return nil } seconds := int64(retryAfter / time.Second) if retryAfter%time.Second != 0 { seconds++ } if seconds < 1 { seconds = 1 } return http.Header{"Retry-After": []string{strconv.FormatInt(seconds, 10)}} } func homeConcurrencyInstallError(err error) error { if errors.Is(err, ErrMalformedHomeConcurrencyTuple) { return invalidHomeConcurrencyResponse(err.Error()) } return &Error{Code: "home_unavailable", Message: fmt.Sprintf("home execution registry unavailable: %v", err), Retryable: true, HTTPStatus: http.StatusServiceUnavailable} }