package promptkit import ( "errors" "fmt" "strings" "go.maximumdirect.net/promptkit/internal/capacity" "go.maximumdirect.net/promptkit/internal/generation" "go.maximumdirect.net/promptkit/internal/llm" "go.maximumdirect.net/promptkit/internal/profile" "go.maximumdirect.net/promptkit/internal/promptdef" "go.maximumdirect.net/promptkit/internal/usecase" ) // publicOperationError keeps classification separate from inspectable causes. // Cache wording so formatting nested operations does not repeatedly rebuild it. type publicOperationError struct { owner error cause error message string } func (e *publicOperationError) Error() string { return e.message } func (e *publicOperationError) Unwrap() error { return e.cause } func (e *publicOperationError) Is(target error) bool { return errors.Is(e.owner, target) } func mapPublicError(err error) error { if err == nil { return nil } var operation *usecase.OperationError if errors.As(err, &operation) && operation != nil { owner := publicErrorFor(operation.Category) if owner != nil { // Project the underlying specialized error separately, retaining both // the outer operation and the original cause for Is/As callers. mapped := mapPublicError(operation.Cause) if errors.Is(operation.Cause, llm.ErrTransport) { mapped = errors.Join(ErrTransport, mapped) } if errors.Is(operation.Cause, llm.ErrMalformedResponse) { mapped = errors.Join(ErrMalformedResponse, mapped) } // The projection already retains ordinary causes; adding the original // again would duplicate its entire subtree at each enclosing operation. return &publicOperationError{owner: owner, cause: mapped, message: fmt.Sprintf("%v: %v", owner, mapped)} } } return mapUnownedPublicError(err) } func mapUnownedPublicError(err error) error { var internalCapacityError *usecase.CapacityError if errors.As(err, &internalCapacityError) && internalCapacityError != nil && strings.TrimSpace(internalCapacityError.BackendID) != "" { return &CapacityError{BackendID: internalCapacityError.BackendID} } var internalExhausted *generation.ExhaustedError if errors.As(err, &internalExhausted) && internalExhausted != nil { mapped := newRetryExhaustedError(internalExhausted.Attempts(), mapPublicError(internalExhausted.Unwrap())) if errors.Is(err, usecase.ErrValidation) && !errors.Is(mapped, ErrValidation) { return fmt.Errorf("%w: %w", ErrValidation, mapped) } if publicErr := publicErrorFor(err); publicErr != nil && !errors.Is(mapped, publicErr) { return fmt.Errorf("%w: %w", publicErr, mapped) } return mapped } publicErr := publicErrorFor(err) var providerHTTPError *llm.ProviderHTTPError if errors.As(err, &providerHTTPError) && providerHTTPError != nil { generationErr := newGenerationError( providerHTTPError.StatusCode(), providerHTTPError.ProviderCode(), providerHTTPError.ProviderType(), providerHTTPError.ProviderMessage(), ) if publicErr != nil && !errors.Is(publicErr, ErrLLMGenerate) { return fmt.Errorf("%w: %w", publicErr, generationErr) } return generationErr } if publicErr == nil { return err } switch { case errors.Is(err, llm.ErrTransport): return fmt.Errorf("%w: %w: %w", publicErr, ErrTransport, err) case errors.Is(err, llm.ErrMalformedResponse): return fmt.Errorf("%w: %w: %w", publicErr, ErrMalformedResponse, err) default: return fmt.Errorf("%w: %w", publicErr, err) } } func publicErrorFor(err error) error { switch { case errors.Is(err, promptdef.ErrPromptDefinitionNotFound): return ErrPromptNotFound case errors.Is(err, profile.ErrProfileNotFound): return ErrProfileNotFound case errors.Is(err, usecase.ErrProfileRequired): return errors.Join(ErrInvalidRequest, ErrProfileRequired) case errors.Is(err, usecase.ErrPromptLoad): return ErrPromptLoad case errors.Is(err, usecase.ErrProfileLoad): return ErrProfileLoad case errors.Is(err, usecase.ErrAPIKeyEnvMissing): return errors.Join(ErrInvalidRequest, ErrAPIKeyEnvMissing) case errors.Is(err, capacity.ErrCapacityExceeded): return ErrCapacityExceeded case errors.Is(err, usecase.ErrArtifactLoad): return ErrArtifactLoad case errors.Is(err, usecase.ErrPromptRender): return ErrPromptRender case errors.Is(err, usecase.ErrLLMGenerate): return ErrLLMGenerate case errors.Is(err, usecase.ErrValidation): return ErrValidation case errors.Is(err, usecase.ErrInvalidRequest): return ErrInvalidRequest default: return nil } }