From 003ea475e293f77368ea0cfb9a3edb5b13ce8fac Mon Sep 17 00:00:00 2001 From: Aditya Tanikanti Date: Tue, 11 Nov 2025 13:44:02 -0600 Subject: [PATCH 01/22] cleaning up and easing container setup --- README.md | 11 +- docker-compose.yml | 75 ++--------- generate_auth_token.py | 36 ----- inference-auth-token.py | 172 ------------------------ materialized_views.sql | 82 ------------ materialized_views_batch.sql | 135 ------------------- metrics_processing.sql | 250 ----------------------------------- pgloader.load | 18 --- reset_cursor.py | 46 ------- 9 files changed, 18 insertions(+), 807 deletions(-) delete mode 100644 generate_auth_token.py delete mode 100644 inference-auth-token.py delete mode 100644 materialized_views.sql delete mode 100644 materialized_views_batch.sql delete mode 100644 metrics_processing.sql delete mode 100644 pgloader.load delete mode 100644 reset_cursor.py diff --git a/README.md b/README.md index c6caa3a7..5fbbaeb8 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ FIRST (Federated Inference Resource Scheduling Toolkit) is a system that enables ## Table of Contents +- [Documentation](#documentation) - [System Architecture](#system-architecture) - [Prerequisites](#prerequisites) - [Setup Overview](#setup-overview) @@ -32,6 +33,12 @@ FIRST (Federated Inference Resource Scheduling Toolkit) is a system that enables - [Monitoring](#monitoring) - [Troubleshooting](#troubleshooting) +## Documentation + +Detailed installation and deployment recipes now live under `docs/`. Start with the Docker quickstart if you are looking for a lightweight path to run the gateway locally: + +- [Administrator Guide — Docker Quickstart](docs/admin-guide/docker-quickstart.md) + ## System Architecture ![System Architecture](./first_architecture.png) @@ -61,6 +68,8 @@ These parts can be done in parallel, but configuration details from each are nee ## Gateway Setup +> 📝 Looking for a shorter, task-oriented walkthrough? See the [Docker Quickstart](docs/admin-guide/docker-quickstart.md) in the Administrator Guide. + This section covers setting up the central Django application. ### Installation (Docker or Bare Metal) @@ -768,7 +777,7 @@ For an example of how user-facing documentation might look for a deployed instan ## Troubleshooting -* **Docker Nginx 404/502/504 Errors:** Verify `nginx_app.conf` mount and upstream definition, check `inference-gateway` logs. +* **Docker Nginx 404/502/504 Errors:** Verify `deploy/docker/nginx.conf` mount and upstream definition, check `inference-gateway` logs. * **Database Connection Errors:** Check `.env` variables (`PGHOST`, etc.) match context (Docker vs. Host vs. Bare-metal) and firewall/`pg_hba.conf` rules. * **Globus Auth Errors**: Ensure Redirect URIs match in Globus Developer portal and `.env` credentials are correct. * **Compute Endpoint Issues**: Check endpoint logs (`~/.globus_compute//endpoint.log`) for function execution errors, environment problems, or connection issues. diff --git a/docker-compose.yml b/docker-compose.yml index 060661c5..3e71696a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,17 +6,17 @@ services: ports: - "8000:80" volumes: - - ./nginx_app.conf:/etc/nginx/conf.d/default.conf:ro + - ./deploy/docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro - ./staticfiles:/app/staticfiles:ro depends_on: - inference-gateway networks: - - monitoring + - gateway inference-gateway: build: context: . - dockerfile: Dockerfile + dockerfile: deploy/docker/Dockerfile env_file: - .env volumes: @@ -26,7 +26,7 @@ services: - postgres - redis networks: - - monitoring + - gateway postgres: image: postgres:15 @@ -36,75 +36,16 @@ services: volumes: - postgres_data:/var/lib/postgresql/data networks: - - monitoring + - gateway redis: image: redis:7 networks: - - monitoring - - prometheus: - image: prom/prometheus:latest - container_name: prometheus - ports: - - "9090:9090" - volumes: - - ./prometheus:/etc/prometheus - command: - - '--config.file=/etc/prometheus/prometheus.yml' - networks: - - monitoring - - grafana: - image: grafana/grafana:latest - container_name: grafana - ports: - - "3000:3000" - volumes: - - grafana_data:/var/lib/grafana - env_file: - - .env - restart: unless-stopped - depends_on: - - prometheus - networks: - - monitoring - - node-exporter: - image: prom/node-exporter:latest - container_name: node-exporter - ports: - - "9100:9100" - volumes: - - /proc:/host/proc:ro - - /sys:/host/sys:ro - - /:/rootfs:ro - command: - - '--path.procfs=/host/proc' - - '--path.sysfs=/host/sys' - - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' - restart: unless-stopped - networks: - - monitoring - - postgres-exporter: - image: prometheuscommunity/postgres-exporter:latest - container_name: postgres-exporter - ports: - - "9187:9187" - environment: - - DATA_SOURCE_NAME=postgresql://${PGUSER}:${PGPASSWORD}@postgres:5432/${PGDATABASE}?sslmode=disable - restart: unless-stopped - depends_on: - - postgres - networks: - - monitoring + - gateway volumes: - prometheus_data: - grafana_data: postgres_data: networks: - monitoring: - driver: bridge \ No newline at end of file + gateway: + driver: bridge \ No newline at end of file diff --git a/generate_auth_token.py b/generate_auth_token.py deleted file mode 100644 index 2a2dc803..00000000 --- a/generate_auth_token.py +++ /dev/null @@ -1,36 +0,0 @@ -import globus_sdk -import requests -import json - -# Globus ID and Scope -# =================== - -# Define your Globus thick-client ID -# https://app.globus.org/settings/developers -# This needs to be created by users (they can also re-use one of the existing clients) -auth_client_id = "58fdd3bc-e1c3-4ce5-80ea-8d6b87cfb944" - -# Define the inference-gateway resource service scope -# This will be publicly available to users -gateway_client_id = "681c10cc-f684-4540-bcd7-0b4df3bc26ef" -gateway_scope = f"https://auth.globus.org/scopes/{gateway_client_id}/action_all" - -# Authentication and Access Token -# =============================== - -# Start an Auth client with the vLLM scope -auth_client = globus_sdk.NativeAppAuthClient(auth_client_id) -auth_client.oauth2_start_flow(requested_scopes=gateway_scope) - -# Authenticate with your Globus account -authorize_url = auth_client.oauth2_get_authorize_url() -print(f"Please go to this URL and login:\n\n{authorize_url}\n") - -# Get the code from the command line -auth_code = input("Enter the code you just received: ") - -# Exchange the code for an access token -# Collect access token to vLLM service -token_response = auth_client.oauth2_exchange_code_for_tokens(auth_code) -access_token = token_response.by_resource_server[gateway_client_id]["access_token"] -print(f"Access token: {access_token}") \ No newline at end of file diff --git a/inference-auth-token.py b/inference-auth-token.py deleted file mode 100644 index 9daf56e0..00000000 --- a/inference-auth-token.py +++ /dev/null @@ -1,172 +0,0 @@ -import globus_sdk -from globus_sdk.login_flows import LocalServerLoginFlowManager # Needed to access globus_sdk.gare -import os -from dotenv import load_dotenv -import json # For parsing domains list -import sys # Import sys to redirect print output - -# Load .env variables first -load_dotenv() - -# --- Configuration --- -# Globus UserApp name (can remain constant or be made configurable if needed via .env) -APP_NAME = os.getenv("CLI_APP_NAME", "inference_app") # Default to original name - -# Public client ID for this CLI authentication flow (reads from .env, falls back to original default) -CLI_AUTH_CLIENT_ID = os.getenv("CLI_AUTH_CLIENT_ID", "58fdd3bc-e1c3-4ce5-80ea-8d6b87cfb944") - -# Inference gateway Application Client ID (reads from .env, MUST BE SET) -GATEWAY_CLIENT_ID = os.getenv("GLOBUS_APPLICATION_ID") -if not GATEWAY_CLIENT_ID: - raise Exception("GLOBUS_APPLICATION_ID must be set in the .env file.") - -# Define the Service API scope -GATEWAY_SCOPE = f"https://auth.globus.org/scopes/{GATEWAY_CLIENT_ID}/action_all" - -# Path where access and refresh tokens are stored (uses original logic based on CLI_AUTH_CLIENT_ID and APP_NAME) -# Ensure the parent directory exists -TOKENS_DIR = os.path.expanduser(f"~/.globus/app/{CLI_AUTH_CLIENT_ID}/{APP_NAME}") -os.makedirs(TOKENS_DIR, exist_ok=True) # Create dir if it doesn't exist -TOKENS_PATH = os.path.join(TOKENS_DIR, "tokens.json") # Original path construction - - -# Allowed identity provider domains (reads from .env, falls back to original default) -# Example: CLI_ALLOWED_DOMAINS="anl.gov,alcf.anl.gov" -allowed_domains_str = os.getenv("CLI_ALLOWED_DOMAINS", "anl.gov,alcf.anl.gov") # Default to original list -ALLOWED_DOMAINS = [domain.strip() for domain in allowed_domains_str.split(',') if domain.strip()] - -# Globus authorizer parameters to point to specific identity providers -if ALLOWED_DOMAINS: - GA_PARAMS = globus_sdk.gare.GlobusAuthorizationParameters(session_required_single_domain=ALLOWED_DOMAINS) -else: - GA_PARAMS = None - print("INFO: No domain restrictions specified for login.", file=sys.stderr) # To stderr - - -# --- Original Logic --- - -# Error handler to guide user through specific identity providers -class DomainBasedErrorHandler: - def __call__(self, app, error): - print(f"Encountered error '{error}', initiating login...", file=sys.stderr) # To stderr - app.login(auth_params=GA_PARAMS) - - -# Get refresh authorizer object -def get_auth_object(force=False): - """ - Create a Globus UserApp with the inference service scope - and trigger the authentication process. If authentication - has already happened, existing tokens will be reused. - Uses default token storage mechanisms. - """ - - # Create Globus user application - # NOTE: Not specifying token_storage uses the default behavior which - # should align with the standard path defined in TOKENS_PATH. - app = globus_sdk.UserApp( - APP_NAME, - client_id=CLI_AUTH_CLIENT_ID, - scope_requirements={GATEWAY_CLIENT_ID: [GATEWAY_SCOPE]}, - config=globus_sdk.GlobusAppConfig( - request_refresh_tokens=True, - token_validation_error_handler=DomainBasedErrorHandler() - ), - ) - - # Force re-login if required - if force: - # Original script just calls login, which handles clearing old tokens internally - print("INFO: Forcing new login (clearing existing tokens implicitly)...", file=sys.stderr) # To stderr - app.login(auth_params=GA_PARAMS) - - # Authenticate using your Globus account or reuse existing tokens - auth = app.get_authorizer(GATEWAY_CLIENT_ID) - - # Return the Globus refresh token authorizer - return auth - - -# Get access token -def get_access_token(): - """ - Load existing tokens, refresh the access token if necessary, - and return the valid access token. If there is no token stored - in the default location, or if the refresh token is expired following - inactivity, an authentication will be triggered. - """ - - # Get authorizer object and authenticate if need be - auth = get_auth_object() # force=False - - # Make sure the stored access token if valid, and refresh otherwise - try: - auth.ensure_valid_token() - except globus_sdk.AuthAPIError as e: - print(f"Error ensuring token validity: {e}", file=sys.stderr) # To stderr - print("Attempting re-authentication...", file=sys.stderr) # To stderr - auth = get_auth_object(force=True) # Force re-auth if refresh fails - auth.ensure_valid_token() # Try again - - # Return the access token - return auth.access_token - - -# If this file is executed as a script ... -if __name__ == "__main__": - - # Imports - import argparse - - # Exception to raise in case of errors - class InferenceAuthError(Exception): - pass - - # Constant - AUTHENTICATE_ACTION = "authenticate" - GET_ACCESS_TOKEN_ACTION = "get_access_token" - - # Define possible arguments - parser = argparse.ArgumentParser(description="Authenticate with Globus and get access tokens for the Inference Gateway.") - parser.add_argument('action', choices=[AUTHENTICATE_ACTION, GET_ACCESS_TOKEN_ACTION], help="Action to perform: 'authenticate' or 'get_access_token'.") - parser.add_argument("-f", "--force", action="store_true", help="Force re-authentication, ignoring existing tokens.") - args = parser.parse_args() - - # Authentication - if args.action == AUTHENTICATE_ACTION: - print("Attempting authentication...", file=sys.stderr) # To stderr - try: - # Authenticate using Globus account (original logic) - _ = get_auth_object(force=args.force) - # Info message to stderr - print(f"Authentication successful. Tokens stored/updated in default location derived from Client ID and App Name (likely near {TOKENS_PATH}).", file=sys.stderr) - except Exception as e: - # Error message to stderr - print(f"Authentication failed: {e}", file=sys.stderr) - raise InferenceAuthError(f"Authentication process failed. Details: {e}") - - # Get token - elif args.action == GET_ACCESS_TOKEN_ACTION: - - # Make sure tokens exist (original check based on constructed path) - if not os.path.isfile(TOKENS_PATH): - # Error message to stderr - print('Access token file not found at expected location. Please authenticate first by running:', file=sys.stderr) - print('python inference-auth_token.py authenticate', file=sys.stderr) - exit(1) - - # Make sure no force flag was provided (original check) - if args.force: - # argparse handles printing error to stderr and exiting - parser.error("The --force flag should be used with the 'authenticate' action, not 'get_access_token'. To force re-authentication, run: python inference_auth_token.py authenticate --force") - - try: - # Load tokens, refresh token if necessary, and print access token - access_token = get_access_token() - print(access_token) # <<< Print the token itself to STDOUT - except Exception as e: - # Error message to stderr - print(f"Failed to get access token: {e}", file=sys.stderr) - print("You might need to re-authenticate:", file=sys.stderr) - print("python inference_auth_token.py authenticate --force", file=sys.stderr) - raise InferenceAuthError(f"Failed to retrieve access token. Details: {e}") \ No newline at end of file diff --git a/materialized_views.sql b/materialized_views.sql deleted file mode 100644 index 5dfaaeb9..00000000 --- a/materialized_views.sql +++ /dev/null @@ -1,82 +0,0 @@ --- Materialized Views Export --- Exported on Tue Mar 18 12:20:16 AM CDT 2025 - -DROP MATERIALIZED VIEW IF EXISTS public.mv_model_throughput CASCADE; -CREATE MATERIALIZED VIEW public.mv_model_throughput AS - WITH parsed_throughput AS ( SELECT e.model, (json_extract_path_text((r.result)::json, VARIADIC ARRAY['throughput_tokens_per_second'::text]))::numeric AS throughput FROM (resource_server_log r JOIN resource_server_endpoint e ON (((e.endpoint_slug)::text = (r.endpoint_slug)::text))) WHERE ((r.response_status = 200) AND (r.result IS NOT NULL) AND (r.result <> ''::text) AND (((r.result)::json ->> 'throughput_tokens_per_second'::text) IS NOT NULL)) ) SELECT parsed_throughput.model, round(avg(parsed_throughput.throughput), 2) AS avg_throughput, count(*) AS successful_requests_with_throughput FROM parsed_throughput WHERE (parsed_throughput.throughput IS NOT NULL) GROUP BY parsed_throughput.model ORDER BY parsed_throughput.model;; - -DROP MATERIALIZED VIEW IF EXISTS public.mv_model_throughput_new CASCADE; -CREATE MATERIALIZED VIEW public.mv_model_throughput_new AS - WITH parsed_throughput AS ( SELECT e.model, (json_extract_path_text((r.result)::json, VARIADIC ARRAY['throughput_tokens_per_second'::text]))::numeric AS throughput FROM (resource_server_log r JOIN resource_server_endpoint e ON (((e.endpoint_slug)::text = (r.endpoint_slug)::text))) WHERE ((r.response_status = 200) AND (r.result IS NOT NULL) AND (r.result <> ''::text) AND (((r.result)::json ->> 'throughput_tokens_per_second'::text) IS NOT NULL)) ) SELECT parsed_throughput.model, round(avg(parsed_throughput.throughput), 2) AS avg_throughput, count(*) AS successful_requests_with_throughput FROM parsed_throughput WHERE (parsed_throughput.throughput IS NOT NULL) GROUP BY parsed_throughput.model ORDER BY parsed_throughput.model;; - -DROP MATERIALIZED VIEW IF EXISTS public.mv_model_requests CASCADE; -CREATE MATERIALIZED VIEW public.mv_model_requests AS - SELECT e.model, count(*) AS total_requests, count(*) FILTER (WHERE ((r.response_status = 200) OR (r.response_status IS NULL))) AS successful_requests, count(*) FILTER (WHERE (NOT ((r.response_status = 200) OR (r.response_status IS NULL)))) AS failed_requests FROM (resource_server_log r JOIN resource_server_endpoint e ON (((e.endpoint_slug)::text = (r.endpoint_slug)::text))) GROUP BY e.model;; - -DROP MATERIALIZED VIEW IF EXISTS public.mv_overall_stats CASCADE; -CREATE MATERIALIZED VIEW public.mv_overall_stats AS - SELECT count(*) AS total_requests, count(*) FILTER (WHERE ((resource_server_log.response_status = 200) OR (resource_server_log.response_status IS NULL))) AS successful_requests, count(*) FILTER (WHERE (NOT ((resource_server_log.response_status = 200) OR (resource_server_log.response_status IS NULL)))) AS failed_requests, count(DISTINCT resource_server_log.username) AS total_users FROM resource_server_log;; - -DROP MATERIALIZED VIEW IF EXISTS public.mv_user_details CASCADE; -CREATE MATERIALIZED VIEW public.mv_user_details AS - SELECT DISTINCT resource_server_log.name, resource_server_log.username FROM resource_server_log;; - -DROP MATERIALIZED VIEW IF EXISTS public.mv_model_latency CASCADE; -CREATE MATERIALIZED VIEW public.mv_model_latency AS - SELECT e.model, avg((r.timestamp_response - r.timestamp_receive)) AS avg_latency FROM (resource_server_log r JOIN resource_server_endpoint e ON (((e.endpoint_slug)::text = (r.endpoint_slug)::text))) WHERE ((r.response_status = 200) OR (r.response_status IS NULL)) GROUP BY e.model;; - -DROP MATERIALIZED VIEW IF EXISTS public.mv_users_per_model CASCADE; -CREATE MATERIALIZED VIEW public.mv_users_per_model AS - SELECT e.model, count(DISTINCT r.username) AS user_count FROM (resource_server_log r JOIN resource_server_endpoint e ON (((e.endpoint_slug)::text = (r.endpoint_slug)::text))) GROUP BY e.model;; - -DROP MATERIALIZED VIEW IF EXISTS public.mv_weekly_usage CASCADE; -CREATE MATERIALIZED VIEW public.mv_weekly_usage AS - SELECT date_trunc('week'::text, resource_server_log.timestamp_receive) AS week_start, count(*) AS request_count FROM resource_server_log GROUP BY (date_trunc('week'::text, resource_server_log.timestamp_receive)) ORDER BY (date_trunc('week'::text, resource_server_log.timestamp_receive));; - -DROP MATERIALIZED VIEW IF EXISTS public.mv_daily_usage_2_weeks CASCADE; -CREATE MATERIALIZED VIEW public.mv_daily_usage_2_weeks AS - SELECT date_trunc('day'::text, resource_server_log.timestamp_receive) AS day, count(*) AS request_count FROM resource_server_log WHERE (resource_server_log.timestamp_receive >= (CURRENT_DATE - '14 days'::interval)) GROUP BY (date_trunc('day'::text, resource_server_log.timestamp_receive)) ORDER BY (date_trunc('day'::text, resource_server_log.timestamp_receive));; - -DROP MATERIALIZED VIEW IF EXISTS public.mv_requests_per_user CASCADE; -CREATE MATERIALIZED VIEW public.mv_requests_per_user AS - SELECT resource_server_log.name, resource_server_log.username, count(*) AS total_requests, count(*) FILTER (WHERE ((resource_server_log.response_status = 200) OR (resource_server_log.response_status IS NULL))) AS successful_requests, count(*) FILTER (WHERE (NOT ((resource_server_log.response_status = 200) OR (resource_server_log.response_status IS NULL)))) AS failed_requests FROM resource_server_log GROUP BY resource_server_log.name, resource_server_log.username;; - -DROP MATERIALIZED VIEW IF EXISTS public.mv_monthly_usage CASCADE; -CREATE MATERIALIZED VIEW public.mv_monthly_usage AS - SELECT date_trunc('month'::text, resource_server_log.timestamp_receive) AS month_start, - count(*) AS request_count - FROM resource_server_log - GROUP BY (date_trunc('month'::text, resource_server_log.timestamp_receive)) - ORDER BY (date_trunc('month'::text, resource_server_log.timestamp_receive));; - -DROP MATERIALIZED VIEW IF EXISTS public.mv_total_token_counts CASCADE; - -CREATE MATERIALIZED VIEW public.mv_total_token_counts AS -SELECT - -- Sum prompt_tokens, converting the JSON text value to bigint - SUM((r.result::jsonb -> 'usage' ->> 'prompt_tokens')::bigint) AS total_prompt_tokens, - - -- Sum completion_tokens, converting the JSON text value to bigint - SUM((r.result::jsonb -> 'usage' ->> 'completion_tokens')::bigint) AS total_completion_tokens, - - -- Optionally, sum the total_tokens provided in the usage object as well - SUM((r.result::jsonb -> 'usage' ->> 'total_tokens')::bigint) AS grand_total_tokens -FROM - resource_server_log r -WHERE - -- Filter for successful requests - r.response_status = 200 - -- Ensure the result field is not null or empty - AND r.result IS NOT NULL - AND r.result <> '' - -- Crucially, ensure the 'usage' key exists and is a JSON object - AND jsonb_typeof(r.result::jsonb -> 'usage') = 'object' - -- Add checks to ensure the token fields exist within 'usage' and contain valid numeric strings - -- This prevents errors if the JSON structure is missing keys or has non-numeric values - AND r.result::jsonb -> 'usage' ->> 'prompt_tokens' ~ '^[0-9]+$' - AND r.result::jsonb -> 'usage' ->> 'completion_tokens' ~ '^[0-9]+$' - -- Optional check for total_tokens if you are summing it directly - AND r.result::jsonb -> 'usage' ->> 'total_tokens' ~ '^[0-9]+$'; - --- Example command to refresh the view (run periodically) --- REFRESH MATERIALIZED VIEW public.mv_total_token_counts; diff --git a/materialized_views_batch.sql b/materialized_views_batch.sql deleted file mode 100644 index d949ec77..00000000 --- a/materialized_views_batch.sql +++ /dev/null @@ -1,135 +0,0 @@ --- a. Total Number of batch jobs -DROP MATERIALIZED VIEW IF EXISTS public.mv_batch_total_jobs CASCADE; -CREATE MATERIALIZED VIEW public.mv_batch_total_jobs AS -SELECT - COUNT(*) AS total_batch_jobs, - COUNT(*) FILTER (WHERE status = 'completed') AS completed_batch_jobs, - COUNT(*) FILTER (WHERE status = 'failed') AS failed_batch_jobs, - COUNT(*) FILTER (WHERE status = 'pending') AS pending_batch_jobs, - COUNT(*) FILTER (WHERE status = 'running') AS running_batch_jobs -FROM resource_server_batch; - --- b. Total number of successful batch requests (based on num_responses) -DROP MATERIALIZED VIEW IF EXISTS public.mv_batch_successful_requests CASCADE; -CREATE MATERIALIZED VIEW public.mv_batch_successful_requests AS -SELECT - SUM( - (json_extract_path_text((result)::json, VARIADIC ARRAY['metrics', 'num_responses']))::numeric - ) AS total_successful_requests -FROM resource_server_batch -WHERE status = 'completed' -AND result IS NOT NULL -AND result <> '' -AND (result)::json -> 'metrics' -> 'num_responses' IS NOT NULL; - --- c. Batch Requests per model -DROP MATERIALIZED VIEW IF EXISTS public.mv_batch_requests_per_model CASCADE; -CREATE MATERIALIZED VIEW public.mv_batch_requests_per_model AS -SELECT - model, - COUNT(*) AS total_jobs, - COUNT(*) FILTER (WHERE status = 'completed') AS completed_jobs, - SUM( - CASE - WHEN status = 'completed' AND result IS NOT NULL AND result <> '' AND - (result)::json -> 'metrics' -> 'num_responses' IS NOT NULL - THEN (json_extract_path_text((result)::json, VARIADIC ARRAY['metrics', 'num_responses']))::numeric - ELSE 0 - END - ) AS total_requests -FROM resource_server_batch -GROUP BY model -ORDER BY model; - --- d. Total number of batch users -DROP MATERIALIZED VIEW IF EXISTS public.mv_batch_unique_users CASCADE; -CREATE MATERIALIZED VIEW public.mv_batch_unique_users AS -SELECT - COUNT(DISTINCT username) AS unique_users, - array_agg(DISTINCT username) AS user_list -FROM resource_server_batch; - --- e. Total number of tokens processed -DROP MATERIALIZED VIEW IF EXISTS public.mv_batch_total_tokens CASCADE; -CREATE MATERIALIZED VIEW public.mv_batch_total_tokens AS -SELECT - SUM( - (json_extract_path_text((result)::json, VARIADIC ARRAY['metrics', 'total_tokens']))::numeric - ) AS total_tokens_processed -FROM resource_server_batch -WHERE status = 'completed' -AND result IS NOT NULL -AND result <> '' -AND (result)::json -> 'metrics' -> 'total_tokens' IS NOT NULL; - --- f. Average latency per model -DROP MATERIALIZED VIEW IF EXISTS public.mv_batch_avg_latency CASCADE; -CREATE MATERIALIZED VIEW public.mv_batch_avg_latency AS -SELECT - model, - AVG( - (json_extract_path_text((result)::json, VARIADIC ARRAY['metrics', 'response_time']))::numeric - ) AS avg_response_time_sec -FROM resource_server_batch -WHERE status = 'completed' -AND result IS NOT NULL -AND result <> '' -AND (result)::json -> 'metrics' -> 'response_time' IS NOT NULL -GROUP BY model -ORDER BY model; - --- g. Average throughput per model -DROP MATERIALIZED VIEW IF EXISTS public.mv_batch_avg_throughput CASCADE; -CREATE MATERIALIZED VIEW public.mv_batch_avg_throughput AS -SELECT - model, - AVG( - (json_extract_path_text((result)::json, VARIADIC ARRAY['metrics', 'throughput_tokens_per_second']))::numeric - ) AS avg_throughput_tokens_per_sec -FROM resource_server_batch -WHERE status = 'completed' -AND result IS NOT NULL -AND result <> '' -AND (result)::json -> 'metrics' -> 'throughput_tokens_per_second' IS NOT NULL -GROUP BY model -ORDER BY model; - --- Daily batch usage (all time) -DROP MATERIALIZED VIEW IF EXISTS public.mv_batch_daily_usage CASCADE; -CREATE MATERIALIZED VIEW public.mv_batch_daily_usage AS -SELECT - date_trunc('day'::text, created_at) AS day, - COUNT(*) AS batch_count, - COUNT(*) FILTER (WHERE status = 'completed') AS completed_count, - COUNT(*) FILTER (WHERE status = 'failed') AS failed_count, - SUM( - CASE - WHEN status = 'completed' AND result IS NOT NULL AND result <> '' AND - (result)::json -> 'metrics' -> 'num_responses' IS NOT NULL - THEN (json_extract_path_text((result)::json, VARIADIC ARRAY['metrics', 'num_responses']))::numeric - ELSE 0 - END - ) AS total_requests -FROM resource_server_batch -GROUP BY date_trunc('day'::text, created_at) -ORDER BY date_trunc('day'::text, created_at); - --- Monthly batch usage (all time) -DROP MATERIALIZED VIEW IF EXISTS public.mv_batch_monthly_usage CASCADE; -CREATE MATERIALIZED VIEW public.mv_batch_monthly_usage AS -SELECT - date_trunc('month'::text, created_at) AS month_start, - COUNT(*) AS batch_count, - COUNT(*) FILTER (WHERE status = 'completed') AS completed_count, - COUNT(*) FILTER (WHERE status = 'failed') AS failed_count, - SUM( - CASE - WHEN status = 'completed' AND result IS NOT NULL AND result <> '' AND - (result)::json -> 'metrics' -> 'num_responses' IS NOT NULL - THEN (json_extract_path_text((result)::json, VARIADIC ARRAY['metrics', 'num_responses']))::numeric - ELSE 0 - END - ) AS total_requests_in_completed -FROM resource_server_batch -GROUP BY date_trunc('month'::text, created_at) -ORDER BY date_trunc('month'::text, created_at); \ No newline at end of file diff --git a/metrics_processing.sql b/metrics_processing.sql deleted file mode 100644 index 72af5890..00000000 --- a/metrics_processing.sql +++ /dev/null @@ -1,250 +0,0 @@ --- ============================================================================= --- Request Metrics Batch Processing Setup --- ============================================================================= --- This script sets up asynchronous batch processing for request metrics --- instead of using expensive synchronous triggers. --- --- Prerequisites: --- - metrics_processed column added to resource_server_async_requestlog table --- - Old trigger trg_upsert_requestmetrics removed --- - Old function fn_upsert_requestmetrics removed --- --- Deployment: --- 1. Run this script: psql -d yourdb -f batch_processing_setup.sql --- 2. Backfill existing data: SELECT backfill_existing_metrics(1000); --- 3. Set up cron job: */30 * * * * psql -d yourdb -c "SELECT process_unprocessed_metrics(500);" --- ============================================================================= - --- Create index for fast unprocessed lookup -CREATE INDEX IF NOT EXISTS idx_requestlog_unprocessed -ON resource_server_async_requestlog (metrics_processed, timestamp_compute_response) -WHERE metrics_processed = FALSE AND timestamp_compute_response IS NOT NULL; - -<<<<<<< HEAD --- Other indexes -CREATE INDEX idx_accesslog_status_code ON resource_server_async_accesslog(status_code); -CREATE INDEX idx_requestlog_result_tokens -ON resource_server_async_requestlog(id) -WHERE result ~ '"total_tokens"\s*:\s*\d+'; - -======= ->>>>>>> model_upgrades --- Create lightweight trigger to mark records for processing -CREATE OR REPLACE FUNCTION fn_mark_for_processing() RETURNS trigger AS $$ -BEGIN - -- Mark for processing when we get response data - IF NEW.timestamp_compute_response IS NOT NULL AND - NEW.result IS NOT NULL AND - NEW.result != '' THEN - NEW.metrics_processed := FALSE; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER trg_mark_for_processing -BEFORE INSERT OR UPDATE OF result, timestamp_compute_response -ON resource_server_async_requestlog -FOR EACH ROW EXECUTE FUNCTION fn_mark_for_processing(); - --- Main batch processor function -CREATE OR REPLACE FUNCTION process_unprocessed_metrics(batch_size INTEGER DEFAULT 1000) -RETURNS INTEGER AS $$ -DECLARE - processed_count INTEGER := 0; - batch_ids UUID[]; -BEGIN - -- Get batch of unprocessed IDs - SELECT ARRAY( - SELECT rl.id - FROM resource_server_async_requestlog rl - JOIN resource_server_async_accesslog al ON rl.access_log_id = al.id - WHERE rl.metrics_processed = FALSE - AND rl.timestamp_compute_response IS NOT NULL - AND rl.result IS NOT NULL - AND rl.result != '' -<<<<<<< HEAD - AND rl.result ~ '"total_tokens"\s*:\s*\d+' -- Pre-filter with regex - AND EXISTS ( - SELECT 1 FROM resource_server_async_accesslog al - WHERE al.id = rl.access_log_id - AND al.status_code BETWEEN 200 AND 299 - ) -======= - AND al.status_code >= 200 - AND al.status_code < 300 - AND rl.result ~ '"total_tokens"\s*:\s*\d+' ->>>>>>> model_upgrades - ORDER BY rl.timestamp_compute_response - LIMIT batch_size - FOR UPDATE SKIP LOCKED -- Prevent conflicts with multiple workers - ) INTO batch_ids; - - -- Exit if nothing to process - IF array_length(batch_ids, 1) IS NULL THEN - RETURN 0; - END IF; - - -- Process the batch - insert/update metrics - INSERT INTO resource_server_async_requestmetrics - (request_id, cluster, framework, model, status_code, - prompt_tokens, completion_tokens, total_tokens, - response_time_sec, throughput_tokens_per_sec, - timestamp_compute_request, timestamp_compute_response, created_at) - SELECT - rl.id, - rl.cluster, - rl.framework, - rl.model, - al.status_code, - COALESCE((regexp_match(rl.result, '"prompt_tokens"\s*:\s*(\d+)'))[1]::bigint, NULL), - COALESCE((regexp_match(rl.result, '"completion_tokens"\s*:\s*(\d+)'))[1]::bigint, NULL), - COALESCE((regexp_match(rl.result, '"total_tokens"\s*:\s*(\d+)'))[1]::bigint, NULL), - EXTRACT(EPOCH FROM (rl.timestamp_compute_response - rl.timestamp_compute_request)), - COALESCE((regexp_match(rl.result, '"throughput_tokens_per_second"\s*:\s*([0-9.]+)'))[1]::double precision, NULL), - rl.timestamp_compute_request, - rl.timestamp_compute_response, - NOW() - FROM resource_server_async_requestlog rl - JOIN resource_server_async_accesslog al ON rl.access_log_id = al.id - WHERE rl.id = ANY(batch_ids) - ON CONFLICT (request_id) DO UPDATE SET - cluster = EXCLUDED.cluster, - framework = EXCLUDED.framework, - model = EXCLUDED.model, - status_code = EXCLUDED.status_code, - prompt_tokens = EXCLUDED.prompt_tokens, - completion_tokens = EXCLUDED.completion_tokens, - total_tokens = EXCLUDED.total_tokens, - response_time_sec = EXCLUDED.response_time_sec, - throughput_tokens_per_sec = EXCLUDED.throughput_tokens_per_sec, - timestamp_compute_request = EXCLUDED.timestamp_compute_request, - timestamp_compute_response = EXCLUDED.timestamp_compute_response; - - -- Mark batch as processed - UPDATE resource_server_async_requestlog - SET metrics_processed = TRUE - WHERE id = ANY(batch_ids); - - GET DIAGNOSTICS processed_count = ROW_COUNT; - RETURN processed_count; -END; -$$ LANGUAGE plpgsql; - --- Monitoring function to check unprocessed records -CREATE OR REPLACE FUNCTION get_unprocessed_metrics_count() -RETURNS TABLE( - total_unprocessed BIGINT, - oldest_unprocessed TIMESTAMP WITH TIME ZONE, - newest_unprocessed TIMESTAMP WITH TIME ZONE -) AS $$ -BEGIN - RETURN QUERY - SELECT - COUNT(*), - MIN(rl.timestamp_compute_response), - MAX(rl.timestamp_compute_response) - FROM resource_server_async_requestlog rl - JOIN resource_server_async_accesslog al ON rl.access_log_id = al.id - WHERE rl.metrics_processed = FALSE - AND rl.timestamp_compute_response IS NOT NULL - AND rl.result IS NOT NULL - AND rl.result != '' - AND al.status_code >= 200 - AND al.status_code < 300 - AND rl.result ~ '"total_tokens"\s*:\s*\d+'; -END; -$$ LANGUAGE plpgsql; - --- One-time backfill function for existing data -CREATE OR REPLACE FUNCTION backfill_existing_metrics(batch_size INTEGER DEFAULT 5000) -RETURNS INTEGER AS $$ -DECLARE - total_processed INTEGER := 0; - batch_processed INTEGER; -BEGIN - RAISE NOTICE 'Starting backfill process...'; - - -- First, mark all existing records as unprocessed if they have the required data - UPDATE resource_server_async_requestlog - SET metrics_processed = FALSE - WHERE metrics_processed IS NULL - AND timestamp_compute_response IS NOT NULL - AND result IS NOT NULL - AND result != ''; - - RAISE NOTICE 'Marked existing records as unprocessed. Processing in batches of %...', batch_size; - - -- Process in batches to avoid overwhelming the system - LOOP - SELECT process_unprocessed_metrics(batch_size) INTO batch_processed; - total_processed := total_processed + batch_processed; - - -- Log progress - IF batch_processed > 0 THEN - RAISE NOTICE 'Processed % records (total: %)', batch_processed, total_processed; - END IF; - - -- Exit when no more to process - EXIT WHEN batch_processed = 0; - - -- Small delay to prevent overwhelming the system - PERFORM pg_sleep(0.1); - END LOOP; - - RAISE NOTICE 'Backfill complete. Total processed: %', total_processed; - RETURN total_processed; -END; -$$ LANGUAGE plpgsql; - --- Monitoring view for easy status checking -CREATE OR REPLACE VIEW v_metrics_processing_status AS -SELECT - COUNT(*) FILTER (WHERE metrics_processed = FALSE) as unprocessed_count, - COUNT(*) FILTER (WHERE metrics_processed = TRUE) as processed_count, - COUNT(*) as total_count, - ROUND( - (COUNT(*) FILTER (WHERE metrics_processed = TRUE)::NUMERIC / - NULLIF(COUNT(*), 0) * 100), 2 - ) as processed_percentage, - MIN(timestamp_compute_response) FILTER (WHERE metrics_processed = FALSE) as oldest_unprocessed, - MAX(timestamp_compute_response) FILTER (WHERE metrics_processed = FALSE) as newest_unprocessed -FROM resource_server_async_requestlog -WHERE timestamp_compute_response IS NOT NULL; - --- ============================================================================= --- USAGE INSTRUCTIONS --- ============================================================================= -/* - -AFTER RUNNING THIS SCRIPT: - -1. Backfill existing data (run once): - SELECT backfill_existing_metrics(1000); - -2. Set up cron job for ongoing processing: - Add to crontab: crontab -e - */30 * * * * psql -d yourdb -c "SELECT process_unprocessed_metrics(500);" >/dev/null 2>&1 - -3. Monitor the system: - SELECT * FROM v_metrics_processing_status; - SELECT * FROM get_unprocessed_metrics_count(); - -4. Manual processing if needed: - SELECT process_unprocessed_metrics(1000); - -5. For high-volume periods, run multiple workers: - - SKIP LOCKED prevents conflicts between workers - - Adjust batch_size based on system capacity (100-2000) - -PERFORMANCE TUNING: -- For light load: Run every 1-2 minutes with batch_size=200 -- For heavy load: Run every 30 seconds with batch_size=500-1000 -- For very heavy load: Run multiple workers with different batch sizes - -MONITORING QUERIES: -- Current backlog: SELECT unprocessed_count FROM v_metrics_processing_status; -- Processing lag: SELECT oldest_unprocessed FROM v_metrics_processing_status; -- Success rate: SELECT processed_percentage FROM v_metrics_processing_status; - -*/ \ No newline at end of file diff --git a/pgloader.load b/pgloader.load deleted file mode 100644 index 63eb5b63..00000000 --- a/pgloader.load +++ /dev/null @@ -1,18 +0,0 @@ -LOAD DATABASE - FROM sqlite:///path/db.sqlite3 - INTO postgresql://$(PGUSER)@$(PGHOST):$(PGPORT)/$(PGDATABASE) - -WITH include drop, create tables, create indexes, reset sequences, batch rows = 50000, batch size = 100MB, prefetch rows = 5000, data only - -SET work_mem to '128MB', - maintenance_work_mem to '512MB', - search_path to 'public' - -CAST - type text to varchar, - type bigint to integer, - type bigserial to serial, - type boolean to boolean, - type integer to integer, - type smallint to smallint -; diff --git a/reset_cursor.py b/reset_cursor.py deleted file mode 100644 index df87f406..00000000 --- a/reset_cursor.py +++ /dev/null @@ -1,46 +0,0 @@ -import psycopg2 -from psycopg2 import sql -import os - -# Fetch database connection settings from environment variables -db_name = os.getenv('PGDATABASE', 'default_db_name') -db_user = os.getenv('PGUSER', 'default_user') -db_password = os.getenv('PGPASSWORD', '') # This will be empty since .pgpass will handle the password -db_host = os.getenv('PGHOST', 'localhost') -db_port = os.getenv('PGPORT', '5432') - -# Connect to the PostgreSQL database -conn = psycopg2.connect( - dbname=db_name, user=db_user, password=db_password, host=db_host, port=db_port -) -cur = conn.cursor() - -# Fetch all tables in the public schema -cur.execute(""" - SELECT table_name - FROM information_schema.tables - WHERE table_schema = 'public' AND table_type = 'BASE TABLE'; -""") -tables = cur.fetchall() - -# Reset sequences for all tables -for table in tables: - table_name = table[0] - - # Try to reset the sequence for the primary key column - try: - reset_query = sql.SQL(""" - SELECT setval(pg_get_serial_sequence(%s, 'id'), COALESCE(MAX(id)+1, 1), false) - FROM {} - """).format(sql.Identifier(table_name)) - - cur.execute(reset_query, (table_name,)) - print(f"Reset sequence for table {table_name}") - - except Exception as e: - print(f"Could not reset sequence for table {table_name}: {e}") - -# Commit the changes and close the connection -conn.commit() -cur.close() -conn.close() From b95e9ecde5be3da6e68caa677c2f5657e43e2ce0 Mon Sep 17 00:00:00 2001 From: Aditya Tanikanti Date: Tue, 11 Nov 2025 15:04:34 -0600 Subject: [PATCH 02/22] Added github docs pages --- .github/workflows/deploy-docs.yml | 219 +++++ README.md | 784 +----------------- .../sophia-villm-config-template-v2.0.yaml | 56 -- .../llamacpp_register_function.py | 55 -- compute-functions/vllm_register_function.py | 177 ---- docs/DEPLOYMENT.md | 199 +++++ docs/README.md | 18 + docs/admin-guide/docker-quickstart.md | 246 ++++++ docs/admin-guide/setup.md | 454 ++++++++++ docs/usage-guide.md | 347 ++++++++ 10 files changed, 1514 insertions(+), 1041 deletions(-) create mode 100644 .github/workflows/deploy-docs.yml delete mode 100644 compute-endpoints/sophia-villm-config-template-v2.0.yaml delete mode 100644 compute-functions/llamacpp_register_function.py delete mode 100644 compute-functions/vllm_register_function.py create mode 100644 docs/DEPLOYMENT.md create mode 100644 docs/README.md create mode 100644 docs/admin-guide/docker-quickstart.md create mode 100644 docs/admin-guide/setup.md create mode 100644 docs/usage-guide.md diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 00000000..f3a706cf --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,219 @@ +name: Deploy Documentation + +on: + push: + branches: + - main + paths: + - 'docs/**' + - 'README.md' + - '.github/workflows/deploy-docs.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Build documentation site + run: | + mkdir -p _site + cp -r docs/* _site/ + cp README.md _site/index.md + + # Create a simple index.html that redirects to the main docs + cat > _site/index.html << 'EOF' + + + + + + FIRST Inference Gateway Documentation + + + +

🚀 FIRST Inference Gateway Documentation

+ +

Welcome to the documentation for the Federated Inference Resource Scheduling Toolkit (FIRST). FIRST enables LLM inference as a service across distributed HPC clusters through an OpenAI-compatible API.

+ + + + + + + +
+ +

+ License: Apache 2.0 | + Maintained by: Argonne National Laboratory +

+ + + EOF + + # Convert markdown files to HTML using a simple approach + # For a more sophisticated setup, you could use Jekyll, MkDocs, or Sphinx + for md_file in $(find _site -name "*.md"); do + html_file="${md_file%.md}.html" + cat > "$html_file" << 'HEADER' + + + + + + FIRST Documentation + + + + +
+ + HEADER + + # Use a simple markdown renderer - in production you'd want something more robust + echo '
' >> "$html_file" + echo '' >> "$html_file" + echo '' >> "$html_file" + echo '
' >> "$html_file" + done + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + diff --git a/README.md b/README.md index 5fbbaeb8..006e9e1a 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,10 @@ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ![Build](https://github.com/auroraGPT-ANL/inference-gateway/workflows/Django/badge.svg) -# Inference Gateway for FIRST toolkit +# Inference Gateway for FIRST Toolkit FIRST (Federated Inference Resource Scheduling Toolkit) is a system that enables LLM (Large Language Model) inference as a service, allowing secure, remote execution of LLMs through an [OpenAI](https://platform.openai.com/docs/overview)-compatible API. FIRST's Inference Gateway is a RESTful API that validates and authorizes inference requests to scientific computing clusters using [Globus Auth](https://www.globus.org/globus-auth-service) and [Globus Compute](https://www.globus.org/compute). -## Table of Contents - -- [Documentation](#documentation) -- [System Architecture](#system-architecture) -- [Prerequisites](#prerequisites) -- [Setup Overview](#setup-overview) -- [Gateway Setup](#gateway-setup) - - [Installation (Docker or Bare Metal)](#installation-docker-or-bare-metal) - - [Register Globus Application](#register-globus-application) - - [Configure Environment (.env)](#configure-environment-env) - - [Initialize Gateway Database](#initialize-gateway-database) -- [Inference Backend Setup (Remote/Local)](#inference-backend-setup-remotelocal) - - [Create Virtual Environment](#virtual-python-environment) - - [Install Inference Server (e.g., vLLM) and Globus Compute](#install-inference-server-eg-vllm-and-globus-compute) - - [Register Globus Compute Functions](#register-globus-compute-functions) - - [Configure and Start a Globus Compute Endpoint](#configure-and-start-a-globus-compute-endpoint) -- [Connecting Gateway and Backend](#connecting-gateway-and-backend) - - [Update Fixtures](#update-fixtures) - - [Load Fixtures](#load-fixtures) -- [Starting the Services](#starting-the-services) - - [Gateway (Docker or Bare Metal)](#gateway-docker-or-bare-metal) - - [Inference Backend (Globus Compute Endpoint)](#inference-backend-globus-compute-endpoint) -- [Verifying the Setup](#verifying-the-setup) -- [Benchmarking](#benchmarking) -- [Production Considerations (Nginx)](#production-considerations-nginx) -- [Monitoring](#monitoring) -- [Troubleshooting](#troubleshooting) - -## Documentation - -Detailed installation and deployment recipes now live under `docs/`. Start with the Docker quickstart if you are looking for a lightweight path to run the gateway locally: - -- [Administrator Guide — Docker Quickstart](docs/admin-guide/docker-quickstart.md) - ## System Architecture ![System Architecture](./first_architecture.png) @@ -49,736 +15,48 @@ The Inference Gateway consists of several components: - **Globus Compute Endpoints**: Remote execution framework on HPC clusters (or local machines). - **Inference Server Backend**: (e.g., [vLLM](https://docs.vllm.ai/en/latest/)) High-performance inference service for LLMs running alongside the Globus Compute Endpoint. -## Prerequisites - -- [Python](https://www.python.org/) 3.11+ -- [PostgreSQL](https://www.postgresql.org/docs/) Server (included in the Docker deployment) -- [Poetry](https://python-poetry.org/docs/#installation) -- [Docker](https://docs.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) (Recommended for Gateway deployment) -- [Globus Account](https://www.globus.org/) -- Access to a compute resource (HPC cluster or a local machine with sufficient resources for the chosen inference server and models) - -## Setup Overview - -The setup involves two main parts: -1. **Gateway Setup**: Installing and configuring the central API gateway service. -2. **Inference Backend Setup**: Setting up the inference server (like vLLM) and the Globus Compute components on the machine(s) where models will run. - -These parts can be done in parallel, but configuration details from each are needed to link them. - -## Gateway Setup - -> 📝 Looking for a shorter, task-oriented walkthrough? See the [Docker Quickstart](docs/admin-guide/docker-quickstart.md) in the Administrator Guide. - -This section covers setting up the central Django application. - -### Installation (Docker or Bare Metal) - -Clone the repository first: -```bash -git clone https://github.com/auroraGPT-ANL/inference-gateway.git -cd inference-gateway -``` - -**Option 1: Docker Deployment (Recommended)** - -```bash -# Create necessary directories needed by docker-compose.yml -mkdir -p logs prometheus -# Create a basic prometheus config if you don't have one -# echo "global:\n scrape_interval: 15s\nscrape_configs:\n - job_name: 'prometheus'\n static_configs:\n - targets: ['localhost:9090']" > prometheus/prometheus.yml - -# Configuration is done via the .env file (see next steps) -``` -*See [Starting the Services](#starting-the-services) for how to run this after configuration.* -*See `docker-compose.yml` for details on included services (Postgres, Redis, optional monitoring).* - -**Option 2: Bare Metal Setup / Local Development (need a PostgreSQL server)** - -```bash -# Set up Python environment with Poetry -poetry config virtualenvs.in-project true -poetry env use python3.11 -poetry install - -# Activate the environment -poetry shell -# Can also use 'source .venv/bin/activate' - -# Ensure PostgreSQL server is running and accessible. -# Configuration is done via the .env file (see next steps) -``` - -### Register Two Globus Applications - -**Service API Application** - -To handle authorization within the API, the Gateway needs to be registered as a Globus Service API application: - -1. Visit [developers.globus.org](https://app.globus.org/settings/developers) and sign in. -2. Under **Register an...**, click on **Register a service API ...**. -3. Select **none of the above - create a new project** or select one of your existing projects. -4. Complete the new project form (not needed if you selected an existing project). -5. Complete the registration form: - * Set **App Name** (e.g., "My Inference Gateway"). - * Add **Redirect URIs**. For local development with the default Django server (`runserver`), use `http://localhost:8000/complete/globus/`. For production, use `https:///complete/globus/`. - * You can leave the check boxes to their default setting. - * Set **Privacy Policy** and **Terms & Conditions** URLs if applicable. -6. After registration, a **Client UUID** will be assigned to your Globus application. Generate a **Client Secret** by clicking on the **Add Client Secret** button on the right-hand side. **You will need both for the `.env` configuration.** The UUID will be for `GLOBUS_APPLICATION_ID`, and the secret will be for `GLOBUS_APPLICATION_SECRET`. - -**Add a Globus Scope to your Service API Application** - -A scope is needed for users to generate an access token to access the inference service. First export the API client credentials in a terminal: -```bash -export CLIENT_ID="" -export CLIENT_SECRET="" -``` - -Make a request to Globus Auth to attach an `action_all` scope to your API client. The `curl` command below will also embed a dependent Globus Group scope to the main scope, which is needed for the API to query the user's Group memberships during the authorization process. -```bash -curl -X POST -s --user $CLIENT_ID:$CLIENT_SECRET \ - https://auth.globus.org/v2/api/clients/$CLIENT_ID/scopes \ - -H "Content-Type: application/json" \ - -d '{ - "scope": { - "name": "Action Provider - all", - "description": "Access to inference service.", - "scope_suffix": "action_all", - "dependent_scopes": [ - { - "scope": "73320ffe-4cb4-4b25-a0a3-83d53d59ce4f", - "optional": false, - "requires_refresh_token": true - } - ] - } - }' -``` - -To verify that the scope was successfully created, query the details of your API client, and look for the UUID in the `scopes` field: -```bash -curl -s --user $CLIENT_ID:$CLIENT_SECRET https://auth.globus.org/v2/api/clients/$CLIENT_ID -``` - -Query the details of your newly created scope (you shoud see `73320ffe-4cb4-4b25-a0a3-83d53d59ce4f` in the `dependent_scopes` field): -```bash -export SCOPE_ID="" -curl -s --user $CLIENT_ID:$CLIENT_SECRET https://auth.globus.org/v2/api/clients/$CLIENT_ID/scopes/$SCOPE_ID -``` - -**Service Account Application** - -To handle the communication between the Gateway API and the compute resources (the Inference Backend), you need to create a Globus **Service Account application**. This application represents the Globus identity that will own the Globus Compute endpoints. - -1. Visit [developers.globus.org](https://app.globus.org/settings/developers) and sign in. -2. Under **Projects**, click on project used to register your Service API application from the previous step. -3. Click on **Add an App**. -4. Select **Register a service account ...**. -5. Complete the registration form: - * Set **App Name** (e.g., "My Inference Endpoints"). - * Set **Privacy Policy** and **Terms & Conditions** URLs if applicable. -6. After registration, a **Client UUID** will be assigned to your Globus application. Generate a **Client Secret** by clicking on the **Add Client Secret** button on the right-hand side. **You will need both for the `.env` configuration.** The UUID will be for `POLARIS_ENDPOINT_ID`, and the secret will be for `POLARIS_ENDPOINT_SECRET`. - -### Configure Environment (.env) - -Create a `.env` file in the project root (`inference-gateway/`). This file is used by both Docker and bare-metal setups (if using `python-dotenv`). - -```dotenv -# --- Core Django Settings --- -SECRET_KEY="" # Can be generate with Django, e.g. python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' -DEBUG=True # Set to False for production -ALLOWED_HOSTS="localhost,127.0.0.1" # Add your gateway domain/IP for production - -# --- Globus Credentials (from the "Register Globus Application" step) --- -# Client ID and Secret of the Globus Service API application -GLOBUS_APPLICATION_ID="" -GLOBUS_APPLICATION_SECRET="" -# Optional: Restrict access to specific Globus Groups (space-separated UUIDs) -# GLOBUS_GROUPS=" " -# Optional: Enforce specific Identity Provider usage (JSON string) -# AUTHORIZED_IDPS='{"Your Institution Domain": "your-institution-uuid"}' -# AUTHORIZED_GROUPS_PER_IDP='{"Institution Domain": "coma-separated list of group uuids"}' -# Optional: Enforce Globus high assurance policies (space-separated UUIDs) -# GLOBUS_POLICIES="" -# Client ID and Secret of the Globus Service Account application -POLARIS_ENDPOINT_ID="" -POLARIS_ENDPOINT_SECRET="" - -# --- CLI Authentication Helper --- -# Public Client ID used by the inference-auth-token.py script for user authentication -CLI_AUTH_CLIENT_ID="58fdd3bc-e1c3-4ce5-80ea-8d6b87cfb944" # Default public client, replace if needed -# Optional: Comma-separated list of allowed domains for CLI login (e.g., "anl.gov,alcf.anl.gov") -# CLI_ALLOWED_DOMAINS="anl.gov,alcf.anl.gov" -# Optional: Override token storage directory for CLI script -# CLI_TOKEN_DIR="~/.globus/my_custom_token_dir" -# Optional: Override App Name used by CLI script -# CLI_APP_NAME="my_custom_cli_app" - - -# --- Database Credentials --- -# Used by Django Gateway, Postgres container, postgres-exporter -POSTGRES_DB="inferencegateway" -POSTGRES_USER="inferencedev" -POSTGRES_PASSWORD="inferencedevpwd" # CHANGE THIS for production -# Hostname: Use "postgres" for Docker-compose networking. -# Use "localhost" for bare-metal if DB is local. -# Use "host.docker.internal" if Gateway runs in Docker but DB runs on the host machine. -PGHOST="postgres" # Important: Use "postgres" for Docker -PGPORT=5432 -PGUSER="dataportaldev" -PGPASSWORD="inferencedevpwd" # CHANGE THIS for production -PGDATABASE="inferencegateway" - -# --- Redis --- Used for caching, async tasks -# Use "redis" for Docker-compose networking. -# Use "localhost" (or relevant hostname) for bare-metal. -REDIS_URL="redis://redis:6379/0" - -# --- Gateway Specific Settings --- -MAX_BATCHES_PER_USER=2 # Max concurrent batch jobs allowed per user - -# --- Streaming Configuration --- -# Internal streaming server configuration (required for streaming functionality) -STREAMING_SERVER_HOST="localhost:8080" # Host and port of your internal streaming server -INTERNAL_STREAMING_SECRET="your-internal-streaming-secret-key" # Secret key for internal streaming authentication -# Both /chat/completions and /completions endpoints support streaming via the 'stream' parameter -# However, 'stream_options' parameter is only supported in /completions endpoint - -# --- Optional: Grafana Admin Credentials (for Docker setup) --- -# GF_SECURITY_ADMIN_USER=admin -# GF_SECURITY_ADMIN_PASSWORD=admin - -# QSTAT ENDPOINTS -# SOPHIA_QSTAT_ENDPOINT_UUID="" -# SOPHIA_QSTAT_FUNCTION_UUID="" -``` - -**Important**: Securely store all of your credentials and secrets, especially in production. The `CLI_AUTH_CLIENT_ID` is typically a public client and doesn't need to be kept secret. - -### Initialize Gateway Database - -Once you have configured the `.env` file, initialize the Gateway's database schame. - -**Option 1: Docker (also works with Podman)** - -First, build and run the Gateway's containers (there should be 7 in total): - -```bash -docker-compose -f docker-compose.yml up -d -``` - -Initialize the database: -```bash -docker-compose -f docker-compose.yml exec inference-gateway python manage.py makemigrations -docker-compose -f docker-compose.yml exec inference-gateway python manage.py migrate -``` - -**Option 2: Bare Metal** - -If you are not using Docker, you must have a PostgreSQL service running. -```bash -# Ensure DB connection vars are exported in your shell or use python-dotenv -# (e.g., export PGHOST=localhost POSTGRES_USER=...) -python manage.py makemigrations -python manage.py migrate -``` - -## Inference Backend Setup (Remote/Local) - -This section covers setting up the components on the machine where the AI models will actually run (e.g., an HPC compute node, a powerful workstation). - -### Virtual Python Environment - -All of the instruction below must be done within a Python virtual environment. Make sure to use a **virtual environment with the same Python version as the one used to deploy Gateway API** (Python 3.11 in this example). This will avoid version mismatch errors when using Globus Compute. - -```bash -# Example 1 with conda -conda create -n vllm-env python=3.11 -y -conda activate vllm-env - -# Example 2 with python venv -python3.11 -m venv vllm-env -source vllm-env/bin/activate # On Windows use `vllm-env\Scripts\activate` -``` - -### Install Inference Server (e.g., vLLM) and Globus Compute - -Choose and install an inference serving framework. vLLM is recommended for performance with many transformer models: - -```bash -# Make sure you have activated your virtual environment - -# Basic vLLM installation from source -git clone https://github.com/vllm-project/vllm.git -cd vllm -pip install -e . -# For specific hardware acceleration (CUDA, ROCm), follow official docs: -# https://docs.vllm.ai/en/latest/getting_started/installation.html -``` - -Install the Globus Compute Endpoint software and the Globus Compute SDK: -```bash -pip install globus-compute-sdk globus-compute-endpoint -``` - -### Register Globus Compute Functions - -The Gateway interacts with the inference server via functions registered with Globus Compute. You need to register: - -1. **Inference Function**: Wraps the call to your inference server (e.g., vLLM OpenAI-compatible endpoint). -2. **Status Function (Optional but Recommended)**: Queries the cluster scheduler (e.g., PBS `qstat`) and node status to aid federated routing. - -**Important:** When registering Globus Compute functions and endpoints, you need to explicitly tie the function/endpoint identity back to your Globus **Service Account** application (not the Service API application). Do this by exporting the client ID (value of `POLARIS_ENDPOINT_ID` in `.env`) and Secret (value of `POLARIS_ENDPOINT_SECRET` in `.env`) as environment variables, **before** running the registration script or configuring the endpoint: -```bash -export GLOBUS_COMPUTE_CLIENT_ID="" -export GLOBUS_COMPUTE_CLIENT_SECRET="" -``` - -Now, register the necessary functions: - -```bash -# Ensure you are in the correct Python environment - -# Navigate to the `compute-functions` directory in your local clone of the repository. -cd path/to/inference-gateway/compute-functions - -# Register the vLLM inference function (modify the script if needed) -# See compute-functions/vllm_register_function.py -python vllm_register_function.py -# Note the output Function UUID (e.g., ) -# Output example: -# Function registered with UUID - .... -# The UUID is stored in vllm_register_function_sophia_multiple_models.txt. - -# Register the qstat/status function (modify script for your scheduler if needed) -python qstat_register_function.py -# Note the output Function UUID (e.g., ) - -# (Register other functions like batch processing if needed) -``` - -**Important**: Keep track of the Function UUIDs generated. - -### Configure a Globus Compute Endpoint - -This endpoint runs on the backend machine, listens for tasks from Globus Compute, and executes the registered functions. - -```bash -# Ensure you are in the correct Python environment - -# Configure a new endpoint (follow prompts) -# Here we use "my-compute-endpoint" for the name but you can choose another name (e.g. local-vllm) -globus-compute-endpoint configure my-compute-endpoint - -# This creates a configuration directory, e.g., ~/.globus_compute/my-compute-endpoint/ -# Edit the config.yaml inside that directory. -``` - -### Note on HPC Configuration Examples: -See [`local-vllm-endpoint.yaml`](./compute-endpoints/local-vllm-endpoint.yaml) for a configuration example that submits tasks on local hardware. For an example of submitting inference tasks through a cluster scheduler like PBS (specifically on ALCF's Sophia cluster), see [`sophia-vllm-config-template-v2.0.yaml`](./compute-endpoints/sophia-villm-config-template-v2.0.yaml). Adapting cluster examples requires understanding the specific HPC environment (scheduler, modules, file paths, etc.). -Refer to [Globus Compute Endpoint Docs](https://globus-compute.readthedocs.io/en/latest/endpoints.html) for all options. - -If you adopted the above configuration examples, make sure to edit the following: - -* `allowed_functions`: Make sure the function UUIDs (one per line) are the ones you registered in the [Register Globus Compute Functions](#register-globus-compute-functions) section. -* `worker_init`: *Note:* For local setups, you'll typically activate your environment directly (e.g., `source vllm-env/bin/activate`). For cluster setups like the Sophia example, you might use a shared setup script (e.g., `source inference-gateway/compute-endpoints/common_setup.sh`). Ensure you point to the correct setup script or activation command for your environment. - -**After configuring `config.yaml`:** - -```bash -# Start the endpoint -globus-compute-endpoint start my-compute-endpoint - -# Note the Endpoint UUID displayed after starting. -# You can always recover the UUID by typing `globus-compute-endpoint list` -``` - -**Keep track of the Endpoint UUID.** - -## Connecting Gateway and Backend - -### Update Fixtures - -Edit the relevant fixtures file in the Gateway project directory (`inference-gateway/fixtures/`). - -For federated access, edit `federated_endpoints.json`. - -**Example: `fixtures/federated_endpoints.json`** - -```json -[ - { - "model": "resource_server.federatedendpoint", - "pk": 1, - "fields": { - "name": "OPT 125M (Federated)", - "slug": "federated-opt-125m", - "target_model_name": "facebook/opt-125m", // Model users request - "description": "Federated access point for the facebook/opt-125m model.", - "targets": [ - { - "cluster": "local", - "framework": "vllm", - "model": "facebook/opt-125m", // Model served by this specific target - "endpoint_slug": "local-vllm-facebook-opt-125m", // Unique ID for this target - "endpoint_uuid": "", - "function_uuid": "", - "api_port": 8001 // Port your local vLLM server runs on - } - // Add more targets here for the same model on different clusters/frameworks - // Example: A target on an HPC cluster like Sophia - // { - // "cluster": "sophia", - // "framework": "vllm", - // "model": "facebook/opt-125m", // Assuming OPT-125m is also deployed there - // "endpoint_slug": "sophia-vllm-facebook-opt-125m", - // "endpoint_uuid": "", - // "function_uuid": "", - // "api_port": 8000 // Port on Sophia's compute node - // } - ] - } - } - // Add more FederatedEndpoint entries for other models -] -``` - -For standard, non-federated access, edit `endpoints.json`. - -**Example: `fixtures/endpoints.json`** - -```json -[ - { - "model": "resource_server.endpoint", - "pk": 1, // Or next available primary key - "fields": { - "endpoint_slug": "local-vllm-facebook-opt-125m", // Example for local OPT-125m - "cluster": "local", // Or wherever your compute endpoint is hosted - "framework": "vllm", - "model": "facebook/opt-125m", // Model served by this endpoint - "api_port": 8001, // Example port for local vLLM server - "endpoint_uuid": "", - "function_uuid": "", - "batch_endpoint_uuid": "", - "batch_function_uuid": "", - "allowed_globus_groups": "" // Optional: Restrict this target further - } - } - // Add more Endpoint entries for other models/clusters -] -``` - -Replace placeholders (`<...>`) with the UUIDs and details from the previous steps. - -### Load Fixtures - -Load the updated fixture file into the Gateway database. - -**Option 1: Docker:** - -Make sure the fixture `json` files are updated within the running container. You can do this by editing the files directly within the container: -```bash -docker-compose -f docker-compose.yml exec inference-gateway /bin/bash -# The edit the /app/fixtures/endpoints.json (or federated_endpoints.json) file -``` - -or by transfering the edited local file directly into the container: -```bash -docker cp fixtures/endpoints.json inference-gateway_inference-gateway_1:/app/fixtures/ -# Or: docker cp fixtures/federated_endpoints.json inference-gateway_inference-gateway_1:/app/fixtures/ -``` - -Load the fixtures into the database: -```bash -docker-compose -f docker-compose.yml exec inference-gateway python manage.py loaddata fixtures/endpoints.json -# Or: docker-compose ... exec inference-gateway python manage.py loaddata fixtures/federated_endpoints.json -``` - -**Option 2: Bare Metal:** -```bash -python manage.py loaddata fixtures/endpoints.json -# Or: python manage.py loaddata fixtures/federated_endpoints.json -``` - -## Starting the Services - -### Gateway (Docker or Bare Metal) - -**Docker:** -```bash -# Start all services defined in docker-compose.yml (if not already running) -docker-compose -f docker-compose.yml up --build -d -``` - -**Bare Metal (Development):** -```bash -# Ensure DB connection vars are set -poetry shell -python manage.py runserver 0.0.0.0:8000 # Or your preferred port -``` - -**Bare Metal (Production with Gunicorn):** -```bash -# Ensure environment variables are set -poetry shell -poetry run gunicorn \ - inference_gateway.asgi:application \ - -k uvicorn.workers.UvicornWorker \ - -b 0.0.0.0:8000 \ - --workers 5 \ - --log-level info - # Add other Gunicorn flags as needed (--threads, --timeout, log files etc.) -``` - -### Inference Backend (Globus Compute Endpoint) - -Ensure the Globus Compute endpoint is running on the backend machine: - -```bash -# On the HPC login node / backend machine -globus-compute-endpoint start -``` - -Verify the associated inference server (e.g., vLLM) is started by the endpoint's worker initialization or job submission process. - -## Verifying the Setup - -Once both the Gateway and at least one Backend Compute Endpoint (with its inference server) are running, you can send a test request. You'll need a valid Globus authentication token obtained for the gateway's scope. - -### Setting up ngrok for Testing Streaming (Optional) - -For testing streaming functionality especially when working with remote endpoints, you can use ngrok to create a secure tunnel to your local gateway: - -1. **Install ngrok**: Visit [ngrok.com](https://ngrok.com/) and follow the installation instructions for your platform. - -2. **Start ngrok tunnel**: Start ngrok to create a tunnel to your local gateway (assuming it's running on port 8000): - ```bash - ngrok http 8000 - ``` - This will display a public URL (e.g., `https://abcd1234.ngrok.io`) that tunnels to your local gateway. - -3. **Test streaming with Python script**: Here's an example Python script for testing streaming functionality: - - ```python - import sys - import json - import time - import openai - - if len(sys.argv) != 2: - print("Usage: python test_streaming.py ") - sys.exit(1) - - access_token = sys.argv[1] - - # Configure the OpenAI client to connect to your gateway via ngrok - client = openai.OpenAI( - # Replace with your ngrok URL or direct gateway URL - base_url="https://your-ngrok-url.ngrok.io/resource_server/sophia/vllm/v1", - # Use your Globus access token - api_key=access_token - ) - - # Define your prompt - messages = [ - {"role": "user", "content": "Explain what is the best way to learn python"} - ] - - start_time = time.time() - total_chars = 0 - - print("🤖 AI Response: ", end="") - - # Call the chat completions endpoint with stream=True - try: - stream = client.chat.completions.create( - model="openai/gpt-oss-20b", # Replace with your deployed model - messages=messages, - stream=True - ) - - # Iterate over the stream to get the response chunks in real-time - for chunk in stream: - # Extract the content from the chunk (standard OpenAI format) - if chunk.choices[0].delta.content is not None: - content = chunk.choices[0].delta.content - # Print the content without a newline and flush the buffer - print(content, end="", flush=True) - total_chars += len(content) - - # Print a final newline character after the stream is complete - print() - - except Exception as e: - print(f"\n❌ Error: {e}") - - end_time = time.time() - latency = end_time - start_time - print(f"\n⏱️ Streaming Latency: {latency:.2f} seconds") - print(f"📦 Throughput: {total_chars/latency:.2f} chars/sec") - ``` - - Save this as `test_streaming.py` and run it with your access token: - - ```bash - python test_streaming.py $MY_TOKEN - ``` - -4. **Get a Token using the Helper Script**: - * Ensure your `.env` file has `GLOBUS_APPLICATION_ID` (for the gateway) and `CLI_AUTH_CLIENT_ID` (for the helper script, a default public one is provided) set. You also need to configure `CLI_ALLOWED_DOMAINS` to target your specific identity provider (e.g. your institution's SSO). By default, without specifying `CLI_ALLOWED_DOMAINS`, the only allowed providers are `anl.gov` and `alcf.anl.gov`. - * **Authenticate (First time or if tokens expire):** Run the authentication script. This will open a browser window for Globus login. - ```bash - python inference-auth-token.py authenticate - ``` - This stores refresh and access tokens locally (typically in `~/.globus/app/...`). - * **Force Re-authentication:** If you need to change Globus accounts or encounter permission errors possibly related to expired sessions or policy changes, log out via [https://app.globus.org/logout](https://app.globus.org/logout) and force re-authentication: - ```bash - python inference-auth-token.py authenticate --force - ``` - * **Get Access Token:** Retrieve your current, valid access token: - ```bash - export MY_TOKEN=$(python inference-auth-token.py get_access_token) - echo "Token stored in MY_TOKEN environment variable." - # echo $MY_TOKEN # Uncomment to view the token directly - ``` - This command automatically uses the stored refresh token to get a new access token if the current one is expired. - - * **Token Validity:** Access tokens are valid for 48 hours. Refresh tokens allow getting new access tokens without re-logging in via browser, but they expire after 6 months of inactivity. Some institutions or policies might enforce re-authentication more frequently (e.g., weekly). - - -2. **Send Request using cURL**: You can adjust the model name and payload as appropriate. - - Example with a federated Globus Compute endpoint (assuming `federated_endpoints.json` was used): - - ```bash - # Example using the federated endpoint for OPT-125m - curl -X POST http://127.0.0.1:8000/resource_server/v1/chat/completions \\ - -H "Authorization: Bearer $MY_TOKEN" \\ - -H "Content-Type: application/json" \\ - -d '{ - "model": "facebook/opt-125m", - "messages": [ - {"role": "user", "content": "Explain the concept of Globus Compute in simple terms."} - ], - "max_tokens": 150 - }' - ``` - - Example with a standard, non-federated Globus Compute endpoint (assuming `endpoints.json` was used): - - ```bash - # Example targeting a specific vLLM endpoint on the 'local' cluster for OPT-125m - curl -X POST http://127.0.0.1:8000/resource_server/local/vllm/v1/chat/completions \\ - -H "Authorization: Bearer $MY_TOKEN" \\ - -H "Content-Type: application/json" \\ - -d '{ - "model": "facebook/opt-125m", - "messages": [ - {"role": "user", "content": "Explain the concept of Globus Compute in simple terms."} - ], - "max_tokens": 150 - }' - ``` - -A successful response will be a JSON object containing the model's completion. - -## Benchmarking - -A benchmark script is provided in `examples/load-testing/benchmark-serving.py` (adapted from vLLM's benchmarks) to test the performance of your deployed endpoints. - -1. **Download Dataset**: The script often uses the ShareGPT dataset. You can download it manually or let the script attempt to download it. A common location to place it might be `examples/load-testing/ShareGPT_V3_unfiltered_cleaned_split.json`. - ```bash - # Example: Download ShareGPT dataset (check the script for the exact URL if needed) - wget https://huggingface.co/datasets/anon8231489123/ShareGPT_Vicuna_unfiltered/resolve/main/ShareGPT_V3_unfiltered_cleaned_split.json -P examples/load-testing/ - ``` - -2. **Run Benchmark**: Execute the script, pointing it to your gateway's API endpoint. - ```bash - # Example: Benchmark local federated endpoint for opt-125m - python examples/load-testing/benchmark-serving.py \ - --backend vllm \ - --model facebook/opt-125m \ - --base-url http://127.0.0.1:8000/resource_server/v1/chat/completions \ - --dataset-name sharegpt \ - --dataset-path examples/load-testing/ShareGPT_V3_unfiltered_cleaned_split.json \ - --output-file benchmark_results_local_opt125m.jsonl \ - --num-prompts 100 # Adjust as needed - # Add --request-rate or --max-concurrency for more controlled tests - # Add --disable-ssl-verification if using self-signed certs locally - # Add --disable-stream if benchmarking a non-streaming endpoint - ``` - - You can adapt the `--base-url` to point to other OpenAI-compatible endpoints (like the OpenAI API itself, Anthropic, etc., potentially requiring environment variables like `OPENAI_API_KEY`). See the script's arguments (`--help`) for more options. - -## Production Considerations (Nginx) +## Documentation -For production deployments (especially bare-metal), running Django/Gunicorn behind a reverse proxy like Nginx is highly recommended for: +### For Administrators -* **HTTPS/SSL Termination**: Securely handle TLS encryption. -* **Load Balancing**: Distribute requests across multiple Gunicorn workers/instances. -* **Serving Static Files**: Efficiently serve CSS, JS, images. -* **Security**: Add rate limiting, header checks, etc. -* **Hostname Routing**: Direct traffic based on domain names. +- **[Setup Guide](docs/admin-guide/setup.md)** - Complete installation and configuration instructions +- **[Docker Quickstart](docs/admin-guide/docker-quickstart.md)** - Fast-track Docker deployment guide -**Example Nginx Configuration Snippet (`/etc/nginx/sites-available/inference_gateway`):** +### For Users -```nginx -upstream app_server { - # fail_timeout=0 means we always retry an upstream even if it failed - # to return a good HTTP response (in case the Gunicorn worker recovers). - server 127.0.0.1:8000 fail_timeout=0; - # Add more servers here if running multiple Gunicorn instances -} +- **[Usage Guide](docs/usage-guide.md)** - How to authenticate and make inference requests -server { - listen 80; - # listen 443 ssl; - server_name your-gateway-domain.org; +### Example Deployment - # ssl_certificate /path/to/your/cert.pem; - # ssl_certificate_key /path/to/your/key.pem; +For an example of a production deployment, see the [ALCF Inference Endpoints documentation](https://github.com/argonne-lcf/inference-endpoints). - client_max_body_size 4G; +## Citation - location /static/ { - alias /path/to/inference-gateway/static/; - } +If you use ALCF Inference Endpoints or the Federated Inference Resource Scheduling Toolkit (FIRST) in your research or workflows, please cite our paper: - location / { - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Host $http_host; - proxy_redirect off; - proxy_pass http://app_server; - } +```bibtex +@inproceedings{10.1145/3731599.3767346, + author = {Tanikanti, Aditya and C\^{o}t\'{e}, Benoit and Guo, Yanfei and Chen, Le and Saint, Nickolaus and Chard, Ryan and Raffenetti, Ken and Thakur, Rajeev and Uram, Thomas and Foster, Ian and Papka, Michael E. and Vishwanath, Venkatram}, + title = {FIRST: Federated Inference Resource Scheduling Toolkit for Scientific AI Model Access}, + year = {2025}, + isbn = {9798400718717}, + publisher = {Association for Computing Machinery}, + address = {New York, NY, USA}, + url = {https://doi.org/10.1145/3731599.3767346}, + doi = {10.1145/3731599.3767346}, + abstract = {We present the Federated Inference Resource Scheduling Toolkit (FIRST), a framework enabling Inference-as-a-Service across distributed High-Performance Computing (HPC) clusters. FIRST provides cloud-like access to diverse AI models, like Large Language Models (LLMs), on existing HPC infrastructure. Leveraging Globus Auth and Globus Compute, the system allows researchers to run parallel inference workloads via an OpenAI-compliant API on private, secure environments. This cluster-agnostic API allows requests to be distributed across federated clusters, targeting numerous hosted models. FIRST supports multiple inference backends (e.g., vLLM), auto-scales resources, maintains "hot" nodes for low-latency execution, and offers both high-throughput batch and interactive modes. The framework addresses the growing demand for private, secure, and scalable AI inference in scientific workflows, allowing researchers to generate billions of tokens daily on-premises without relying on commercial cloud infrastructure.}, + booktitle = {Proceedings of the SC '25 Workshops of the International Conference for High Performance Computing, Networking, Storage and Analysis}, + pages = {52–60}, + numpages = {9}, + keywords = {Inference as a Service, High Performance Computing, Job Schedulers, Large Language Models, Globus, Scientific Computing}, + series = {SC Workshops '25} } ``` -*Remember to collect static files (`python manage.py collectstatic`) and configure Nginx to serve them from the specified `alias` path.* -*Consult Nginx documentation for details on SSL setup and other options.* - -## Monitoring - -Access the monitoring dashboard (if deployed via Docker Compose with monitoring enabled): - -- **Grafana**: http://localhost:3000 (default credentials: admin/admin) - Visualizes metrics. -- **Prometheus**: http://localhost:9090 - Collects metrics. - -The Grafana dashboard includes: -- Application metrics (request rates, latency, error rates) -- System metrics (CPU, memory, disk I/O via Node Exporter) -- Database metrics (connection counts, query performance via PostgreSQL Exporter) -- Custom Gateway metrics (inference rates, token counts - requires metrics endpoint in Gateway) - -## Example Usage Documentation +## Acknowledgements -For an example of how user-facing documentation might look for a deployed instance of this gateway, see the [ALCF Inference Endpoints documentation](https://github.com/argonne-lcf/inference-endpoints). +This work was supported by the U.S. Department of Energy, Office of Science, Office of Advanced Scientific Computing Research, under Contract No. DE-AC02-06CH11357. This research used resources of the Argonne Leadership Computing Facility, which is a DOE Office of Science User Facility. -## Troubleshooting +## License -* **Docker Nginx 404/502/504 Errors:** Verify `deploy/docker/nginx.conf` mount and upstream definition, check `inference-gateway` logs. -* **Database Connection Errors:** Check `.env` variables (`PGHOST`, etc.) match context (Docker vs. Host vs. Bare-metal) and firewall/`pg_hba.conf` rules. -* **Globus Auth Errors**: Ensure Redirect URIs match in Globus Developer portal and `.env` credentials are correct. -* **Compute Endpoint Issues**: Check endpoint logs (`~/.globus_compute//endpoint.log`) for function execution errors, environment problems, or connection issues. -* **500 Server Errors on Gateway**: Check gateway logs (`docker-compose logs inference-gateway` or Gunicorn log files) for Python tracebacks. +This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. diff --git a/compute-endpoints/sophia-villm-config-template-v2.0.yaml b/compute-endpoints/sophia-villm-config-template-v2.0.yaml deleted file mode 100644 index bb0099c6..00000000 --- a/compute-endpoints/sophia-villm-config-template-v2.0.yaml +++ /dev/null @@ -1,56 +0,0 @@ -amqp_port: 443 -display_name: sophia-vllm-llama3.1-artiller-test-endpoint -engine: - type: GlobusComputeEngine - max_retries_on_system_failure: 2 - max_workers_per_node: 100 - job_status_kwargs: - max_idletime: 86400 - address: - type: address_by_interface - ifname: ens10f0 - provider: - type: PBSProProvider - launcher: - type: SimpleLauncher - account: argonne_tpc - select_options: ngpus=8 - scheduler_options: '#PBS -l filesystems=home:eagle' - queue: 'by-node' - init_blocks: 0 - max_blocks: 2 - min_blocks: 0 - nodes_per_block: 1 - walltime: 24:00:00 - worker_init: | - # single_model_launch.sh - # Source the common script - source /home/openinference_svc/sophia_common_scripts.sh # Replace with the actual path to common.sh - # Setup the environment - setup_environment - # Define model parameters - model_name="Meta-Llama-3.1-8B-Instruct" - model_command="vllm serve meta-llama/Meta-Llama-3.1-8B-Instruct --host 127.0.0.1 --port 8000 \ - --tensor-parallel-size 8 --gpu-memory-utilization 0.95 \ - --disable-log-requests --enable-chunked-prefill \ - --enable-prefix-caching --multi-step-stream-outputs False \ - --served-model-name meta-llama/Meta-Llama-3.1-8B-Instruct/artillery-test \ - --ssl-keyfile ~/certificates/mykey.key --ssl-certfile ~/certificates/mycert.crt" - log_file="$PWD/logfile_sophia_vllm_$(model_name)_$(hostname).log" - - # Initialize retry counter for the model - retry_counter_model_1=0 - - # Start the model - # Loop to start models and restart if any model fails - while true; do - echo "Starting models sequence..." - # Start first model - if ! start_model "$model_name_1" "$model_command_1" "$logfile_1" retry_counter_model_1; then - continue # Restart from the beginning if this fails - fi - echo "All models started successfully." - break # Exit the loop if all models start successfully - done -allowed_functions: - - 3073ed77-6a17-4e85-826a-e1dca5309e01 \ No newline at end of file diff --git a/compute-functions/llamacpp_register_function.py b/compute-functions/llamacpp_register_function.py deleted file mode 100644 index 1691cd1f..00000000 --- a/compute-functions/llamacpp_register_function.py +++ /dev/null @@ -1,55 +0,0 @@ -# Import packages -import globus_compute_sdk - -# Define Globus Compute function -def llamacpp_inference (parameters): - import socket - import json - import os - import time - import requests - - # Determine the hostname - hostname = socket.gethostname() - os.environ['no_proxy'] = "localhost,{hostname}".format(hostname=hostname) - # Construct the base_url - base_url = f"http://localhost:8080/completion" - # Get the API key from environment variable - api_key = os.getenv("OPENAI_API_KEY") - if not api_key: - api_key="random_api_key" - - start_time = time.time() - print("parameters",parameters) - response = requests.post(base_url, data=json.dumps(parameters['model_params'])) - end_time = time.time() - response_time = end_time - start_time - - # Convert the response to a JSON-formatted string - json_response = response.json() - json_response['response_time'] = response_time - - # Print the JSON response for debugging - print(json_response) - return json_response - -# Creating Globus Compute client -gcc = globus_compute_sdk.Client() - -# Register the function -COMPUTE_FUNCTION_ID = gcc.register_function(llamacpp_inference) - -# Write function UUID in a file -uuid_file_name = "llamacpp_function_uuid.txt" -with open(uuid_file_name, "w") as file: - file.write(COMPUTE_FUNCTION_ID) - file.write("\n") -file.close() - -# End of script -print("Function registered with UUID -", COMPUTE_FUNCTION_ID) -print("The UUID is stored in " + uuid_file_name + ".") -print("") - -#Test function -#llamacpp_inference({'model_params': {'model': 'meta-llama3-70b-instruct', 'temperature': 0.2, 'max_tokens': 150, 'prompt': 'List all proteins that interact with RAD51'}}) \ No newline at end of file diff --git a/compute-functions/vllm_register_function.py b/compute-functions/vllm_register_function.py deleted file mode 100644 index 7e55a565..00000000 --- a/compute-functions/vllm_register_function.py +++ /dev/null @@ -1,177 +0,0 @@ -import globus_compute_sdk -import random - -def vllm_inference_function(parameters): - import socket - import os - import time - import requests - import json - from requests.exceptions import RequestException - - try: - # Determine the hostname - hostname = socket.gethostname() - os.environ['no_proxy'] = f"localhost,{hostname},127.0.0.1" - - # Get the API key from environment variable - api_key = os.getenv("OPENAI_API_KEY", "random_api_key") - - # Prepare headers - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {api_key}" - } - - print("parameters", parameters) - - # Determine the endpoint based on the URL parameter - openai_endpoint = parameters['model_params'].pop('openai_endpoint') - - # Determine the port based on the URL parameter - api_port = parameters['model_params'].pop('api_port') - - base_url = f"https://127.0.0.1:{api_port}/v1/" - url = base_url + openai_endpoint - - # Prepare the payload - payload = parameters['model_params'].copy() - - start_time = time.time() - - # Make the POST request - response = requests.post(url, headers=headers, json=payload, verify=False) - - end_time = time.time() - response_time = end_time - start_time - - # Initialize metrics - metrics = { - 'response_time': response_time, - 'throughput_tokens_per_second': 0 - } - - # Handle different response scenarios - if response.status_code == 200: - try: - # Try to parse JSON response - completion = response.json() - - # Extract usage information if available - usage = completion.get('usage', {}) - total_num_tokens = usage.get('total_tokens', 0) - metrics['throughput_tokens_per_second'] = total_num_tokens / response_time if response_time > 0 else 0 - - # Return the response even if empty - output = {**completion, **metrics} - return json.dumps(output, indent=4) - except json.JSONDecodeError: - # If response is not JSON but status is 200, return the raw text - return json.dumps({ - 'completion': response.text, - **metrics - }, indent=4) - else: - # For non-200 responses, raise an exception with detailed error information - error_msg = f"API request failed with status code: {response.status_code}\n" - error_msg += f"Response text: {response.text}\n" - error_msg += f"Response headers: {dict(response.headers)}" - raise Exception(error_msg) - - except RequestException as e: - # Handle network-related errors - error_msg = f"Network error occurred: {str(e)}" - if 'start_time' in locals(): - error_msg += f"\nResponse time: {time.time() - start_time}" - raise Exception(error_msg) - except Exception as e: - # Handle any other unexpected errors - error_msg = f"Unexpected error of type {type(e).__name__}: {str(e)}" - if 'start_time' in locals(): - error_msg += f"\nResponse time: {time.time() - start_time}" - raise Exception(error_msg) - -# Creating Globus Compute client -gcc = globus_compute_sdk.Client() - -# # Register the function -COMPUTE_FUNCTION_ID = gcc.register_function(vllm_inference_function) - -# # Write function UUID in a file -uuid_file_name = "vllm_register_function_sophia_multiple_models.txt" -with open(uuid_file_name, "w") as file: - file.write(COMPUTE_FUNCTION_ID) - file.write("\n") -file.close() - -# # End of script -print("Function registered with UUID -", COMPUTE_FUNCTION_ID) -print("The UUID is stored in " + uuid_file_name + ".") -print("") - -# Example calls - -# List of sample prompts -# prompts = [ -# "Explain the concept of machine learning in simple terms.", -# "What are the main differences between Python and JavaScript?", -# "Write a short story about a robot learning to paint.", -# "Describe the process of photosynthesis.", -# "What are the key features of a good user interface design?" -# ] - - -# # # Chat completion example -# chat_out = vllm_inference_function({ -# 'model_params': { -# 'openai_endpoint': 'chat/completions', -# 'model': 'meta-llama/Meta-Llama-3-8B-Instruct', -# 'api_port': 8001, -# "messages": [{"role": "user", "content": random.choice(prompts)}], -# 'logprobs': True -# } -# }) -# print("Chat Completion Output for meta-llama/Meta-Llama-3-8B-Instruct") -# print(chat_out) - - - -# # # Chat completion example -# chat_out = vllm_inference_function({ -# 'model_params': { -# 'openai_endpoint': 'chat/completions', -# 'model': 'meta-llama/Meta-Llama-3-70B-Instruct', -# 'api_port': 8000, -# "messages": [{"role": "user", "content": random.choice(prompts)}], -# 'logprobs': True -# } -# }) -# print("Chat Completion Output for meta-llama/Meta-Llama-3-70B-Instruct") -# print(chat_out) - -# # # Chat completion example -# chat_out = vllm_inference_function({ -# 'model_params': { -# 'openai_endpoint': 'chat/completions', -# 'model': 'mistralai/Mistral-7B-Instruct-v0.3', -# 'api_port': 8002, -# "messages": [{"role": "user", "content": random.choice(prompts)}], -# 'logprobs': True -# } -# }) -# print("Chat Completion Output for meta-llama/Meta-Llama-3-8B-Instruct") -# print(chat_out) - -# # Text completion example -# text_out = vllm_inference_function({ -# 'model_params': { -# 'openai_endpoint': 'completions', -# 'model': 'meta-llama/Meta-Llama-3-8B-Instruct', -# 'temperature': 0.2, -# 'max_tokens': 150, -# 'prompt': "List all proteins that interact with RAD51", -# 'logprobs': True -# } -# }) -# print("\nText Completion Output:") -# print(text_out) \ No newline at end of file diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 00000000..317572dd --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,199 @@ +# GitHub Pages Deployment Instructions + +This guide explains how to enable and deploy the documentation to GitHub Pages. + +## Prerequisites + +- Admin access to the GitHub repository +- Documentation changes committed to the repository + +## Step 1: Enable GitHub Pages + +1. Go to your GitHub repository: `https://github.com/auroraGPT-ANL/inference-gateway` +2. Click on **Settings** (top navigation bar) +3. In the left sidebar, click on **Pages** (under "Code and automation") +4. Under **Source**, select: + - Source: `GitHub Actions` + +That's it! The workflow is already configured in `.github/workflows/deploy-docs.yml`. + +## Step 2: Push to Main Branch + +The documentation will automatically deploy when you push changes to the `main` branch: + +```bash +git add . +git commit -m "docs: reorganize documentation structure" +git push origin main +``` + +The workflow will trigger automatically when: +- Files in `docs/` directory change +- `README.md` changes +- The workflow file itself changes + +## Step 3: View Your Documentation + +After the workflow completes (usually 1-2 minutes): + +1. Go to **Settings** → **Pages** +2. You'll see a message: "Your site is live at `https://auroragpt-anl.github.io/inference-gateway/`" +3. Click the link to view your documentation + +## Manual Deployment + +You can also manually trigger the deployment: + +1. Go to **Actions** tab in your repository +2. Click on "Deploy Documentation" workflow +3. Click **Run workflow** button +4. Select the `main` branch +5. Click **Run workflow** + +## Workflow Details + +The workflow (`.github/workflows/deploy-docs.yml`) does the following: + +1. **Triggers on**: + - Push to `main` branch (when docs files change) + - Manual workflow dispatch + +2. **Build Process**: + - Checks out the repository + - Copies documentation files to `_site` directory + - Creates an HTML index page with navigation + - Converts Markdown files to HTML using client-side rendering + - Applies GitHub Markdown CSS for consistent styling + +3. **Deployment**: + - Uploads the built site as an artifact + - Deploys to GitHub Pages + +## Customization + +### Change Branch + +To deploy from a different branch, edit `.github/workflows/deploy-docs.yml`: + +```yaml +on: + push: + branches: + - main # Change this to your preferred branch +``` + +### Add Custom Domain + +1. Go to **Settings** → **Pages** +2. Under **Custom domain**, enter your domain (e.g., `docs.example.com`) +3. Add a `CNAME` file in the `docs/` directory with your domain: + +```bash +echo "docs.example.com" > docs/CNAME +``` + +### Use a Documentation Framework + +For more advanced features, consider using: + +- **MkDocs**: Material theme, search, versioning +- **Sphinx**: API documentation, multiple formats +- **Jekyll**: Native GitHub Pages support, extensive themes +- **Docusaurus**: Modern React-based, built by Facebook + +Example with MkDocs: + +```yaml +# Add to .github/workflows/deploy-docs.yml +- name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + +- name: Install dependencies + run: | + pip install mkdocs-material + +- name: Build with MkDocs + run: mkdocs build +``` + +## Monitoring Deployments + +### Check Deployment Status + +1. Go to **Actions** tab +2. Click on the latest "Deploy Documentation" run +3. View logs to troubleshoot issues + +### Common Issues + +**404 Error**: +- Ensure GitHub Pages is enabled in Settings +- Check that the workflow completed successfully +- Verify the source is set to "GitHub Actions" + +**Changes Not Appearing**: +- Clear browser cache (Ctrl+Shift+R or Cmd+Shift+R) +- Wait a few minutes for CDN propagation +- Check the Actions tab for failed workflows + +**Workflow Not Triggering**: +- Ensure the changed files match the `paths` filter in the workflow +- Check branch protection rules aren't blocking the workflow +- Verify GitHub Actions is enabled for your repository + +## Viewing Deployment History + +All deployments are tracked: + +1. Go to **Settings** → **Pages** +2. Scroll down to see deployment history +3. Each deployment shows commit SHA and timestamp + +## Rolling Back + +To roll back to a previous version: + +1. Find the commit with the working documentation +2. Create a new commit that reverts changes: + +```bash +git revert +git push origin main +``` + +Or checkout the old version: + +```bash +git checkout -- docs/ +git commit -m "docs: rollback documentation" +git push origin main +``` + +## Security + +The workflow uses: +- `permissions`: Limited to only what's needed +- `id-token: write`: For GitHub Pages deployment +- `contents: read`: For reading repository files + +No secrets are required for basic deployment. + +## Next Steps + +After deploying: + +1. Update README badges with documentation link +2. Add documentation link to repository description +3. Share documentation URL with users +4. Set up monitoring for broken links +5. Consider adding a search feature (available with MkDocs Material) + +## Support + +For issues with GitHub Pages deployment: +- [GitHub Pages Documentation](https://docs.github.com/en/pages) +- [GitHub Actions Documentation](https://docs.github.com/en/actions) +- [Repository Issues](https://github.com/auroraGPT-ANL/inference-gateway/issues) + diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..4233109a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,18 @@ +# FIRST Inference Gateway Documentation + +Welcome to the FIRST (Federated Inference Resource Scheduling Toolkit) documentation. + +## Quick Links + +### For Administrators +- [Docker Quickstart](admin-guide/docker-quickstart.md) - Get started in minutes +- [Complete Setup Guide](admin-guide/setup.md) - Full installation and configuration + +### For Users +- [Usage Guide](usage-guide.md) - Authentication and making requests + +### Additional Resources +- [Main README](../README.md) +- [GitHub Repository](https://github.com/auroraGPT-ANL/inference-gateway) +- [Example Deployment](https://github.com/argonne-lcf/inference-endpoints) + diff --git a/docs/admin-guide/docker-quickstart.md b/docs/admin-guide/docker-quickstart.md new file mode 100644 index 00000000..9b384c37 --- /dev/null +++ b/docs/admin-guide/docker-quickstart.md @@ -0,0 +1,246 @@ +# Docker Quickstart + +This guide spins up the Inference Gateway, PostgreSQL, Redis, and an Nginx reverse proxy with a single `docker compose` command. It is designed for administrators who want a lean, reproducible environment to validate credentials, database connectivity, and backend routing before moving on to Kubernetes or bare-metal deployments. + +## Prerequisites + +- Docker Desktop 4.29+ (or Docker Engine 24+) with Docker Compose v2 +- Globus Service API application (client ID/secret) +- Globus Service Account application for Globus Compute (client ID/secret) +- Access to at least one backend (OpenAI-compatible API or Globus Compute endpoint) +- macOS or Linux host; Windows WSL2 works but paths below assume POSIX shells + +## What the Compose Stack Provides + +The repository now keeps container assets under `deploy/docker/`: + +``` +deploy/ + docker/ + Dockerfile # Builds the Django API image + nginx.conf # Minimal reverse proxy config + .env.example # Starter environment variables + fixtures/ + local_vllm_endpoint.json + federated_example.json +``` + +Only the required core services run: + +- `inference-gateway` (Gunicorn + Django) +- `postgres` +- `redis` +- `nginx` + +Monitoring add-ons such as Prometheus, Grafana, and exporters were removed to keep the footprint minimal. Bring your own observability stack when you deploy to production. + +## 1. Clone and switch into the repository + +```bash +git clone https://github.com/auroraGPT-ANL/inference-gateway.git +cd inference-gateway +``` + +## 2. Create your `.env` + +Copy the starter file and edit it with real credentials: + +```bash +cp deploy/docker/.env.example .env +``` + +Update at least the following keys: + +- `SECRET_KEY`: generate with `python -c 'import secrets; print(secrets.token_urlsafe(50))'` +- `GLOBUS_APPLICATION_ID` / `GLOBUS_APPLICATION_SECRET` +- `POLARIS_ENDPOINT_ID` / `POLARIS_ENDPOINT_SECRET` +- `INTERNAL_STREAMING_SECRET`: secret shared with your streaming workers +- `METIS_STATUS_URL` / `METIS_API_TOKENS` if you plan to hit a direct API + +> ⚠️ The application always enforces Globus access tokens. Even in Docker you must authenticate through Globus to call the gateway. + +## 3. Prepare writable bind mounts + +```bash +mkdir -p logs staticfiles +``` + +`logs` stores Gunicorn output from inside the container. `staticfiles` is populated when you run `collectstatic` later. + +## 4. Start the core services + +```bash +docker compose up -d --build +``` + +The first build can take several minutes because Poetry installs all dependencies inside the image. + +## 5. Run database migrations + +```bash +docker compose run --rm inference-gateway python manage.py migrate +``` + +You can also create an admin account for the Django admin (optional but handy for inspections): + +```bash +docker compose run --rm inference-gateway python manage.py createsuperuser +``` + +## 6. Collect static assets (optional but recommended for the dashboard) + +```bash +docker compose run --rm inference-gateway python manage.py collectstatic --noinput +``` + +Nginx already mounts `./staticfiles`, so collected assets become available immediately. + +## 7. Verify the stack + +- Gateway API: http://localhost:8000/ +- Django admin: http://localhost:8000/admin/ +- Logs: `tail -f logs/backend_gateway.error.log` + +Once the API is up, configure backends so the router knows where to send inference requests. + +--- + +## Connecting Backends + +The gateway is agnostic to how models are served. This section shows two common options. + +### Option A — Direct OpenAI-Compatible API + +Use this path when you already operate an HTTPS endpoint that mimics the OpenAI REST contract (official OpenAI, Anthropic-compatible adapters, custom deployments, etc.). The gateway treats these as the `metis` cluster, which relies on a status manifest and per-endpoint API tokens. + +1. **Host a status manifest** reachable over HTTP(S). A minimal example: + + ```json + { + "openai-gateway": { + "status": "Live", + "model": "OpenAI Pass-through", + "description": "Routes to OpenAI's GPT models", + "experts": ["openai/gpt-4o-mini"], + "url": "https://api.openai.com/v1", + "endpoint_id": "openai-production" + } + } + ``` + + Place it somewhere the Docker container can reach. For local testing you can run: + + ```bash + mkdir -p deploy/docker/examples + cat > deploy/docker/examples/metis-status.json <<'JSON' + { + "openai-gateway": { + "status": "Live", + "model": "OpenAI Pass-through", + "description": "Routes to OpenAI's GPT models", + "experts": ["openai/gpt-4o-mini"], + "url": "https://api.openai.com/v1", + "endpoint_id": "openai-production" + } + } + JSON + python -m http.server 8055 --directory deploy/docker/examples + ``` + + Then set `METIS_STATUS_URL=http://host.docker.internal:8055/metis-status.json` in `.env` (Docker Desktop exposes the host using `host.docker.internal`). + +2. **Provide the API token** in `.env` using the manifest `endpoint_id` as the key: + + ```dotenv + METIS_API_TOKENS={"openai-production": "sk-your-openai-key"} + ``` + +3. Restart the API container to pick up the new environment: + + ```bash + docker compose up -d inference-gateway + ``` + +4. Call the gateway with a Globus access token: + + ```bash + curl -X POST \ + http://localhost:8000/resource_server/metis/api/v1/chat/completions \ + -H "Authorization: Bearer $MY_GLOBUS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "openai/gpt-4o-mini", + "messages": [{"role": "user", "content": "Hello from Docker"}], + "stream": false + }' + ``` + +The gateway fetches the manifest, validates the model is Live, injects your API key, and forwards the OpenAI payload for you. + +### Option B — Globus Compute + vLLM + +Use this path when the model executes on an HPC cluster or GPU node exposed through Globus Compute. You need three artefacts: registered Globus function UUID(s), the endpoint UUID, and a Django fixture describing the target. + +1. **Register the inference function** from your compute node: + + ```bash + # On the machine that owns the Globus Compute endpoint + cd compute-functions + poetry run python vllm_register_function.py + ``` + + Capture the printed function UUID. Repeat for the status or batch functions if required. + +2. **Start (or restart) your Globus Compute endpoint** and note the endpoint UUID: + + ```bash + globus-compute-endpoint start my-vllm-endpoint + globus-compute-endpoint list # to see the endpoint UUID + ``` + +3. **Create a minimal endpoint fixture** using the template shipped in the repo: + + ```bash + cp deploy/docker/fixtures/local_vllm_endpoint.json fixtures/endpoints.json + ``` + + Edit `fixtures/endpoints.json` and replace: + + - `replace-with-endpoint-uuid` + - `replace-with-function-uuid` + - Update the `cluster` name if you do not want to use `local` + - Set `api_port` to the port exposed by your inference server + +4. **Load the fixture into the running containers:** + + ```bash + docker compose run --rm inference-gateway python manage.py loaddata fixtures/endpoints.json + ``` + + Repeat the process for `fixtures/federated_endpoints.json` if you plan to expose federated routing; the starter file lives at `deploy/docker/fixtures/federated_example.json`. + +5. **Test the endpoint**: + + ```bash + curl -X POST \ + http://localhost:8000/resource_server/local/vllm/v1/chat/completions \ + -H "Authorization: Bearer $MY_GLOBUS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "meta-llama/Meta-Llama-3-8B-Instruct", + "messages": [{"role": "user", "content": "Status check"}], + "stream": false + }' + ``` + +If the endpoint is online and your Globus token has the required group membership, the gateway streams responses back in OpenAI format. + +--- + +## Housekeeping + +- **Restart services** after environment or fixture changes: `docker compose up -d inference-gateway nginx` +- **Shut everything down** when you are done: `docker compose down` +- **Clean database state** during testing: `docker compose down -v` + +You now have a lean Docker deployment that mirrors production authentication and routing behaviour without extra monitoring dependencies. Continue to the Kubernetes or bare-metal guides once you are comfortable with the workflow. diff --git a/docs/admin-guide/setup.md b/docs/admin-guide/setup.md new file mode 100644 index 00000000..2a042473 --- /dev/null +++ b/docs/admin-guide/setup.md @@ -0,0 +1,454 @@ +# Administrator Setup Guide + +This guide covers the complete setup process for deploying the FIRST Inference Gateway. + +## Prerequisites + +- Python 3.11+ +- PostgreSQL Server (included in the Docker deployment) +- Poetry +- Docker and Docker Compose (Recommended for Gateway deployment) +- Globus Account +- Access to a compute resource (HPC cluster or a local machine with sufficient resources for the chosen inference server and models) + +## Setup Overview + +The setup involves two main parts: +1. **Gateway Setup**: Installing and configuring the central API gateway service. +2. **Inference Backend Setup**: Setting up the inference server (like vLLM) and the Globus Compute components on the machine(s) where models will run. + +These parts can be done in parallel, but configuration details from each are needed to link them. + +--- + +## Part 1: Gateway Setup + +> 📝 Looking for a shorter, task-oriented walkthrough? See the [Docker Quickstart](docker-quickstart.md). + +### Step 1: Clone the Repository + +```bash +git clone https://github.com/auroraGPT-ANL/inference-gateway.git +cd inference-gateway +``` + +### Step 2: Choose Deployment Method + +**Option A: Docker Deployment (Recommended)** + +```bash +# Create necessary directories +mkdir -p logs prometheus + +# Configuration is done via the .env file (see next steps) +``` + +See [Starting the Services](#starting-the-services) for how to run this after configuration. + +**Option B: Bare Metal Setup** + +```bash +# Set up Python environment with Poetry +poetry config virtualenvs.in-project true +poetry env use python3.11 +poetry install + +# Activate the environment +poetry shell + +# Ensure PostgreSQL server is running and accessible +``` + +### Step 3: Register Globus Applications + +#### Service API Application + +The Gateway needs to be registered as a Globus Service API application: + +1. Visit [developers.globus.org](https://app.globus.org/settings/developers) and sign in. +2. Under **Register an...**, click on **Register a service API ...**. +3. Complete the registration form: + - Set **App Name** (e.g., "My Inference Gateway"). + - Add **Redirect URIs**: + - Local dev: `http://localhost:8000/complete/globus/` + - Production: `https:///complete/globus/` + - Set **Privacy Policy** and **Terms & Conditions** URLs if applicable. +4. After registration, note the **Client UUID** and generate a **Client Secret**. + +#### Add Globus Scope + +Export your API client credentials: + +```bash +export CLIENT_ID="" +export CLIENT_SECRET="" +``` + +Create the scope: + +```bash +curl -X POST -s --user $CLIENT_ID:$CLIENT_SECRET \ + https://auth.globus.org/v2/api/clients/$CLIENT_ID/scopes \ + -H "Content-Type: application/json" \ + -d '{ + "scope": { + "name": "Action Provider - all", + "description": "Access to inference service.", + "scope_suffix": "action_all", + "dependent_scopes": [ + { + "scope": "73320ffe-4cb4-4b25-a0a3-83d53d59ce4f", + "optional": false, + "requires_refresh_token": true + } + ] + } + }' +``` + +Verify the scope was created: + +```bash +curl -s --user $CLIENT_ID:$CLIENT_SECRET https://auth.globus.org/v2/api/clients/$CLIENT_ID +``` + +#### Service Account Application + +Create a Globus Service Account application for Globus Compute endpoints: + +1. Visit [developers.globus.org](https://app.globus.org/settings/developers). +2. Click on **Add an App** under your project. +3. Select **Register a service account ...**. +4. Complete the registration form. +5. Note the **Client UUID** and generate a **Client Secret**. + +### Step 4: Configure Environment + +Create a `.env` file in the project root: + +```dotenv +# --- Core Django Settings --- +SECRET_KEY="" +DEBUG=True # Set to False for production +ALLOWED_HOSTS="localhost,127.0.0.1" + +# --- Globus Credentials --- +GLOBUS_APPLICATION_ID="" +GLOBUS_APPLICATION_SECRET="" +POLARIS_ENDPOINT_ID="" +POLARIS_ENDPOINT_SECRET="" + +# --- Database Credentials --- +POSTGRES_DB="inferencegateway" +POSTGRES_USER="inferencedev" +POSTGRES_PASSWORD="inferencedevpwd" # CHANGE THIS for production +PGHOST="postgres" # Use "postgres" for Docker, "localhost" for bare-metal +PGPORT=5432 +PGUSER="dataportaldev" +PGPASSWORD="inferencedevpwd" +PGDATABASE="inferencegateway" + +# --- Redis --- +REDIS_URL="redis://redis:6379/0" # Use "redis" for Docker + +# --- Gateway Specific Settings --- +MAX_BATCHES_PER_USER=2 +STREAMING_SERVER_HOST="localhost:8080" +INTERNAL_STREAMING_SECRET="your-internal-streaming-secret-key" +CLI_AUTH_CLIENT_ID="58fdd3bc-e1c3-4ce5-80ea-8d6b87cfb944" +``` + +Generate a strong secret key: + +```bash +python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' +``` + +### Step 5: Initialize Database + +**Docker:** + +```bash +docker-compose up -d +docker-compose exec inference-gateway python manage.py makemigrations +docker-compose exec inference-gateway python manage.py migrate +``` + +**Bare Metal:** + +```bash +python manage.py makemigrations +python manage.py migrate +``` + +--- + +## Part 2: Inference Backend Setup + +This section covers setting up the components on the machine where AI models will run. + +### Step 1: Create Python Virtual Environment + +Use the same Python version as the Gateway API (Python 3.11): + +```bash +# Example with conda +conda create -n vllm-env python=3.11 -y +conda activate vllm-env + +# Example with python venv +python3.11 -m venv vllm-env +source vllm-env/bin/activate +``` + +### Step 2: Install Inference Server and Globus Compute + +Install vLLM: + +```bash +git clone https://github.com/vllm-project/vllm.git +cd vllm +pip install -e . +``` + +Install Globus Compute: + +```bash +pip install globus-compute-sdk globus-compute-endpoint +``` + +### Step 3: Register Globus Compute Functions + +Export your Service Account credentials: + +```bash +export GLOBUS_COMPUTE_CLIENT_ID="" +export GLOBUS_COMPUTE_CLIENT_SECRET="" +``` + +Register the inference function: + +```bash +cd path/to/inference-gateway/compute-functions + +# Register vLLM inference function +python vllm_register_function_with_streaming.py +# Note the Function UUID + +# Register status function (optional but recommended) +python qstat_register_function.py +# Note the Function UUID +``` + +### Step 4: Configure Globus Compute Endpoint + +Create and configure an endpoint: + +```bash +globus-compute-endpoint configure my-compute-endpoint +``` + +Edit the generated `config.yaml` file: +- Add your function UUIDs to `allowed_functions` +- Configure `worker_init` to activate your environment +- Adjust executor settings for your hardware + +See [local-vllm-endpoint.yaml](../../compute-endpoints/local-vllm-endpoint.yaml) for an example configuration. + +Start the endpoint: + +```bash +globus-compute-endpoint start my-compute-endpoint +# Note the Endpoint UUID +``` + +--- + +## Part 3: Connect Gateway and Backend + +### Step 1: Update Fixtures + +Edit `fixtures/endpoints.json` or `fixtures/federated_endpoints.json`: + +**Non-federated example (endpoints.json):** + +```json +[ + { + "model": "resource_server.endpoint", + "pk": 1, + "fields": { + "endpoint_slug": "local-vllm-facebook-opt-125m", + "cluster": "local", + "framework": "vllm", + "model": "facebook/opt-125m", + "api_port": 8001, + "endpoint_uuid": "", + "function_uuid": "", + "batch_endpoint_uuid": "", + "batch_function_uuid": "", + "allowed_globus_groups": "" + } + } +] +``` + +**Federated example (federated_endpoints.json):** + +```json +[ + { + "model": "resource_server.federatedendpoint", + "pk": 1, + "fields": { + "name": "OPT 125M (Federated)", + "slug": "federated-opt-125m", + "target_model_name": "facebook/opt-125m", + "description": "Federated access point for the facebook/opt-125m model.", + "targets": [ + { + "cluster": "local", + "framework": "vllm", + "model": "facebook/opt-125m", + "endpoint_slug": "local-vllm-facebook-opt-125m", + "endpoint_uuid": "", + "function_uuid": "", + "api_port": 8001 + } + ] + } + } +] +``` + +### Step 2: Load Fixtures + +**Docker:** + +```bash +docker-compose exec inference-gateway python manage.py loaddata fixtures/endpoints.json +``` + +**Bare Metal:** + +```bash +python manage.py loaddata fixtures/endpoints.json +``` + +--- + +## Starting the Services + +### Gateway + +**Docker:** + +```bash +docker-compose up --build -d +``` + +**Bare Metal (Development):** + +```bash +poetry shell +python manage.py runserver 0.0.0.0:8000 +``` + +**Bare Metal (Production with Gunicorn):** + +```bash +poetry shell +poetry run gunicorn \ + inference_gateway.asgi:application \ + -k uvicorn.workers.UvicornWorker \ + -b 0.0.0.0:8000 \ + --workers 5 \ + --log-level info +``` + +### Inference Backend + +Ensure the Globus Compute endpoint is running: + +```bash +globus-compute-endpoint start +``` + +--- + +## Production Considerations + +For production deployments, use Nginx as a reverse proxy: + +### Example Nginx Configuration + +```nginx +upstream app_server { + server 127.0.0.1:8000 fail_timeout=0; +} + +server { + listen 80; + server_name your-gateway-domain.org; + + client_max_body_size 4G; + + location /static/ { + alias /path/to/inference-gateway/static/; + } + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://app_server; + } +} +``` + +Remember to: +- Collect static files: `python manage.py collectstatic` +- Configure SSL/TLS certificates +- Set up rate limiting and security headers +- Configure firewall rules + +--- + +## Monitoring + +If deployed via Docker Compose with monitoring enabled: + +- **Grafana**: http://localhost:3000 (admin/admin) +- **Prometheus**: http://localhost:9090 + +The dashboard includes: +- Application metrics (request rates, latency, error rates) +- System metrics (CPU, memory, disk I/O) +- Database metrics (connection counts, query performance) +- Custom Gateway metrics (inference rates, token counts) + +--- + +## Troubleshooting + +**Database Connection Errors** +- Check `.env` variables match your deployment context +- Verify PostgreSQL is running and accessible +- Check firewall rules and `pg_hba.conf` + +**Globus Auth Errors** +- Ensure Redirect URIs match in Globus Developer portal +- Verify credentials in `.env` are correct +- Check scope was created successfully + +**Compute Endpoint Issues** +- Check endpoint logs: `~/.globus_compute//endpoint.log` +- Verify function UUIDs in `config.yaml` +- Ensure environment activation works correctly + +**500 Server Errors** +- Check gateway logs: `docker-compose logs inference-gateway` +- Look for Python tracebacks in Gunicorn logs +- Verify all required environment variables are set + diff --git a/docs/usage-guide.md b/docs/usage-guide.md new file mode 100644 index 00000000..6772f4a7 --- /dev/null +++ b/docs/usage-guide.md @@ -0,0 +1,347 @@ +# Usage Guide + +This guide explains how to use the FIRST Inference Gateway to access Large Language Models through an OpenAI-compatible API. + +## Authentication + +The Gateway uses Globus Auth for authentication. You need a valid access token to make requests. + +### Getting Your Access Token + +Use the provided authentication script: + +#### First-Time Setup + +```bash +# Authenticate (opens browser for Globus login) +python inference-auth-token.py authenticate +``` + +This stores refresh and access tokens locally (typically in `~/.globus/app/...`). + +#### Getting an Access Token + +```bash +# Retrieve your current valid access token +export MY_TOKEN=$(python inference-auth-token.py get_access_token) +echo "Token stored in MY_TOKEN environment variable." +``` + +The script automatically refreshes expired tokens using your stored refresh token. + +#### Force Re-authentication + +If you need to change accounts or encounter permission errors: + +```bash +# Log out from Globus +# Visit: https://app.globus.org/logout + +# Force re-authentication +python inference-auth-token.py authenticate --force +``` + +#### Token Validity + +- **Access tokens**: Valid for 48 hours +- **Refresh tokens**: Valid for 6 months of inactivity +- Some institutions may enforce more frequent re-authentication (e.g., weekly) + +--- + +## Making Inference Requests + +The Gateway provides an OpenAI-compatible API with two main endpoints: + +### Chat Completions Endpoint + +For conversational interactions: + +**Federated endpoint (routes across multiple backends):** + +```bash +curl -X POST http://127.0.0.1:8000/resource_server/v1/chat/completions \ + -H "Authorization: Bearer $MY_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "facebook/opt-125m", + "messages": [ + {"role": "user", "content": "Explain the concept of Globus Compute in simple terms."} + ], + "max_tokens": 150 + }' +``` + +**Specific backend (targets a particular cluster/framework):** + +```bash +curl -X POST http://127.0.0.1:8000/resource_server/local/vllm/v1/chat/completions \ + -H "Authorization: Bearer $MY_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "facebook/opt-125m", + "messages": [ + {"role": "user", "content": "Explain the concept of Globus Compute in simple terms."} + ], + "max_tokens": 150 + }' +``` + +### Completions Endpoint + +For text completion: + +```bash +curl -X POST http://127.0.0.1:8000/resource_server/v1/completions \ + -H "Authorization: Bearer $MY_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "facebook/opt-125m", + "prompt": "The future of AI is", + "max_tokens": 100 + }' +``` + +### Streaming Responses + +Both endpoints support streaming. Set `stream: true` in your request: + +**Python example with streaming:** + +```python +import openai + +client = openai.OpenAI( + base_url="http://localhost:8000/resource_server/v1", + api_key=MY_TOKEN # Your Globus access token +) + +stream = client.chat.completions.create( + model="facebook/opt-125m", + messages=[{"role": "user", "content": "Explain quantum computing"}], + stream=True +) + +for chunk in stream: + if chunk.choices[0].delta.content is not None: + print(chunk.choices[0].delta.content, end="", flush=True) +``` + +### Testing Streaming with ngrok + +For testing streaming with remote endpoints, use ngrok to create a secure tunnel: + +1. **Install ngrok**: Visit [ngrok.com](https://ngrok.com/) + +2. **Start tunnel**: +```bash +ngrok http 8000 +``` + +3. **Update your test script** to use the ngrok URL: +```python +client = openai.OpenAI( + base_url="https://your-ngrok-url.ngrok.io/resource_server/v1", + api_key=access_token +) +``` + +--- + +## Using the OpenAI Python SDK + +The Gateway is fully compatible with the OpenAI Python SDK: + +```python +import openai + +# Configure client to point to the Gateway +client = openai.OpenAI( + base_url="http://localhost:8000/resource_server/v1", + api_key=MY_TOKEN # Your Globus access token +) + +# Make a request +response = client.chat.completions.create( + model="meta-llama/Meta-Llama-3-8B-Instruct", + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "What is machine learning?"} + ], + max_tokens=200 +) + +print(response.choices[0].message.content) +``` + +--- + +## Available Models + +To see available models, contact your Gateway administrator or check the deployment's model catalog. + +Common model naming conventions: +- `facebook/opt-125m`, `facebook/opt-1.3b`, etc. +- `meta-llama/Meta-Llama-3-8B-Instruct` +- `openai/gpt-4o-mini` (if configured with OpenAI backend) + +The exact models available depend on your deployment's configuration. + +--- + +## Batch Processing + +For large-scale inference workloads, the Gateway supports batch processing: + +```bash +curl -X POST http://127.0.0.1:8000/resource_server/v1/batches \ + -H "Authorization: Bearer $MY_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "input_file_path": "/path/to/input.jsonl", + "output_folder_path": "/path/to/output/", + "model": "meta-llama/Meta-Llama-3-8B-Instruct" + }' +``` + +**Input file format (JSONL):** + +```jsonl +{"messages": [{"role": "user", "content": "What is AI?"}], "max_tokens": 100} +{"messages": [{"role": "user", "content": "Explain ML"}], "max_tokens": 100} +``` + +**Check batch status:** + +```bash +curl -X GET http://127.0.0.1:8000/resource_server/v1/batches/ \ + -H "Authorization: Bearer $MY_TOKEN" +``` + +--- + +## Benchmarking + +The repository includes a benchmark script for performance testing: + +### Download Dataset + +```bash +wget https://huggingface.co/datasets/anon8231489123/ShareGPT_Vicuna_unfiltered/resolve/main/ShareGPT_V3_unfiltered_cleaned_split.json -P examples/load-testing/ +``` + +### Run Benchmark + +```bash +python examples/load-testing/benchmark-serving.py \ + --backend vllm \ + --model facebook/opt-125m \ + --base-url http://127.0.0.1:8000/resource_server/v1/chat/completions \ + --dataset-name sharegpt \ + --dataset-path examples/load-testing/ShareGPT_V3_unfiltered_cleaned_split.json \ + --output-file benchmark_results.jsonl \ + --num-prompts 100 +``` + +**Additional options:** +- `--request-rate`: Control request rate (requests per second) +- `--max-concurrency`: Limit concurrent requests +- `--disable-ssl-verification`: For testing with self-signed certificates +- `--disable-stream`: Test non-streaming mode + +--- + +## Request Parameters + +### Common Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `model` | string | Model identifier (required) | +| `messages` | array | List of message objects (chat endpoint) | +| `prompt` | string | Text prompt (completions endpoint) | +| `max_tokens` | integer | Maximum tokens to generate | +| `temperature` | float | Sampling temperature (0.0-2.0) | +| `top_p` | float | Nucleus sampling parameter | +| `stream` | boolean | Enable streaming responses | +| `stop` | string/array | Stop sequences | + +### Example with Advanced Parameters + +```bash +curl -X POST http://127.0.0.1:8000/resource_server/v1/chat/completions \ + -H "Authorization: Bearer $MY_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "meta-llama/Meta-Llama-3-8B-Instruct", + "messages": [{"role": "user", "content": "Write a story"}], + "max_tokens": 500, + "temperature": 0.7, + "top_p": 0.9, + "stream": false, + "stop": ["\n\n"] + }' +``` + +--- + +## Error Handling + +The Gateway returns standard HTTP status codes: + +| Code | Meaning | +|------|---------| +| 200 | Success | +| 400 | Bad Request (invalid parameters) | +| 401 | Unauthorized (invalid or missing token) | +| 403 | Forbidden (insufficient permissions) | +| 404 | Not Found (model or endpoint not available) | +| 429 | Too Many Requests (rate limited) | +| 500 | Internal Server Error | +| 503 | Service Unavailable (backend offline) | + +**Error response format:** + +```json +{ + "error": { + "message": "Model not found", + "type": "invalid_request_error", + "code": "model_not_found" + } +} +``` + +--- + +## Best Practices + +1. **Token Management** + - Store tokens securely + - Refresh tokens before they expire + - Never commit tokens to version control + +2. **Request Optimization** + - Use appropriate `max_tokens` values + - Implement retry logic with exponential backoff + - Use streaming for long responses + +3. **Rate Limiting** + - Respect rate limits + - Implement client-side throttling + - Use batch processing for bulk operations + +4. **Error Handling** + - Handle network errors gracefully + - Implement timeout logic + - Log errors for debugging + +--- + +## Support + +For issues, questions, or feature requests: +- Check the [GitHub repository](https://github.com/auroraGPT-ANL/inference-gateway) +- Contact your Gateway administrator +- Review the [Administrator Setup Guide](admin-guide/setup.md) for deployment issues + From ed642dec204a4d320128888fb81d30a20d7886a4 Mon Sep 17 00:00:00 2001 From: Aditya Tanikanti Date: Wed, 12 Nov 2025 13:31:39 -0600 Subject: [PATCH 03/22] Added mkdocs and deploy folders for easy installation --- .dockerignore | 64 + .github/workflows/deploy-docs.yml | 190 +- Dockerfile | 49 - README.md | 18 +- deploy/docker/Dockerfile | 45 + deploy/docker/README.md | 127 + .../docker/docker-compose.yml | 9 +- deploy/docker/env.example | 55 + deploy/docker/nginx.conf | 40 + deploy/kubernetes/README.md | 23 + docs/CONTRIBUTING.md | 214 + docs/DEPLOYMENT.md | 199 - docs/README.md | 18 - docs/admin-guide/deployment/kubernetes.md | 65 + docs/admin-guide/deployment/production.md | 535 ++ docs/admin-guide/docker-quickstart.md | 246 - docs/admin-guide/gateway-setup/bare-metal.md | 477 ++ .../gateway-setup/configuration.md | 404 + docs/admin-guide/gateway-setup/docker.md | 344 + docs/admin-guide/index.md | 148 + .../admin-guide/inference-setup/direct-api.md | 383 + .../inference-setup/globus-compute.md | 622 ++ docs/admin-guide/inference-setup/index.md | 223 + .../admin-guide/inference-setup/local-vllm.md | 471 ++ docs/admin-guide/monitoring.md | 330 + docs/admin-guide/setup.md | 454 -- docs/first_architecture.png | Bin 0 -> 417097 bytes docs/index.md | 64 + docs/reference/api.md | 47 + docs/reference/citation.md | 39 + docs/reference/config.md | 4 + docs/{usage-guide.md => user-guide/index.md} | 2 +- inference_gateway/apps.py | 2 +- inference_gateway/settings.py | 4 +- logging_config.py | 96 +- mkdocs.yml | 97 + pyproject.toml | 3 + requirements.txt | 30 + site/404.html | 1 + site/CONTRIBUTING/index.html | 67 + .../deployment/kubernetes/index.html | 1 + .../deployment/production/index.html | 242 + .../gateway-setup/bare-metal/index.html | 234 + .../gateway-setup/configuration/index.html | 174 + .../gateway-setup/docker/index.html | 92 + site/admin-guide/index.html | 12 + .../inference-setup/direct-api/index.html | 145 + .../inference-setup/globus-compute/index.html | 262 + site/admin-guide/inference-setup/index.html | 40 + .../inference-setup/local-vllm/index.html | 234 + site/admin-guide/monitoring/index.html | 133 + site/assets/images/favicon.png | Bin 0 -> 1870 bytes .../assets/javascripts/bundle.e71a0d61.min.js | 16 + .../javascripts/bundle.e71a0d61.min.js.map | 7 + .../javascripts/lunr/min/lunr.ar.min.js | 1 + .../javascripts/lunr/min/lunr.da.min.js | 18 + .../javascripts/lunr/min/lunr.de.min.js | 18 + .../javascripts/lunr/min/lunr.du.min.js | 18 + .../javascripts/lunr/min/lunr.el.min.js | 1 + .../javascripts/lunr/min/lunr.es.min.js | 18 + .../javascripts/lunr/min/lunr.fi.min.js | 18 + .../javascripts/lunr/min/lunr.fr.min.js | 18 + .../javascripts/lunr/min/lunr.he.min.js | 1 + .../javascripts/lunr/min/lunr.hi.min.js | 1 + .../javascripts/lunr/min/lunr.hu.min.js | 18 + .../javascripts/lunr/min/lunr.hy.min.js | 1 + .../javascripts/lunr/min/lunr.it.min.js | 18 + .../javascripts/lunr/min/lunr.ja.min.js | 1 + .../javascripts/lunr/min/lunr.jp.min.js | 1 + .../javascripts/lunr/min/lunr.kn.min.js | 1 + .../javascripts/lunr/min/lunr.ko.min.js | 1 + .../javascripts/lunr/min/lunr.multi.min.js | 1 + .../javascripts/lunr/min/lunr.nl.min.js | 18 + .../javascripts/lunr/min/lunr.no.min.js | 18 + .../javascripts/lunr/min/lunr.pt.min.js | 18 + .../javascripts/lunr/min/lunr.ro.min.js | 18 + .../javascripts/lunr/min/lunr.ru.min.js | 18 + .../javascripts/lunr/min/lunr.sa.min.js | 1 + .../lunr/min/lunr.stemmer.support.min.js | 1 + .../javascripts/lunr/min/lunr.sv.min.js | 18 + .../javascripts/lunr/min/lunr.ta.min.js | 1 + .../javascripts/lunr/min/lunr.te.min.js | 1 + .../javascripts/lunr/min/lunr.th.min.js | 1 + .../javascripts/lunr/min/lunr.tr.min.js | 18 + .../javascripts/lunr/min/lunr.vi.min.js | 1 + .../javascripts/lunr/min/lunr.zh.min.js | 1 + site/assets/javascripts/lunr/tinyseg.js | 206 + site/assets/javascripts/lunr/wordcut.js | 6708 +++++++++++++++++ .../workers/search.7a47a382.min.js | 42 + .../workers/search.7a47a382.min.js.map | 7 + site/assets/stylesheets/main.618322db.min.css | 1 + .../stylesheets/main.618322db.min.css.map | 1 + .../stylesheets/palette.ab4e12ef.min.css | 1 + .../stylesheets/palette.ab4e12ef.min.css.map | 1 + site/first_architecture.png | Bin 0 -> 417097 bytes site/index.html | 1 + site/reference/api/index.html | 9 + site/reference/citation/index.html | 17 + site/reference/config/index.html | 1 + site/search/search_index.json | 1 + site/sitemap.xml | 71 + site/sitemap.xml.gz | Bin 0 -> 366 bytes site/user-guide/index.html | 119 + utils/globus_utils.py | 4 +- 104 files changed, 14091 insertions(+), 1187 deletions(-) delete mode 100644 Dockerfile create mode 100644 deploy/docker/Dockerfile create mode 100644 deploy/docker/README.md rename docker-compose.yml => deploy/docker/docker-compose.yml (76%) create mode 100644 deploy/docker/env.example create mode 100644 deploy/docker/nginx.conf create mode 100644 deploy/kubernetes/README.md create mode 100644 docs/CONTRIBUTING.md delete mode 100644 docs/DEPLOYMENT.md delete mode 100644 docs/README.md create mode 100644 docs/admin-guide/deployment/kubernetes.md create mode 100644 docs/admin-guide/deployment/production.md delete mode 100644 docs/admin-guide/docker-quickstart.md create mode 100644 docs/admin-guide/gateway-setup/bare-metal.md create mode 100644 docs/admin-guide/gateway-setup/configuration.md create mode 100644 docs/admin-guide/gateway-setup/docker.md create mode 100644 docs/admin-guide/index.md create mode 100644 docs/admin-guide/inference-setup/direct-api.md create mode 100644 docs/admin-guide/inference-setup/globus-compute.md create mode 100644 docs/admin-guide/inference-setup/index.md create mode 100644 docs/admin-guide/inference-setup/local-vllm.md create mode 100644 docs/admin-guide/monitoring.md delete mode 100644 docs/admin-guide/setup.md create mode 100644 docs/first_architecture.png create mode 100644 docs/index.md create mode 100644 docs/reference/api.md create mode 100644 docs/reference/citation.md create mode 100644 docs/reference/config.md rename docs/{usage-guide.md => user-guide/index.md} (98%) create mode 100644 mkdocs.yml create mode 100644 site/404.html create mode 100644 site/CONTRIBUTING/index.html create mode 100644 site/admin-guide/deployment/kubernetes/index.html create mode 100644 site/admin-guide/deployment/production/index.html create mode 100644 site/admin-guide/gateway-setup/bare-metal/index.html create mode 100644 site/admin-guide/gateway-setup/configuration/index.html create mode 100644 site/admin-guide/gateway-setup/docker/index.html create mode 100644 site/admin-guide/index.html create mode 100644 site/admin-guide/inference-setup/direct-api/index.html create mode 100644 site/admin-guide/inference-setup/globus-compute/index.html create mode 100644 site/admin-guide/inference-setup/index.html create mode 100644 site/admin-guide/inference-setup/local-vllm/index.html create mode 100644 site/admin-guide/monitoring/index.html create mode 100644 site/assets/images/favicon.png create mode 100644 site/assets/javascripts/bundle.e71a0d61.min.js create mode 100644 site/assets/javascripts/bundle.e71a0d61.min.js.map create mode 100644 site/assets/javascripts/lunr/min/lunr.ar.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.da.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.de.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.du.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.el.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.es.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.fi.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.fr.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.he.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.hi.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.hu.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.hy.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.it.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.ja.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.jp.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.kn.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.ko.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.multi.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.nl.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.no.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.pt.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.ro.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.ru.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.sa.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.sv.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.ta.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.te.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.th.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.tr.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.vi.min.js create mode 100644 site/assets/javascripts/lunr/min/lunr.zh.min.js create mode 100644 site/assets/javascripts/lunr/tinyseg.js create mode 100644 site/assets/javascripts/lunr/wordcut.js create mode 100644 site/assets/javascripts/workers/search.7a47a382.min.js create mode 100644 site/assets/javascripts/workers/search.7a47a382.min.js.map create mode 100644 site/assets/stylesheets/main.618322db.min.css create mode 100644 site/assets/stylesheets/main.618322db.min.css.map create mode 100644 site/assets/stylesheets/palette.ab4e12ef.min.css create mode 100644 site/assets/stylesheets/palette.ab4e12ef.min.css.map create mode 100644 site/first_architecture.png create mode 100644 site/index.html create mode 100644 site/reference/api/index.html create mode 100644 site/reference/citation/index.html create mode 100644 site/reference/config/index.html create mode 100644 site/search/search_index.json create mode 100644 site/sitemap.xml create mode 100644 site/sitemap.xml.gz create mode 100644 site/user-guide/index.html diff --git a/.dockerignore b/.dockerignore index 3196d2dd..5d0a434f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,67 @@ +# Environment files .env .env.* .venv/ +venv/ +env/ + +# Python cache +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# Development tools +.git/ +.gitignore +.github/ +.aider*/ +.cursor/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Documentation +docs/ +site/ +*.md +!README.md + +# Logs +logs/ +*.log + +# Database +*.db +*.sqlite3 + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# Build artifacts +build/ +dist/ +*.egg-info/ + +# Docker +docker-compose.yml +Dockerfile +.dockerignore + +# Examples and development scripts +examples/ +scripts/ + +# Static files (will be collected in container) +staticfiles/ + +# Misc +.DS_Store +*.pyc diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index f3a706cf..92d50cbb 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -6,7 +6,7 @@ on: - main paths: - 'docs/**' - - 'README.md' + - 'mkdocs.yml' - '.github/workflows/deploy-docs.yml' workflow_dispatch: @@ -26,185 +26,24 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup Pages - uses: actions/configure-pages@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' - - name: Build documentation site + - name: Install dependencies run: | - mkdir -p _site - cp -r docs/* _site/ - cp README.md _site/index.md - - # Create a simple index.html that redirects to the main docs - cat > _site/index.html << 'EOF' - - - - - - FIRST Inference Gateway Documentation - - - -

🚀 FIRST Inference Gateway Documentation

- -

Welcome to the documentation for the Federated Inference Resource Scheduling Toolkit (FIRST). FIRST enables LLM inference as a service across distributed HPC clusters through an OpenAI-compatible API.

- - - - - - - -
- -

- License: Apache 2.0 | - Maintained by: Argonne National Laboratory -

- - - EOF - - # Convert markdown files to HTML using a simple approach - # For a more sophisticated setup, you could use Jekyll, MkDocs, or Sphinx - for md_file in $(find _site -name "*.md"); do - html_file="${md_file%.md}.html" - cat > "$html_file" << 'HEADER' - - - - - - FIRST Documentation - - - - -
- - HEADER - - # Use a simple markdown renderer - in production you'd want something more robust - echo '
' >> "$html_file" - echo '' >> "$html_file" - echo '' >> "$html_file" - echo '
' >> "$html_file" - done + pip install mkdocs-material + pip install mkdocs-minify-plugin + + - name: Build MkDocs site + run: mkdocs build - name: Upload artifact uses: actions/upload-pages-artifact@v3 + with: + path: ./site deploy: environment: @@ -216,4 +55,3 @@ jobs: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 - diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 8798d340..00000000 --- a/Dockerfile +++ /dev/null @@ -1,49 +0,0 @@ -# Use Python 3.11 as base image -FROM python:3.11-slim - -# Set environment variables -ENV PYTHONUNBUFFERED=1 -ENV PYTHONDONTWRITEBYTECODE=1 -ENV POETRY_VERSION=1.7.1 - -# Set work directory -WORKDIR /app - -# Install system dependencies -RUN apt-get update && apt-get install -y \ - build-essential \ - libpq-dev \ - && rm -rf /var/lib/apt/lists/* - -# Install Poetry -RUN pip install --no-cache-dir poetry==${POETRY_VERSION} - -# Install uvicorn and gunicorn -RUN pip install --no-cache-dir uvicorn gunicorn - -# Copy project files -COPY . . - -# Install dependencies -RUN poetry config virtualenvs.create false \ - && poetry install --only main --no-interaction --no-ansi - -# Set DJANGO_SETTINGS_MODULE env var specifically for this RUN command -# Collect static files -RUN export DJANGO_SETTINGS_MODULE=inference_gateway.settings - -# Create necessary directories -RUN mkdir -p /var/log/inference-service - -# Run the application with standard Uvicorn worker and explicit parameters (Workaround) -CMD ["gunicorn", \ - "inference_gateway.asgi:application", \ - "-k", "uvicorn.workers.UvicornWorker", \ - "-b", "0.0.0.0:7000", \ - "--workers", "5", \ - "--threads", "4", \ - "--timeout", "1800", \ - "--log-level", "debug", \ - "--access-logfile", "/var/log/inference-service/backend_gateway.access.log", \ - "--error-logfile", "/var/log/inference-service/backend_gateway.error.log", \ - "--capture-output"] diff --git a/README.md b/README.md index 006e9e1a..9bbf1312 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,18 @@ The Inference Gateway consists of several components: ## Documentation -### For Administrators +📚 **Complete documentation is available at: [https://auroragpt-anl.github.io/inference-gateway/](https://auroragpt-anl.github.io/inference-gateway/)** -- **[Setup Guide](docs/admin-guide/setup.md)** - Complete installation and configuration instructions -- **[Docker Quickstart](docs/admin-guide/docker-quickstart.md)** - Fast-track Docker deployment guide +### Quick Links -### For Users +- **[Administrator Guide](https://auroragpt-anl.github.io/inference-gateway/admin-guide/)** - Setup and deployment instructions + - [Docker Deployment](https://auroragpt-anl.github.io/inference-gateway/admin-guide/gateway-setup/docker/) + - [Bare Metal Setup](https://auroragpt-anl.github.io/inference-gateway/admin-guide/gateway-setup/bare-metal/) + - [Inference Backend Setup](https://auroragpt-anl.github.io/inference-gateway/admin-guide/inference-setup/) + +- **[User Guide](https://auroragpt-anl.github.io/inference-gateway/user-guide/)** - How to use the inference API -- **[Usage Guide](docs/usage-guide.md)** - How to authenticate and make inference requests - -### Example Deployment - -For an example of a production deployment, see the [ALCF Inference Endpoints documentation](https://github.com/argonne-lcf/inference-endpoints). +- **[Example Deployment](https://github.com/argonne-lcf/inference-endpoints)** - ALCF production deployment ## Citation diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile new file mode 100644 index 00000000..557b17c7 --- /dev/null +++ b/deploy/docker/Dockerfile @@ -0,0 +1,45 @@ +# Use Python 3.12 as base image +FROM python:3.12-slim + +# Set environment variables +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV DJANGO_SETTINGS_MODULE=inference_gateway.settings + +# Set work directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements file first for better layer caching +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# Copy project files +COPY . . + +# Create necessary directories +RUN mkdir -p /app/staticfiles + +# Expose port +EXPOSE 7000 + +# Run the application with Uvicorn worker +CMD ["gunicorn", \ + "inference_gateway.asgi:application", \ + "-k", "uvicorn.workers.UvicornWorker", \ + "-b", "0.0.0.0:7000", \ + "--workers", "5", \ + "--threads", "4", \ + "--timeout", "1800", \ + "--log-level", "debug", \ + "--access-logfile", "-", \ + "--error-logfile", "-", \ + "--capture-output"] diff --git a/deploy/docker/README.md b/deploy/docker/README.md new file mode 100644 index 00000000..a20d18c8 --- /dev/null +++ b/deploy/docker/README.md @@ -0,0 +1,127 @@ +# Docker Deployment + +This directory contains all the necessary files for deploying the FIRST Inference Gateway using Docker. + +## Files + +- `docker-compose.yml` - Docker Compose configuration for multi-container deployment +- `Dockerfile` - Container image definition for the gateway application +- `nginx.conf` - Nginx reverse proxy configuration +- `env.example` - Example environment variables file + +## Prerequisites + +- Docker Engine 20.10+ +- Docker Compose 2.0+ + +## Quick Start + +**Note**: All commands should be run from this directory (`deploy/docker/`) + +1. Copy the example environment file: + ```bash + cp env.example .env + ``` + +2. Edit `.env` with your configuration: + - Set a strong `SECRET_KEY` (generate with: `python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"`) + - **For development**: Keep `RUNNING_AUTOMATED_TEST_SUITE=True` to skip Globus policy checks + - **For production**: Set `RUNNING_AUTOMATED_TEST_SUITE=False` and configure: + - Globus credentials (`GLOBUS_APPLICATION_ID`, `GLOBUS_APPLICATION_SECRET`, etc.) + - Globus policies (`GLOBUS_POLICIES`) + - Authorized IDP domains (`AUTHORIZED_IDP_DOMAINS`) + - Set `LOG_TO_STDOUT=True` so application logs are visible via `docker-compose logs` + - Adjust database credentials if needed + - Update `ALLOWED_HOSTS` for your domain + +3. Create required directories (if they don't exist): + ```bash + mkdir -p ../../staticfiles + ``` + +4. Start the services: + ```bash + docker-compose up -d + ``` + +5. Initialize the database: + ```bash + docker-compose exec inference-gateway python manage.py migrate + ``` + +6. Create a superuser (optional): + ```bash + docker-compose exec inference-gateway python manage.py createsuperuser + ``` + +7. Collect static files: + ```bash + docker-compose exec inference-gateway python manage.py collectstatic --noinput + ``` + +## Accessing the Gateway + +- Gateway API: http://localhost:8000 +- Admin panel: http://localhost:8000/admin + +## Managing the Services + +- **View logs**: `docker-compose logs -f` +- **Stop services**: `docker-compose down` +- **Rebuild after code changes**: `docker-compose up -d --build` +- **View running containers**: `docker-compose ps` + +## Troubleshooting + +### Application Not Starting / "Globus High Assurance Policy" Error +If the container exits immediately with no logs, or you see an error about Globus policies: +```bash +docker-compose logs inference-gateway +``` + +**Solution**: Ensure your `.env` file has `RUNNING_AUTOMATED_TEST_SUITE=True` for development, or configure proper Globus credentials for production. + +### Database Connection Issues +If you see database connection errors, ensure PostgreSQL is fully initialized: +```bash +docker-compose logs postgres +``` + +### Permission Issues +If you encounter permission issues with volumes, check that the directories exist: +```bash +ls -la ../../staticfiles +``` + +### View Application Logs +To see detailed application logs: +```bash +# Follow all logs +docker-compose logs -f + +# View only gateway logs +docker-compose logs -f inference-gateway + +# Check if container is running +docker-compose ps +``` + +### Rebuilding from Scratch +To completely reset the deployment: +```bash +docker-compose down -v # Warning: This deletes all data! +docker-compose up -d --build +``` + +## Production Considerations + +For production deployments: +1. Set `DEBUG=False` in `.env` +2. Use a strong, randomly generated `SECRET_KEY` +3. Configure proper `ALLOWED_HOSTS` +4. Set up SSL/TLS termination (use nginx with SSL certificates) +5. Configure proper backup strategies for PostgreSQL data +6. Use Docker secrets for sensitive credentials + +For detailed instructions, see the [Docker Deployment Guide](../../docs/admin-guide/gateway-setup/docker.md). + diff --git a/docker-compose.yml b/deploy/docker/docker-compose.yml similarity index 76% rename from docker-compose.yml rename to deploy/docker/docker-compose.yml index 3e71696a..f6897746 100644 --- a/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -6,8 +6,8 @@ services: ports: - "8000:80" volumes: - - ./deploy/docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro - - ./staticfiles:/app/staticfiles:ro + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + - ../../staticfiles:/app/staticfiles:ro depends_on: - inference-gateway networks: @@ -15,13 +15,12 @@ services: inference-gateway: build: - context: . + context: ../.. dockerfile: deploy/docker/Dockerfile env_file: - .env volumes: - - ./staticfiles:/app/staticfiles - - ./logs:/var/log/inference-service + - ../../staticfiles:/app/staticfiles depends_on: - postgres - redis diff --git a/deploy/docker/env.example b/deploy/docker/env.example new file mode 100644 index 00000000..540402ed --- /dev/null +++ b/deploy/docker/env.example @@ -0,0 +1,55 @@ +# --- Core Django Settings --- +SECRET_KEY="" +DEBUG=True +ALLOWED_HOSTS="localhost,127.0.0.1" + +# --- Test Mode (set to True to skip Globus policy checks during development) --- +RUNNING_AUTOMATED_TEST_SUITE=True + +# --- Logging --- +# Set to True to send application logs to stdout/stderr (recommended for Docker) +LOG_TO_STDOUT=True + +# --- Globus Credentials --- +GLOBUS_APPLICATION_ID="" +GLOBUS_APPLICATION_SECRET="" +SERVICE_ACCOUNT_ID="" +SERVICE_ACCOUNT_SECRET="" + +# --- Globus Policies (comma-separated list of policy IDs) --- +# Required: At least one policy ID must be provided +# Example: GLOBUS_POLICIES="policy-id-1,policy-id-2" +GLOBUS_POLICIES="" + +# --- Globus Groups (comma-separated list of group IDs, optional) --- +GLOBUS_GROUPS="" + +# --- Authorized Identity Provider Domains (comma-separated list) --- +# Required: At least one domain must be provided +# Example: AUTHORIZED_IDP_DOMAINS="anl.gov,uchicago.edu" +AUTHORIZED_IDP_DOMAINS="" + +# --- Authorized Groups Per IDP (JSON format, optional) --- +# Example: AUTHORIZED_GROUPS_PER_IDP='{"anl.gov": "group1,group2"}' +AUTHORIZED_GROUPS_PER_IDP='{}' + +# --- Database Credentials --- +POSTGRES_DB="inferencegateway" +POSTGRES_USER="inferencedev" +POSTGRES_PASSWORD="inferencedevpwd" +PGHOST="postgres" +PGPORT=5432 +PGUSER="inferencedev" +PGPASSWORD="inferencedevpwd" +PGDATABASE="inferencegateway" + +# --- Redis --- +REDIS_URL="redis://redis:6379/0" + +# --- Gateway Specific Settings --- +MAX_BATCHES_PER_USER=2 +STREAMING_SERVER_HOST="localhost:8080" +INTERNAL_STREAMING_SECRET="your-internal-streaming-secret-key" +CLI_AUTH_CLIENT_ID="58fdd3bc-e1c3-4ce5-80ea-8d6b87cfb944" + + diff --git a/deploy/docker/nginx.conf b/deploy/docker/nginx.conf new file mode 100644 index 00000000..2c67d8ed --- /dev/null +++ b/deploy/docker/nginx.conf @@ -0,0 +1,40 @@ +upstream app_server { + # The internal hostname and port of the Gunicorn/Django service + server inference-gateway:7000; +} + +server { + listen 80; + server_name localhost; # Or your specific domain + + # Root location - proxy all other requests to the Django app + location / { + proxy_pass http://app_server; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + + # Increase timeouts for potentially long-running requests (adjust as needed) + proxy_connect_timeout 600s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + send_timeout 600s; + + # Buffer settings (adjust as needed) + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + } + + # Serve static files directly from the volume mount + location /static/ { + alias /app/staticfiles/; + expires 30d; # Add caching headers for static files + add_header Cache-Control "public"; + } +} diff --git a/deploy/kubernetes/README.md b/deploy/kubernetes/README.md new file mode 100644 index 00000000..b0d58e77 --- /dev/null +++ b/deploy/kubernetes/README.md @@ -0,0 +1,23 @@ +# Kubernetes Deployment + +## Coming Soon + +Kubernetes deployment manifests and Helm charts for FIRST Inference Gateway will be available here. + +### Planned Components + +- Deployment manifests for the gateway application +- StatefulSet for PostgreSQL +- Service definitions +- Ingress configuration +- ConfigMaps and Secrets management +- Helm chart for easy deployment +- Horizontal Pod Autoscaling configuration +- Resource limits and requests + +### Stay Tuned + +Check back soon or watch the repository for updates on Kubernetes deployment support. + +For now, please use the [Docker deployment](../docker/) option for containerized deployments. + diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 00000000..9d2803dc --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,214 @@ +# Contributing to Documentation + +This guide explains how to contribute to the FIRST Inference Gateway documentation. + +## Documentation Structure + +The documentation is built using [MkDocs](https://www.mkdocs.org/) with the [Material theme](https://squidfunk.github.io/mkdocs-material/). + +``` +docs/ +├── index.md # Homepage +├── admin-guide/ # Administrator documentation +│ ├── index.md +│ ├── gateway-setup/ +│ │ ├── docker.md +│ │ ├── bare-metal.md +│ │ └── configuration.md +│ ├── inference-setup/ +│ │ ├── index.md +│ │ ├── direct-api.md +│ │ ├── local-vllm.md +│ │ └── globus-compute.md +│ ├── deployment/ +│ │ ├── kubernetes.md +│ │ └── production.md +│ └── monitoring.md +├── user-guide/ # User documentation +│ ├── index.md +│ ├── authentication.md +│ ├── requests.md +│ ├── batch.md +│ └── examples.md +└── reference/ # Reference materials + ├── citation.md + ├── api.md + └── config.md +``` + +## Local Development + +### Install Dependencies + +```bash +pip install -r requirements-docs.txt +``` + +Or: + +```bash +pip install mkdocs-material mkdocs-minify-plugin +``` + +### Build Documentation + +```bash +mkdocs build +``` + +This creates the `site/` directory with static HTML files. + +### Serve Locally + +```bash +mkdocs serve +``` + +Then visit: http://127.0.0.1:8000 + +The site will automatically reload when you save changes. + +## Writing Documentation + +### Markdown Files + +All documentation is written in Markdown with support for: + +- Standard Markdown syntax +- [Material for MkDocs extensions](https://squidfunk.github.io/mkdocs-material/reference/) +- Code syntax highlighting +- Admonitions (notes, warnings, tips) +- Tables +- Mermaid diagrams + +### Admonitions + +```markdown +!!! note "Optional Title" + This is a note + +!!! warning + This is a warning + +!!! tip + This is a tip + +!!! danger + This is dangerous! +``` + +### Code Blocks + +````markdown +```python +def hello(): + print("Hello, World!") +``` + +```bash +mkdocs serve +``` +```` + +### Mermaid Diagrams + +````markdown +```mermaid +graph LR + A[User] --> B[Gateway] + B --> C[Backend] +``` +```` + +### Internal Links + +```markdown +[Link text](path/to/file.md) +[Link to section](path/to/file.md#section-name) +``` + +## Navigation + +Navigation is configured in `mkdocs.yml`: + +```yaml +nav: + - Home: index.md + - Administrator Guide: + - Overview: admin-guide/index.md + - Gateway Setup: + - Docker: admin-guide/gateway-setup/docker.md +``` + +## Deployment + +### Automatic Deployment + +Documentation automatically deploys to GitHub Pages when you push to `main`: + +1. Make your changes to files in `docs/` +2. Commit and push: + ```bash + git add docs/ + git commit -m "docs: update documentation" + git push origin main + ``` +3. GitHub Actions builds and deploys automatically +4. View at: https://auroragpt-anl.github.io/inference-gateway/ + +### Manual Deployment + +You can also manually trigger deployment: + +1. Go to Actions tab on GitHub +2. Select "Deploy Documentation" workflow +3. Click "Run workflow" + +## Style Guide + +### Headings + +- Use sentence case for headings +- One H1 (`#`) per page (the page title) +- Use hierarchical heading levels (don't skip levels) + +### Code Examples + +- Always include language identifier for syntax highlighting +- Add comments to explain complex code +- Test code examples before committing + +### File Names + +- Use lowercase with hyphens: `my-file.md` +- Be descriptive: `docker-deployment.md` not `docker.md` + +### Writing Style + +- Use clear, concise language +- Write in second person ("you") for instructions +- Use active voice +- Include examples wherever possible +- Add context before diving into details + +## Contributing Process + +1. Fork the repository +2. Create a branch: `git checkout -b docs/my-improvement` +3. Make your changes +4. Test locally: `mkdocs serve` +5. Commit with clear message: `git commit -m "docs: improve docker guide"` +6. Push and create a Pull Request + +## Questions? + +- Open an issue on GitHub +- Check existing documentation +- Review MkDocs Material documentation + +## Resources + +- [MkDocs Documentation](https://www.mkdocs.org/) +- [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) +- [Markdown Guide](https://www.markdownguide.org/) + diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md deleted file mode 100644 index 317572dd..00000000 --- a/docs/DEPLOYMENT.md +++ /dev/null @@ -1,199 +0,0 @@ -# GitHub Pages Deployment Instructions - -This guide explains how to enable and deploy the documentation to GitHub Pages. - -## Prerequisites - -- Admin access to the GitHub repository -- Documentation changes committed to the repository - -## Step 1: Enable GitHub Pages - -1. Go to your GitHub repository: `https://github.com/auroraGPT-ANL/inference-gateway` -2. Click on **Settings** (top navigation bar) -3. In the left sidebar, click on **Pages** (under "Code and automation") -4. Under **Source**, select: - - Source: `GitHub Actions` - -That's it! The workflow is already configured in `.github/workflows/deploy-docs.yml`. - -## Step 2: Push to Main Branch - -The documentation will automatically deploy when you push changes to the `main` branch: - -```bash -git add . -git commit -m "docs: reorganize documentation structure" -git push origin main -``` - -The workflow will trigger automatically when: -- Files in `docs/` directory change -- `README.md` changes -- The workflow file itself changes - -## Step 3: View Your Documentation - -After the workflow completes (usually 1-2 minutes): - -1. Go to **Settings** → **Pages** -2. You'll see a message: "Your site is live at `https://auroragpt-anl.github.io/inference-gateway/`" -3. Click the link to view your documentation - -## Manual Deployment - -You can also manually trigger the deployment: - -1. Go to **Actions** tab in your repository -2. Click on "Deploy Documentation" workflow -3. Click **Run workflow** button -4. Select the `main` branch -5. Click **Run workflow** - -## Workflow Details - -The workflow (`.github/workflows/deploy-docs.yml`) does the following: - -1. **Triggers on**: - - Push to `main` branch (when docs files change) - - Manual workflow dispatch - -2. **Build Process**: - - Checks out the repository - - Copies documentation files to `_site` directory - - Creates an HTML index page with navigation - - Converts Markdown files to HTML using client-side rendering - - Applies GitHub Markdown CSS for consistent styling - -3. **Deployment**: - - Uploads the built site as an artifact - - Deploys to GitHub Pages - -## Customization - -### Change Branch - -To deploy from a different branch, edit `.github/workflows/deploy-docs.yml`: - -```yaml -on: - push: - branches: - - main # Change this to your preferred branch -``` - -### Add Custom Domain - -1. Go to **Settings** → **Pages** -2. Under **Custom domain**, enter your domain (e.g., `docs.example.com`) -3. Add a `CNAME` file in the `docs/` directory with your domain: - -```bash -echo "docs.example.com" > docs/CNAME -``` - -### Use a Documentation Framework - -For more advanced features, consider using: - -- **MkDocs**: Material theme, search, versioning -- **Sphinx**: API documentation, multiple formats -- **Jekyll**: Native GitHub Pages support, extensive themes -- **Docusaurus**: Modern React-based, built by Facebook - -Example with MkDocs: - -```yaml -# Add to .github/workflows/deploy-docs.yml -- name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: '3.x' - -- name: Install dependencies - run: | - pip install mkdocs-material - -- name: Build with MkDocs - run: mkdocs build -``` - -## Monitoring Deployments - -### Check Deployment Status - -1. Go to **Actions** tab -2. Click on the latest "Deploy Documentation" run -3. View logs to troubleshoot issues - -### Common Issues - -**404 Error**: -- Ensure GitHub Pages is enabled in Settings -- Check that the workflow completed successfully -- Verify the source is set to "GitHub Actions" - -**Changes Not Appearing**: -- Clear browser cache (Ctrl+Shift+R or Cmd+Shift+R) -- Wait a few minutes for CDN propagation -- Check the Actions tab for failed workflows - -**Workflow Not Triggering**: -- Ensure the changed files match the `paths` filter in the workflow -- Check branch protection rules aren't blocking the workflow -- Verify GitHub Actions is enabled for your repository - -## Viewing Deployment History - -All deployments are tracked: - -1. Go to **Settings** → **Pages** -2. Scroll down to see deployment history -3. Each deployment shows commit SHA and timestamp - -## Rolling Back - -To roll back to a previous version: - -1. Find the commit with the working documentation -2. Create a new commit that reverts changes: - -```bash -git revert -git push origin main -``` - -Or checkout the old version: - -```bash -git checkout -- docs/ -git commit -m "docs: rollback documentation" -git push origin main -``` - -## Security - -The workflow uses: -- `permissions`: Limited to only what's needed -- `id-token: write`: For GitHub Pages deployment -- `contents: read`: For reading repository files - -No secrets are required for basic deployment. - -## Next Steps - -After deploying: - -1. Update README badges with documentation link -2. Add documentation link to repository description -3. Share documentation URL with users -4. Set up monitoring for broken links -5. Consider adding a search feature (available with MkDocs Material) - -## Support - -For issues with GitHub Pages deployment: -- [GitHub Pages Documentation](https://docs.github.com/en/pages) -- [GitHub Actions Documentation](https://docs.github.com/en/actions) -- [Repository Issues](https://github.com/auroraGPT-ANL/inference-gateway/issues) - diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 4233109a..00000000 --- a/docs/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# FIRST Inference Gateway Documentation - -Welcome to the FIRST (Federated Inference Resource Scheduling Toolkit) documentation. - -## Quick Links - -### For Administrators -- [Docker Quickstart](admin-guide/docker-quickstart.md) - Get started in minutes -- [Complete Setup Guide](admin-guide/setup.md) - Full installation and configuration - -### For Users -- [Usage Guide](usage-guide.md) - Authentication and making requests - -### Additional Resources -- [Main README](../README.md) -- [GitHub Repository](https://github.com/auroraGPT-ANL/inference-gateway) -- [Example Deployment](https://github.com/argonne-lcf/inference-endpoints) - diff --git a/docs/admin-guide/deployment/kubernetes.md b/docs/admin-guide/deployment/kubernetes.md new file mode 100644 index 00000000..cb9385e7 --- /dev/null +++ b/docs/admin-guide/deployment/kubernetes.md @@ -0,0 +1,65 @@ +# Kubernetes Deployment + +!!! warning "Coming Soon" + Kubernetes deployment manifests and Helm charts are currently under development. + +## Planned Features + +The Kubernetes deployment will include: + +- **Helm Chart**: Easy installation and configuration management +- **High Availability**: Multi-replica deployments with load balancing +- **Auto-Scaling**: Horizontal Pod Autoscaler based on metrics +- **StatefulSets**: For PostgreSQL and Redis persistence +- **Ingress Configuration**: HTTPS/TLS termination and routing +- **Secrets Management**: Kubernetes secrets for sensitive data +- **ConfigMaps**: Environment-specific configuration +- **Health Probes**: Liveness and readiness checks +- **Resource Limits**: CPU and memory management +- **Monitoring Integration**: Prometheus and Grafana + +## Current Status + +We are actively working on: + +1. Creating Kubernetes manifests for all components +2. Developing a Helm chart for simplified deployment +3. Testing on various Kubernetes distributions (EKS, GKE, OpenShift) +4. Documentation and best practices + +## Alternative: Docker Deployment + +For now, please use one of these deployment methods: + +- [Docker Deployment](../gateway-setup/docker.md) - Containerized deployment with Docker Compose +- [Bare Metal Setup](../gateway-setup/bare-metal.md) - Direct installation on servers + +## Get Notified + +To be notified when Kubernetes support is available: + +- :star: Star the [GitHub repository](https://github.com/auroraGPT-ANL/inference-gateway) +- Watch the repository for releases +- Check the [releases page](https://github.com/auroraGPT-ANL/inference-gateway/releases) + +## Contribute + +Interested in helping with Kubernetes deployment? + +- Check open issues tagged with `kubernetes` +- Submit a pull request +- Share your deployment configurations + +## Contact + +For enterprise Kubernetes deployments or consulting: + +- Open an issue on GitHub +- Contact the development team + +--- + +**Last Updated**: November 2025 + +Check back soon for updates! + diff --git a/docs/admin-guide/deployment/production.md b/docs/admin-guide/deployment/production.md new file mode 100644 index 00000000..df1402a1 --- /dev/null +++ b/docs/admin-guide/deployment/production.md @@ -0,0 +1,535 @@ +# Production Best Practices + +This guide covers best practices for deploying FIRST Inference Gateway in production environments. + +## Security + +### Authentication & Authorization + +#### Globus Group Restrictions + +Restrict access to specific Globus groups: + +```dotenv +GLOBUS_GROUPS="group-uuid-1 group-uuid-2" +``` + +#### Identity Provider Restrictions + +Limit to specific institutions: + +```dotenv +AUTHORIZED_IDPS='{"University Name": "idp-uuid"}' +AUTHORIZED_GROUPS_PER_IDP='{"University Name": "group-uuid-1,group-uuid-2"}' +``` + +#### High Assurance Policies + +Require MFA and other security policies: + +```dotenv +GLOBUS_POLICIES="policy-uuid-1 policy-uuid-2" +``` + +### Secrets Management + +**Never** store secrets in code or version control. + +#### Use Environment Files + +```bash +# .env (add to .gitignore) +SECRET_KEY="..." +POSTGRES_PASSWORD="..." +``` + +#### Docker Secrets + +```yaml +services: + gateway: + secrets: + - db_password + - globus_secret + +secrets: + db_password: + file: ./secrets/db_password.txt + globus_secret: + file: ./secrets/globus_secret.txt +``` + +#### Vault Integration + +For enterprise deployments, integrate with HashiCorp Vault or similar. + +### HTTPS/TLS + +Always use HTTPS in production. + +#### Let's Encrypt with Certbot + +```bash +sudo certbot --nginx -d yourdomain.com +``` + +#### Custom Certificates + +```nginx +server { + listen 443 ssl http2; + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; +} +``` + +### Firewall Configuration + +```bash +# Ubuntu/Debian +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp +sudo ufw deny 8000/tcp # Don't expose Django directly + +# CentOS/RHEL +sudo firewall-cmd --permanent --add-service=http +sudo firewall-cmd --permanent --add-service=https +sudo firewall-cmd --reload +``` + +## Performance + +### Database Optimization + +#### Connection Pooling + +```python +# settings.py +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'CONN_MAX_AGE': 600, # Persistent connections + 'OPTIONS': { + 'connect_timeout': 10, + } + } +} +``` + +#### Indexes + +Ensure proper indexes on frequently queried fields: + +```python +python manage.py dbshell +CREATE INDEX idx_endpoint_slug ON resource_server_endpoint(endpoint_slug); +CREATE INDEX idx_created_at ON resource_server_listendpointslog(created_at); +``` + +### Caching + +#### Redis Configuration + +```python +# settings.py +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': 'redis://redis:6379/0', + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + 'CONNECTION_POOL_KWARGS': { + 'max_connections': 50 + } + } + } +} +``` + +### Gunicorn Configuration + +#### Worker Calculation + +```python +workers = (2 * CPU_cores) + 1 +``` + +For a 16-core machine: + +```python +workers = (2 * 16) + 1 = 33 +``` + +#### Production Config + +```python +# gunicorn_asgi.config.py +import multiprocessing + +bind = "0.0.0.0:8000" +workers = multiprocessing.cpu_count() * 2 + 1 +worker_class = "uvicorn.workers.UvicornWorker" +worker_connections = 1000 +timeout = 120 +keepalive = 5 +max_requests = 1000 +max_requests_jitter = 50 +``` + +### Nginx Optimization + +```nginx +upstream gateway { + least_conn; # Load balancing algorithm + server 127.0.0.1:8000 max_fails=3 fail_timeout=30s; + server 127.0.0.1:8001 max_fails=3 fail_timeout=30s; + keepalive 64; +} + +server { + listen 443 ssl http2; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript; + gzip_min_length 1000; + + # Client body size + client_max_body_size 100M; + client_body_buffer_size 1M; + + # Timeouts + proxy_connect_timeout 600s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + send_timeout 600s; + + # Buffering + proxy_buffering off; # Important for streaming + proxy_request_buffering off; + + # Headers + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + + location /static/ { + alias /path/to/staticfiles/; + expires 30d; + add_header Cache-Control "public, immutable"; + } + + location / { + proxy_pass http://gateway; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} +``` + +## Monitoring + +### Application Monitoring + +#### Prometheus Metrics + +Add to `docker-compose.yml`: + +```yaml +services: + prometheus: + image: prom/prometheus + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + ports: + - "9090:9090" +``` + +#### Grafana Dashboards + +```yaml +services: + grafana: + image: grafana/grafana + volumes: + - grafana_data:/var/lib/grafana + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=secure_password +``` + +### Log Aggregation + +#### Structured Logging + +```python +# logging_config.py +LOGGING = { + 'version': 1, + 'formatters': { + 'json': { + 'class': 'pythonjsonlogger.jsonlogger.JsonFormatter', + 'format': '%(asctime)s %(name)s %(levelname)s %(message)s' + } + }, + 'handlers': { + 'file': { + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': 'logs/gateway.log', + 'maxBytes': 10485760, # 10MB + 'backupCount': 10, + 'formatter': 'json' + } + } +} +``` + +#### ELK Stack Integration + +For large deployments, consider Elasticsearch + Logstash + Kibana. + +### Health Checks + +#### Kubernetes Probes + +```yaml +livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 30 + periodSeconds: 10 + +readinessProbe: + httpGet: + path: /ready + port: 8000 + initialDelaySeconds: 5 + periodSeconds: 5 +``` + +#### Custom Health Endpoint + +Create a health check view in Django to verify database, Redis, and Globus Compute connectivity. + +## Backup & Recovery + +### Database Backups + +#### Automated Backups + +```bash +#!/bin/bash +# backup_db.sh + +DATE=$(date +%Y%m%d_%H%M%S) +BACKUP_DIR="/backups/postgres" +BACKUP_FILE="$BACKUP_DIR/backup_$DATE.sql.gz" + +pg_dump -h localhost -U inferencedev inferencegateway | gzip > $BACKUP_FILE + +# Keep only last 30 days +find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +30 -delete +``` + +Add to crontab: + +```bash +0 2 * * * /path/to/backup_db.sh +``` + +#### Point-in-Time Recovery + +Configure PostgreSQL for WAL archiving: + +```ini +# postgresql.conf +wal_level = replica +archive_mode = on +archive_command = 'cp %p /backup/wal/%f' +``` + +### Configuration Backups + +```bash +# Backup environment and fixtures +tar -czf config_backup_$(date +%Y%m%d).tar.gz \ + .env \ + fixtures/ \ + nginx_app.conf \ + gunicorn_asgi.config.py +``` + +## Scaling + +### Horizontal Scaling + +#### Multiple Gateway Instances + +```nginx +upstream gateway { + server gateway1:8000; + server gateway2:8000; + server gateway3:8000; +} +``` + +#### Session Affinity + +For stateful sessions: + +```nginx +upstream gateway { + ip_hash; + server gateway1:8000; + server gateway2:8000; +} +``` + +### Database Scaling + +#### Read Replicas + +```python +# settings.py +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'HOST': 'primary.db.internal', + }, + 'replica': { + 'ENGINE': 'django.db.backends.postgresql', + 'HOST': 'replica.db.internal', + } +} + +DATABASE_ROUTERS = ['path.to.ReplicaRouter'] +``` + +#### Connection Pooling (PgBouncer) + +```ini +# pgbouncer.ini +[databases] +inferencegateway = host=localhost port=5432 dbname=inferencegateway + +[pgbouncer] +pool_mode = transaction +max_client_conn = 1000 +default_pool_size = 20 +``` + +### Inference Backend Scaling + +#### Federated Endpoints + +Deploy multiple Globus Compute endpoints and configure federated routing for automatic load balancing. + +#### Auto-Scaling + +Configure Globus Compute endpoints to auto-scale based on demand: + +```yaml +engine: + provider: + min_blocks: 1 + max_blocks: 20 +``` + +## Maintenance + +### Zero-Downtime Deployments + +#### Blue-Green Deployment + +1. Deploy new version alongside old +2. Switch traffic to new version +3. Monitor for issues +4. Decommission old version + +#### Rolling Updates + +```bash +# Update one instance at a time +for server in gateway1 gateway2 gateway3; do + ssh $server "cd /app && git pull && systemctl restart gateway" + sleep 60 # Allow time to stabilize +done +``` + +### Database Migrations + +Always test migrations in staging first: + +```bash +# Backup before migrating +./backup_db.sh + +# Run migration +python manage.py migrate + +# If issues occur, restore backup +psql -h localhost -U inferencedev inferencegateway < backup.sql +``` + +## Disaster Recovery + +### Disaster Recovery Plan + +1. **Recovery Time Objective (RTO)**: 2 hours +2. **Recovery Point Objective (RPO)**: 1 hour + +### Backup Strategy + +- **Hourly**: Database transaction logs +- **Daily**: Full database backup +- **Weekly**: Complete system backup (config, logs, data) +- **Monthly**: Archived to off-site storage + +### Failover Procedures + +Document step-by-step procedures for: + +1. Gateway failure → Switch to backup gateway +2. Database failure → Promote read replica +3. Complete site failure → Activate DR site + +## Checklist + +### Pre-Production + +- [ ] All secrets are externalized +- [ ] HTTPS/TLS configured +- [ ] Firewall rules applied +- [ ] DEBUG=False +- [ ] Strong passwords set +- [ ] Database backed up +- [ ] Monitoring configured +- [ ] Log aggregation set up +- [ ] Health checks working +- [ ] Load testing completed +- [ ] Disaster recovery plan documented + +### Post-Deployment + +- [ ] Monitor logs for errors +- [ ] Verify all endpoints responding +- [ ] Check database performance +- [ ] Test authentication flow +- [ ] Verify Globus Compute connectivity +- [ ] Run integration tests +- [ ] Document any issues + +## Additional Resources + +- [Django Security Best Practices](https://docs.djangoproject.com/en/stable/topics/security/) +- [Nginx Performance Tuning](https://www.nginx.com/blog/tuning-nginx/) +- [PostgreSQL Performance Tips](https://wiki.postgresql.org/wiki/Performance_Optimization) +- [Monitoring Guide](../monitoring.md) + diff --git a/docs/admin-guide/docker-quickstart.md b/docs/admin-guide/docker-quickstart.md deleted file mode 100644 index 9b384c37..00000000 --- a/docs/admin-guide/docker-quickstart.md +++ /dev/null @@ -1,246 +0,0 @@ -# Docker Quickstart - -This guide spins up the Inference Gateway, PostgreSQL, Redis, and an Nginx reverse proxy with a single `docker compose` command. It is designed for administrators who want a lean, reproducible environment to validate credentials, database connectivity, and backend routing before moving on to Kubernetes or bare-metal deployments. - -## Prerequisites - -- Docker Desktop 4.29+ (or Docker Engine 24+) with Docker Compose v2 -- Globus Service API application (client ID/secret) -- Globus Service Account application for Globus Compute (client ID/secret) -- Access to at least one backend (OpenAI-compatible API or Globus Compute endpoint) -- macOS or Linux host; Windows WSL2 works but paths below assume POSIX shells - -## What the Compose Stack Provides - -The repository now keeps container assets under `deploy/docker/`: - -``` -deploy/ - docker/ - Dockerfile # Builds the Django API image - nginx.conf # Minimal reverse proxy config - .env.example # Starter environment variables - fixtures/ - local_vllm_endpoint.json - federated_example.json -``` - -Only the required core services run: - -- `inference-gateway` (Gunicorn + Django) -- `postgres` -- `redis` -- `nginx` - -Monitoring add-ons such as Prometheus, Grafana, and exporters were removed to keep the footprint minimal. Bring your own observability stack when you deploy to production. - -## 1. Clone and switch into the repository - -```bash -git clone https://github.com/auroraGPT-ANL/inference-gateway.git -cd inference-gateway -``` - -## 2. Create your `.env` - -Copy the starter file and edit it with real credentials: - -```bash -cp deploy/docker/.env.example .env -``` - -Update at least the following keys: - -- `SECRET_KEY`: generate with `python -c 'import secrets; print(secrets.token_urlsafe(50))'` -- `GLOBUS_APPLICATION_ID` / `GLOBUS_APPLICATION_SECRET` -- `POLARIS_ENDPOINT_ID` / `POLARIS_ENDPOINT_SECRET` -- `INTERNAL_STREAMING_SECRET`: secret shared with your streaming workers -- `METIS_STATUS_URL` / `METIS_API_TOKENS` if you plan to hit a direct API - -> ⚠️ The application always enforces Globus access tokens. Even in Docker you must authenticate through Globus to call the gateway. - -## 3. Prepare writable bind mounts - -```bash -mkdir -p logs staticfiles -``` - -`logs` stores Gunicorn output from inside the container. `staticfiles` is populated when you run `collectstatic` later. - -## 4. Start the core services - -```bash -docker compose up -d --build -``` - -The first build can take several minutes because Poetry installs all dependencies inside the image. - -## 5. Run database migrations - -```bash -docker compose run --rm inference-gateway python manage.py migrate -``` - -You can also create an admin account for the Django admin (optional but handy for inspections): - -```bash -docker compose run --rm inference-gateway python manage.py createsuperuser -``` - -## 6. Collect static assets (optional but recommended for the dashboard) - -```bash -docker compose run --rm inference-gateway python manage.py collectstatic --noinput -``` - -Nginx already mounts `./staticfiles`, so collected assets become available immediately. - -## 7. Verify the stack - -- Gateway API: http://localhost:8000/ -- Django admin: http://localhost:8000/admin/ -- Logs: `tail -f logs/backend_gateway.error.log` - -Once the API is up, configure backends so the router knows where to send inference requests. - ---- - -## Connecting Backends - -The gateway is agnostic to how models are served. This section shows two common options. - -### Option A — Direct OpenAI-Compatible API - -Use this path when you already operate an HTTPS endpoint that mimics the OpenAI REST contract (official OpenAI, Anthropic-compatible adapters, custom deployments, etc.). The gateway treats these as the `metis` cluster, which relies on a status manifest and per-endpoint API tokens. - -1. **Host a status manifest** reachable over HTTP(S). A minimal example: - - ```json - { - "openai-gateway": { - "status": "Live", - "model": "OpenAI Pass-through", - "description": "Routes to OpenAI's GPT models", - "experts": ["openai/gpt-4o-mini"], - "url": "https://api.openai.com/v1", - "endpoint_id": "openai-production" - } - } - ``` - - Place it somewhere the Docker container can reach. For local testing you can run: - - ```bash - mkdir -p deploy/docker/examples - cat > deploy/docker/examples/metis-status.json <<'JSON' - { - "openai-gateway": { - "status": "Live", - "model": "OpenAI Pass-through", - "description": "Routes to OpenAI's GPT models", - "experts": ["openai/gpt-4o-mini"], - "url": "https://api.openai.com/v1", - "endpoint_id": "openai-production" - } - } - JSON - python -m http.server 8055 --directory deploy/docker/examples - ``` - - Then set `METIS_STATUS_URL=http://host.docker.internal:8055/metis-status.json` in `.env` (Docker Desktop exposes the host using `host.docker.internal`). - -2. **Provide the API token** in `.env` using the manifest `endpoint_id` as the key: - - ```dotenv - METIS_API_TOKENS={"openai-production": "sk-your-openai-key"} - ``` - -3. Restart the API container to pick up the new environment: - - ```bash - docker compose up -d inference-gateway - ``` - -4. Call the gateway with a Globus access token: - - ```bash - curl -X POST \ - http://localhost:8000/resource_server/metis/api/v1/chat/completions \ - -H "Authorization: Bearer $MY_GLOBUS_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "model": "openai/gpt-4o-mini", - "messages": [{"role": "user", "content": "Hello from Docker"}], - "stream": false - }' - ``` - -The gateway fetches the manifest, validates the model is Live, injects your API key, and forwards the OpenAI payload for you. - -### Option B — Globus Compute + vLLM - -Use this path when the model executes on an HPC cluster or GPU node exposed through Globus Compute. You need three artefacts: registered Globus function UUID(s), the endpoint UUID, and a Django fixture describing the target. - -1. **Register the inference function** from your compute node: - - ```bash - # On the machine that owns the Globus Compute endpoint - cd compute-functions - poetry run python vllm_register_function.py - ``` - - Capture the printed function UUID. Repeat for the status or batch functions if required. - -2. **Start (or restart) your Globus Compute endpoint** and note the endpoint UUID: - - ```bash - globus-compute-endpoint start my-vllm-endpoint - globus-compute-endpoint list # to see the endpoint UUID - ``` - -3. **Create a minimal endpoint fixture** using the template shipped in the repo: - - ```bash - cp deploy/docker/fixtures/local_vllm_endpoint.json fixtures/endpoints.json - ``` - - Edit `fixtures/endpoints.json` and replace: - - - `replace-with-endpoint-uuid` - - `replace-with-function-uuid` - - Update the `cluster` name if you do not want to use `local` - - Set `api_port` to the port exposed by your inference server - -4. **Load the fixture into the running containers:** - - ```bash - docker compose run --rm inference-gateway python manage.py loaddata fixtures/endpoints.json - ``` - - Repeat the process for `fixtures/federated_endpoints.json` if you plan to expose federated routing; the starter file lives at `deploy/docker/fixtures/federated_example.json`. - -5. **Test the endpoint**: - - ```bash - curl -X POST \ - http://localhost:8000/resource_server/local/vllm/v1/chat/completions \ - -H "Authorization: Bearer $MY_GLOBUS_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "model": "meta-llama/Meta-Llama-3-8B-Instruct", - "messages": [{"role": "user", "content": "Status check"}], - "stream": false - }' - ``` - -If the endpoint is online and your Globus token has the required group membership, the gateway streams responses back in OpenAI format. - ---- - -## Housekeeping - -- **Restart services** after environment or fixture changes: `docker compose up -d inference-gateway nginx` -- **Shut everything down** when you are done: `docker compose down` -- **Clean database state** during testing: `docker compose down -v` - -You now have a lean Docker deployment that mirrors production authentication and routing behaviour without extra monitoring dependencies. Continue to the Kubernetes or bare-metal guides once you are comfortable with the workflow. diff --git a/docs/admin-guide/gateway-setup/bare-metal.md b/docs/admin-guide/gateway-setup/bare-metal.md new file mode 100644 index 00000000..f26678dc --- /dev/null +++ b/docs/admin-guide/gateway-setup/bare-metal.md @@ -0,0 +1,477 @@ +# Bare Metal Setup + +This guide covers installing the FIRST Inference Gateway directly on your server without Docker. + +## Prerequisites + +- Linux server (Ubuntu 20.04+, CentOS 8+, or similar) +- Python 3.12 or later +- PostgreSQL 13 or later +- Redis 6 or later +- Poetry (Python dependency manager) +- Sudo access for system packages +- At least 4GB RAM + +## Step 1: Install System Dependencies + +### Ubuntu/Debian + +```bash +sudo apt update +sudo apt install -y python3.12 python3.12-dev python3.12-venv \ + postgresql postgresql-contrib redis-server \ + build-essential libpq-dev git curl +``` + +### CentOS/RHEL + +```bash +sudo dnf install -y python3.12 python3.12-devel \ + postgresql postgresql-server redis \ + gcc gcc-c++ make libpq-devel git +``` + +## Step 2: Install Poetry + +```bash +curl -sSL https://install.python-poetry.org | python3 - + +# Add to PATH +export PATH="$HOME/.local/bin:$PATH" + +# Verify installation +poetry --version +``` + +## Step 3: Clone and Setup Project + +```bash +git clone https://github.com/auroraGPT-ANL/inference-gateway.git +cd inference-gateway + +# Configure Poetry to create venv in project +poetry config virtualenvs.in-project true + +# Set Python version +poetry env use python3.12 + +# Install dependencies +poetry install + +# Activate environment +poetry shell +``` + +## Step 4: Configure PostgreSQL + +### Initialize PostgreSQL (if first time) + +```bash +# Ubuntu/Debian (usually auto-initialized) +sudo systemctl start postgresql +sudo systemctl enable postgresql + +# CentOS/RHEL +sudo postgresql-setup --initdb +sudo systemctl start postgresql +sudo systemctl enable postgresql +``` + +### Create Database and User + +```bash +sudo -u postgres psql + +# In PostgreSQL shell: +CREATE DATABASE inferencegateway; +CREATE USER inferencedev WITH PASSWORD 'your-secure-password'; +ALTER ROLE inferencedev SET client_encoding TO 'utf8'; +ALTER ROLE inferencedev SET default_transaction_isolation TO 'read committed'; +ALTER ROLE inferencedev SET timezone TO 'UTC'; +GRANT ALL PRIVILEGES ON DATABASE inferencegateway TO inferencedev; +\q +``` + +### Configure PostgreSQL Authentication + +Edit `/etc/postgresql/*/main/pg_hba.conf` (path may vary): + +``` +# Add this line (adjust for your security needs) +host inferencegateway inferencedev 127.0.0.1/32 md5 +``` + +Restart PostgreSQL: + +```bash +sudo systemctl restart postgresql +``` + +## Step 5: Configure Redis + +Start and enable Redis: + +```bash +sudo systemctl start redis +sudo systemctl enable redis + +# Verify it's running +redis-cli ping +# Should return: PONG +``` + +## Step 6: Register Globus Applications + +Follow the same steps as in the Docker guide: + +### Service API Application + +1. Visit [developers.globus.org](https://app.globus.org/settings/developers) +2. Register a **service API application** +3. Add redirect URI: `http://your-server-ip:8000/complete/globus/` +4. Note Client UUID and Secret + +### Add Scope + +```bash +export CLIENT_ID="" +export CLIENT_SECRET="" + +curl -X POST -s --user $CLIENT_ID:$CLIENT_SECRET \ + https://auth.globus.org/v2/api/clients/$CLIENT_ID/scopes \ + -H "Content-Type: application/json" \ + -d '{ + "scope": { + "name": "Action Provider - all", + "description": "Access to inference service.", + "scope_suffix": "action_all", + "dependent_scopes": [ + { + "scope": "73320ffe-4cb4-4b25-a0a3-83d53d59ce4f", + "optional": false, + "requires_refresh_token": true + } + ] + } + }' +``` + +### Service Account Application + +1. In the same project, register a **service account application** +2. Note Client UUID and Secret + +## Step 7: Configure Environment + +Create `.env` file in project root: + +```bash +cat > .env << 'EOF' +# --- Core Django Settings --- +SECRET_KEY="" +DEBUG=False +ALLOWED_HOSTS="your-server-ip,your-domain.com" + +# --- Globus Credentials --- +GLOBUS_APPLICATION_ID="" +GLOBUS_APPLICATION_SECRET="" +SERVICE_ACCOUNT_ID="" +SERVICE_ACCOUNT_SECRET="" + +# --- Database Credentials --- +POSTGRES_DB="inferencegateway" +POSTGRES_USER="inferencedev" +POSTGRES_PASSWORD="your-secure-password" +PGHOST="localhost" +PGPORT=5432 +PGUSER="inferencedev" +PGPASSWORD="your-secure-password" +PGDATABASE="inferencegateway" + +# --- Redis --- +REDIS_URL="redis://localhost:6379/0" + +# --- Gateway Settings --- +MAX_BATCHES_PER_USER=2 +STREAMING_SERVER_HOST="localhost:8080" +INTERNAL_STREAMING_SECRET="" +CLI_AUTH_CLIENT_ID="58fdd3bc-e1c3-4ce5-80ea-8d6b87cfb944" +EOF +``` + +Generate secret key: + +```bash +python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' +``` + +## Step 8: Initialize Database + +```bash +# Make sure you're in the poetry shell +poetry shell + +# Run migrations +python manage.py makemigrations +python manage.py migrate + +# Create superuser +python manage.py createsuperuser + +# Collect static files +python manage.py collectstatic --noinput +``` + +## Step 9: Test the Gateway + +Run development server: + +```bash +python manage.py runserver 0.0.0.0:8000 +``` + +Test in another terminal: + +```bash +curl http://localhost:8000/ +``` + +## Step 10: Setup Production Server (Gunicorn) + +### Install Gunicorn (already included in poetry dependencies) + +Create a systemd service file: + +```bash +sudo nano /etc/systemd/system/inference-gateway.service +``` + +Add the following: + +```ini +[Unit] +Description=FIRST Inference Gateway +After=network.target postgresql.service redis.service + +[Service] +Type=notify +User=your-username +Group=your-username +WorkingDirectory=/path/to/inference-gateway +Environment="PATH=/path/to/inference-gateway/.venv/bin" +EnvironmentFile=/path/to/inference-gateway/.env +ExecStart=/path/to/inference-gateway/.venv/bin/gunicorn \ + inference_gateway.asgi:application \ + -k uvicorn.workers.UvicornWorker \ + -b 0.0.0.0:8000 \ + --workers 4 \ + --log-level info \ + --access-logfile /path/to/inference-gateway/logs/access.log \ + --error-logfile /path/to/inference-gateway/logs/error.log + +[Install] +WantedBy=multi-user.target +``` + +### Start and Enable Service + +```bash +# Create logs directory +mkdir -p logs + +# Reload systemd +sudo systemctl daemon-reload + +# Start service +sudo systemctl start inference-gateway + +# Enable on boot +sudo systemctl enable inference-gateway + +# Check status +sudo systemctl status inference-gateway +``` + +## Step 11: Setup Nginx (Recommended) + +### Install Nginx + +```bash +# Ubuntu/Debian +sudo apt install nginx + +# CentOS/RHEL +sudo dnf install nginx +``` + +### Configure Nginx + +Create site configuration: + +```bash +sudo nano /etc/nginx/sites-available/inference-gateway +``` + +Add the following: + +```nginx +upstream inference_gateway { + server 127.0.0.1:8000 fail_timeout=0; +} + +server { + listen 80; + server_name your-domain.com; + client_max_body_size 100M; + + location /static/ { + alias /path/to/inference-gateway/staticfiles/; + } + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_buffering off; + proxy_pass http://inference_gateway; + } +} +``` + +Enable the site: + +```bash +# Ubuntu/Debian +sudo ln -s /etc/nginx/sites-available/inference-gateway /etc/nginx/sites-enabled/ +sudo nginx -t +sudo systemctl restart nginx + +# CentOS/RHEL +sudo ln -s /etc/nginx/sites-available/inference-gateway /etc/nginx/conf.d/ +sudo nginx -t +sudo systemctl restart nginx +``` + +### Setup SSL with Let's Encrypt + +```bash +# Ubuntu/Debian +sudo apt install certbot python3-certbot-nginx + +# CentOS/RHEL +sudo dnf install certbot python3-certbot-nginx + +# Get certificate +sudo certbot --nginx -d your-domain.com +``` + +## Step 12: Configure Firewall + +```bash +# Ubuntu/Debian (UFW) +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp +sudo ufw enable + +# CentOS/RHEL (firewalld) +sudo firewall-cmd --permanent --add-service=http +sudo firewall-cmd --permanent --add-service=https +sudo firewall-cmd --reload +``` + +## Maintenance + +### View Logs + +```bash +# Application logs +tail -f logs/error.log +tail -f logs/access.log + +# System service logs +sudo journalctl -u inference-gateway -f +``` + +### Restart Service + +```bash +sudo systemctl restart inference-gateway +``` + +### Update Application + +```bash +cd /path/to/inference-gateway +git pull origin main +poetry install +python manage.py migrate +python manage.py collectstatic --noinput +sudo systemctl restart inference-gateway +``` + +## Troubleshooting + +### Service won't start + +Check logs: + +```bash +sudo journalctl -u inference-gateway -n 50 +``` + +Check configuration: + +```bash +poetry shell +python manage.py check +``` + +### Database connection errors + +Verify PostgreSQL is running: + +```bash +sudo systemctl status postgresql +``` + +Test connection: + +```bash +psql -h localhost -U inferencedev -d inferencegateway +``` + +### Permission errors + +Ensure the service user owns the files: + +```bash +sudo chown -R your-username:your-username /path/to/inference-gateway +``` + +### Nginx errors + +Check nginx error log: + +```bash +sudo tail -f /var/log/nginx/error.log +``` + +Test configuration: + +```bash +sudo nginx -t +``` + +## Next Steps + +- [Configure Inference Backends](../inference-setup/index.md) +- [Production Best Practices](../deployment/production.md) +- [Monitoring Setup](../monitoring.md) + +## Additional Resources + +- [Configuration Reference](configuration.md) +- [Gunicorn Documentation](https://docs.gunicorn.org/) +- [Nginx Documentation](https://nginx.org/en/docs/) + diff --git a/docs/admin-guide/gateway-setup/configuration.md b/docs/admin-guide/gateway-setup/configuration.md new file mode 100644 index 00000000..ecc73076 --- /dev/null +++ b/docs/admin-guide/gateway-setup/configuration.md @@ -0,0 +1,404 @@ +# Configuration Reference + +This page documents all environment variables and configuration options for the FIRST Inference Gateway. + +## Environment Variables + +All configuration is done through environment variables, typically stored in a `.env` file. + +### Core Django Settings + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `SECRET_KEY` | Yes | - | Django secret key for cryptographic signing | +| `DEBUG` | No | `False` | Enable debug mode (never use in production) | +| `ALLOWED_HOSTS` | Yes | - | Comma-separated list of allowed hostnames | +| `RUNNING_AUTOMATED_TEST_SUITE` | No | `False` | Set to `True` to skip Globus High Assurance policy checks (development/testing only) | +| `LOG_TO_STDOUT` | No | `False` | Set to `True` to output logs to stdout (useful for Docker) | + +!!! danger "Security Warning" + - Never use `DEBUG=True` in production! This exposes sensitive information. + - Never use `RUNNING_AUTOMATED_TEST_SUITE=True` in production! This disables important security checks. + +### Globus Authentication + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `GLOBUS_APPLICATION_ID` | Yes | - | Service API application client UUID | +| `GLOBUS_APPLICATION_SECRET` | Yes | - | Service API application client secret | +| `SERVICE_ACCOUNT_ID` | Yes | - | Service Account application client UUID | +| `SERVICE_ACCOUNT_SECRET` | Yes | - | Service Account application client secret | +| `GLOBUS_GROUPS` | No | - | Space-separated UUIDs of allowed Globus groups | +| `AUTHORIZED_IDPS` | No | - | JSON string of authorized identity providers | +| `AUTHORIZED_GROUPS_PER_IDP` | No | - | JSON string of groups per IDP | +| `GLOBUS_POLICIES` | No | - | Space-separated policy UUIDs | + +Example with group restrictions: + +```dotenv +GLOBUS_GROUPS="uuid-1 uuid-2 uuid-3" +``` + +Example with IDP restrictions: + +```dotenv +AUTHORIZED_IDPS='{"University": "uuid-here"}' +AUTHORIZED_GROUPS_PER_IDP='{"University": "group-uuid-1,group-uuid-2"}' +``` + +### Database Configuration + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `POSTGRES_DB` | Yes | - | Database name | +| `POSTGRES_USER` | Yes | - | Database user | +| `POSTGRES_PASSWORD` | Yes | - | Database password | +| `PGHOST` | Yes | - | Database host (`postgres` for Docker, `localhost` for bare metal) | +| `PGPORT` | No | `5432` | Database port | +| `PGUSER` | Yes | - | Database user (can be same as POSTGRES_USER) | +| `PGPASSWORD` | Yes | - | Database password | +| `PGDATABASE` | Yes | - | Database name | + +!!! tip "Docker Networking" + When using Docker Compose, set `PGHOST=postgres` to use the container name. + For bare metal, use `PGHOST=localhost` or the actual hostname. + +### Redis Configuration + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `REDIS_URL` | Yes | - | Redis connection URL | + +Examples: + +```dotenv +# Docker +REDIS_URL="redis://redis:6379/0" + +# Bare metal +REDIS_URL="redis://localhost:6379/0" + +# With password +REDIS_URL="redis://:password@localhost:6379/0" +``` + +### Gateway Settings + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `MAX_BATCHES_PER_USER` | No | `2` | Maximum concurrent batch jobs per user | +| `STREAMING_SERVER_HOST` | No | - | Internal streaming server host:port | +| `INTERNAL_STREAMING_SECRET` | No | - | Secret for internal streaming authentication | + +### CLI Authentication Helper + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `CLI_AUTH_CLIENT_ID` | No | `58fdd3bc...` | Public client ID for CLI auth script | +| `CLI_ALLOWED_DOMAINS` | No | - | Comma-separated domains for CLI login | +| `CLI_TOKEN_DIR` | No | `~/.globus/app` | Token storage directory | +| `CLI_APP_NAME` | No | `inference_auth` | App name for token storage | + +Example: + +```dotenv +CLI_ALLOWED_DOMAINS="anl.gov,alcf.anl.gov,university.edu" +``` + +### Metis (Direct API) Configuration + +For direct OpenAI-compatible API connections: + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `METIS_STATUS_URL` | No | - | URL to status manifest JSON | +| `METIS_API_TOKENS` | No | - | JSON object of endpoint_id -> API token | + +Example: + +```dotenv +METIS_STATUS_URL="https://example.com/status.json" +METIS_API_TOKENS='{"openai-prod": "sk-...", "anthropic-prod": "sk-ant-..."}' +``` + +### Monitoring (Optional) + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `GF_SECURITY_ADMIN_USER` | No | `admin` | Grafana admin username | +| `GF_SECURITY_ADMIN_PASSWORD` | No | `admin` | Grafana admin password | + +### Qstat Endpoints (Optional) + +For HPC cluster status monitoring: + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `SOPHIA_QSTAT_ENDPOINT_UUID` | No | - | Endpoint UUID for qstat function | +| `SOPHIA_QSTAT_FUNCTION_UUID` | No | - | Function UUID for qstat | + +## Fixture Configuration + +Fixtures are JSON files that define available endpoints and models. + +### Endpoint Fixture Format + +Located at `fixtures/endpoints.json`: + +```json +[ + { + "model": "resource_server.endpoint", + "pk": 1, + "fields": { + "endpoint_slug": "local-vllm-opt-125m", + "cluster": "local", + "framework": "vllm", + "model": "facebook/opt-125m", + "api_port": 8001, + "endpoint_uuid": "", + "function_uuid": "", + "batch_endpoint_uuid": "", + "batch_function_uuid": "", + "allowed_globus_groups": "" + } + } +] +``` + +### Federated Endpoint Fixture Format + +Located at `fixtures/federated_endpoints.json`: + +```json +[ + { + "model": "resource_server.federatedendpoint", + "pk": 1, + "fields": { + "name": "OPT 125M (Federated)", + "slug": "federated-opt-125m", + "target_model_name": "facebook/opt-125m", + "description": "Federated access point", + "targets": [ + { + "cluster": "local", + "framework": "vllm", + "model": "facebook/opt-125m", + "endpoint_slug": "local-vllm-opt-125m", + "endpoint_uuid": "", + "function_uuid": "", + "api_port": 8001 + } + ] + } + } +] +``` + +## Django Settings + +Advanced settings in `inference_gateway/settings.py`: + +### CORS Settings + +```python +CORS_ALLOW_ALL_ORIGINS = False # Set True for development only +CORS_ALLOWED_ORIGINS = [ + "https://yourdomain.com", +] +``` + +### Logging Configuration + +Located in `logging_config.py`: + +```python +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'file': { + 'level': 'INFO', + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': 'logs/django_info.log', + 'maxBytes': 1024 * 1024 * 15, # 15MB + 'backupCount': 10, + }, + }, + # ... more configuration +} +``` + +### Cache Configuration + +```python +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': os.environ.get('REDIS_URL'), + } +} +``` + +## Gunicorn Configuration + +For production deployments, configure Gunicorn in `gunicorn_asgi.config.py`: + +```python +bind = "0.0.0.0:8000" +workers = 4 # Adjust based on CPU cores +worker_class = "uvicorn.workers.UvicornWorker" +timeout = 120 +keepalive = 5 +``` + +### Worker Calculation + +Recommended formula: + +``` +workers = (2 * CPU_cores) + 1 +``` + +For a 4-core machine: +``` +workers = (2 * 4) + 1 = 9 +``` + +## Nginx Configuration + +Example production configuration: + +```nginx +upstream inference_gateway { + server 127.0.0.1:8000 fail_timeout=0; +} + +server { + listen 443 ssl http2; + server_name yourdomain.com; + + # SSL Configuration + ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + # File upload size limit + client_max_body_size 100M; + + # Timeouts + proxy_connect_timeout 600s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + + location /static/ { + alias /path/to/staticfiles/; + expires 30d; + add_header Cache-Control "public, immutable"; + } + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_buffering off; + proxy_pass http://inference_gateway; + } +} + +# Redirect HTTP to HTTPS +server { + listen 80; + server_name yourdomain.com; + return 301 https://$server_name$request_uri; +} +``` + +## Environment-Specific Configurations + +### Development + +```dotenv +DEBUG=True +ALLOWED_HOSTS="localhost,127.0.0.1" +SECRET_KEY="dev-secret-key-not-secure" +PGHOST="localhost" +REDIS_URL="redis://localhost:6379/0" +``` + +### Staging + +```dotenv +DEBUG=False +ALLOWED_HOSTS="staging.yourdomain.com" +SECRET_KEY="" +PGHOST="postgres-staging.internal" +REDIS_URL="redis://redis-staging.internal:6379/0" +``` + +### Production + +```dotenv +DEBUG=False +ALLOWED_HOSTS="yourdomain.com,api.yourdomain.com" +SECRET_KEY="" +PGHOST="postgres-prod.internal" +REDIS_URL="redis://redis-prod.internal:6379/0" + +# Enable security features +SECURE_SSL_REDIRECT=True +SESSION_COOKIE_SECURE=True +CSRF_COOKIE_SECURE=True +``` + +## Secrets Management + +### Using Docker Secrets + +```yaml +services: + inference-gateway: + secrets: + - globus_secret + - db_password + +secrets: + globus_secret: + file: ./secrets/globus_secret.txt + db_password: + file: ./secrets/db_password.txt +``` + +### Using Environment Files + +```bash +# .env.local (gitignored) +source .env.production +export POSTGRES_PASSWORD="" +export GLOBUS_APPLICATION_SECRET="" +``` + +## Validation + +Check your configuration: + +```bash +# Django check +python manage.py check + +# Database connectivity +python manage.py dbshell + +# Show current settings (dev only!) +python manage.py diffsettings +``` + +## Next Steps + +- [Docker Deployment](docker.md) +- [Bare Metal Setup](bare-metal.md) +- [Production Best Practices](../deployment/production.md) + diff --git a/docs/admin-guide/gateway-setup/docker.md b/docs/admin-guide/gateway-setup/docker.md new file mode 100644 index 00000000..2b89e74c --- /dev/null +++ b/docs/admin-guide/gateway-setup/docker.md @@ -0,0 +1,344 @@ +# Docker Deployment + +This guide shows you how to deploy the FIRST Inference Gateway using Docker and Docker Compose. + +## Prerequisites + +- Docker Desktop 4.29+ (or Docker Engine 24+) with Docker Compose v2 +- Git +- Globus Account and registered applications +- At least 4GB RAM available for containers + +## Step 1: Clone the Repository + +```bash +git clone https://github.com/auroraGPT-ANL/inference-gateway.git +cd inference-gateway +``` + +## Step 2: Register Globus Applications + +Before deploying, you need to register two Globus applications. + +### Service API Application + +This handles API authorization: + +1. Visit [developers.globus.org](https://app.globus.org/settings/developers) +2. Click **Register a service API** +3. Fill in the form: + - **App Name**: "My Inference Gateway" + - **Redirect URIs**: `http://localhost:8000/complete/globus/` (for local development) + - Add your production URL if deploying to a server +4. Note the **Client UUID** and generate a **Client Secret** + +### Add Scope to Service API Application + +```bash +export CLIENT_ID="" +export CLIENT_SECRET="" + +curl -X POST -s --user $CLIENT_ID:$CLIENT_SECRET \ + https://auth.globus.org/v2/api/clients/$CLIENT_ID/scopes \ + -H "Content-Type: application/json" \ + -d '{ + "scope": { + "name": "Action Provider - all", + "description": "Access to inference service.", + "scope_suffix": "action_all", + "dependent_scopes": [ + { + "scope": "73320ffe-4cb4-4b25-a0a3-83d53d59ce4f", + "optional": false, + "requires_refresh_token": true + } + ] + } + }' +``` + +Verify the scope: + +```bash +curl -s --user $CLIENT_ID:$CLIENT_SECRET https://auth.globus.org/v2/api/clients/$CLIENT_ID +``` + +### Service Account Application + +To handle the communication between the Gateway API and the compute resources (the Inference Backend), you need to create a Globus **Service Account application**. This application represents the Globus identity that will own the Globus Compute endpoints. + +1. Visit [developers.globus.org](https://app.globus.org/settings/developers) and sign in. +2. Under **Projects**, click on the project used to register your Service API application from the previous step. +3. Click on **Add an App**. +4. Select **Register a service account ...**. +5. Complete the registration form: + - Set **App Name** (e.g., "My Inference Endpoints"). + - Set **Privacy Policy** and **Terms & Conditions** URLs if applicable. +6. After registration, a **Client UUID** will be assigned to your Globus application. Generate a **Client Secret** by clicking on the **Add Client Secret** button on the right-hand side. **You will need both for the `.env` configuration.** The UUID will be for `SERVICE_ACCOUNT_ID`, and the secret will be for `SERVICE_ACCOUNT_SECRET`. + +## Step 3: Configure Environment + +Copy the example environment file: + +```bash +cp deploy/docker/env.example .env +``` + +Edit `.env` with your configuration: + +```dotenv +# --- Core Django Settings --- +SECRET_KEY="" +DEBUG=True +ALLOWED_HOSTS="localhost,127.0.0.1" + +# --- Testing/Development Flags --- +# Set to True to skip Globus High Assurance policy checks (for development/testing) +# Set to False for production deployment +RUNNING_AUTOMATED_TEST_SUITE=True +LOG_TO_STDOUT=True # Makes logs visible via docker-compose logs + +# --- Globus Credentials --- +GLOBUS_APPLICATION_ID="" +GLOBUS_APPLICATION_SECRET="" +SERVICE_ACCOUNT_ID="" +SERVICE_ACCOUNT_SECRET="" + +# --- Database Credentials (change for production) --- +POSTGRES_DB="inferencegateway" +POSTGRES_USER="inferencedev" +POSTGRES_PASSWORD="change-this-password" +PGHOST="postgres" +PGPORT=5432 +PGUSER="inferencedev" +PGPASSWORD="change-this-password" +PGDATABASE="inferencegateway" + +# --- Redis --- +REDIS_URL="redis://redis:6379/0" + +# --- Gateway Settings --- +MAX_BATCHES_PER_USER=2 +STREAMING_SERVER_HOST="localhost:8080" +INTERNAL_STREAMING_SECRET="change-this-secret" +CLI_AUTH_CLIENT_ID="58fdd3bc-e1c3-4ce5-80ea-8d6b87cfb944" +``` + +Generate a secret key: + +```bash +python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' +``` + +!!! warning "Production Security" + For production deployments: + + - Set `RUNNING_AUTOMATED_TEST_SUITE=False` + - Change all passwords and secrets + - Set `DEBUG=False` + - Add your domain to `ALLOWED_HOSTS` + - Configure proper Globus policies (`GLOBUS_POLICIES`) + - Set authorized IDP domains (`AUTHORIZED_IDP_DOMAINS`) + - Use strong, unique passwords + - Consider using secrets management (e.g., Docker secrets) + +## Step 4: Start the Services + +```bash +cd deploy/docker +docker-compose up -d --build +``` + +This starts: + +- `inference-gateway`: The Django API application +- `postgres`: PostgreSQL database +- `redis`: Redis cache +- `nginx`: Reverse proxy (optional, if configured) + +Verify services are running: + +```bash +docker-compose ps +``` + +## Step 5: Initialize the Database + +Run migrations: + +```bash +docker-compose exec inference-gateway python manage.py migrate +``` + +Create a superuser (optional, for Django admin access): + +```bash +docker-compose exec inference-gateway python manage.py createsuperuser +``` + +Collect static files: + +```bash +docker-compose exec inference-gateway python manage.py collectstatic --noinput +``` + +## Step 6: Verify the Gateway + +Check that the gateway is running: + +```bash +curl http://localhost:8000/ +``` + +Access the Django admin (if superuser was created): + +- URL: http://localhost:8000/admin/ +- Login with your superuser credentials + +## Step 7: Configure Backends + +Now you need to connect inference backends. Choose one: + +- [Direct API Connection](../inference-setup/direct-api.md) - Connect to OpenAI or similar APIs +- [Local vLLM](../inference-setup/local-vllm.md) - Run vLLM locally +- [Globus Compute + vLLM](../inference-setup/globus-compute.md) - HPC cluster deployment + +## Docker Compose Services + +The `docker-compose.yml` includes: + +### Core Services + +- **inference-gateway**: Django application (port 8000) +- **postgres**: PostgreSQL 15 (port 5432) +- **redis**: Redis 7 (port 6379) + +### Optional Services + +You can add these to your compose file: + +- **nginx**: Reverse proxy for production +- **prometheus**: Metrics collection +- **grafana**: Visualization dashboard + +## Common Commands + +### View logs + +```bash +# All services +docker-compose logs -f + +# Specific service +docker-compose logs -f inference-gateway +``` + +### Restart services + +```bash +# All services +docker-compose restart + +# Specific service +docker-compose restart inference-gateway +``` + +### Stop services + +```bash +docker-compose down +``` + +### Stop and remove volumes (clean slate) + +```bash +docker-compose down -v +``` + +### Access container shell + +```bash +docker-compose exec inference-gateway /bin/bash +``` + +### Run Django management commands + +```bash +docker-compose exec inference-gateway python manage.py +``` + +## Updating the Deployment + +Pull latest changes: + +```bash +git pull origin main +docker-compose build +docker-compose up -d +docker-compose exec inference-gateway python manage.py migrate +``` + +## Troubleshooting + +### Gateway won't start + +Check logs: + +```bash +docker-compose logs inference-gateway +``` + +Common issues: + +- Missing environment variables +- Database connection failed +- Port 8000 already in use + +### Database connection errors + +Verify PostgreSQL is running: + +```bash +docker-compose ps postgres +``` + +Check database logs: + +```bash +docker-compose logs postgres +``` + +### Can't access admin panel + +Ensure you created a superuser: + +```bash +docker-compose exec inference-gateway python manage.py createsuperuser +``` + +### 502 Bad Gateway from Nginx + +Check that the gateway container is running: + +```bash +docker-compose ps inference-gateway +``` + +Verify nginx configuration: + +```bash +docker-compose exec nginx nginx -t +``` + +## Next Steps + +- [Configure Inference Backends](../inference-setup/index.md) +- [Production Best Practices](../deployment/production.md) +- [Monitoring Setup](../monitoring.md) + +## Additional Resources + +- [Docker Compose Documentation](https://docs.docker.com/compose/) +- [Configuration Reference](configuration.md) +- [User Guide](../../user-guide/index.md) + diff --git a/docs/admin-guide/index.md b/docs/admin-guide/index.md new file mode 100644 index 00000000..03d19e78 --- /dev/null +++ b/docs/admin-guide/index.md @@ -0,0 +1,148 @@ +# Administrator Guide + +Welcome to the FIRST Inference Gateway Administrator Guide. This guide will help you deploy and configure the gateway for your organization. + +## Overview + +Setting up FIRST involves two main components: + +1. **Gateway Setup**: The central API service that handles authentication and routing +2. **Inference Backend Setup**: The actual inference servers where models run + +These can be deployed independently and connected together through configuration. + +## Prerequisites + +Before you begin, ensure you have: + +- [x] Python 3.12 or later +- [x] Docker and Docker Compose (for Docker deployment) +- [x] PostgreSQL Server (or use Docker) +- [x] Globus Account +- [x] Access to compute resources (for inference backends) + +## Deployment Architecture + +Choose your deployment approach: + +### Gateway Deployment Options + +**Docker Deployment (Recommended)** + +- Quick setup with Docker Compose +- **Pros**: Easy to deploy, includes all dependencies, portable +- **Cons**: Requires Docker knowledge +- **[Docker Guide →](gateway-setup/docker.md)** + +**Bare Metal Deployment** + +- Direct installation on your server infrastructure +- **Pros**: More control, better performance, easier debugging +- **Cons**: Manual dependency management +- **[Bare Metal Guide →](gateway-setup/bare-metal.md)** + +### Inference Backend Options + +**Globus Compute + vLLM** _(Recommended for Production)_ + +- Deploy vLLM on HPC clusters with Globus Compute for remote execution +- **Best for**: Multi-cluster, federated deployments, HPC environments +- **[Globus Compute Setup →](inference-setup/globus-compute.md)** + +**Local vLLM** + +- Run vLLM inference server locally without Globus Compute +- **Best for**: Single-node deployments, development +- **[Local vLLM Setup →](inference-setup/local-vllm.md)** + +**Direct API Connection** + +- Connect to existing OpenAI-compatible APIs (OpenAI, Anthropic, etc.) +- **Best for**: Simple setup, using commercial APIs +- **[Direct API Setup →](inference-setup/direct-api.md)** + +## Setup Workflow + +### Phase 1: Gateway Setup + +1. Choose your deployment method (Docker or Bare Metal) +2. Register Globus applications +3. Configure environment variables +4. Initialize the database +5. Start the gateway service + +### Phase 2: Inference Backend Setup + +1. Choose your backend type +2. Install required software (vLLM, Globus Compute, etc.) +3. Configure the backend +4. Register endpoints/functions +5. Test the connection + +### Phase 3: Connect Gateway and Backend + +1. Update fixture files with backend details +2. Load fixtures into the gateway database +3. Verify end-to-end functionality + +## Common Patterns + +### Pattern 1: Quick Local Development + +```mermaid +graph LR + A[Docker Gateway] --> B[Local vLLM] + B --> C[Small Model
OPT-125M] +``` + +**Use**: Development and testing + +**Setup Time**: ~15 minutes + +**Resources**: 1 GPU or CPU + +### Pattern 2: Production Single Cluster + +```mermaid +graph LR + A[Bare Metal Gateway] --> B[Globus Compute] + B --> C[HPC Cluster] + C --> D[Multiple Models] +``` + +**Use**: Production deployment on single HPC cluster + +**Setup Time**: ~2 hours + +**Resources**: HPC cluster access + +### Pattern 3: Federated Multi-Cluster + +```mermaid +graph LR + A[Gateway] --> B[Cluster 1
Globus Compute] + A --> C[Cluster 2
Globus Compute] + A --> D[Cluster 3
Globus Compute] +``` + +**Use**: Maximum availability and resource pooling + +**Setup Time**: ~4 hours + +**Resources**: Multiple HPC clusters + +## Next Steps + +Ready to get started? Choose your path: + +- **Quick Start**: [Docker Deployment](gateway-setup/docker.md) +- **Full Setup**: [Bare Metal Deployment](gateway-setup/bare-metal.md) +- **Backend Setup**: [Inference Backend Overview](inference-setup/index.md) + +## Additional Resources + +- [Configuration Reference](gateway-setup/configuration.md) +- [Production Best Practices](deployment/production.md) +- [Monitoring & Troubleshooting](monitoring.md) +- [Kubernetes Deployment](deployment/kubernetes.md) (Coming Soon) + diff --git a/docs/admin-guide/inference-setup/direct-api.md b/docs/admin-guide/inference-setup/direct-api.md new file mode 100644 index 00000000..9d60c66e --- /dev/null +++ b/docs/admin-guide/inference-setup/direct-api.md @@ -0,0 +1,383 @@ +# Direct API Connection + +This guide shows you how to connect the FIRST Gateway to existing OpenAI-compatible APIs without running any local inference infrastructure. + +## Overview + +The Direct API backend allows you to: + +- Proxy requests to commercial APIs (OpenAI, Anthropic, etc.) +- Add Globus authentication to existing APIs +- Manage API keys centrally +- Route between multiple API providers + +## Architecture + +```mermaid +graph LR + A[User] -->|Globus Token| B[FIRST Gateway] + B -->|API Key| C[OpenAI API] + B -->|API Key| D[Anthropic API] + B -->|API Key| E[Custom API] +``` + +## Prerequisites + +- FIRST Gateway deployed and running +- API keys from your providers +- A way to host a status manifest (static file or endpoint) + +## Step 1: Create Status Manifest + +The gateway uses a status manifest to discover available endpoints. Create a JSON file: + +```json +{ + "openai-gpt4": { + "status": "Live", + "model": "OpenAI GPT-4", + "description": "GPT-4 models via OpenAI API", + "experts": [ + "gpt-4", + "gpt-4-turbo", + "gpt-4o", + "gpt-4o-mini" + ], + "url": "https://api.openai.com/v1", + "endpoint_id": "openai-production" + }, + "anthropic-claude": { + "status": "Live", + "model": "Anthropic Claude", + "description": "Claude models via Anthropic API", + "experts": [ + "claude-3-opus-20240229", + "claude-3-sonnet-20240229", + "claude-3-haiku-20240307" + ], + "url": "https://api.anthropic.com/v1", + "endpoint_id": "anthropic-production" + } +} +``` + +### Manifest Field Descriptions + +| Field | Required | Description | +|-------|----------|-------------| +| `status` | Yes | "Live", "Offline", or "Maintenance" | +| `model` | Yes | Human-readable model description | +| `description` | Yes | Detailed description | +| `experts` | Yes | Array of model identifiers | +| `url` | Yes | Base URL for the API | +| `endpoint_id` | Yes | Unique identifier (used for API key mapping) | + +## Step 2: Host the Status Manifest + +### Option A: Static File Server + +```bash +# Simple Python HTTP server +mkdir -p /var/www/metis +cp status.json /var/www/metis/ +cd /var/www/metis +python3 -m http.server 8055 +``` + +### Option B: Nginx + +```nginx +server { + listen 80; + server_name status.yourdomain.com; + + location / { + root /var/www/metis; + add_header Content-Type application/json; + add_header Access-Control-Allow-Origin *; + } +} +``` + +### Option C: S3/Cloud Storage + +Upload `status.json` to a public S3 bucket or equivalent: + +```bash +# AWS S3 +aws s3 cp status.json s3://your-bucket/status.json --acl public-read + +# Access via: https://your-bucket.s3.amazonaws.com/status.json +``` + +### Option D: Local for Docker (Development Only) + +For local Docker testing: + +```bash +# On host machine +mkdir -p deploy/docker/examples +cat > deploy/docker/examples/metis-status.json << 'EOF' +{ + "openai-gateway": { + "status": "Live", + "model": "OpenAI Pass-through", + "description": "Routes to OpenAI's GPT models", + "experts": ["gpt-4o-mini", "gpt-4"], + "url": "https://api.openai.com/v1", + "endpoint_id": "openai-production" + } +} +EOF + +# Serve it +python3 -m http.server 8055 --directory deploy/docker/examples +``` + +Then use `http://host.docker.internal:8055/metis-status.json` in your Docker `.env`. + +## Step 3: Configure Gateway + +Add these to your gateway's `.env` file: + +```dotenv +# URL to your status manifest +METIS_STATUS_URL="http://your-server:8055/status.json" + +# API keys mapped to endpoint_id from the manifest +METIS_API_TOKENS='{"openai-production": "sk-proj-...", "anthropic-production": "sk-ant-..."}' +``` + +### Environment Variable Format + +**METIS_STATUS_URL**: Direct URL to your JSON manifest + +**METIS_API_TOKENS**: JSON object where: + +- Keys are `endpoint_id` values from your manifest +- Values are the API keys for those services + +Example with multiple providers: + +```dotenv +METIS_API_TOKENS='{ + "openai-production": "sk-proj-abc123...", + "anthropic-production": "sk-ant-xyz789...", + "custom-api": "custom-key-here" +}' +``` + +## Step 4: Restart Gateway + +### Docker + +```bash +docker-compose up -d inference-gateway +``` + +### Bare Metal + +```bash +sudo systemctl restart inference-gateway +``` + +## Step 5: Test the Connection + +Get a Globus token: + +```bash +export TOKEN=$(python inference-auth-token.py get_access_token) +``` + +Test OpenAI endpoint: + +```bash +curl -X POST http://localhost:8000/resource_server/metis/api/v1/chat/completions \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gpt-4o-mini", + "messages": [{"role": "user", "content": "Hello from FIRST!"}], + "stream": false + }' +``` + +Expected response: + +```json +{ + "id": "chatcmpl-...", + "object": "chat.completion", + "created": 1234567890, + "model": "gpt-4o-mini", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "Hello! How can I assist you today?" + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 15, + "total_tokens": 25 + } +} +``` + +## Advanced Configuration + +### Multiple Endpoints Per Provider + +```json +{ + "openai-us-east": { + "status": "Live", + "model": "OpenAI US East", + "experts": ["gpt-4"], + "url": "https://api.openai.com/v1", + "endpoint_id": "openai-us-east" + }, + "openai-eu-west": { + "status": "Live", + "model": "OpenAI EU West", + "experts": ["gpt-4"], + "url": "https://api.openai.com/v1", + "endpoint_id": "openai-eu-west" + } +} +``` + +### Custom API Headers + +For APIs requiring additional headers, you can extend the gateway code or use environment variables. Contact your administrator for custom integration. + +### Load Balancing + +The gateway automatically load-balances across all "Live" endpoints for the same model. + +### Failover + +If one endpoint returns an error, the gateway automatically tries the next available endpoint. + +## Monitoring + +### Check Endpoint Status + +The gateway periodically fetches the status manifest. View logs: + +```bash +# Docker +docker-compose logs -f inference-gateway | grep metis + +# Bare metal +tail -f logs/django_info.log | grep metis +``` + +### Track API Usage + +Monitor usage through: + +- Gateway access logs +- Provider API dashboards (OpenAI, Anthropic) +- Custom usage tracking (implement in gateway) + +## Cost Management + +### Setting Budgets + +Configure per-user or per-group budgets in your application logic or via API key restrictions at the provider level. + +### Rate Limiting + +The gateway supports rate limiting per user/group. Configure in Django admin or via settings. + +## Security Considerations + +### API Key Security + +!!! danger "Protect Your API Keys" + - Never commit API keys to version control + - Use environment variables or secrets management + - Rotate keys regularly + - Use separate keys for dev/staging/production + +### Status Manifest Security + +If your manifest contains sensitive information: + +- Serve over HTTPS +- Implement authentication (basic auth, token) +- Restrict IP access via firewall + +### Access Control + +Restrict which Globus groups can access which APIs: + +```dotenv +GLOBUS_GROUPS="allowed-group-uuid-1 allowed-group-uuid-2" +``` + +## Troubleshooting + +### Gateway can't fetch status manifest + +Check connectivity: + +```bash +curl http://your-server:8055/status.json +``` + +Verify `METIS_STATUS_URL` is correct and accessible from the gateway. + +### Authentication errors from provider + +- Verify API key is correct +- Check key hasn't expired +- Ensure key has required permissions +- Check provider status page + +### Model not found + +Ensure the model name matches exactly what's in the `experts` array of your manifest. + +### Rate limiting errors + +- Check provider rate limits +- Implement gateway-side rate limiting +- Consider upgrading provider plan + +## Example: Adding Azure OpenAI + +```json +{ + "azure-openai": { + "status": "Live", + "model": "Azure OpenAI", + "description": "GPT models via Azure OpenAI Service", + "experts": [ + "gpt-4", + "gpt-35-turbo" + ], + "url": "https://your-resource.openai.azure.com/openai/deployments/your-deployment", + "endpoint_id": "azure-openai-prod" + } +} +``` + +Azure requires additional configuration - consult Azure OpenAI documentation. + +## Next Steps + +- [User Guide](../../user-guide/index.md) - How to use the API +- [Monitoring](../monitoring.md) - Set up monitoring and alerts +- [Local vLLM](local-vllm.md) - Add local inference capabilities +- [Production Best Practices](../deployment/production.md) + +## Additional Resources + +- [OpenAI API Documentation](https://platform.openai.com/docs/api-reference) +- [Anthropic API Documentation](https://docs.anthropic.com/claude/reference) +- [Gateway Configuration Reference](../gateway-setup/configuration.md) + diff --git a/docs/admin-guide/inference-setup/globus-compute.md b/docs/admin-guide/inference-setup/globus-compute.md new file mode 100644 index 00000000..e119c279 --- /dev/null +++ b/docs/admin-guide/inference-setup/globus-compute.md @@ -0,0 +1,622 @@ +# Globus Compute + vLLM Setup + +This guide shows you how to deploy vLLM on HPC clusters or remote servers using Globus Compute for federated inference. + +## Overview + +This is the recommended approach for: + +- Multi-cluster federated deployments +- HPC environments with job schedulers (PBS, Slurm) +- Organizations requiring high availability +- Remote execution with secure authentication + +## Architecture + +```mermaid +graph TB + A[User] -->|Globus Token| B[FIRST Gateway] + B -->|Globus Compute| C[HPC Cluster 1] + B -->|Globus Compute| D[HPC Cluster 2] + B -->|Globus Compute| E[Local Server] + C -->|Job Scheduler| F[vLLM + Model] + D -->|Job Scheduler| G[vLLM + Model] + E -->|Direct| H[vLLM + Model] +``` + +## Prerequisites + +- FIRST Gateway deployed with Service Account application credentials +- Access to compute resources (HPC cluster or powerful workstation) +- GPU resources for running models +- Python 3.12+ (same version as gateway) +- Globus Compute SDK and Endpoint software + +## Part 1: Setup on Compute Resource + +This is done on the machine(s) where models will run. + +### Step 1: Create Python Environment + +!!! warning "Version Match" + Use the **same Python version** as your gateway to avoid compatibility issues. + +```bash +# Using conda (recommended for HPC) +conda create -n vllm-env python=3.12 -y +conda activate vllm-env + +# OR using venv +python3.12 -m venv vllm-env +source vllm-env/bin/activate +``` + +### Step 2: Install vLLM + +```bash +# For CUDA 12.1 (default) +pip install vllm + +# For specific CUDA version +pip install vllm-cu118 # CUDA 11.8 + +# From source (for latest features) +git clone https://github.com/vllm-project/vllm.git +cd vllm +pip install -e . +``` + +### Step 3: Install Globus Compute + +```bash +pip install globus-compute-sdk globus-compute-endpoint +``` + +### Step 4: Export Service Account Credentials + +These are from your Gateway's Service Account application: + +```bash +export GLOBUS_COMPUTE_CLIENT_ID="" +export GLOBUS_COMPUTE_CLIENT_SECRET="" +``` + +Add to your `~/.bashrc` or `~/.bash_profile` for persistence: + +```bash +echo 'export GLOBUS_COMPUTE_CLIENT_ID="your-id"' >> ~/.bashrc +echo 'export GLOBUS_COMPUTE_CLIENT_SECRET="your-secret"' >> ~/.bashrc +``` + +### Step 5: Register Globus Compute Functions + +Navigate to the gateway repository's compute-functions directory: + +```bash +cd /path/to/inference-gateway/compute-functions +``` + +#### Register Inference Function + +```bash +python vllm_register_function_with_streaming.py +``` + +Output: + +``` +Function registered with UUID: 12345678-1234-1234-1234-123456789abc +The UUID is stored in vllm_register_function_streaming.txt +``` + +**Save this Function UUID** - you'll need it later. + +#### Register Status Function (Optional but Recommended) + +For HPC clusters with job schedulers: + +```bash +python qstat_register_function.py +``` + +Save the Function UUID from the output. + +#### Register Batch Function (Optional) + +For batch processing support: + +```bash +python vllm_batch_function.py +``` + +Save the Function UUID. + +### Step 6: Configure Globus Compute Endpoint + +Create a new endpoint: + +```bash +globus-compute-endpoint configure my-inference-endpoint +``` + +This creates `~/.globus_compute/my-inference-endpoint/config.yaml`. + +#### For Local/Workstation Deployment + +Edit `config.yaml`: + +```yaml +display_name: My Inference Endpoint +engine: + type: GlobusComputeEngine + provider: + type: LocalProvider + init_blocks: 1 + max_blocks: 1 + min_blocks: 0 + +# Allow only your registered functions +allowed_functions: + - 12345678-1234-1234-1234-123456789abc # Your vLLM function UUID + - 87654321-4321-4321-4321-cba987654321 # Your qstat function UUID (if any) + +# Activate your environment before running functions +worker_init: | + source /path/to/vllm-env/bin/activate + # OR: conda activate vllm-env +``` + +#### For HPC with PBS (e.g., ALCF Sophia) + +```yaml +display_name: Sophia vLLM Endpoint +engine: + type: GlobusComputeEngine + provider: + type: PBSProProvider + account: YourProjectAccount + queue: demand + nodes_per_block: 1 + init_blocks: 1 + max_blocks: 4 + min_blocks: 0 + walltime: "06:00:00" + scheduler_options: | + #PBS -l filesystems=home:eagle + #PBS -l place=scatter + worker_init: | + module load conda + conda activate /path/to/vllm-env + # OR: source /path/to/common_setup.sh + +# GPU allocation +max_workers_per_node: 1 + +# Allow only your registered functions +allowed_functions: + - 12345678-1234-1234-1234-123456789abc +``` + +#### For HPC with Slurm + +```yaml +display_name: Slurm vLLM Endpoint +engine: + type: GlobusComputeEngine + provider: + type: SlurmProvider + account: your_account + partition: gpu + nodes_per_block: 1 + init_blocks: 1 + max_blocks: 4 + walltime: "06:00:00" + scheduler_options: | + #SBATCH --gpus-per-node=1 + #SBATCH --mem=64G + worker_init: | + source /path/to/vllm-env/bin/activate + +allowed_functions: + - 12345678-1234-1234-1234-123456789abc +``` + +### Step 7: Start the Endpoint + +```bash +globus-compute-endpoint start my-inference-endpoint +``` + +Output: + +``` +Starting endpoint; registered ID: abcdef12-3456-7890-abcd-ef1234567890 +``` + +**Save this Endpoint UUID** - you'll need it for gateway configuration. + +Verify it's running: + +```bash +globus-compute-endpoint list +``` + +View logs: + +```bash +globus-compute-endpoint log my-inference-endpoint +``` + +## Part 2: Configure Gateway + +Now configure the gateway to use your Globus Compute endpoint. + +### Step 1: Create Endpoint Fixture + +On your gateway machine, edit `fixtures/endpoints.json`: + +#### Single Endpoint Configuration + +```json +[ + { + "model": "resource_server.endpoint", + "pk": 1, + "fields": { + "endpoint_slug": "sophia-vllm-llama3-8b", + "cluster": "sophia", + "framework": "vllm", + "model": "meta-llama/Meta-Llama-3-8B-Instruct", + "api_port": 8000, + "endpoint_uuid": "abcdef12-3456-7890-abcd-ef1234567890", + "function_uuid": "12345678-1234-1234-1234-123456789abc", + "batch_endpoint_uuid": "", + "batch_function_uuid": "", + "allowed_globus_groups": "" + } + } +] +``` + +#### Multiple Clusters Configuration + +```json +[ + { + "model": "resource_server.endpoint", + "pk": 1, + "fields": { + "endpoint_slug": "sophia-vllm-llama3-8b", + "cluster": "sophia", + "framework": "vllm", + "model": "meta-llama/Meta-Llama-3-8B-Instruct", + "api_port": 8000, + "endpoint_uuid": "sophia-endpoint-uuid", + "function_uuid": "sophia-function-uuid", + "allowed_globus_groups": "" + } + }, + { + "model": "resource_server.endpoint", + "pk": 2, + "fields": { + "endpoint_slug": "polaris-vllm-llama3-70b", + "cluster": "polaris", + "framework": "vllm", + "model": "meta-llama/Llama-2-70b-chat-hf", + "api_port": 8000, + "endpoint_uuid": "polaris-endpoint-uuid", + "function_uuid": "polaris-function-uuid", + "allowed_globus_groups": "" + } + } +] +``` + +### Step 2: Federated Endpoints (Optional but Recommended) + +For automatic load balancing and failover, use federated endpoints. + +Edit `fixtures/federated_endpoints.json`: + +```json +[ + { + "model": "resource_server.federatedendpoint", + "pk": 1, + "fields": { + "name": "Llama-3 8B (Federated)", + "slug": "federated-llama3-8b", + "target_model_name": "meta-llama/Meta-Llama-3-8B-Instruct", + "description": "Federated access to Llama-3 8B across multiple clusters", + "targets": [ + { + "cluster": "sophia", + "framework": "vllm", + "model": "meta-llama/Meta-Llama-3-8B-Instruct", + "endpoint_slug": "sophia-vllm-llama3-8b", + "endpoint_uuid": "sophia-endpoint-uuid", + "function_uuid": "sophia-function-uuid", + "api_port": 8000 + }, + { + "cluster": "polaris", + "framework": "vllm", + "model": "meta-llama/Meta-Llama-3-8B-Instruct", + "endpoint_slug": "polaris-vllm-llama3-8b", + "endpoint_uuid": "polaris-endpoint-uuid", + "function_uuid": "polaris-function-uuid", + "api_port": 8000 + } + ] + } + } +] +``` + +### Step 3: Load Fixtures + +```bash +# Docker +docker-compose exec inference-gateway python manage.py loaddata fixtures/endpoints.json +docker-compose exec inference-gateway python manage.py loaddata fixtures/federated_endpoints.json + +# Bare Metal +python manage.py loaddata fixtures/endpoints.json +python manage.py loaddata fixtures/federated_endpoints.json +``` + +## Part 3: Testing + +### Test Globus Compute Endpoint + +From your gateway machine, test that you can reach the endpoint: + +```python +from globus_compute_sdk import Client + +gcc = Client() +endpoint_id = "abcdef12-3456-7890-abcd-ef1234567890" +function_id = "12345678-1234-1234-1234-123456789abc" + +# Simple test function +def hello(): + return "Hello from Globus Compute!" + +# Register and run +func_uuid = gcc.register_function(hello) +task = gcc.run(endpoint_id=endpoint_id, function_id=func_uuid) +print(gcc.get_result(task)) +``` + +### Test vLLM via Gateway + +Get a Globus token: + +```bash +export TOKEN=$(python inference-auth-token.py get_access_token) +``` + +Test non-federated endpoint: + +```bash +curl -X POST http://localhost:8000/resource_server/sophia/vllm/v1/chat/completions \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "meta-llama/Meta-Llama-3-8B-Instruct", + "messages": [{"role": "user", "content": "What is Globus Compute?"}], + "max_tokens": 100 + }' +``` + +Test federated endpoint: + +```bash +curl -X POST http://localhost:8000/resource_server/v1/chat/completions \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "meta-llama/Meta-Llama-3-8B-Instruct", + "messages": [{"role": "user", "content": "What is Globus Compute?"}], + "max_tokens": 100 + }' +``` + +## Advanced Configuration + +### Hot Nodes / Keep-Alive + +For low-latency inference, keep compute nodes warm: + +```yaml +# In config.yaml +engine: + provider: + min_blocks: 1 # Always keep 1 node running + init_blocks: 2 # Start with 2 nodes +``` + +### Multi-Node vLLM + +For very large models (70B+), use tensor parallelism across multiple GPUs/nodes: + +See `compute-endpoints/sophia-vllm-multinode-example.yaml` for configuration examples. + +### Custom Inference Scripts + +The registered functions use scripts in `compute-functions/`. You can customize: + +- `launch_vllm_model.sh`: How vLLM is started +- `vllm_register_function_with_streaming.py`: The inference function logic + +### Batch Processing + +Enable batch processing by registering and configuring batch functions: + +```bash +python vllm_batch_function.py +``` + +Add batch UUIDs to your endpoint fixture: + +```json +{ + "batch_endpoint_uuid": "batch-endpoint-uuid", + "batch_function_uuid": "batch-function-uuid" +} +``` + +## Monitoring + +### Endpoint Status + +```bash +# Check endpoint status +globus-compute-endpoint status my-inference-endpoint + +# View logs +globus-compute-endpoint log my-inference-endpoint -n 50 + +# Follow logs +tail -f ~/.globus_compute/my-inference-endpoint/endpoint.log +``` + +### Job Scheduler Monitoring + +```bash +# PBS +qstat -u $USER + +# Slurm +squeue -u $USER +``` + +### Gateway Logs + +```bash +# Docker +docker-compose logs -f inference-gateway + +# Bare Metal +tail -f logs/django_info.log +``` + +## Troubleshooting + +### Endpoint won't start + +Check logs: + +```bash +globus-compute-endpoint log my-inference-endpoint +``` + +Common issues: + +- Service account credentials not set +- Function UUIDs not in `allowed_functions` +- Python environment activation fails +- Job scheduler configuration incorrect + +### Function execution fails + +- Verify environment can run vLLM: Test manually on compute node +- Check function is allowed in `config.yaml` +- Ensure Service Account has permissions +- Check job scheduler logs + +### Model not loading + +- Verify GPU allocation in scheduler options +- Check VRAM requirements +- Ensure model path/name is correct +- Check Hugging Face token if needed + +### Gateway can't connect + +- Verify endpoint is running: `globus-compute-endpoint list` +- Check endpoint UUID in fixtures is correct +- Ensure Service Account credentials match in both places +- Check network connectivity (firewall, VPN) + +## HPC-Specific Considerations + +### ALCF Systems (Polaris, Sophia) + +- Use `/lus/eagle` for model cache +- Load CUDA modules in `worker_init` +- Set appropriate walltime +- Use `#PBS -l filesystems=home:eagle` + +### Other HPC Centers + +- Check local documentation for: + - GPU allocation syntax + - File system paths + - Module system + - Queue/partition names + - Accounting project codes + +## Scaling + +### Auto-Scaling + +Configure `max_blocks` to allow automatic scaling: + +```yaml +engine: + provider: + init_blocks: 1 + min_blocks: 0 + max_blocks: 10 # Scale up to 10 nodes +``` + +### Load Balancing + +Federated endpoints automatically load balance across all "Live" targets. + +### Geographic Distribution + +Deploy endpoints at different locations and configure federated routing for optimal latency. + +## Security + +### Function Allowlisting + +Always specify `allowed_functions` in `config.yaml`: + +```yaml +allowed_functions: + - 12345678-1234-1234-1234-123456789abc # Only your functions +``` + +### Group-Based Access + +Restrict endpoints to specific Globus groups: + +```json +{ + "allowed_globus_groups": "group-uuid-1,group-uuid-2" +} +``` + +### Network Security + +- Globus Compute uses HTTPS and mutual TLS +- No inbound ports need to be opened on compute resources +- All communication is outbound from endpoint to Globus services + +## Next Steps + +- [Production Best Practices](../deployment/production.md) +- [Monitoring & Troubleshooting](../monitoring.md) +- [User Guide](../../user-guide/index.md) + +## Additional Resources + +- [Globus Compute Documentation](https://globus-compute.readthedocs.io/) +- [vLLM Documentation](https://docs.vllm.ai/) +- [Example Configurations](../../../compute-endpoints/) +- [Function Registration Scripts](../../../compute-functions/) + diff --git a/docs/admin-guide/inference-setup/index.md b/docs/admin-guide/inference-setup/index.md new file mode 100644 index 00000000..1450fa10 --- /dev/null +++ b/docs/admin-guide/inference-setup/index.md @@ -0,0 +1,223 @@ +# Inference Backend Setup + +The FIRST Inference Gateway supports multiple types of inference backends. Choose the approach that best fits your use case. + +## Backend Options + +### 1. Globus Compute + vLLM (Recommended) + +Deploy vLLM on HPC clusters or multiple servers with Globus Compute for remote execution and federated routing. + +**Best for:** + +- Multi-cluster deployments +- HPC environments +- Federated inference across organizations +- Production deployments requiring high availability + +**Pros:** + +- Federated routing across clusters +- Automatic failover +- HPC job scheduler integration +- Scales across organizations +- Secure, authenticated remote execution + +**Cons:** + +- More complex setup +- Requires Globus Compute knowledge +- Additional configuration overhead + +[→ Setup Globus Compute + vLLM](globus-compute.md) + +--- + +### 2. Local vLLM Setup + +Run vLLM inference server locally without Globus Compute. + +**Best for:** + +- Single-server deployments +- Development and testing with local models +- Controlled environments where Globus Compute isn't needed +- Simple architectures + +**Pros:** + +- Full control over models and data +- No external dependencies +- Simple architecture +- Lower latency for local requests + +**Cons:** + +- Requires GPU resources +- Single point of failure +- Manual scaling +- No federated routing + +[→ Setup Local vLLM](local-vllm.md) + +--- + +### 3. Direct API Connection + +Connect to existing OpenAI-compatible APIs without any local inference infrastructure. + +**Best for:** + +- Quick testing and development +- Using commercial API services (OpenAI, Anthropic, etc.) +- Organizations without GPU resources +- Proxying/adding authentication to existing APIs + +**Pros:** + +- Fastest setup (5-10 minutes) +- No local compute resources needed +- Scales with the provider +- Multiple models immediately available + +**Cons:** + +- Costs per API call +- Data leaves your infrastructure +- Dependent on third-party service + +[→ Setup Direct API Connection](direct-api.md) + +--- + +## Comparison Matrix + +| Feature | Globus Compute + vLLM | Local vLLM | Direct API | +|---------|----------------------|------------|------------| +| Setup Time | 2-4 hours | 30-60 min | 5-10 min | +| GPU Required | Yes | Yes | No | +| Data Privacy | Local | Local | External | +| Multi-Cluster | Yes | No | No | +| Failover | Automatic | Manual | Provider | +| Cost | Infrastructure | Infrastructure | Pay-per-use | +| HPC Integration | Yes | No | No | +| Complexity | High | Medium | Low | + +## Decision Tree + +```mermaid +graph TD + A[Choose Backend] --> B{Have GPU?} + B -->|No| C[Direct API] + B -->|Yes| D{Multiple Clusters?} + D -->|No| E{Need HPC Integration?} + D -->|Yes| F[Globus Compute + vLLM] + E -->|No| G[Local vLLM] + E -->|Yes| F +``` + +## Combined Approach + +You can use multiple backends simultaneously! The gateway supports: + +- **Multiple Direct API connections**: Route to OpenAI, Anthropic, etc. +- **Mix Local and Remote**: Some models local, some via Globus Compute +- **Federated Endpoints**: Route same model across multiple clusters + +Example combined setup: + +```json +{ + "endpoints": [ + { + "cluster": "openai", + "model": "gpt-4", + "type": "direct_api" + }, + { + "cluster": "local", + "model": "llama-3-8b", + "type": "vllm" + }, + { + "cluster": "sophia", + "model": "llama-3-70b", + "type": "globus_compute" + } + ] +} +``` + +## Prerequisites by Backend Type + +### All Backends + +- FIRST Gateway already deployed +- Access to gateway configuration +- Admin access to load fixtures + +### Direct API + +- API keys from providers +- Status manifest endpoint (can be static file) + +### Local vLLM + +- GPU with sufficient VRAM for your model +- Python 3.12+ +- Ability to run vLLM server + +### Globus Compute + vLLM + +- All Local vLLM requirements plus: +- Globus Service Account application +- Access to HPC cluster (optional) +- Ability to configure Globus Compute endpoints + +## Setup Workflow + +### Phase 1: Choose and Setup Backend + +Follow the guide for your chosen backend type. + +### Phase 2: Register with Gateway + +Update fixture files (`fixtures/endpoints.json` or `fixtures/federated_endpoints.json`) with your backend details. + +### Phase 3: Load Configuration + +```bash +# Docker +docker-compose exec inference-gateway python manage.py loaddata fixtures/endpoints.json + +# Bare metal +python manage.py loaddata fixtures/endpoints.json +``` + +### Phase 4: Test + +Send a test request to verify the connection: + +```bash +curl -X POST http://localhost:8000/resource_server/v1/chat/completions \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "your-model-name", + "messages": [{"role": "user", "content": "Hello!"}] + }' +``` + +## Next Steps + +Choose your backend setup guide: + +- [Globus Compute + vLLM](globus-compute.md) _(Recommended for Production)_ +- [Local vLLM Setup](local-vllm.md) +- [Direct API Connection](direct-api.md) + +After setup: + +- [Production Best Practices](../deployment/production.md) +- [Monitoring & Troubleshooting](../monitoring.md) + diff --git a/docs/admin-guide/inference-setup/local-vllm.md b/docs/admin-guide/inference-setup/local-vllm.md new file mode 100644 index 00000000..4db606d6 --- /dev/null +++ b/docs/admin-guide/inference-setup/local-vllm.md @@ -0,0 +1,471 @@ +# Local vLLM Setup + +This guide shows you how to run vLLM inference server locally and connect it to the FIRST Gateway **without** Globus Compute. + +## Overview + +This setup is ideal for: + +- Single-server deployments +- Development and testing +- Scenarios where Globus Compute overhead isn't needed +- Direct control over the inference process + +## Architecture + +```mermaid +graph LR + A[User] -->|Globus Token| B[FIRST Gateway] + B -->|HTTP| C[vLLM Server] + C -->|GPU| D[Model] +``` + +## Prerequisites + +- NVIDIA GPU with sufficient VRAM for your model +- CUDA 11.8 or later +- Python 3.12+ +- Docker (optional, for containerized vLLM) + +## Step 1: Install vLLM + +### Option A: Install from Source (Recommended) + +```bash +# Create virtual environment +python3.12 -m venv vllm-env +source vllm-env/bin/activate + +# Clone and install vLLM +git clone https://github.com/vllm-project/vllm.git +cd vllm +pip install -e . + +# Install additional dependencies +pip install openai # For testing +``` + +### Option B: Install via pip + +```bash +python3.12 -m venv vllm-env +source vllm-env/bin/activate + +pip install vllm + +# For specific CUDA version +pip install vllm # CUDA 12.1 by default +# OR +pip install vllm-cu118 # For CUDA 11.8 +``` + +### Option C: Use Docker + +```bash +docker pull vllm/vllm-openai:latest +``` + +## Step 2: Download a Model + +Choose a model based on your GPU VRAM: + +| Model | VRAM Required | Performance | +|-------|---------------|-------------| +| facebook/opt-125m | ~1GB | Fast, good for testing | +| meta-llama/Llama-2-7b-chat-hf | ~14GB | Good quality | +| meta-llama/Meta-Llama-3-8B-Instruct | ~16GB | Better quality | +| meta-llama/Llama-2-13b-chat-hf | ~26GB | High quality | +| meta-llama/Llama-2-70b-chat-hf | ~140GB | Best quality (multi-GPU) | + +### Using Hugging Face + +```bash +# Login to Hugging Face (for gated models like Llama) +pip install huggingface-hub +huggingface-cli login + +# Models will auto-download on first use +# Or pre-download: +huggingface-cli download meta-llama/Meta-Llama-3-8B-Instruct +``` + +## Step 3: Start vLLM Server + +### Basic Start + +```bash +source vllm-env/bin/activate + +python -m vllm.entrypoints.openai.api_server \ + --model facebook/opt-125m \ + --host 0.0.0.0 \ + --port 8001 +``` + +### Production Configuration + +```bash +python -m vllm.entrypoints.openai.api_server \ + --model meta-llama/Meta-Llama-3-8B-Instruct \ + --host 0.0.0.0 \ + --port 8001 \ + --tensor-parallel-size 1 \ + --gpu-memory-utilization 0.9 \ + --max-model-len 4096 \ + --dtype auto \ + --enable-prefix-caching +``` + +### Multi-GPU Configuration + +```bash +# For 4 GPUs +python -m vllm.entrypoints.openai.api_server \ + --model meta-llama/Llama-2-70b-chat-hf \ + --host 0.0.0.0 \ + --port 8001 \ + --tensor-parallel-size 4 \ + --gpu-memory-utilization 0.9 +``` + +### Using Docker + +```bash +docker run --gpus all \ + -v ~/.cache/huggingface:/root/.cache/huggingface \ + -p 8001:8000 \ + --env "HUGGING_FACE_HUB_TOKEN=" \ + vllm/vllm-openai:latest \ + --model facebook/opt-125m \ + --host 0.0.0.0 \ + --port 8000 +``` + +## Step 4: Test vLLM Server + +```bash +curl http://localhost:8001/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "facebook/opt-125m", + "messages": [ + {"role": "user", "content": "Hello!"} + ] + }' +``` + +## Step 5: Create systemd Service (Optional) + +For production deployments, run vLLM as a system service: + +```bash +sudo nano /etc/systemd/system/vllm.service +``` + +Add: + +```ini +[Unit] +Description=vLLM Inference Server +After=network.target + +[Service] +Type=simple +User=your-username +WorkingDirectory=/home/your-username +Environment="PATH=/home/your-username/vllm-env/bin" +ExecStart=/home/your-username/vllm-env/bin/python -m vllm.entrypoints.openai.api_server \ + --model meta-llama/Meta-Llama-3-8B-Instruct \ + --host 0.0.0.0 \ + --port 8001 \ + --tensor-parallel-size 1 \ + --gpu-memory-utilization 0.9 +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +Enable and start: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable vllm +sudo systemctl start vllm +sudo systemctl status vllm +``` + +## Step 6: Configure Gateway + +### Update Gateway Environment + +Edit your gateway's `.env`: + +```dotenv +# Add if using local vLLM without Globus Compute +LOCAL_VLLM_URL="http://localhost:8001" +``` + +### Create Endpoint Fixture + +Create or edit `fixtures/endpoints.json` in your gateway directory: + +```json +[ + { + "model": "resource_server.endpoint", + "pk": 1, + "fields": { + "endpoint_slug": "local-vllm-opt-125m", + "cluster": "local", + "framework": "vllm", + "model": "facebook/opt-125m", + "api_port": 8001, + "endpoint_uuid": "", + "function_uuid": "", + "batch_endpoint_uuid": "", + "batch_function_uuid": "", + "allowed_globus_groups": "" + } + } +] +``` + +For a Llama model: + +```json +[ + { + "model": "resource_server.endpoint", + "pk": 2, + "fields": { + "endpoint_slug": "local-vllm-llama3-8b", + "cluster": "local", + "framework": "vllm", + "model": "meta-llama/Meta-Llama-3-8B-Instruct", + "api_port": 8001, + "endpoint_uuid": "", + "function_uuid": "", + "batch_endpoint_uuid": "", + "batch_function_uuid": "", + "allowed_globus_groups": "" + } + } +] +``` + +### Load Fixture + +```bash +# Docker +docker-compose exec inference-gateway python manage.py loaddata fixtures/endpoints.json + +# Bare metal +python manage.py loaddata fixtures/endpoints.json +``` + +## Step 7: Test End-to-End + +Get a Globus token: + +```bash +export TOKEN=$(python inference-auth-token.py get_access_token) +``` + +Test via gateway: + +```bash +curl -X POST http://localhost:8000/resource_server/local/vllm/v1/chat/completions \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "facebook/opt-125m", + "messages": [{"role": "user", "content": "Explain machine learning in one sentence"}], + "max_tokens": 50 + }' +``` + +## Performance Tuning + +### GPU Memory Optimization + +```bash +# Use less GPU memory (if OOM errors) +--gpu-memory-utilization 0.8 + +# Use more GPU memory (if you have headroom) +--gpu-memory-utilization 0.95 +``` + +### Batch Processing + +```bash +# Increase batch size for throughput +--max-num-batched-tokens 8192 +--max-num-seqs 256 +``` + +### Context Length + +```bash +# Reduce for better throughput +--max-model-len 2048 + +# Increase for longer contexts +--max-model-len 8192 +``` + +### Quantization + +```bash +# Use 4-bit quantization (AWQ) +--quantization awq +--model TheBloke/Llama-2-7B-Chat-AWQ + +# Use 8-bit quantization (GPTQ) +--quantization gptq +--model TheBloke/Llama-2-7B-Chat-GPTQ +``` + +## Monitoring + +### vLLM Metrics + +vLLM exposes Prometheus metrics at `http://localhost:8001/metrics`: + +```bash +curl http://localhost:8001/metrics +``` + +### GPU Monitoring + +```bash +# Watch GPU usage +watch -n 1 nvidia-smi + +# More detailed stats +nvidia-smi dmon +``` + +### Log Monitoring + +```bash +# systemd service logs +sudo journalctl -u vllm -f + +# Or direct output if running in terminal +python -m vllm.entrypoints.openai.api_server ... | tee vllm.log +``` + +## Troubleshooting + +### Out of Memory (OOM) Errors + +```bash +# Reduce GPU memory usage +--gpu-memory-utilization 0.7 + +# Reduce context length +--max-model-len 2048 + +# Use quantization +--quantization awq + +# Use smaller model +``` + +### Slow Response Times + +- Check GPU utilization with `nvidia-smi` +- Increase `--gpu-memory-utilization` if GPU memory is underutilized +- Enable `--enable-prefix-caching` for repeated prompts +- Use tensor parallelism for multi-GPU setups + +### Model Not Found + +```bash +# Pre-download model +huggingface-cli download meta-llama/Meta-Llama-3-8B-Instruct + +# Or set cache directory +export HF_HOME=/path/to/cache +``` + +### CUDA Errors + +```bash +# Check CUDA version +nvidia-smi + +# Install matching vLLM version +pip install vllm-cu118 # For CUDA 11.8 +``` + +### Connection Refused from Gateway + +- Verify vLLM is running: `curl http://localhost:8001/health` +- Check firewall settings +- Ensure correct port in fixture configuration +- Verify host is `0.0.0.0` not `localhost` + +## Running Multiple Models + +You can run multiple vLLM instances on different ports: + +```bash +# Terminal 1 - OPT-125M +python -m vllm.entrypoints.openai.api_server \ + --model facebook/opt-125m \ + --port 8001 + +# Terminal 2 - Llama-2-7B +python -m vllm.entrypoints.openai.api_server \ + --model meta-llama/Llama-2-7b-chat-hf \ + --port 8002 +``` + +Add both to your fixtures: + +```json +[ + { + "model": "resource_server.endpoint", + "pk": 1, + "fields": { + "endpoint_slug": "local-vllm-opt-125m", + "cluster": "local", + "framework": "vllm", + "model": "facebook/opt-125m", + "api_port": 8001, + ... + } + }, + { + "model": "resource_server.endpoint", + "pk": 2, + "fields": { + "endpoint_slug": "local-vllm-llama2-7b", + "cluster": "local", + "framework": "vllm", + "model": "meta-llama/Llama-2-7b-chat-hf", + "api_port": 8002, + ... + } + } +] +``` + +## Next Steps + +- [Production Best Practices](../deployment/production.md) +- [Monitoring Setup](../monitoring.md) +- [User Guide](../../user-guide/index.md) +- Upgrade to [Globus Compute + vLLM](globus-compute.md) for federated deployment + +## Additional Resources + +- [vLLM Documentation](https://docs.vllm.ai/) +- [Hugging Face Models](https://huggingface.co/models) +- [NVIDIA GPU Documentation](https://docs.nvidia.com/cuda/) + diff --git a/docs/admin-guide/monitoring.md b/docs/admin-guide/monitoring.md new file mode 100644 index 00000000..3a1b3def --- /dev/null +++ b/docs/admin-guide/monitoring.md @@ -0,0 +1,330 @@ +# Monitoring & Troubleshooting + +This guide covers monitoring the FIRST Inference Gateway and troubleshooting common issues. + +## Monitoring + +### Application Logs + +#### Docker Deployment + +```bash +# View all logs +docker-compose logs -f + +# View gateway logs only +docker-compose logs -f inference-gateway + +# Last 100 lines +docker-compose logs --tail=100 inference-gateway +``` + +#### Bare Metal Deployment + +```bash +# Application logs +tail -f logs/django_info.log + +# Gunicorn logs +tail -f logs/backend_gateway.error.log +tail -f logs/backend_gateway.access.log + +# Systemd service logs +sudo journalctl -u inference-gateway -f +``` + +### Database Monitoring + +```bash +# Connection stats +psql -h localhost -U inferencedev -d inferencegateway -c "SELECT * FROM pg_stat_activity;" + +# Table sizes +psql -h localhost -U inferencedev -d inferencegateway -c " +SELECT schemaname,tablename,pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) +FROM pg_tables WHERE schemaname='public' ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;" +``` + +### Redis Monitoring + +```bash +# Connect to Redis CLI +redis-cli + +# In Redis CLI: +INFO +DBSIZE +MONITOR # Watch commands in real-time +``` + +### Globus Compute Endpoints + +```bash +# List endpoints +globus-compute-endpoint list + +# Check status +globus-compute-endpoint status my-endpoint + +# View logs +globus-compute-endpoint log my-endpoint -n 100 + +# Follow logs +tail -f ~/.globus_compute/my-endpoint/endpoint.log +``` + +## Common Issues + +### Gateway Won't Start + +**Symptoms**: Container/service fails to start + +**Check**: + +```bash +# Docker +docker-compose logs inference-gateway + +# Bare metal +sudo journalctl -u inference-gateway -n 50 +python manage.py check +``` + +**Common Causes**: + +- Missing environment variables +- Database connection failure +- Port already in use +- Syntax error in settings + +### Database Connection Errors + +**Symptoms**: `OperationalError: could not connect to server` + +**Solutions**: + +```bash +# Verify PostgreSQL is running +sudo systemctl status postgresql +docker-compose ps postgres + +# Test connection +psql -h localhost -U inferencedev -d inferencegateway + +# Check pg_hba.conf +sudo nano /etc/postgresql/*/main/pg_hba.conf + +# Restart PostgreSQL +sudo systemctl restart postgresql +``` + +### Authentication Failures + +**Symptoms**: 401 Unauthorized, Globus token errors + +**Solutions**: + +1. Verify Globus application credentials in `.env` +2. Check scope was created successfully: + ```bash + curl -s --user $CLIENT_ID:$CLIENT_SECRET \ + https://auth.globus.org/v2/api/clients/$CLIENT_ID + ``` +3. Force re-authentication: + ```bash + python inference-auth-token.py authenticate --force + ``` +4. Verify redirect URIs match in Globus app settings + +### Globus Compute Errors + +**Symptoms**: Function execution failures, timeout errors + +**Solutions**: + +```bash +# Check endpoint is running +globus-compute-endpoint list + +# Restart endpoint +globus-compute-endpoint restart my-endpoint + +# View detailed logs +globus-compute-endpoint log my-endpoint -n 200 + +# Verify function UUID is allowed +cat ~/.globus_compute/my-endpoint/config.yaml +``` + +### Model Not Found + +**Symptoms**: `Model 'xxx' not found` errors + +**Solutions**: + +1. Verify fixture was loaded: + ```bash + python manage.py dumpdata resource_server.endpoint + ``` +2. Check model name matches exactly in fixture +3. Reload fixtures: + ```bash + python manage.py loaddata fixtures/endpoints.json + ``` + +### Slow Response Times + +**Causes**: + +- Cold start (first request to endpoint) +- GPU not available +- Model loading time +- Network latency + +**Solutions**: + +1. Enable hot nodes (min_blocks > 0 in Globus Compute config) +2. Monitor GPU usage: `nvidia-smi` +3. Check vLLM logs for bottlenecks +4. Increase Gunicorn timeout: + ```python + timeout = 300 + ``` + +### Out of Memory Errors + +**Symptoms**: OOM kills, CUDA out of memory + +**Solutions**: + +```bash +# vLLM: Reduce GPU memory usage +--gpu-memory-utilization 0.7 + +# vLLM: Use quantization +--quantization awq + +# vLLM: Reduce context length +--max-model-len 2048 + +# System: Add swap space +sudo fallocate -l 32G /swapfile +sudo chmod 600 /swapfile +sudo mkswap /swapfile +sudo swapon /swapfile +``` + +## Health Checks + +### Manual Health Checks + +```bash +# Gateway health +curl http://localhost:8000/ + +# vLLM health +curl http://localhost:8001/health + +# Database connectivity +python manage.py dbshell + +# Redis connectivity +redis-cli ping +``` + +### Automated Health Monitoring + +Create a health check script: + +```bash +#!/bin/bash +# health_check.sh + +# Check gateway +if curl -s http://localhost:8000/ > /dev/null; then + echo "✓ Gateway is healthy" +else + echo "✗ Gateway is down" + systemctl restart inference-gateway +fi + +# Check database +if psql -h localhost -U inferencedev -d inferencegateway -c "SELECT 1;" > /dev/null 2>&1; then + echo "✓ Database is healthy" +else + echo "✗ Database is down" +fi +``` + +Add to crontab: + +```bash +*/5 * * * * /path/to/health_check.sh >> /var/log/health_check.log 2>&1 +``` + +## Performance Metrics + +### Key Metrics to Monitor + +1. **Request Rate**: Requests per second +2. **Latency**: Response time (p50, p95, p99) +3. **Error Rate**: Percentage of failed requests +4. **Queue Depth**: Pending Globus Compute tasks +5. **GPU Utilization**: GPU memory and compute usage +6. **Database Connections**: Active connections +7. **Cache Hit Rate**: Redis cache effectiveness + +### Prometheus Metrics + +If using Prometheus, key metrics to track: + +``` +# Request metrics +http_requests_total +http_request_duration_seconds + +# Globus Compute metrics +globus_compute_tasks_submitted +globus_compute_tasks_completed +globus_compute_tasks_failed + +# System metrics +process_cpu_seconds_total +process_resident_memory_bytes +``` + +## Troubleshooting Checklist + +When issues occur, work through this checklist: + +- [ ] Check application logs +- [ ] Verify all services are running +- [ ] Test database connectivity +- [ ] Check Redis connectivity +- [ ] Verify Globus Compute endpoints are online +- [ ] Test authentication flow +- [ ] Check network connectivity +- [ ] Review recent configuration changes +- [ ] Check disk space +- [ ] Monitor resource usage (CPU, RAM, GPU) + +## Getting Help + +If you're still stuck: + +1. **Check documentation**: Review the relevant setup guides +2. **Search issues**: Look for similar issues on [GitHub](https://github.com/auroraGPT-ANL/inference-gateway/issues) +3. **Enable debug logging**: Set `DEBUG=True` temporarily +4. **Collect information**: + - Version information + - Error messages and stack traces + - Configuration (sanitize secrets!) + - Relevant log excerpts +5. **Open an issue**: Provide all collected information + +## Additional Resources + +- [Production Best Practices](deployment/production.md) +- [Configuration Reference](gateway-setup/configuration.md) +- [Globus Compute Documentation](https://globus-compute.readthedocs.io/) + diff --git a/docs/admin-guide/setup.md b/docs/admin-guide/setup.md deleted file mode 100644 index 2a042473..00000000 --- a/docs/admin-guide/setup.md +++ /dev/null @@ -1,454 +0,0 @@ -# Administrator Setup Guide - -This guide covers the complete setup process for deploying the FIRST Inference Gateway. - -## Prerequisites - -- Python 3.11+ -- PostgreSQL Server (included in the Docker deployment) -- Poetry -- Docker and Docker Compose (Recommended for Gateway deployment) -- Globus Account -- Access to a compute resource (HPC cluster or a local machine with sufficient resources for the chosen inference server and models) - -## Setup Overview - -The setup involves two main parts: -1. **Gateway Setup**: Installing and configuring the central API gateway service. -2. **Inference Backend Setup**: Setting up the inference server (like vLLM) and the Globus Compute components on the machine(s) where models will run. - -These parts can be done in parallel, but configuration details from each are needed to link them. - ---- - -## Part 1: Gateway Setup - -> 📝 Looking for a shorter, task-oriented walkthrough? See the [Docker Quickstart](docker-quickstart.md). - -### Step 1: Clone the Repository - -```bash -git clone https://github.com/auroraGPT-ANL/inference-gateway.git -cd inference-gateway -``` - -### Step 2: Choose Deployment Method - -**Option A: Docker Deployment (Recommended)** - -```bash -# Create necessary directories -mkdir -p logs prometheus - -# Configuration is done via the .env file (see next steps) -``` - -See [Starting the Services](#starting-the-services) for how to run this after configuration. - -**Option B: Bare Metal Setup** - -```bash -# Set up Python environment with Poetry -poetry config virtualenvs.in-project true -poetry env use python3.11 -poetry install - -# Activate the environment -poetry shell - -# Ensure PostgreSQL server is running and accessible -``` - -### Step 3: Register Globus Applications - -#### Service API Application - -The Gateway needs to be registered as a Globus Service API application: - -1. Visit [developers.globus.org](https://app.globus.org/settings/developers) and sign in. -2. Under **Register an...**, click on **Register a service API ...**. -3. Complete the registration form: - - Set **App Name** (e.g., "My Inference Gateway"). - - Add **Redirect URIs**: - - Local dev: `http://localhost:8000/complete/globus/` - - Production: `https:///complete/globus/` - - Set **Privacy Policy** and **Terms & Conditions** URLs if applicable. -4. After registration, note the **Client UUID** and generate a **Client Secret**. - -#### Add Globus Scope - -Export your API client credentials: - -```bash -export CLIENT_ID="" -export CLIENT_SECRET="" -``` - -Create the scope: - -```bash -curl -X POST -s --user $CLIENT_ID:$CLIENT_SECRET \ - https://auth.globus.org/v2/api/clients/$CLIENT_ID/scopes \ - -H "Content-Type: application/json" \ - -d '{ - "scope": { - "name": "Action Provider - all", - "description": "Access to inference service.", - "scope_suffix": "action_all", - "dependent_scopes": [ - { - "scope": "73320ffe-4cb4-4b25-a0a3-83d53d59ce4f", - "optional": false, - "requires_refresh_token": true - } - ] - } - }' -``` - -Verify the scope was created: - -```bash -curl -s --user $CLIENT_ID:$CLIENT_SECRET https://auth.globus.org/v2/api/clients/$CLIENT_ID -``` - -#### Service Account Application - -Create a Globus Service Account application for Globus Compute endpoints: - -1. Visit [developers.globus.org](https://app.globus.org/settings/developers). -2. Click on **Add an App** under your project. -3. Select **Register a service account ...**. -4. Complete the registration form. -5. Note the **Client UUID** and generate a **Client Secret**. - -### Step 4: Configure Environment - -Create a `.env` file in the project root: - -```dotenv -# --- Core Django Settings --- -SECRET_KEY="" -DEBUG=True # Set to False for production -ALLOWED_HOSTS="localhost,127.0.0.1" - -# --- Globus Credentials --- -GLOBUS_APPLICATION_ID="" -GLOBUS_APPLICATION_SECRET="" -POLARIS_ENDPOINT_ID="" -POLARIS_ENDPOINT_SECRET="" - -# --- Database Credentials --- -POSTGRES_DB="inferencegateway" -POSTGRES_USER="inferencedev" -POSTGRES_PASSWORD="inferencedevpwd" # CHANGE THIS for production -PGHOST="postgres" # Use "postgres" for Docker, "localhost" for bare-metal -PGPORT=5432 -PGUSER="dataportaldev" -PGPASSWORD="inferencedevpwd" -PGDATABASE="inferencegateway" - -# --- Redis --- -REDIS_URL="redis://redis:6379/0" # Use "redis" for Docker - -# --- Gateway Specific Settings --- -MAX_BATCHES_PER_USER=2 -STREAMING_SERVER_HOST="localhost:8080" -INTERNAL_STREAMING_SECRET="your-internal-streaming-secret-key" -CLI_AUTH_CLIENT_ID="58fdd3bc-e1c3-4ce5-80ea-8d6b87cfb944" -``` - -Generate a strong secret key: - -```bash -python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' -``` - -### Step 5: Initialize Database - -**Docker:** - -```bash -docker-compose up -d -docker-compose exec inference-gateway python manage.py makemigrations -docker-compose exec inference-gateway python manage.py migrate -``` - -**Bare Metal:** - -```bash -python manage.py makemigrations -python manage.py migrate -``` - ---- - -## Part 2: Inference Backend Setup - -This section covers setting up the components on the machine where AI models will run. - -### Step 1: Create Python Virtual Environment - -Use the same Python version as the Gateway API (Python 3.11): - -```bash -# Example with conda -conda create -n vllm-env python=3.11 -y -conda activate vllm-env - -# Example with python venv -python3.11 -m venv vllm-env -source vllm-env/bin/activate -``` - -### Step 2: Install Inference Server and Globus Compute - -Install vLLM: - -```bash -git clone https://github.com/vllm-project/vllm.git -cd vllm -pip install -e . -``` - -Install Globus Compute: - -```bash -pip install globus-compute-sdk globus-compute-endpoint -``` - -### Step 3: Register Globus Compute Functions - -Export your Service Account credentials: - -```bash -export GLOBUS_COMPUTE_CLIENT_ID="" -export GLOBUS_COMPUTE_CLIENT_SECRET="" -``` - -Register the inference function: - -```bash -cd path/to/inference-gateway/compute-functions - -# Register vLLM inference function -python vllm_register_function_with_streaming.py -# Note the Function UUID - -# Register status function (optional but recommended) -python qstat_register_function.py -# Note the Function UUID -``` - -### Step 4: Configure Globus Compute Endpoint - -Create and configure an endpoint: - -```bash -globus-compute-endpoint configure my-compute-endpoint -``` - -Edit the generated `config.yaml` file: -- Add your function UUIDs to `allowed_functions` -- Configure `worker_init` to activate your environment -- Adjust executor settings for your hardware - -See [local-vllm-endpoint.yaml](../../compute-endpoints/local-vllm-endpoint.yaml) for an example configuration. - -Start the endpoint: - -```bash -globus-compute-endpoint start my-compute-endpoint -# Note the Endpoint UUID -``` - ---- - -## Part 3: Connect Gateway and Backend - -### Step 1: Update Fixtures - -Edit `fixtures/endpoints.json` or `fixtures/federated_endpoints.json`: - -**Non-federated example (endpoints.json):** - -```json -[ - { - "model": "resource_server.endpoint", - "pk": 1, - "fields": { - "endpoint_slug": "local-vllm-facebook-opt-125m", - "cluster": "local", - "framework": "vllm", - "model": "facebook/opt-125m", - "api_port": 8001, - "endpoint_uuid": "", - "function_uuid": "", - "batch_endpoint_uuid": "", - "batch_function_uuid": "", - "allowed_globus_groups": "" - } - } -] -``` - -**Federated example (federated_endpoints.json):** - -```json -[ - { - "model": "resource_server.federatedendpoint", - "pk": 1, - "fields": { - "name": "OPT 125M (Federated)", - "slug": "federated-opt-125m", - "target_model_name": "facebook/opt-125m", - "description": "Federated access point for the facebook/opt-125m model.", - "targets": [ - { - "cluster": "local", - "framework": "vllm", - "model": "facebook/opt-125m", - "endpoint_slug": "local-vllm-facebook-opt-125m", - "endpoint_uuid": "", - "function_uuid": "", - "api_port": 8001 - } - ] - } - } -] -``` - -### Step 2: Load Fixtures - -**Docker:** - -```bash -docker-compose exec inference-gateway python manage.py loaddata fixtures/endpoints.json -``` - -**Bare Metal:** - -```bash -python manage.py loaddata fixtures/endpoints.json -``` - ---- - -## Starting the Services - -### Gateway - -**Docker:** - -```bash -docker-compose up --build -d -``` - -**Bare Metal (Development):** - -```bash -poetry shell -python manage.py runserver 0.0.0.0:8000 -``` - -**Bare Metal (Production with Gunicorn):** - -```bash -poetry shell -poetry run gunicorn \ - inference_gateway.asgi:application \ - -k uvicorn.workers.UvicornWorker \ - -b 0.0.0.0:8000 \ - --workers 5 \ - --log-level info -``` - -### Inference Backend - -Ensure the Globus Compute endpoint is running: - -```bash -globus-compute-endpoint start -``` - ---- - -## Production Considerations - -For production deployments, use Nginx as a reverse proxy: - -### Example Nginx Configuration - -```nginx -upstream app_server { - server 127.0.0.1:8000 fail_timeout=0; -} - -server { - listen 80; - server_name your-gateway-domain.org; - - client_max_body_size 4G; - - location /static/ { - alias /path/to/inference-gateway/static/; - } - - location / { - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Host $http_host; - proxy_redirect off; - proxy_pass http://app_server; - } -} -``` - -Remember to: -- Collect static files: `python manage.py collectstatic` -- Configure SSL/TLS certificates -- Set up rate limiting and security headers -- Configure firewall rules - ---- - -## Monitoring - -If deployed via Docker Compose with monitoring enabled: - -- **Grafana**: http://localhost:3000 (admin/admin) -- **Prometheus**: http://localhost:9090 - -The dashboard includes: -- Application metrics (request rates, latency, error rates) -- System metrics (CPU, memory, disk I/O) -- Database metrics (connection counts, query performance) -- Custom Gateway metrics (inference rates, token counts) - ---- - -## Troubleshooting - -**Database Connection Errors** -- Check `.env` variables match your deployment context -- Verify PostgreSQL is running and accessible -- Check firewall rules and `pg_hba.conf` - -**Globus Auth Errors** -- Ensure Redirect URIs match in Globus Developer portal -- Verify credentials in `.env` are correct -- Check scope was created successfully - -**Compute Endpoint Issues** -- Check endpoint logs: `~/.globus_compute//endpoint.log` -- Verify function UUIDs in `config.yaml` -- Ensure environment activation works correctly - -**500 Server Errors** -- Check gateway logs: `docker-compose logs inference-gateway` -- Look for Python tracebacks in Gunicorn logs -- Verify all required environment variables are set - diff --git a/docs/first_architecture.png b/docs/first_architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..b8078a45a2b8433b82cb9c4d2c7a44a317d16802 GIT binary patch literal 417097 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ξnot>2U)PsWd{%a-pA&wKerY#bk;8u9T2jXPlbqw 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 literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..a8163d39 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,64 @@ +# FIRST Inference Gateway Documentation + +Welcome to the documentation for the **Federated Inference Resource Scheduling Toolkit (FIRST)**. FIRST enables LLM inference as a service across distributed HPC clusters through an OpenAI-compatible API. + +## What is FIRST? + +FIRST (Federated Inference Resource Scheduling Toolkit) is a system that allows secure, remote execution of Large Language Models through an OpenAI-compatible API. It validates and authorizes inference requests to scientific computing clusters using Globus Auth and Globus Compute. + +## System Architecture + +![System Architecture](first_architecture.png) + +The Inference Gateway consists of several components: + +- **API Gateway**: Django-based REST/Ninja API that handles authorization and request routing +- **Globus Auth**: Authentication and authorization service +- **Globus Compute Endpoints**: Remote execution framework on HPC clusters (or local machines) +- **Inference Server Backend**: High-performance inference service for LLMs (e.g., vLLM) + +## Quick Links + +### For Administrators + +- **[Docker Deployment](admin-guide/gateway-setup/docker.md)** - Fast-track Docker deployment in under 10 minutes +- **[Bare Metal Setup](admin-guide/gateway-setup/bare-metal.md)** - Complete installation on your own infrastructure +- **[Inference Backend](admin-guide/inference-setup/index.md)** - Connect to OpenAI API, local vLLM, or Globus Compute +- **[Kubernetes](admin-guide/deployment/kubernetes.md)** - Deploy on Kubernetes clusters (Coming Soon) + +### For Users + +- **[User Guide](user-guide/index.md)** - Complete guide for authentication and making requests +- **[API Reference](reference/api.md)** - API endpoint documentation +- **[Examples](user-guide/index.md#using-the-openai-python-sdk)** - Code examples and tutorials + +## Key Features + +- **Federated Access**: Route requests across multiple HPC clusters automatically +- **OpenAI-Compatible**: Works with existing OpenAI SDK and tools +- **Secure**: Globus Auth integration with group-based access control +- **High Performance**: Support for vLLM and other optimized inference backends +- **Flexible**: Deploy via Docker, bare metal, or Kubernetes +- **Scalable**: Auto-scaling and resource management for HPC environments + +## Example Deployment + +For a production example, see the [ALCF Inference Endpoints](https://github.com/argonne-lcf/inference-endpoints) documentation. + +## Getting Help + +- **GitHub**: [Report issues or contribute](https://github.com/auroraGPT-ANL/inference-gateway) +- **Citation**: [Research Paper](reference/citation.md) +- **API Reference**: [Complete API documentation](reference/api.md) + +## License + +This project is licensed under the Apache License 2.0. See the [LICENSE](https://github.com/auroraGPT-ANL/inference-gateway/blob/main/LICENSE) file for details. + +--- + +!!! tip "Quick Start Paths" + - **Just want to try it out?** → [Docker Quickstart](admin-guide/gateway-setup/docker.md) + - **Need full control?** → [Bare Metal Setup](admin-guide/gateway-setup/bare-metal.md) + - **Want to use the API?** → [User Guide](user-guide/index.md) + diff --git a/docs/reference/api.md b/docs/reference/api.md new file mode 100644 index 00000000..2f82093a --- /dev/null +++ b/docs/reference/api.md @@ -0,0 +1,47 @@ +# API Reference + +The FIRST Inference Gateway provides an OpenAI-compatible API. + +## Base URL + +``` +http://your-gateway-domain:8000/resource_server +``` + +## Authentication + +All requests require a Globus access token in the Authorization header: + +```http +Authorization: Bearer +``` + +## Endpoints + +### Chat Completions + +```http +POST /v1/chat/completions +POST /{cluster}/{framework}/v1/chat/completions +``` + +### Completions + +```http +POST /v1/completions +POST /{cluster}/{framework}/v1/completions +``` + +### Batch Processing + +```http +POST /v1/batches +GET /v1/batches/{batch_id} +``` + +For detailed API documentation, refer to the [OpenAI API Reference](https://platform.openai.com/docs/api-reference) as FIRST follows the same schema. + +## Request Parameters + +See the [User Guide](../user-guide/index.md#request-parameters) for detailed parameter documentation. + diff --git a/docs/reference/citation.md b/docs/reference/citation.md new file mode 100644 index 00000000..c6776ea3 --- /dev/null +++ b/docs/reference/citation.md @@ -0,0 +1,39 @@ +# Citation + +If you use ALCF Inference Endpoints or the Federated Inference Resource Scheduling Toolkit (FIRST) in your research or workflows, please cite our paper: + +## BibTeX + +```bibtex +@inproceedings{10.1145/3731599.3767346, + author = {Tanikanti, Aditya and C\^{o}t\'{e}, Benoit and Guo, Yanfei and Chen, Le and Saint, Nickolaus and Chard, Ryan and Raffenetti, Ken and Thakur, Rajeev and Uram, Thomas and Foster, Ian and Papka, Michael E. and Vishwanath, Venkatram}, + title = {FIRST: Federated Inference Resource Scheduling Toolkit for Scientific AI Model Access}, + year = {2025}, + isbn = {9798400718717}, + publisher = {Association for Computing Machinery}, + address = {New York, NY, USA}, + url = {https://doi.org/10.1145/3731599.3767346}, + doi = {10.1145/3731599.3767346}, + abstract = {We present the Federated Inference Resource Scheduling Toolkit (FIRST), a framework enabling Inference-as-a-Service across distributed High-Performance Computing (HPC) clusters. FIRST provides cloud-like access to diverse AI models, like Large Language Models (LLMs), on existing HPC infrastructure. Leveraging Globus Auth and Globus Compute, the system allows researchers to run parallel inference workloads via an OpenAI-compliant API on private, secure environments. This cluster-agnostic API allows requests to be distributed across federated clusters, targeting numerous hosted models. FIRST supports multiple inference backends (e.g., vLLM), auto-scales resources, maintains "hot" nodes for low-latency execution, and offers both high-throughput batch and interactive modes. The framework addresses the growing demand for private, secure, and scalable AI inference in scientific workflows, allowing researchers to generate billions of tokens daily on-premises without relying on commercial cloud infrastructure.}, + booktitle = {Proceedings of the SC '25 Workshops of the International Conference for High Performance Computing, Networking, Storage and Analysis}, + pages = {52–60}, + numpages = {9}, + keywords = {Inference as a Service, High Performance Computing, Job Schedulers, Large Language Models, Globus, Scientific Computing}, + series = {SC Workshops '25} +} +``` + +## Acknowledgements + +This work was supported by the U.S. Department of Energy, Office of Science, Office of Advanced Scientific Computing Research, under Contract No. DE-AC02-06CH11357. + +This research used resources of the Argonne Leadership Computing Facility, which is a DOE Office of Science User Facility. + +## Related Publications + +For more information about FIRST and related work: + +- [Research Paper](https://doi.org/10.1145/3731599.3767346) +- [ALCF Inference Endpoints](https://github.com/argonne-lcf/inference-endpoints) +- [GitHub Repository](https://github.com/auroraGPT-ANL/inference-gateway) + diff --git a/docs/reference/config.md b/docs/reference/config.md new file mode 100644 index 00000000..7d69ce47 --- /dev/null +++ b/docs/reference/config.md @@ -0,0 +1,4 @@ +# Configuration Options + +For a complete list of configuration options, see the [Configuration Reference](../admin-guide/gateway-setup/configuration.md). + diff --git a/docs/usage-guide.md b/docs/user-guide/index.md similarity index 98% rename from docs/usage-guide.md rename to docs/user-guide/index.md index 6772f4a7..3f56c678 100644 --- a/docs/usage-guide.md +++ b/docs/user-guide/index.md @@ -343,5 +343,5 @@ The Gateway returns standard HTTP status codes: For issues, questions, or feature requests: - Check the [GitHub repository](https://github.com/auroraGPT-ANL/inference-gateway) - Contact your Gateway administrator -- Review the [Administrator Setup Guide](admin-guide/setup.md) for deployment issues +- Review the [Administrator Setup Guide](../admin-guide/index.md) for deployment issues diff --git a/inference_gateway/apps.py b/inference_gateway/apps.py index 1945d6c1..e7ed0c29 100644 --- a/inference_gateway/apps.py +++ b/inference_gateway/apps.py @@ -27,7 +27,7 @@ def ready(self): raise ImproperlyConfigured("AUTHORIZED_IDP_DOMAINS cannot be empty.") # Recover the Globus policy - client = globus_sdk.ConfidentialAppAuthClient(settings.POLARIS_ENDPOINT_ID, settings.POLARIS_ENDPOINT_SECRET) + client = globus_sdk.ConfidentialAppAuthClient(settings.SERVICE_ACCOUNT_ID, settings.SERVICE_ACCOUNT_SECRET) token_response = client.oauth2_client_credentials_tokens() globus_auth_token = token_response.by_resource_server["auth.globus.org"]["access_token"] auth_client = globus_sdk.AuthClient(authorizer=globus_sdk.AccessTokenAuthorizer(globus_auth_token)) diff --git a/inference_gateway/settings.py b/inference_gateway/settings.py index 1e0c3ece..0c053ddd 100644 --- a/inference_gateway/settings.py +++ b/inference_gateway/settings.py @@ -34,8 +34,8 @@ # Globus App credentials GLOBUS_APPLICATION_ID = os.getenv("GLOBUS_APPLICATION_ID") GLOBUS_APPLICATION_SECRET = os.getenv("GLOBUS_APPLICATION_SECRET") -POLARIS_ENDPOINT_ID = os.getenv("POLARIS_ENDPOINT_ID") -POLARIS_ENDPOINT_SECRET = os.getenv("POLARIS_ENDPOINT_SECRET") +SERVICE_ACCOUNT_ID = os.getenv("SERVICE_ACCOUNT_ID") +SERVICE_ACCOUNT_SECRET = os.getenv("SERVICE_ACCOUNT_SECRET") GLOBUS_GROUP_MANAGER_ID = os.getenv("GLOBUS_GROUP_MANAGER_ID", "") GLOBUS_GROUP_MANAGER_SECRET = os.getenv("GLOBUS_GROUP_MANAGER_SECRET", "") diff --git a/logging_config.py b/logging_config.py index 86b35ede..7a826617 100644 --- a/logging_config.py +++ b/logging_config.py @@ -1,13 +1,62 @@ import os -# Define the location of the log files (same as in gunicorn_asgi.config.py) +# Determine logging behaviour for different environments environment = os.getenv("ENV", "production") -if environment == "development": - accesslog = "./logs/backend_gateway.access.log" - errorlog = "./logs/backend_gateway.error.log" +log_to_stdout = os.getenv("LOG_TO_STDOUT", "false").lower() in ("true", "1", "t") + +handlers = {} +logger_handlers = { + "uvicorn.error": None, + "uvicorn.access": None, + "resource_server_async": None, + "utils": None, +} + +if log_to_stdout: + handlers["default"] = { + "formatter": "default", + "class": "logging.StreamHandler", + "stream": "ext://sys.stderr", + } + handlers["access"] = { + "formatter": "access", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + } + + logger_handlers["uvicorn.error"] = ["default"] + logger_handlers["uvicorn.access"] = ["access"] + logger_handlers["resource_server_async"] = ["default"] + logger_handlers["utils"] = ["default"] + root_handlers = ["default"] else: - accesslog = "/var/log/inference-service/backend_gateway.access.log" - errorlog = "/var/log/inference-service/backend_gateway.error.log" + if environment == "development": + accesslog = "./logs/backend_gateway.access.log" + errorlog = "./logs/backend_gateway.error.log" + else: + accesslog = "/var/log/inference-service/backend_gateway.access.log" + errorlog = "/var/log/inference-service/backend_gateway.error.log" + + handlers["default"] = { + "formatter": "default", + "class": "logging.FileHandler", + "filename": errorlog, + } + handlers["access"] = { + "formatter": "access", + "class": "logging.FileHandler", + "filename": accesslog, + } + handlers["console"] = { + "formatter": "default", + "class": "logging.StreamHandler", + } + + logger_handlers["uvicorn.error"] = ["default"] + logger_handlers["uvicorn.access"] = ["access"] + logger_handlers["resource_server_async"] = ["default", "console"] + logger_handlers["utils"] = ["default", "console"] + root_handlers = ["default", "console"] LOGGING_CONFIG = { @@ -25,30 +74,27 @@ "datefmt": "%Y-%m-%d %H:%M:%S", }, }, - "handlers": { - "default": { - "formatter": "default", - "class": "logging.FileHandler", - "filename": errorlog, + "handlers": handlers, + "loggers": { + "uvicorn.error": {"handlers": logger_handlers["uvicorn.error"], "level": "INFO"}, + "uvicorn.access": { + "handlers": logger_handlers["uvicorn.access"], + "level": "INFO", + "propagate": False, }, - "access": { - "formatter": "access", - "class": "logging.FileHandler", - "filename": accesslog, + "resource_server_async": { + "handlers": logger_handlers["resource_server_async"], + "level": "INFO", + "propagate": False, }, - "console": { - "formatter": "default", - "class": "logging.StreamHandler", + "utils": { + "handlers": logger_handlers["utils"], + "level": "INFO", + "propagate": False, }, }, - "loggers": { - "uvicorn.error": {"handlers": ["default"], "level": "INFO"}, - "uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False}, - "resource_server_async": {"handlers": ["default", "console"], "level": "INFO", "propagate": False}, - "utils": {"handlers": ["default", "console"], "level": "INFO", "propagate": False}, - }, "root": { "level": "INFO", - "handlers": ["default", "console"], + "handlers": root_handlers, }, } \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..c43b9312 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,97 @@ +site_name: FIRST Inference Gateway Documentation +site_description: Federated Inference Resource Scheduling Toolkit - Documentation for deploying and using LLM inference as a service +site_author: Argonne National Laboratory +site_url: https://auroragpt-anl.github.io/inference-gateway/ + +repo_name: auroraGPT-ANL/inference-gateway +repo_url: https://github.com/auroraGPT-ANL/inference-gateway +edit_uri: edit/main/docs/ + +theme: + name: material + palette: + # Light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + # Dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + font: + text: Roboto + code: Roboto Mono + features: + - navigation.tabs + - navigation.sections + - navigation.expand + - navigation.top + - navigation.tracking + - search.suggest + - search.highlight + - content.code.copy + - content.code.annotate + icon: + repo: fontawesome/brands/github + +plugins: + - search + - minify: + minify_html: true + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.tabbed: + alternate_style: true + - tables + - attr_list + - md_in_html + - toc: + permalink: true + +nav: + - Home: index.md + - Administrator Guide: + - Overview: admin-guide/index.md + - Gateway Setup: + - Docker Deployment: admin-guide/gateway-setup/docker.md + - Bare Metal Setup: admin-guide/gateway-setup/bare-metal.md + - Configuration Reference: admin-guide/gateway-setup/configuration.md + - Inference Backend Setup: + - Overview: admin-guide/inference-setup/index.md + - Globus Compute + vLLM: admin-guide/inference-setup/globus-compute.md + - Local vLLM Setup: admin-guide/inference-setup/local-vllm.md + - Direct API Connection: admin-guide/inference-setup/direct-api.md + - Deployment: + - Kubernetes: admin-guide/deployment/kubernetes.md + - Production Best Practices: admin-guide/deployment/production.md + - Monitoring & Troubleshooting: admin-guide/monitoring.md + - User Guide: user-guide/index.md + - Reference: + - Citation: reference/citation.md + - API Endpoints: reference/api.md + - Configuration Options: reference/config.md + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/auroraGPT-ANL/inference-gateway + - icon: fontawesome/solid/book + link: https://doi.org/10.1145/3731599.3767346 + +copyright: Copyright © 2025 Argonne National Laboratory - Apache 2.0 License + diff --git a/pyproject.toml b/pyproject.toml index da218a9c..48e9207c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ django-filter = "^24.3" psycopg = "^3.2.2" psycopg-pool = "^3.2.3" websockets = "^13.0.1" +httpx = "^0.27.2" django-ninja = "^1.3.0" asyncache = "^0.3.1" globus-compute-common = "^0.7.1" @@ -37,6 +38,8 @@ requests = "^2.32.4" redis = "^6.3.0" django-redis = "^6.0.0" hiredis = "^3.2.1" +mkdocs-material = "^9.7.0" +mkdocs-minify-plugin = "^0.8.0" [build-system] requires = ["poetry-core"] diff --git a/requirements.txt b/requirements.txt index f46fdcf2..5e8d8293 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,8 @@ annotated-types==0.7.0 ; python_full_version >= "3.12.6" and python_full_version asgiref==3.10.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" asyncache==0.3.1 ; python_full_version >= "3.12.6" and python_version < "4.0" attrs==25.4.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +babel==2.17.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +backrefs==6.0.1 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" cachetools==5.5.2 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" certifi==2025.10.5 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" cffi==2.0.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" and platform_python_implementation != "PyPy" @@ -9,6 +11,7 @@ charset-normalizer==3.4.4 ; python_full_version >= "3.12.6" and python_full_vers click==8.3.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" colorama==0.4.6 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" cryptography==46.0.3 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +csscompressor==0.9.5 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" dill==0.3.9 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" django-cors-headers==4.9.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" django-filter==24.3 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" @@ -18,20 +21,36 @@ django==5.2.8 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0 djangorestframework==3.16.1 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" drf-spectacular==0.27.2 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" exceptiongroup==1.3.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +ghp-import==2.1.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" globus-compute-common==0.7.1 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" globus-compute-sdk==3.16.1 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" globus-sdk==3.65.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" gunicorn==23.0.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" h11==0.16.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" hiredis==3.3.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +htmlmin2==0.1.13 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" idna==3.11 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" inflection==0.5.1 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +httpx==0.27.2 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +jinja2==3.1.6 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +jsmin==3.0.1 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" jsonschema-specifications==2025.9.1 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" jsonschema==4.25.1 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" markdown-it-py==4.0.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +markdown==3.10 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +markupsafe==3.0.3 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" mdurl==0.1.2 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +mergedeep==1.3.4 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +mkdocs-get-deps==0.2.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +mkdocs-material-extensions==1.3.1 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +mkdocs-material==9.7.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +mkdocs-minify-plugin==0.8.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +mkdocs==1.6.1 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" packaging==25.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +paginate==0.5.7 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +pathspec==0.12.1 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" pika==1.3.2 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +platformdirs==4.5.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" psutil==5.9.8 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" psycopg-pool==3.2.7 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" psycopg==3.2.12 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" @@ -40,7 +59,10 @@ pydantic-core==2.41.5 ; python_full_version >= "3.12.6" and python_full_version pydantic==2.12.4 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" pygments==2.19.2 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" pyjwt==2.10.1 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +pymdown-extensions==10.17.1 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +python-dateutil==2.9.0.post0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" python-dotenv==1.2.1 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +pyyaml-env-tag==1.1 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" pyyaml==6.0.3 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" redis==6.4.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" referencing==0.37.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" @@ -48,6 +70,7 @@ requests==2.32.5 ; python_full_version >= "3.12.6" and python_full_version < "4. rich==14.2.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" rpds-py==0.28.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" shellingham==1.5.4 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +six==1.17.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" sqlparse==0.5.3 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" tblib==1.7.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" texttable==1.7.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" @@ -58,4 +81,11 @@ tzdata==2025.2 ; python_full_version >= "3.12.6" and python_full_version < "4.0. uritemplate==4.2.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" urllib3==2.5.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" uvicorn==0.30.6 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +watchdog==6.0.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" websockets==13.1 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +# Documentation dependencies +mkdocs>=1.5.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +mkdocs-material>=9.4.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +mkdocs-minify-plugin>=0.7.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" +pymdown-extensions>=10.0 ; python_full_version >= "3.12.6" and python_full_version < "4.0.0" + diff --git a/site/404.html b/site/404.html new file mode 100644 index 00000000..5e9b3889 --- /dev/null +++ b/site/404.html @@ -0,0 +1 @@ + FIRST Inference Gateway Documentation

404 - Not found

\ No newline at end of file diff --git a/site/CONTRIBUTING/index.html b/site/CONTRIBUTING/index.html new file mode 100644 index 00000000..b3c300fa --- /dev/null +++ b/site/CONTRIBUTING/index.html @@ -0,0 +1,67 @@ + Contributing to Documentation - FIRST Inference Gateway Documentation

Contributing to Documentation

This guide explains how to contribute to the FIRST Inference Gateway documentation.

Documentation Structure

The documentation is built using MkDocs with the Material theme.

docs/
+├── index.md                    # Homepage
+├── admin-guide/                # Administrator documentation
+│   ├── index.md
+│   ├── gateway-setup/
+│   │   ├── docker.md
+│   │   ├── bare-metal.md
+│   │   └── configuration.md
+│   ├── inference-setup/
+│   │   ├── index.md
+│   │   ├── direct-api.md
+│   │   ├── local-vllm.md
+│   │   └── globus-compute.md
+│   ├── deployment/
+│   │   ├── kubernetes.md
+│   │   └── production.md
+│   └── monitoring.md
+├── user-guide/                 # User documentation
+│   ├── index.md
+│   ├── authentication.md
+│   ├── requests.md
+│   ├── batch.md
+│   └── examples.md
+└── reference/                  # Reference materials
+    ├── citation.md
+    ├── api.md
+    └── config.md
+

Local Development

Install Dependencies

pip install -r requirements-docs.txt
+

Or:

pip install mkdocs-material mkdocs-minify-plugin
+

Build Documentation

mkdocs build
+

This creates the site/ directory with static HTML files.

Serve Locally

mkdocs serve
+

Then visit: http://127.0.0.1:8000

The site will automatically reload when you save changes.

Writing Documentation

Markdown Files

All documentation is written in Markdown with support for:

Admonitions

!!! note "Optional Title"
+    This is a note
+
+!!! warning
+    This is a warning
+
+!!! tip
+    This is a tip
+
+!!! danger
+    This is dangerous!
+

Code Blocks

```python
+def hello():
+    print("Hello, World!")
+```
+
+```bash
+mkdocs serve
+```
+

Mermaid Diagrams

```mermaid
+graph LR
+    A[User] --> B[Gateway]
+    B --> C[Backend]
+```
+
[Link text](path/to/file.md)
+[Link to section](path/to/file.md#section-name)
+

Navigation is configured in mkdocs.yml:

nav:
+  - Home: index.md
+  - Administrator Guide:
+      - Overview: admin-guide/index.md
+      - Gateway Setup:
+          - Docker: admin-guide/gateway-setup/docker.md
+

Deployment

Automatic Deployment

Documentation automatically deploys to GitHub Pages when you push to main:

  1. Make your changes to files in docs/
  2. Commit and push:
    git add docs/
    +git commit -m "docs: update documentation"
    +git push origin main
    +
  3. GitHub Actions builds and deploys automatically
  4. View at: https://auroragpt-anl.github.io/inference-gateway/

Manual Deployment

You can also manually trigger deployment:

  1. Go to Actions tab on GitHub
  2. Select "Deploy Documentation" workflow
  3. Click "Run workflow"

Style Guide

Headings

  • Use sentence case for headings
  • One H1 (#) per page (the page title)
  • Use hierarchical heading levels (don't skip levels)

Code Examples

  • Always include language identifier for syntax highlighting
  • Add comments to explain complex code
  • Test code examples before committing

File Names

  • Use lowercase with hyphens: my-file.md
  • Be descriptive: docker-deployment.md not docker.md

Writing Style

  • Use clear, concise language
  • Write in second person ("you") for instructions
  • Use active voice
  • Include examples wherever possible
  • Add context before diving into details

Contributing Process

  1. Fork the repository
  2. Create a branch: git checkout -b docs/my-improvement
  3. Make your changes
  4. Test locally: mkdocs serve
  5. Commit with clear message: git commit -m "docs: improve docker guide"
  6. Push and create a Pull Request

Questions?

  • Open an issue on GitHub
  • Check existing documentation
  • Review MkDocs Material documentation

Resources

\ No newline at end of file diff --git a/site/admin-guide/deployment/kubernetes/index.html b/site/admin-guide/deployment/kubernetes/index.html new file mode 100644 index 00000000..f936c5ef --- /dev/null +++ b/site/admin-guide/deployment/kubernetes/index.html @@ -0,0 +1 @@ + Kubernetes - FIRST Inference Gateway Documentation

Kubernetes Deployment

Coming Soon

Kubernetes deployment manifests and Helm charts are currently under development.

Planned Features

The Kubernetes deployment will include:

  • Helm Chart: Easy installation and configuration management
  • High Availability: Multi-replica deployments with load balancing
  • Auto-Scaling: Horizontal Pod Autoscaler based on metrics
  • StatefulSets: For PostgreSQL and Redis persistence
  • Ingress Configuration: HTTPS/TLS termination and routing
  • Secrets Management: Kubernetes secrets for sensitive data
  • ConfigMaps: Environment-specific configuration
  • Health Probes: Liveness and readiness checks
  • Resource Limits: CPU and memory management
  • Monitoring Integration: Prometheus and Grafana

Current Status

We are actively working on:

  1. Creating Kubernetes manifests for all components
  2. Developing a Helm chart for simplified deployment
  3. Testing on various Kubernetes distributions (EKS, GKE, OpenShift)
  4. Documentation and best practices

Alternative: Docker Deployment

For now, please use one of these deployment methods:

Get Notified

To be notified when Kubernetes support is available:

Contribute

Interested in helping with Kubernetes deployment?

  • Check open issues tagged with kubernetes
  • Submit a pull request
  • Share your deployment configurations

Contact

For enterprise Kubernetes deployments or consulting:

  • Open an issue on GitHub
  • Contact the development team

Last Updated: November 2025

Check back soon for updates!

\ No newline at end of file diff --git a/site/admin-guide/deployment/production/index.html b/site/admin-guide/deployment/production/index.html new file mode 100644 index 00000000..54f295fe --- /dev/null +++ b/site/admin-guide/deployment/production/index.html @@ -0,0 +1,242 @@ + Production Best Practices - FIRST Inference Gateway Documentation

Production Best Practices

This guide covers best practices for deploying FIRST Inference Gateway in production environments.

Security

Authentication & Authorization

Globus Group Restrictions

Restrict access to specific Globus groups:

GLOBUS_GROUPS="group-uuid-1 group-uuid-2"
+

Identity Provider Restrictions

Limit to specific institutions:

AUTHORIZED_IDPS='{"University Name": "idp-uuid"}'
+AUTHORIZED_GROUPS_PER_IDP='{"University Name": "group-uuid-1,group-uuid-2"}'
+

High Assurance Policies

Require MFA and other security policies:

GLOBUS_POLICIES="policy-uuid-1 policy-uuid-2"
+

Secrets Management

Never store secrets in code or version control.

Use Environment Files

# .env (add to .gitignore)
+SECRET_KEY="..."
+POSTGRES_PASSWORD="..."
+

Docker Secrets

services:
+  gateway:
+    secrets:
+      - db_password
+      - globus_secret
+
+secrets:
+  db_password:
+    file: ./secrets/db_password.txt
+  globus_secret:
+    file: ./secrets/globus_secret.txt
+

Vault Integration

For enterprise deployments, integrate with HashiCorp Vault or similar.

HTTPS/TLS

Always use HTTPS in production.

Let's Encrypt with Certbot

sudo certbot --nginx -d yourdomain.com
+

Custom Certificates

server {
+    listen 443 ssl http2;
+    ssl_certificate /path/to/cert.pem;
+    ssl_certificate_key /path/to/key.pem;
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_ciphers HIGH:!aNULL:!MD5;
+}
+

Firewall Configuration

# Ubuntu/Debian
+sudo ufw allow 80/tcp
+sudo ufw allow 443/tcp
+sudo ufw deny 8000/tcp  # Don't expose Django directly
+
+# CentOS/RHEL
+sudo firewall-cmd --permanent --add-service=http
+sudo firewall-cmd --permanent --add-service=https
+sudo firewall-cmd --reload
+

Performance

Database Optimization

Connection Pooling

# settings.py
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.postgresql',
+        'CONN_MAX_AGE': 600,  # Persistent connections
+        'OPTIONS': {
+            'connect_timeout': 10,
+        }
+    }
+}
+

Indexes

Ensure proper indexes on frequently queried fields:

python manage.py dbshell
+CREATE INDEX idx_endpoint_slug ON resource_server_endpoint(endpoint_slug);
+CREATE INDEX idx_created_at ON resource_server_listendpointslog(created_at);
+

Caching

Redis Configuration

# settings.py
+CACHES = {
+    'default': {
+        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
+        'LOCATION': 'redis://redis:6379/0',
+        'OPTIONS': {
+            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
+            'CONNECTION_POOL_KWARGS': {
+                'max_connections': 50
+            }
+        }
+    }
+}
+

Gunicorn Configuration

Worker Calculation

workers = (2 * CPU_cores) + 1
+

For a 16-core machine:

workers = (2 * 16) + 1 = 33
+

Production Config

# gunicorn_asgi.config.py
+import multiprocessing
+
+bind = "0.0.0.0:8000"
+workers = multiprocessing.cpu_count() * 2 + 1
+worker_class = "uvicorn.workers.UvicornWorker"
+worker_connections = 1000
+timeout = 120
+keepalive = 5
+max_requests = 1000
+max_requests_jitter = 50
+

Nginx Optimization

upstream gateway {
+    least_conn;  # Load balancing algorithm
+    server 127.0.0.1:8000 max_fails=3 fail_timeout=30s;
+    server 127.0.0.1:8001 max_fails=3 fail_timeout=30s;
+    keepalive 64;
+}
+
+server {
+    listen 443 ssl http2;
+
+    # Gzip compression
+    gzip on;
+    gzip_types text/plain text/css application/json application/javascript;
+    gzip_min_length 1000;
+
+    # Client body size
+    client_max_body_size 100M;
+    client_body_buffer_size 1M;
+
+    # Timeouts
+    proxy_connect_timeout 600s;
+    proxy_send_timeout 600s;
+    proxy_read_timeout 600s;
+    send_timeout 600s;
+
+    # Buffering
+    proxy_buffering off;  # Important for streaming
+    proxy_request_buffering off;
+
+    # Headers
+    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    proxy_set_header X-Forwarded-Proto $scheme;
+    proxy_set_header Host $http_host;
+    proxy_set_header X-Real-IP $remote_addr;
+
+    location /static/ {
+        alias /path/to/staticfiles/;
+        expires 30d;
+        add_header Cache-Control "public, immutable";
+    }
+
+    location / {
+        proxy_pass http://gateway;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+    }
+}
+

Monitoring

Application Monitoring

Prometheus Metrics

Add to docker-compose.yml:

services:
+  prometheus:
+    image: prom/prometheus
+    volumes:
+      - ./prometheus.yml:/etc/prometheus/prometheus.yml
+      - prometheus_data:/prometheus
+    ports:
+      - "9090:9090"
+

Grafana Dashboards

services:
+  grafana:
+    image: grafana/grafana
+    volumes:
+      - grafana_data:/var/lib/grafana
+    ports:
+      - "3000:3000"
+    environment:
+      - GF_SECURITY_ADMIN_PASSWORD=secure_password
+

Log Aggregation

Structured Logging

# logging_config.py
+LOGGING = {
+    'version': 1,
+    'formatters': {
+        'json': {
+            'class': 'pythonjsonlogger.jsonlogger.JsonFormatter',
+            'format': '%(asctime)s %(name)s %(levelname)s %(message)s'
+        }
+    },
+    'handlers': {
+        'file': {
+            'class': 'logging.handlers.RotatingFileHandler',
+            'filename': 'logs/gateway.log',
+            'maxBytes': 10485760,  # 10MB
+            'backupCount': 10,
+            'formatter': 'json'
+        }
+    }
+}
+

ELK Stack Integration

For large deployments, consider Elasticsearch + Logstash + Kibana.

Health Checks

Kubernetes Probes

livenessProbe:
+  httpGet:
+    path: /health
+    port: 8000
+  initialDelaySeconds: 30
+  periodSeconds: 10
+
+readinessProbe:
+  httpGet:
+    path: /ready
+    port: 8000
+  initialDelaySeconds: 5
+  periodSeconds: 5
+

Custom Health Endpoint

Create a health check view in Django to verify database, Redis, and Globus Compute connectivity.

Backup & Recovery

Database Backups

Automated Backups

#!/bin/bash
+# backup_db.sh
+
+DATE=$(date +%Y%m%d_%H%M%S)
+BACKUP_DIR="/backups/postgres"
+BACKUP_FILE="$BACKUP_DIR/backup_$DATE.sql.gz"
+
+pg_dump -h localhost -U inferencedev inferencegateway | gzip > $BACKUP_FILE
+
+# Keep only last 30 days
+find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +30 -delete
+

Add to crontab:

0 2 * * * /path/to/backup_db.sh
+

Point-in-Time Recovery

Configure PostgreSQL for WAL archiving:

# postgresql.conf
+wal_level = replica
+archive_mode = on
+archive_command = 'cp %p /backup/wal/%f'
+

Configuration Backups

# Backup environment and fixtures
+tar -czf config_backup_$(date +%Y%m%d).tar.gz \
+    .env \
+    fixtures/ \
+    nginx_app.conf \
+    gunicorn_asgi.config.py
+

Scaling

Horizontal Scaling

Multiple Gateway Instances

upstream gateway {
+    server gateway1:8000;
+    server gateway2:8000;
+    server gateway3:8000;
+}
+

Session Affinity

For stateful sessions:

upstream gateway {
+    ip_hash;
+    server gateway1:8000;
+    server gateway2:8000;
+}
+

Database Scaling

Read Replicas

# settings.py
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.postgresql',
+        'HOST': 'primary.db.internal',
+    },
+    'replica': {
+        'ENGINE': 'django.db.backends.postgresql',
+        'HOST': 'replica.db.internal',
+    }
+}
+
+DATABASE_ROUTERS = ['path.to.ReplicaRouter']
+

Connection Pooling (PgBouncer)

# pgbouncer.ini
+[databases]
+inferencegateway = host=localhost port=5432 dbname=inferencegateway
+
+[pgbouncer]
+pool_mode = transaction
+max_client_conn = 1000
+default_pool_size = 20
+

Inference Backend Scaling

Federated Endpoints

Deploy multiple Globus Compute endpoints and configure federated routing for automatic load balancing.

Auto-Scaling

Configure Globus Compute endpoints to auto-scale based on demand:

engine:
+  provider:
+    min_blocks: 1
+    max_blocks: 20
+

Maintenance

Zero-Downtime Deployments

Blue-Green Deployment

  1. Deploy new version alongside old
  2. Switch traffic to new version
  3. Monitor for issues
  4. Decommission old version

Rolling Updates

# Update one instance at a time
+for server in gateway1 gateway2 gateway3; do
+    ssh $server "cd /app && git pull && systemctl restart gateway"
+    sleep 60  # Allow time to stabilize
+done
+

Database Migrations

Always test migrations in staging first:

# Backup before migrating
+./backup_db.sh
+
+# Run migration
+python manage.py migrate
+
+# If issues occur, restore backup
+psql -h localhost -U inferencedev inferencegateway < backup.sql
+

Disaster Recovery

Disaster Recovery Plan

  1. Recovery Time Objective (RTO): 2 hours
  2. Recovery Point Objective (RPO): 1 hour

Backup Strategy

  • Hourly: Database transaction logs
  • Daily: Full database backup
  • Weekly: Complete system backup (config, logs, data)
  • Monthly: Archived to off-site storage

Failover Procedures

Document step-by-step procedures for:

  1. Gateway failure → Switch to backup gateway
  2. Database failure → Promote read replica
  3. Complete site failure → Activate DR site

Checklist

Pre-Production

  • [ ] All secrets are externalized
  • [ ] HTTPS/TLS configured
  • [ ] Firewall rules applied
  • [ ] DEBUG=False
  • [ ] Strong passwords set
  • [ ] Database backed up
  • [ ] Monitoring configured
  • [ ] Log aggregation set up
  • [ ] Health checks working
  • [ ] Load testing completed
  • [ ] Disaster recovery plan documented

Post-Deployment

  • [ ] Monitor logs for errors
  • [ ] Verify all endpoints responding
  • [ ] Check database performance
  • [ ] Test authentication flow
  • [ ] Verify Globus Compute connectivity
  • [ ] Run integration tests
  • [ ] Document any issues

Additional Resources

\ No newline at end of file diff --git a/site/admin-guide/gateway-setup/bare-metal/index.html b/site/admin-guide/gateway-setup/bare-metal/index.html new file mode 100644 index 00000000..3a569fdb --- /dev/null +++ b/site/admin-guide/gateway-setup/bare-metal/index.html @@ -0,0 +1,234 @@ + Bare Metal Setup - FIRST Inference Gateway Documentation

Bare Metal Setup

This guide covers installing the FIRST Inference Gateway directly on your server without Docker.

Prerequisites

  • Linux server (Ubuntu 20.04+, CentOS 8+, or similar)
  • Python 3.12 or later
  • PostgreSQL 13 or later
  • Redis 6 or later
  • Poetry (Python dependency manager)
  • Sudo access for system packages
  • At least 4GB RAM

Step 1: Install System Dependencies

Ubuntu/Debian

sudo apt update
+sudo apt install -y python3.12 python3.12-dev python3.12-venv \
+    postgresql postgresql-contrib redis-server \
+    build-essential libpq-dev git curl
+

CentOS/RHEL

sudo dnf install -y python3.12 python3.12-devel \
+    postgresql postgresql-server redis \
+    gcc gcc-c++ make libpq-devel git
+

Step 2: Install Poetry

curl -sSL https://install.python-poetry.org | python3 -
+
+# Add to PATH
+export PATH="$HOME/.local/bin:$PATH"
+
+# Verify installation
+poetry --version
+

Step 3: Clone and Setup Project

git clone https://github.com/auroraGPT-ANL/inference-gateway.git
+cd inference-gateway
+
+# Configure Poetry to create venv in project
+poetry config virtualenvs.in-project true
+
+# Set Python version
+poetry env use python3.12
+
+# Install dependencies
+poetry install
+
+# Activate environment
+poetry shell
+

Step 4: Configure PostgreSQL

Initialize PostgreSQL (if first time)

# Ubuntu/Debian (usually auto-initialized)
+sudo systemctl start postgresql
+sudo systemctl enable postgresql
+
+# CentOS/RHEL
+sudo postgresql-setup --initdb
+sudo systemctl start postgresql
+sudo systemctl enable postgresql
+

Create Database and User

sudo -u postgres psql
+
+# In PostgreSQL shell:
+CREATE DATABASE inferencegateway;
+CREATE USER inferencedev WITH PASSWORD 'your-secure-password';
+ALTER ROLE inferencedev SET client_encoding TO 'utf8';
+ALTER ROLE inferencedev SET default_transaction_isolation TO 'read committed';
+ALTER ROLE inferencedev SET timezone TO 'UTC';
+GRANT ALL PRIVILEGES ON DATABASE inferencegateway TO inferencedev;
+\q
+

Configure PostgreSQL Authentication

Edit /etc/postgresql/*/main/pg_hba.conf (path may vary):

# Add this line (adjust for your security needs)
+host    inferencegateway    inferencedev    127.0.0.1/32    md5
+

Restart PostgreSQL:

sudo systemctl restart postgresql
+

Step 5: Configure Redis

Start and enable Redis:

sudo systemctl start redis
+sudo systemctl enable redis
+
+# Verify it's running
+redis-cli ping
+# Should return: PONG
+

Step 6: Register Globus Applications

Follow the same steps as in the Docker guide:

Service API Application

  1. Visit developers.globus.org
  2. Register a service API application
  3. Add redirect URI: http://your-server-ip:8000/complete/globus/
  4. Note Client UUID and Secret

Add Scope

export CLIENT_ID="<Your-Service-API-Client-UUID>"
+export CLIENT_SECRET="<Your-Service-API-Client-Secret>"
+
+curl -X POST -s --user $CLIENT_ID:$CLIENT_SECRET \
+    https://auth.globus.org/v2/api/clients/$CLIENT_ID/scopes \
+    -H "Content-Type: application/json" \
+    -d '{
+        "scope": {
+            "name": "Action Provider - all",
+            "description": "Access to inference service.",
+            "scope_suffix": "action_all",
+            "dependent_scopes": [
+                {
+                    "scope": "73320ffe-4cb4-4b25-a0a3-83d53d59ce4f",
+                    "optional": false,
+                    "requires_refresh_token": true
+                }
+            ]
+        }
+    }'
+

Service Account Application

  1. In the same project, register a service account application
  2. Note Client UUID and Secret

Step 7: Configure Environment

Create .env file in project root:

cat > .env << 'EOF'
+# --- Core Django Settings ---
+SECRET_KEY="<generate-with-command-below>"
+DEBUG=False
+ALLOWED_HOSTS="your-server-ip,your-domain.com"
+
+# --- Globus Credentials ---
+GLOBUS_APPLICATION_ID="<Your-Service-API-Client-UUID>"
+GLOBUS_APPLICATION_SECRET="<Your-Service-API-Client-Secret>"
+SERVICE_ACCOUNT_ID="<Your-Service-Account-Client-UUID>"
+SERVICE_ACCOUNT_SECRET="<Your-Service-Account-Client-Secret>"
+
+# --- Database Credentials ---
+POSTGRES_DB="inferencegateway"
+POSTGRES_USER="inferencedev"
+POSTGRES_PASSWORD="your-secure-password"
+PGHOST="localhost"
+PGPORT=5432
+PGUSER="inferencedev"
+PGPASSWORD="your-secure-password"
+PGDATABASE="inferencegateway"
+
+# --- Redis ---
+REDIS_URL="redis://localhost:6379/0"
+
+# --- Gateway Settings ---
+MAX_BATCHES_PER_USER=2
+STREAMING_SERVER_HOST="localhost:8080"
+INTERNAL_STREAMING_SECRET="<generate-random-secret>"
+CLI_AUTH_CLIENT_ID="58fdd3bc-e1c3-4ce5-80ea-8d6b87cfb944"
+EOF
+

Generate secret key:

python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
+

Step 8: Initialize Database

# Make sure you're in the poetry shell
+poetry shell
+
+# Run migrations
+python manage.py makemigrations
+python manage.py migrate
+
+# Create superuser
+python manage.py createsuperuser
+
+# Collect static files
+python manage.py collectstatic --noinput
+

Step 9: Test the Gateway

Run development server:

python manage.py runserver 0.0.0.0:8000
+

Test in another terminal:

curl http://localhost:8000/
+

Step 10: Setup Production Server (Gunicorn)

Install Gunicorn (already included in poetry dependencies)

Create a systemd service file:

sudo nano /etc/systemd/system/inference-gateway.service
+

Add the following:

[Unit]
+Description=FIRST Inference Gateway
+After=network.target postgresql.service redis.service
+
+[Service]
+Type=notify
+User=your-username
+Group=your-username
+WorkingDirectory=/path/to/inference-gateway
+Environment="PATH=/path/to/inference-gateway/.venv/bin"
+EnvironmentFile=/path/to/inference-gateway/.env
+ExecStart=/path/to/inference-gateway/.venv/bin/gunicorn \
+    inference_gateway.asgi:application \
+    -k uvicorn.workers.UvicornWorker \
+    -b 0.0.0.0:8000 \
+    --workers 4 \
+    --log-level info \
+    --access-logfile /path/to/inference-gateway/logs/access.log \
+    --error-logfile /path/to/inference-gateway/logs/error.log
+
+[Install]
+WantedBy=multi-user.target
+

Start and Enable Service

# Create logs directory
+mkdir -p logs
+
+# Reload systemd
+sudo systemctl daemon-reload
+
+# Start service
+sudo systemctl start inference-gateway
+
+# Enable on boot
+sudo systemctl enable inference-gateway
+
+# Check status
+sudo systemctl status inference-gateway
+

Install Nginx

# Ubuntu/Debian
+sudo apt install nginx
+
+# CentOS/RHEL
+sudo dnf install nginx
+

Configure Nginx

Create site configuration:

sudo nano /etc/nginx/sites-available/inference-gateway
+

Add the following:

upstream inference_gateway {
+    server 127.0.0.1:8000 fail_timeout=0;
+}
+
+server {
+    listen 80;
+    server_name your-domain.com;
+    client_max_body_size 100M;
+
+    location /static/ {
+        alias /path/to/inference-gateway/staticfiles/;
+    }
+
+    location / {
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header Host $http_host;
+        proxy_redirect off;
+        proxy_buffering off;
+        proxy_pass http://inference_gateway;
+    }
+}
+

Enable the site:

# Ubuntu/Debian
+sudo ln -s /etc/nginx/sites-available/inference-gateway /etc/nginx/sites-enabled/
+sudo nginx -t
+sudo systemctl restart nginx
+
+# CentOS/RHEL
+sudo ln -s /etc/nginx/sites-available/inference-gateway /etc/nginx/conf.d/
+sudo nginx -t
+sudo systemctl restart nginx
+

Setup SSL with Let's Encrypt

# Ubuntu/Debian
+sudo apt install certbot python3-certbot-nginx
+
+# CentOS/RHEL
+sudo dnf install certbot python3-certbot-nginx
+
+# Get certificate
+sudo certbot --nginx -d your-domain.com
+

Step 12: Configure Firewall

# Ubuntu/Debian (UFW)
+sudo ufw allow 80/tcp
+sudo ufw allow 443/tcp
+sudo ufw enable
+
+# CentOS/RHEL (firewalld)
+sudo firewall-cmd --permanent --add-service=http
+sudo firewall-cmd --permanent --add-service=https
+sudo firewall-cmd --reload
+

Maintenance

View Logs

# Application logs
+tail -f logs/error.log
+tail -f logs/access.log
+
+# System service logs
+sudo journalctl -u inference-gateway -f
+

Restart Service

sudo systemctl restart inference-gateway
+

Update Application

cd /path/to/inference-gateway
+git pull origin main
+poetry install
+python manage.py migrate
+python manage.py collectstatic --noinput
+sudo systemctl restart inference-gateway
+

Troubleshooting

Service won't start

Check logs:

sudo journalctl -u inference-gateway -n 50
+

Check configuration:

poetry shell
+python manage.py check
+

Database connection errors

Verify PostgreSQL is running:

sudo systemctl status postgresql
+

Test connection:

psql -h localhost -U inferencedev -d inferencegateway
+

Permission errors

Ensure the service user owns the files:

sudo chown -R your-username:your-username /path/to/inference-gateway
+

Nginx errors

Check nginx error log:

sudo tail -f /var/log/nginx/error.log
+

Test configuration:

sudo nginx -t
+

Next Steps

Additional Resources

\ No newline at end of file diff --git a/site/admin-guide/gateway-setup/configuration/index.html b/site/admin-guide/gateway-setup/configuration/index.html new file mode 100644 index 00000000..25fbeb44 --- /dev/null +++ b/site/admin-guide/gateway-setup/configuration/index.html @@ -0,0 +1,174 @@ + Configuration Reference - FIRST Inference Gateway Documentation

Configuration Reference

This page documents all environment variables and configuration options for the FIRST Inference Gateway.

Environment Variables

All configuration is done through environment variables, typically stored in a .env file.

Core Django Settings

Variable Required Default Description
SECRET_KEY Yes - Django secret key for cryptographic signing
DEBUG No False Enable debug mode (never use in production)
ALLOWED_HOSTS Yes - Comma-separated list of allowed hostnames
RUNNING_AUTOMATED_TEST_SUITE No False Set to True to skip Globus High Assurance policy checks (development/testing only)
LOG_TO_STDOUT No False Set to True to output logs to stdout (useful for Docker)

Security Warning

  • Never use DEBUG=True in production! This exposes sensitive information.
  • Never use RUNNING_AUTOMATED_TEST_SUITE=True in production! This disables important security checks.

Globus Authentication

Variable Required Default Description
GLOBUS_APPLICATION_ID Yes - Service API application client UUID
GLOBUS_APPLICATION_SECRET Yes - Service API application client secret
SERVICE_ACCOUNT_ID Yes - Service Account application client UUID
SERVICE_ACCOUNT_SECRET Yes - Service Account application client secret
GLOBUS_GROUPS No - Space-separated UUIDs of allowed Globus groups
AUTHORIZED_IDPS No - JSON string of authorized identity providers
AUTHORIZED_GROUPS_PER_IDP No - JSON string of groups per IDP
GLOBUS_POLICIES No - Space-separated policy UUIDs

Example with group restrictions:

GLOBUS_GROUPS="uuid-1 uuid-2 uuid-3"
+

Example with IDP restrictions:

AUTHORIZED_IDPS='{"University": "uuid-here"}'
+AUTHORIZED_GROUPS_PER_IDP='{"University": "group-uuid-1,group-uuid-2"}'
+

Database Configuration

Variable Required Default Description
POSTGRES_DB Yes - Database name
POSTGRES_USER Yes - Database user
POSTGRES_PASSWORD Yes - Database password
PGHOST Yes - Database host (postgres for Docker, localhost for bare metal)
PGPORT No 5432 Database port
PGUSER Yes - Database user (can be same as POSTGRES_USER)
PGPASSWORD Yes - Database password
PGDATABASE Yes - Database name

Docker Networking

When using Docker Compose, set PGHOST=postgres to use the container name. For bare metal, use PGHOST=localhost or the actual hostname.

Redis Configuration

Variable Required Default Description
REDIS_URL Yes - Redis connection URL

Examples:

# Docker
+REDIS_URL="redis://redis:6379/0"
+
+# Bare metal
+REDIS_URL="redis://localhost:6379/0"
+
+# With password
+REDIS_URL="redis://:password@localhost:6379/0"
+

Gateway Settings

Variable Required Default Description
MAX_BATCHES_PER_USER No 2 Maximum concurrent batch jobs per user
STREAMING_SERVER_HOST No - Internal streaming server host:port
INTERNAL_STREAMING_SECRET No - Secret for internal streaming authentication

CLI Authentication Helper

Variable Required Default Description
CLI_AUTH_CLIENT_ID No 58fdd3bc... Public client ID for CLI auth script
CLI_ALLOWED_DOMAINS No - Comma-separated domains for CLI login
CLI_TOKEN_DIR No ~/.globus/app Token storage directory
CLI_APP_NAME No inference_auth App name for token storage

Example:

CLI_ALLOWED_DOMAINS="anl.gov,alcf.anl.gov,university.edu"
+

Metis (Direct API) Configuration

For direct OpenAI-compatible API connections:

Variable Required Default Description
METIS_STATUS_URL No - URL to status manifest JSON
METIS_API_TOKENS No - JSON object of endpoint_id -> API token

Example:

METIS_STATUS_URL="https://example.com/status.json"
+METIS_API_TOKENS='{"openai-prod": "sk-...", "anthropic-prod": "sk-ant-..."}'
+

Monitoring (Optional)

Variable Required Default Description
GF_SECURITY_ADMIN_USER No admin Grafana admin username
GF_SECURITY_ADMIN_PASSWORD No admin Grafana admin password

Qstat Endpoints (Optional)

For HPC cluster status monitoring:

Variable Required Default Description
SOPHIA_QSTAT_ENDPOINT_UUID No - Endpoint UUID for qstat function
SOPHIA_QSTAT_FUNCTION_UUID No - Function UUID for qstat

Fixture Configuration

Fixtures are JSON files that define available endpoints and models.

Endpoint Fixture Format

Located at fixtures/endpoints.json:

[
+    {
+        "model": "resource_server.endpoint",
+        "pk": 1,
+        "fields": {
+            "endpoint_slug": "local-vllm-opt-125m",
+            "cluster": "local",
+            "framework": "vllm",
+            "model": "facebook/opt-125m",
+            "api_port": 8001,
+            "endpoint_uuid": "<globus-compute-endpoint-uuid>",
+            "function_uuid": "<globus-compute-function-uuid>",
+            "batch_endpoint_uuid": "",
+            "batch_function_uuid": "",
+            "allowed_globus_groups": ""
+        }
+    }
+]
+

Federated Endpoint Fixture Format

Located at fixtures/federated_endpoints.json:

[
+    {
+        "model": "resource_server.federatedendpoint",
+        "pk": 1,
+        "fields": {
+            "name": "OPT 125M (Federated)",
+            "slug": "federated-opt-125m",
+            "target_model_name": "facebook/opt-125m",
+            "description": "Federated access point",
+            "targets": [
+                {
+                    "cluster": "local",
+                    "framework": "vllm",
+                    "model": "facebook/opt-125m",
+                    "endpoint_slug": "local-vllm-opt-125m",
+                    "endpoint_uuid": "<endpoint-uuid>",
+                    "function_uuid": "<function-uuid>",
+                    "api_port": 8001
+                }
+            ]
+        }
+    }
+]
+

Django Settings

Advanced settings in inference_gateway/settings.py:

CORS Settings

CORS_ALLOW_ALL_ORIGINS = False  # Set True for development only
+CORS_ALLOWED_ORIGINS = [
+    "https://yourdomain.com",
+]
+

Logging Configuration

Located in logging_config.py:

LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': False,
+    'handlers': {
+        'file': {
+            'level': 'INFO',
+            'class': 'logging.handlers.RotatingFileHandler',
+            'filename': 'logs/django_info.log',
+            'maxBytes': 1024 * 1024 * 15,  # 15MB
+            'backupCount': 10,
+        },
+    },
+    # ... more configuration
+}
+

Cache Configuration

CACHES = {
+    'default': {
+        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
+        'LOCATION': os.environ.get('REDIS_URL'),
+    }
+}
+

Gunicorn Configuration

For production deployments, configure Gunicorn in gunicorn_asgi.config.py:

bind = "0.0.0.0:8000"
+workers = 4  # Adjust based on CPU cores
+worker_class = "uvicorn.workers.UvicornWorker"
+timeout = 120
+keepalive = 5
+

Worker Calculation

Recommended formula:

workers = (2 * CPU_cores) + 1
+

For a 4-core machine:

workers = (2 * 4) + 1 = 9
+

Nginx Configuration

Example production configuration:

upstream inference_gateway {
+    server 127.0.0.1:8000 fail_timeout=0;
+}
+
+server {
+    listen 443 ssl http2;
+    server_name yourdomain.com;
+
+    # SSL Configuration
+    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_ciphers HIGH:!aNULL:!MD5;
+
+    # File upload size limit
+    client_max_body_size 100M;
+
+    # Timeouts
+    proxy_connect_timeout 600s;
+    proxy_send_timeout 600s;
+    proxy_read_timeout 600s;
+
+    location /static/ {
+        alias /path/to/staticfiles/;
+        expires 30d;
+        add_header Cache-Control "public, immutable";
+    }
+
+    location / {
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header Host $http_host;
+        proxy_redirect off;
+        proxy_buffering off;
+        proxy_pass http://inference_gateway;
+    }
+}
+
+# Redirect HTTP to HTTPS
+server {
+    listen 80;
+    server_name yourdomain.com;
+    return 301 https://$server_name$request_uri;
+}
+

Environment-Specific Configurations

Development

DEBUG=True
+ALLOWED_HOSTS="localhost,127.0.0.1"
+SECRET_KEY="dev-secret-key-not-secure"
+PGHOST="localhost"
+REDIS_URL="redis://localhost:6379/0"
+

Staging

DEBUG=False
+ALLOWED_HOSTS="staging.yourdomain.com"
+SECRET_KEY="<strong-secret-key>"
+PGHOST="postgres-staging.internal"
+REDIS_URL="redis://redis-staging.internal:6379/0"
+

Production

DEBUG=False
+ALLOWED_HOSTS="yourdomain.com,api.yourdomain.com"
+SECRET_KEY="<strong-secret-key>"
+PGHOST="postgres-prod.internal"
+REDIS_URL="redis://redis-prod.internal:6379/0"
+
+# Enable security features
+SECURE_SSL_REDIRECT=True
+SESSION_COOKIE_SECURE=True
+CSRF_COOKIE_SECURE=True
+

Secrets Management

Using Docker Secrets

services:
+  inference-gateway:
+    secrets:
+      - globus_secret
+      - db_password
+
+secrets:
+  globus_secret:
+    file: ./secrets/globus_secret.txt
+  db_password:
+    file: ./secrets/db_password.txt
+

Using Environment Files

# .env.local (gitignored)
+source .env.production
+export POSTGRES_PASSWORD="<secret>"
+export GLOBUS_APPLICATION_SECRET="<secret>"
+

Validation

Check your configuration:

# Django check
+python manage.py check
+
+# Database connectivity
+python manage.py dbshell
+
+# Show current settings (dev only!)
+python manage.py diffsettings
+

Next Steps

\ No newline at end of file diff --git a/site/admin-guide/gateway-setup/docker/index.html b/site/admin-guide/gateway-setup/docker/index.html new file mode 100644 index 00000000..60a413bc --- /dev/null +++ b/site/admin-guide/gateway-setup/docker/index.html @@ -0,0 +1,92 @@ + Docker Deployment - FIRST Inference Gateway Documentation

Docker Deployment

This guide shows you how to deploy the FIRST Inference Gateway using Docker and Docker Compose.

Prerequisites

  • Docker Desktop 4.29+ (or Docker Engine 24+) with Docker Compose v2
  • Git
  • Globus Account and registered applications
  • At least 4GB RAM available for containers

Step 1: Clone the Repository

git clone https://github.com/auroraGPT-ANL/inference-gateway.git
+cd inference-gateway
+

Step 2: Register Globus Applications

Before deploying, you need to register two Globus applications.

Service API Application

This handles API authorization:

  1. Visit developers.globus.org
  2. Click Register a service API
  3. Fill in the form:
  4. App Name: "My Inference Gateway"
  5. Redirect URIs: http://localhost:8000/complete/globus/ (for local development)
  6. Add your production URL if deploying to a server
  7. Note the Client UUID and generate a Client Secret

Add Scope to Service API Application

export CLIENT_ID="<Your-Service-API-Client-UUID>"
+export CLIENT_SECRET="<Your-Service-API-Client-Secret>"
+
+curl -X POST -s --user $CLIENT_ID:$CLIENT_SECRET \
+    https://auth.globus.org/v2/api/clients/$CLIENT_ID/scopes \
+    -H "Content-Type: application/json" \
+    -d '{
+        "scope": {
+            "name": "Action Provider - all",
+            "description": "Access to inference service.",
+            "scope_suffix": "action_all",
+            "dependent_scopes": [
+                {
+                    "scope": "73320ffe-4cb4-4b25-a0a3-83d53d59ce4f",
+                    "optional": false,
+                    "requires_refresh_token": true
+                }
+            ]
+        }
+    }'
+

Verify the scope:

curl -s --user $CLIENT_ID:$CLIENT_SECRET https://auth.globus.org/v2/api/clients/$CLIENT_ID
+

Service Account Application

To handle the communication between the Gateway API and the compute resources (the Inference Backend), you need to create a Globus Service Account application. This application represents the Globus identity that will own the Globus Compute endpoints.

  1. Visit developers.globus.org and sign in.
  2. Under Projects, click on the project used to register your Service API application from the previous step.
  3. Click on Add an App.
  4. Select Register a service account ....
  5. Complete the registration form:
  6. Set App Name (e.g., "My Inference Endpoints").
  7. Set Privacy Policy and Terms & Conditions URLs if applicable.
  8. After registration, a Client UUID will be assigned to your Globus application. Generate a Client Secret by clicking on the Add Client Secret button on the right-hand side. You will need both for the .env configuration. The UUID will be for SERVICE_ACCOUNT_ID, and the secret will be for SERVICE_ACCOUNT_SECRET.

Step 3: Configure Environment

Copy the example environment file:

cp deploy/docker/env.example .env
+

Edit .env with your configuration:

# --- Core Django Settings ---
+SECRET_KEY="<generate-with-command-below>"
+DEBUG=True
+ALLOWED_HOSTS="localhost,127.0.0.1"
+
+# --- Testing/Development Flags ---
+# Set to True to skip Globus High Assurance policy checks (for development/testing)
+# Set to False for production deployment
+RUNNING_AUTOMATED_TEST_SUITE=True
+LOG_TO_STDOUT=True  # Makes logs visible via docker-compose logs
+
+# --- Globus Credentials ---
+GLOBUS_APPLICATION_ID="<Your-Service-API-Client-UUID>"
+GLOBUS_APPLICATION_SECRET="<Your-Service-API-Client-Secret>"
+SERVICE_ACCOUNT_ID="<Your-Service-Account-Client-UUID>"
+SERVICE_ACCOUNT_SECRET="<Your-Service-Account-Client-Secret>"
+
+# --- Database Credentials (change for production) ---
+POSTGRES_DB="inferencegateway"
+POSTGRES_USER="inferencedev"
+POSTGRES_PASSWORD="change-this-password"
+PGHOST="postgres"
+PGPORT=5432
+PGUSER="inferencedev"
+PGPASSWORD="change-this-password"
+PGDATABASE="inferencegateway"
+
+# --- Redis ---
+REDIS_URL="redis://redis:6379/0"
+
+# --- Gateway Settings ---
+MAX_BATCHES_PER_USER=2
+STREAMING_SERVER_HOST="localhost:8080"
+INTERNAL_STREAMING_SECRET="change-this-secret"
+CLI_AUTH_CLIENT_ID="58fdd3bc-e1c3-4ce5-80ea-8d6b87cfb944"
+

Generate a secret key:

python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
+

Production Security

For production deployments:

  • Set RUNNING_AUTOMATED_TEST_SUITE=False
  • Change all passwords and secrets
  • Set DEBUG=False
  • Add your domain to ALLOWED_HOSTS
  • Configure proper Globus policies (GLOBUS_POLICIES)
  • Set authorized IDP domains (AUTHORIZED_IDP_DOMAINS)
  • Use strong, unique passwords
  • Consider using secrets management (e.g., Docker secrets)

Step 4: Start the Services

cd deploy/docker
+docker-compose up -d --build
+

This starts:

  • inference-gateway: The Django API application
  • postgres: PostgreSQL database
  • redis: Redis cache
  • nginx: Reverse proxy (optional, if configured)

Verify services are running:

docker-compose ps
+

Step 5: Initialize the Database

Run migrations:

docker-compose exec inference-gateway python manage.py migrate
+

Create a superuser (optional, for Django admin access):

docker-compose exec inference-gateway python manage.py createsuperuser
+

Collect static files:

docker-compose exec inference-gateway python manage.py collectstatic --noinput
+

Step 6: Verify the Gateway

Check that the gateway is running:

curl http://localhost:8000/
+

Access the Django admin (if superuser was created):

  • URL: http://localhost:8000/admin/
  • Login with your superuser credentials

Step 7: Configure Backends

Now you need to connect inference backends. Choose one:

Docker Compose Services

The docker-compose.yml includes:

Core Services

  • inference-gateway: Django application (port 8000)
  • postgres: PostgreSQL 15 (port 5432)
  • redis: Redis 7 (port 6379)

Optional Services

You can add these to your compose file:

  • nginx: Reverse proxy for production
  • prometheus: Metrics collection
  • grafana: Visualization dashboard

Common Commands

View logs

# All services
+docker-compose logs -f
+
+# Specific service
+docker-compose logs -f inference-gateway
+

Restart services

# All services
+docker-compose restart
+
+# Specific service
+docker-compose restart inference-gateway
+

Stop services

docker-compose down
+

Stop and remove volumes (clean slate)

docker-compose down -v
+

Access container shell

docker-compose exec inference-gateway /bin/bash
+

Run Django management commands

docker-compose exec inference-gateway python manage.py <command>
+

Updating the Deployment

Pull latest changes:

git pull origin main
+docker-compose build
+docker-compose up -d
+docker-compose exec inference-gateway python manage.py migrate
+

Troubleshooting

Gateway won't start

Check logs:

docker-compose logs inference-gateway
+

Common issues:

  • Missing environment variables
  • Database connection failed
  • Port 8000 already in use

Database connection errors

Verify PostgreSQL is running:

docker-compose ps postgres
+

Check database logs:

docker-compose logs postgres
+

Can't access admin panel

Ensure you created a superuser:

docker-compose exec inference-gateway python manage.py createsuperuser
+

502 Bad Gateway from Nginx

Check that the gateway container is running:

docker-compose ps inference-gateway
+

Verify nginx configuration:

docker-compose exec nginx nginx -t
+

Next Steps

Additional Resources

\ No newline at end of file diff --git a/site/admin-guide/index.html b/site/admin-guide/index.html new file mode 100644 index 00000000..cac97012 --- /dev/null +++ b/site/admin-guide/index.html @@ -0,0 +1,12 @@ + Overview - FIRST Inference Gateway Documentation

Administrator Guide

Welcome to the FIRST Inference Gateway Administrator Guide. This guide will help you deploy and configure the gateway for your organization.

Overview

Setting up FIRST involves two main components:

  1. Gateway Setup: The central API service that handles authentication and routing
  2. Inference Backend Setup: The actual inference servers where models run

These can be deployed independently and connected together through configuration.

Prerequisites

Before you begin, ensure you have:

  • [x] Python 3.12 or later
  • [x] Docker and Docker Compose (for Docker deployment)
  • [x] PostgreSQL Server (or use Docker)
  • [x] Globus Account
  • [x] Access to compute resources (for inference backends)

Deployment Architecture

Choose your deployment approach:

Gateway Deployment Options

Docker Deployment (Recommended)

  • Quick setup with Docker Compose
  • Pros: Easy to deploy, includes all dependencies, portable
  • Cons: Requires Docker knowledge
  • Docker Guide →

Bare Metal Deployment

  • Direct installation on your server infrastructure
  • Pros: More control, better performance, easier debugging
  • Cons: Manual dependency management
  • Bare Metal Guide →

Inference Backend Options

Globus Compute + vLLM (Recommended for Production)

  • Deploy vLLM on HPC clusters with Globus Compute for remote execution
  • Best for: Multi-cluster, federated deployments, HPC environments
  • Globus Compute Setup →

Local vLLM

  • Run vLLM inference server locally without Globus Compute
  • Best for: Single-node deployments, development
  • Local vLLM Setup →

Direct API Connection

  • Connect to existing OpenAI-compatible APIs (OpenAI, Anthropic, etc.)
  • Best for: Simple setup, using commercial APIs
  • Direct API Setup →

Setup Workflow

Phase 1: Gateway Setup

  1. Choose your deployment method (Docker or Bare Metal)
  2. Register Globus applications
  3. Configure environment variables
  4. Initialize the database
  5. Start the gateway service

Phase 2: Inference Backend Setup

  1. Choose your backend type
  2. Install required software (vLLM, Globus Compute, etc.)
  3. Configure the backend
  4. Register endpoints/functions
  5. Test the connection

Phase 3: Connect Gateway and Backend

  1. Update fixture files with backend details
  2. Load fixtures into the gateway database
  3. Verify end-to-end functionality

Common Patterns

Pattern 1: Quick Local Development

graph LR
+    A[Docker Gateway] --> B[Local vLLM]
+    B --> C[Small Model<br/>OPT-125M]
+

Use: Development and testing

Setup Time: ~15 minutes

Resources: 1 GPU or CPU

Pattern 2: Production Single Cluster

graph LR
+    A[Bare Metal Gateway] --> B[Globus Compute]
+    B --> C[HPC Cluster]
+    C --> D[Multiple Models]
+

Use: Production deployment on single HPC cluster

Setup Time: ~2 hours

Resources: HPC cluster access

Pattern 3: Federated Multi-Cluster

graph LR
+    A[Gateway] --> B[Cluster 1<br/>Globus Compute]
+    A --> C[Cluster 2<br/>Globus Compute]
+    A --> D[Cluster 3<br/>Globus Compute]
+

Use: Maximum availability and resource pooling

Setup Time: ~4 hours

Resources: Multiple HPC clusters

Next Steps

Ready to get started? Choose your path:

Additional Resources

\ No newline at end of file diff --git a/site/admin-guide/inference-setup/direct-api/index.html b/site/admin-guide/inference-setup/direct-api/index.html new file mode 100644 index 00000000..7857ba81 --- /dev/null +++ b/site/admin-guide/inference-setup/direct-api/index.html @@ -0,0 +1,145 @@ + Direct API Connection - FIRST Inference Gateway Documentation

Direct API Connection

This guide shows you how to connect the FIRST Gateway to existing OpenAI-compatible APIs without running any local inference infrastructure.

Overview

The Direct API backend allows you to:

  • Proxy requests to commercial APIs (OpenAI, Anthropic, etc.)
  • Add Globus authentication to existing APIs
  • Manage API keys centrally
  • Route between multiple API providers

Architecture

graph LR
+    A[User] -->|Globus Token| B[FIRST Gateway]
+    B -->|API Key| C[OpenAI API]
+    B -->|API Key| D[Anthropic API]
+    B -->|API Key| E[Custom API]
+

Prerequisites

  • FIRST Gateway deployed and running
  • API keys from your providers
  • A way to host a status manifest (static file or endpoint)

Step 1: Create Status Manifest

The gateway uses a status manifest to discover available endpoints. Create a JSON file:

{
+  "openai-gpt4": {
+    "status": "Live",
+    "model": "OpenAI GPT-4",
+    "description": "GPT-4 models via OpenAI API",
+    "experts": [
+      "gpt-4",
+      "gpt-4-turbo",
+      "gpt-4o",
+      "gpt-4o-mini"
+    ],
+    "url": "https://api.openai.com/v1",
+    "endpoint_id": "openai-production"
+  },
+  "anthropic-claude": {
+    "status": "Live",
+    "model": "Anthropic Claude",
+    "description": "Claude models via Anthropic API",
+    "experts": [
+      "claude-3-opus-20240229",
+      "claude-3-sonnet-20240229",
+      "claude-3-haiku-20240307"
+    ],
+    "url": "https://api.anthropic.com/v1",
+    "endpoint_id": "anthropic-production"
+  }
+}
+

Manifest Field Descriptions

Field Required Description
status Yes "Live", "Offline", or "Maintenance"
model Yes Human-readable model description
description Yes Detailed description
experts Yes Array of model identifiers
url Yes Base URL for the API
endpoint_id Yes Unique identifier (used for API key mapping)

Step 2: Host the Status Manifest

Option A: Static File Server

# Simple Python HTTP server
+mkdir -p /var/www/metis
+cp status.json /var/www/metis/
+cd /var/www/metis
+python3 -m http.server 8055
+

Option B: Nginx

server {
+    listen 80;
+    server_name status.yourdomain.com;
+
+    location / {
+        root /var/www/metis;
+        add_header Content-Type application/json;
+        add_header Access-Control-Allow-Origin *;
+    }
+}
+

Option C: S3/Cloud Storage

Upload status.json to a public S3 bucket or equivalent:

# AWS S3
+aws s3 cp status.json s3://your-bucket/status.json --acl public-read
+
+# Access via: https://your-bucket.s3.amazonaws.com/status.json
+

Option D: Local for Docker (Development Only)

For local Docker testing:

# On host machine
+mkdir -p deploy/docker/examples
+cat > deploy/docker/examples/metis-status.json << 'EOF'
+{
+  "openai-gateway": {
+    "status": "Live",
+    "model": "OpenAI Pass-through",
+    "description": "Routes to OpenAI's GPT models",
+    "experts": ["gpt-4o-mini", "gpt-4"],
+    "url": "https://api.openai.com/v1",
+    "endpoint_id": "openai-production"
+  }
+}
+EOF
+
+# Serve it
+python3 -m http.server 8055 --directory deploy/docker/examples
+

Then use http://host.docker.internal:8055/metis-status.json in your Docker .env.

Step 3: Configure Gateway

Add these to your gateway's .env file:

# URL to your status manifest
+METIS_STATUS_URL="http://your-server:8055/status.json"
+
+# API keys mapped to endpoint_id from the manifest
+METIS_API_TOKENS='{"openai-production": "sk-proj-...", "anthropic-production": "sk-ant-..."}'
+

Environment Variable Format

METIS_STATUS_URL: Direct URL to your JSON manifest

METIS_API_TOKENS: JSON object where:

  • Keys are endpoint_id values from your manifest
  • Values are the API keys for those services

Example with multiple providers:

METIS_API_TOKENS='{
+  "openai-production": "sk-proj-abc123...",
+  "anthropic-production": "sk-ant-xyz789...",
+  "custom-api": "custom-key-here"
+}'
+

Step 4: Restart Gateway

Docker

docker-compose up -d inference-gateway
+

Bare Metal

sudo systemctl restart inference-gateway
+

Step 5: Test the Connection

Get a Globus token:

export TOKEN=$(python inference-auth-token.py get_access_token)
+

Test OpenAI endpoint:

curl -X POST http://localhost:8000/resource_server/metis/api/v1/chat/completions \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "model": "gpt-4o-mini",
+    "messages": [{"role": "user", "content": "Hello from FIRST!"}],
+    "stream": false
+  }'
+

Expected response:

{
+  "id": "chatcmpl-...",
+  "object": "chat.completion",
+  "created": 1234567890,
+  "model": "gpt-4o-mini",
+  "choices": [{
+    "index": 0,
+    "message": {
+      "role": "assistant",
+      "content": "Hello! How can I assist you today?"
+    },
+    "finish_reason": "stop"
+  }],
+  "usage": {
+    "prompt_tokens": 10,
+    "completion_tokens": 15,
+    "total_tokens": 25
+  }
+}
+

Advanced Configuration

Multiple Endpoints Per Provider

{
+  "openai-us-east": {
+    "status": "Live",
+    "model": "OpenAI US East",
+    "experts": ["gpt-4"],
+    "url": "https://api.openai.com/v1",
+    "endpoint_id": "openai-us-east"
+  },
+  "openai-eu-west": {
+    "status": "Live",
+    "model": "OpenAI EU West",
+    "experts": ["gpt-4"],
+    "url": "https://api.openai.com/v1",
+    "endpoint_id": "openai-eu-west"
+  }
+}
+

Custom API Headers

For APIs requiring additional headers, you can extend the gateway code or use environment variables. Contact your administrator for custom integration.

Load Balancing

The gateway automatically load-balances across all "Live" endpoints for the same model.

Failover

If one endpoint returns an error, the gateway automatically tries the next available endpoint.

Monitoring

Check Endpoint Status

The gateway periodically fetches the status manifest. View logs:

# Docker
+docker-compose logs -f inference-gateway | grep metis
+
+# Bare metal
+tail -f logs/django_info.log | grep metis
+

Track API Usage

Monitor usage through:

  • Gateway access logs
  • Provider API dashboards (OpenAI, Anthropic)
  • Custom usage tracking (implement in gateway)

Cost Management

Setting Budgets

Configure per-user or per-group budgets in your application logic or via API key restrictions at the provider level.

Rate Limiting

The gateway supports rate limiting per user/group. Configure in Django admin or via settings.

Security Considerations

API Key Security

Protect Your API Keys

  • Never commit API keys to version control
  • Use environment variables or secrets management
  • Rotate keys regularly
  • Use separate keys for dev/staging/production

Status Manifest Security

If your manifest contains sensitive information:

  • Serve over HTTPS
  • Implement authentication (basic auth, token)
  • Restrict IP access via firewall

Access Control

Restrict which Globus groups can access which APIs:

GLOBUS_GROUPS="allowed-group-uuid-1 allowed-group-uuid-2"
+

Troubleshooting

Gateway can't fetch status manifest

Check connectivity:

curl http://your-server:8055/status.json
+

Verify METIS_STATUS_URL is correct and accessible from the gateway.

Authentication errors from provider

  • Verify API key is correct
  • Check key hasn't expired
  • Ensure key has required permissions
  • Check provider status page

Model not found

Ensure the model name matches exactly what's in the experts array of your manifest.

Rate limiting errors

  • Check provider rate limits
  • Implement gateway-side rate limiting
  • Consider upgrading provider plan

Example: Adding Azure OpenAI

{
+  "azure-openai": {
+    "status": "Live",
+    "model": "Azure OpenAI",
+    "description": "GPT models via Azure OpenAI Service",
+    "experts": [
+      "gpt-4",
+      "gpt-35-turbo"
+    ],
+    "url": "https://your-resource.openai.azure.com/openai/deployments/your-deployment",
+    "endpoint_id": "azure-openai-prod"
+  }
+}
+

Azure requires additional configuration - consult Azure OpenAI documentation.

Next Steps

Additional Resources

\ No newline at end of file diff --git a/site/admin-guide/inference-setup/globus-compute/index.html b/site/admin-guide/inference-setup/globus-compute/index.html new file mode 100644 index 00000000..69a21876 --- /dev/null +++ b/site/admin-guide/inference-setup/globus-compute/index.html @@ -0,0 +1,262 @@ + Globus Compute + vLLM - FIRST Inference Gateway Documentation

Globus Compute + vLLM Setup

This guide shows you how to deploy vLLM on HPC clusters or remote servers using Globus Compute for federated inference.

Overview

This is the recommended approach for:

  • Multi-cluster federated deployments
  • HPC environments with job schedulers (PBS, Slurm)
  • Organizations requiring high availability
  • Remote execution with secure authentication

Architecture

graph TB
+    A[User] -->|Globus Token| B[FIRST Gateway]
+    B -->|Globus Compute| C[HPC Cluster 1]
+    B -->|Globus Compute| D[HPC Cluster 2]
+    B -->|Globus Compute| E[Local Server]
+    C -->|Job Scheduler| F[vLLM + Model]
+    D -->|Job Scheduler| G[vLLM + Model]
+    E -->|Direct| H[vLLM + Model]
+

Prerequisites

  • FIRST Gateway deployed with Service Account application credentials
  • Access to compute resources (HPC cluster or powerful workstation)
  • GPU resources for running models
  • Python 3.12+ (same version as gateway)
  • Globus Compute SDK and Endpoint software

Part 1: Setup on Compute Resource

This is done on the machine(s) where models will run.

Step 1: Create Python Environment

Version Match

Use the same Python version as your gateway to avoid compatibility issues.

# Using conda (recommended for HPC)
+conda create -n vllm-env python=3.12 -y
+conda activate vllm-env
+
+# OR using venv
+python3.12 -m venv vllm-env
+source vllm-env/bin/activate
+

Step 2: Install vLLM

# For CUDA 12.1 (default)
+pip install vllm
+
+# For specific CUDA version
+pip install vllm-cu118  # CUDA 11.8
+
+# From source (for latest features)
+git clone https://github.com/vllm-project/vllm.git
+cd vllm
+pip install -e .
+

Step 3: Install Globus Compute

pip install globus-compute-sdk globus-compute-endpoint
+

Step 4: Export Service Account Credentials

These are from your Gateway's Service Account application:

export GLOBUS_COMPUTE_CLIENT_ID="<SERVICE_ACCOUNT_ID-from-gateway-.env>"
+export GLOBUS_COMPUTE_CLIENT_SECRET="<SERVICE_ACCOUNT_SECRET-from-gateway-.env>"
+

Add to your ~/.bashrc or ~/.bash_profile for persistence:

echo 'export GLOBUS_COMPUTE_CLIENT_ID="your-id"' >> ~/.bashrc
+echo 'export GLOBUS_COMPUTE_CLIENT_SECRET="your-secret"' >> ~/.bashrc
+

Step 5: Register Globus Compute Functions

Navigate to the gateway repository's compute-functions directory:

cd /path/to/inference-gateway/compute-functions
+

Register Inference Function

python vllm_register_function_with_streaming.py
+

Output:

Function registered with UUID: 12345678-1234-1234-1234-123456789abc
+The UUID is stored in vllm_register_function_streaming.txt
+

Save this Function UUID - you'll need it later.

For HPC clusters with job schedulers:

python qstat_register_function.py
+

Save the Function UUID from the output.

Register Batch Function (Optional)

For batch processing support:

python vllm_batch_function.py
+

Save the Function UUID.

Step 6: Configure Globus Compute Endpoint

Create a new endpoint:

globus-compute-endpoint configure my-inference-endpoint
+

This creates ~/.globus_compute/my-inference-endpoint/config.yaml.

For Local/Workstation Deployment

Edit config.yaml:

display_name: My Inference Endpoint
+engine:
+  type: GlobusComputeEngine
+  provider:
+    type: LocalProvider
+    init_blocks: 1
+    max_blocks: 1
+    min_blocks: 0
+
+# Allow only your registered functions
+allowed_functions:
+  - 12345678-1234-1234-1234-123456789abc  # Your vLLM function UUID
+  - 87654321-4321-4321-4321-cba987654321  # Your qstat function UUID (if any)
+
+# Activate your environment before running functions
+worker_init: |
+  source /path/to/vllm-env/bin/activate
+  # OR: conda activate vllm-env
+

For HPC with PBS (e.g., ALCF Sophia)

display_name: Sophia vLLM Endpoint
+engine:
+  type: GlobusComputeEngine
+  provider:
+    type: PBSProProvider
+    account: YourProjectAccount
+    queue: demand
+    nodes_per_block: 1
+    init_blocks: 1
+    max_blocks: 4
+    min_blocks: 0
+    walltime: "06:00:00"
+    scheduler_options: |
+      #PBS -l filesystems=home:eagle
+      #PBS -l place=scatter
+    worker_init: |
+      module load conda
+      conda activate /path/to/vllm-env
+      # OR: source /path/to/common_setup.sh
+
+# GPU allocation
+max_workers_per_node: 1
+
+# Allow only your registered functions
+allowed_functions:
+  - 12345678-1234-1234-1234-123456789abc
+

For HPC with Slurm

display_name: Slurm vLLM Endpoint
+engine:
+  type: GlobusComputeEngine
+  provider:
+    type: SlurmProvider
+    account: your_account
+    partition: gpu
+    nodes_per_block: 1
+    init_blocks: 1
+    max_blocks: 4
+    walltime: "06:00:00"
+    scheduler_options: |
+      #SBATCH --gpus-per-node=1
+      #SBATCH --mem=64G
+    worker_init: |
+      source /path/to/vllm-env/bin/activate
+
+allowed_functions:
+  - 12345678-1234-1234-1234-123456789abc
+

Step 7: Start the Endpoint

globus-compute-endpoint start my-inference-endpoint
+

Output:

Starting endpoint; registered ID: abcdef12-3456-7890-abcd-ef1234567890
+

Save this Endpoint UUID - you'll need it for gateway configuration.

Verify it's running:

globus-compute-endpoint list
+

View logs:

globus-compute-endpoint log my-inference-endpoint
+

Part 2: Configure Gateway

Now configure the gateway to use your Globus Compute endpoint.

Step 1: Create Endpoint Fixture

On your gateway machine, edit fixtures/endpoints.json:

Single Endpoint Configuration

[
+    {
+        "model": "resource_server.endpoint",
+        "pk": 1,
+        "fields": {
+            "endpoint_slug": "sophia-vllm-llama3-8b",
+            "cluster": "sophia",
+            "framework": "vllm",
+            "model": "meta-llama/Meta-Llama-3-8B-Instruct",
+            "api_port": 8000,
+            "endpoint_uuid": "abcdef12-3456-7890-abcd-ef1234567890",
+            "function_uuid": "12345678-1234-1234-1234-123456789abc",
+            "batch_endpoint_uuid": "",
+            "batch_function_uuid": "",
+            "allowed_globus_groups": ""
+        }
+    }
+]
+

Multiple Clusters Configuration

[
+    {
+        "model": "resource_server.endpoint",
+        "pk": 1,
+        "fields": {
+            "endpoint_slug": "sophia-vllm-llama3-8b",
+            "cluster": "sophia",
+            "framework": "vllm",
+            "model": "meta-llama/Meta-Llama-3-8B-Instruct",
+            "api_port": 8000,
+            "endpoint_uuid": "sophia-endpoint-uuid",
+            "function_uuid": "sophia-function-uuid",
+            "allowed_globus_groups": ""
+        }
+    },
+    {
+        "model": "resource_server.endpoint",
+        "pk": 2,
+        "fields": {
+            "endpoint_slug": "polaris-vllm-llama3-70b",
+            "cluster": "polaris",
+            "framework": "vllm",
+            "model": "meta-llama/Llama-2-70b-chat-hf",
+            "api_port": 8000,
+            "endpoint_uuid": "polaris-endpoint-uuid",
+            "function_uuid": "polaris-function-uuid",
+            "allowed_globus_groups": ""
+        }
+    }
+]
+

For automatic load balancing and failover, use federated endpoints.

Edit fixtures/federated_endpoints.json:

[
+    {
+        "model": "resource_server.federatedendpoint",
+        "pk": 1,
+        "fields": {
+            "name": "Llama-3 8B (Federated)",
+            "slug": "federated-llama3-8b",
+            "target_model_name": "meta-llama/Meta-Llama-3-8B-Instruct",
+            "description": "Federated access to Llama-3 8B across multiple clusters",
+            "targets": [
+                {
+                    "cluster": "sophia",
+                    "framework": "vllm",
+                    "model": "meta-llama/Meta-Llama-3-8B-Instruct",
+                    "endpoint_slug": "sophia-vllm-llama3-8b",
+                    "endpoint_uuid": "sophia-endpoint-uuid",
+                    "function_uuid": "sophia-function-uuid",
+                    "api_port": 8000
+                },
+                {
+                    "cluster": "polaris",
+                    "framework": "vllm",
+                    "model": "meta-llama/Meta-Llama-3-8B-Instruct",
+                    "endpoint_slug": "polaris-vllm-llama3-8b",
+                    "endpoint_uuid": "polaris-endpoint-uuid",
+                    "function_uuid": "polaris-function-uuid",
+                    "api_port": 8000
+                }
+            ]
+        }
+    }
+]
+

Step 3: Load Fixtures

# Docker
+docker-compose exec inference-gateway python manage.py loaddata fixtures/endpoints.json
+docker-compose exec inference-gateway python manage.py loaddata fixtures/federated_endpoints.json
+
+# Bare Metal
+python manage.py loaddata fixtures/endpoints.json
+python manage.py loaddata fixtures/federated_endpoints.json
+

Part 3: Testing

Test Globus Compute Endpoint

From your gateway machine, test that you can reach the endpoint:

from globus_compute_sdk import Client
+
+gcc = Client()
+endpoint_id = "abcdef12-3456-7890-abcd-ef1234567890"
+function_id = "12345678-1234-1234-1234-123456789abc"
+
+# Simple test function
+def hello():
+    return "Hello from Globus Compute!"
+
+# Register and run
+func_uuid = gcc.register_function(hello)
+task = gcc.run(endpoint_id=endpoint_id, function_id=func_uuid)
+print(gcc.get_result(task))
+

Test vLLM via Gateway

Get a Globus token:

export TOKEN=$(python inference-auth-token.py get_access_token)
+

Test non-federated endpoint:

curl -X POST http://localhost:8000/resource_server/sophia/vllm/v1/chat/completions \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "model": "meta-llama/Meta-Llama-3-8B-Instruct",
+    "messages": [{"role": "user", "content": "What is Globus Compute?"}],
+    "max_tokens": 100
+  }'
+

Test federated endpoint:

curl -X POST http://localhost:8000/resource_server/v1/chat/completions \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "model": "meta-llama/Meta-Llama-3-8B-Instruct",
+    "messages": [{"role": "user", "content": "What is Globus Compute?"}],
+    "max_tokens": 100
+  }'
+

Advanced Configuration

Hot Nodes / Keep-Alive

For low-latency inference, keep compute nodes warm:

# In config.yaml
+engine:
+  provider:
+    min_blocks: 1  # Always keep 1 node running
+    init_blocks: 2  # Start with 2 nodes
+

Multi-Node vLLM

For very large models (70B+), use tensor parallelism across multiple GPUs/nodes:

See compute-endpoints/sophia-vllm-multinode-example.yaml for configuration examples.

Custom Inference Scripts

The registered functions use scripts in compute-functions/. You can customize:

  • launch_vllm_model.sh: How vLLM is started
  • vllm_register_function_with_streaming.py: The inference function logic

Batch Processing

Enable batch processing by registering and configuring batch functions:

python vllm_batch_function.py
+

Add batch UUIDs to your endpoint fixture:

{
+    "batch_endpoint_uuid": "batch-endpoint-uuid",
+    "batch_function_uuid": "batch-function-uuid"
+}
+

Monitoring

Endpoint Status

# Check endpoint status
+globus-compute-endpoint status my-inference-endpoint
+
+# View logs
+globus-compute-endpoint log my-inference-endpoint -n 50
+
+# Follow logs
+tail -f ~/.globus_compute/my-inference-endpoint/endpoint.log
+

Job Scheduler Monitoring

# PBS
+qstat -u $USER
+
+# Slurm
+squeue -u $USER
+

Gateway Logs

# Docker
+docker-compose logs -f inference-gateway
+
+# Bare Metal
+tail -f logs/django_info.log
+

Troubleshooting

Endpoint won't start

Check logs:

globus-compute-endpoint log my-inference-endpoint
+

Common issues:

  • Service account credentials not set
  • Function UUIDs not in allowed_functions
  • Python environment activation fails
  • Job scheduler configuration incorrect

Function execution fails

  • Verify environment can run vLLM: Test manually on compute node
  • Check function is allowed in config.yaml
  • Ensure Service Account has permissions
  • Check job scheduler logs

Model not loading

  • Verify GPU allocation in scheduler options
  • Check VRAM requirements
  • Ensure model path/name is correct
  • Check Hugging Face token if needed

Gateway can't connect

  • Verify endpoint is running: globus-compute-endpoint list
  • Check endpoint UUID in fixtures is correct
  • Ensure Service Account credentials match in both places
  • Check network connectivity (firewall, VPN)

HPC-Specific Considerations

ALCF Systems (Polaris, Sophia)

  • Use /lus/eagle for model cache
  • Load CUDA modules in worker_init
  • Set appropriate walltime
  • Use #PBS -l filesystems=home:eagle

Other HPC Centers

  • Check local documentation for:
  • GPU allocation syntax
  • File system paths
  • Module system
  • Queue/partition names
  • Accounting project codes

Scaling

Auto-Scaling

Configure max_blocks to allow automatic scaling:

engine:
+  provider:
+    init_blocks: 1
+    min_blocks: 0
+    max_blocks: 10  # Scale up to 10 nodes
+

Load Balancing

Federated endpoints automatically load balance across all "Live" targets.

Geographic Distribution

Deploy endpoints at different locations and configure federated routing for optimal latency.

Security

Function Allowlisting

Always specify allowed_functions in config.yaml:

allowed_functions:
+  - 12345678-1234-1234-1234-123456789abc  # Only your functions
+

Group-Based Access

Restrict endpoints to specific Globus groups:

{
+    "allowed_globus_groups": "group-uuid-1,group-uuid-2"
+}
+

Network Security

  • Globus Compute uses HTTPS and mutual TLS
  • No inbound ports need to be opened on compute resources
  • All communication is outbound from endpoint to Globus services

Next Steps

Additional Resources

\ No newline at end of file diff --git a/site/admin-guide/inference-setup/index.html b/site/admin-guide/inference-setup/index.html new file mode 100644 index 00000000..1e8dafa5 --- /dev/null +++ b/site/admin-guide/inference-setup/index.html @@ -0,0 +1,40 @@ + Overview - FIRST Inference Gateway Documentation

Inference Backend Setup

The FIRST Inference Gateway supports multiple types of inference backends. Choose the approach that best fits your use case.

Backend Options

Deploy vLLM on HPC clusters or multiple servers with Globus Compute for remote execution and federated routing.

Best for:

  • Multi-cluster deployments
  • HPC environments
  • Federated inference across organizations
  • Production deployments requiring high availability

Pros:

  • Federated routing across clusters
  • Automatic failover
  • HPC job scheduler integration
  • Scales across organizations
  • Secure, authenticated remote execution

Cons:

  • More complex setup
  • Requires Globus Compute knowledge
  • Additional configuration overhead

→ Setup Globus Compute + vLLM


2. Local vLLM Setup

Run vLLM inference server locally without Globus Compute.

Best for:

  • Single-server deployments
  • Development and testing with local models
  • Controlled environments where Globus Compute isn't needed
  • Simple architectures

Pros:

  • Full control over models and data
  • No external dependencies
  • Simple architecture
  • Lower latency for local requests

Cons:

  • Requires GPU resources
  • Single point of failure
  • Manual scaling
  • No federated routing

→ Setup Local vLLM


3. Direct API Connection

Connect to existing OpenAI-compatible APIs without any local inference infrastructure.

Best for:

  • Quick testing and development
  • Using commercial API services (OpenAI, Anthropic, etc.)
  • Organizations without GPU resources
  • Proxying/adding authentication to existing APIs

Pros:

  • Fastest setup (5-10 minutes)
  • No local compute resources needed
  • Scales with the provider
  • Multiple models immediately available

Cons:

  • Costs per API call
  • Data leaves your infrastructure
  • Dependent on third-party service

→ Setup Direct API Connection


Comparison Matrix

Feature Globus Compute + vLLM Local vLLM Direct API
Setup Time 2-4 hours 30-60 min 5-10 min
GPU Required Yes Yes No
Data Privacy Local Local External
Multi-Cluster Yes No No
Failover Automatic Manual Provider
Cost Infrastructure Infrastructure Pay-per-use
HPC Integration Yes No No
Complexity High Medium Low

Decision Tree

graph TD
+    A[Choose Backend] --> B{Have GPU?}
+    B -->|No| C[Direct API]
+    B -->|Yes| D{Multiple Clusters?}
+    D -->|No| E{Need HPC Integration?}
+    D -->|Yes| F[Globus Compute + vLLM]
+    E -->|No| G[Local vLLM]
+    E -->|Yes| F
+

Combined Approach

You can use multiple backends simultaneously! The gateway supports:

  • Multiple Direct API connections: Route to OpenAI, Anthropic, etc.
  • Mix Local and Remote: Some models local, some via Globus Compute
  • Federated Endpoints: Route same model across multiple clusters

Example combined setup:

{
+  "endpoints": [
+    {
+      "cluster": "openai",
+      "model": "gpt-4",
+      "type": "direct_api"
+    },
+    {
+      "cluster": "local",
+      "model": "llama-3-8b",
+      "type": "vllm"
+    },
+    {
+      "cluster": "sophia",
+      "model": "llama-3-70b",
+      "type": "globus_compute"
+    }
+  ]
+}
+

Prerequisites by Backend Type

All Backends

  • FIRST Gateway already deployed
  • Access to gateway configuration
  • Admin access to load fixtures

Direct API

  • API keys from providers
  • Status manifest endpoint (can be static file)

Local vLLM

  • GPU with sufficient VRAM for your model
  • Python 3.12+
  • Ability to run vLLM server

Globus Compute + vLLM

  • All Local vLLM requirements plus:
  • Globus Service Account application
  • Access to HPC cluster (optional)
  • Ability to configure Globus Compute endpoints

Setup Workflow

Phase 1: Choose and Setup Backend

Follow the guide for your chosen backend type.

Phase 2: Register with Gateway

Update fixture files (fixtures/endpoints.json or fixtures/federated_endpoints.json) with your backend details.

Phase 3: Load Configuration

# Docker
+docker-compose exec inference-gateway python manage.py loaddata fixtures/endpoints.json
+
+# Bare metal
+python manage.py loaddata fixtures/endpoints.json
+

Phase 4: Test

Send a test request to verify the connection:

curl -X POST http://localhost:8000/resource_server/v1/chat/completions \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "model": "your-model-name",
+    "messages": [{"role": "user", "content": "Hello!"}]
+  }'
+

Next Steps

Choose your backend setup guide:

After setup:

\ No newline at end of file diff --git a/site/admin-guide/inference-setup/local-vllm/index.html b/site/admin-guide/inference-setup/local-vllm/index.html new file mode 100644 index 00000000..cd95451a --- /dev/null +++ b/site/admin-guide/inference-setup/local-vllm/index.html @@ -0,0 +1,234 @@ + Local vLLM Setup - FIRST Inference Gateway Documentation

Local vLLM Setup

This guide shows you how to run vLLM inference server locally and connect it to the FIRST Gateway without Globus Compute.

Overview

This setup is ideal for:

  • Single-server deployments
  • Development and testing
  • Scenarios where Globus Compute overhead isn't needed
  • Direct control over the inference process

Architecture

graph LR
+    A[User] -->|Globus Token| B[FIRST Gateway]
+    B -->|HTTP| C[vLLM Server]
+    C -->|GPU| D[Model]
+

Prerequisites

  • NVIDIA GPU with sufficient VRAM for your model
  • CUDA 11.8 or later
  • Python 3.12+
  • Docker (optional, for containerized vLLM)

Step 1: Install vLLM

# Create virtual environment
+python3.12 -m venv vllm-env
+source vllm-env/bin/activate
+
+# Clone and install vLLM
+git clone https://github.com/vllm-project/vllm.git
+cd vllm
+pip install -e .
+
+# Install additional dependencies
+pip install openai  # For testing
+

Option B: Install via pip

python3.12 -m venv vllm-env
+source vllm-env/bin/activate
+
+pip install vllm
+
+# For specific CUDA version
+pip install vllm  # CUDA 12.1 by default
+# OR
+pip install vllm-cu118  # For CUDA 11.8
+

Option C: Use Docker

docker pull vllm/vllm-openai:latest
+

Step 2: Download a Model

Choose a model based on your GPU VRAM:

Model VRAM Required Performance
facebook/opt-125m ~1GB Fast, good for testing
meta-llama/Llama-2-7b-chat-hf ~14GB Good quality
meta-llama/Meta-Llama-3-8B-Instruct ~16GB Better quality
meta-llama/Llama-2-13b-chat-hf ~26GB High quality
meta-llama/Llama-2-70b-chat-hf ~140GB Best quality (multi-GPU)

Using Hugging Face

# Login to Hugging Face (for gated models like Llama)
+pip install huggingface-hub
+huggingface-cli login
+
+# Models will auto-download on first use
+# Or pre-download:
+huggingface-cli download meta-llama/Meta-Llama-3-8B-Instruct
+

Step 3: Start vLLM Server

Basic Start

source vllm-env/bin/activate
+
+python -m vllm.entrypoints.openai.api_server \
+    --model facebook/opt-125m \
+    --host 0.0.0.0 \
+    --port 8001
+

Production Configuration

python -m vllm.entrypoints.openai.api_server \
+    --model meta-llama/Meta-Llama-3-8B-Instruct \
+    --host 0.0.0.0 \
+    --port 8001 \
+    --tensor-parallel-size 1 \
+    --gpu-memory-utilization 0.9 \
+    --max-model-len 4096 \
+    --dtype auto \
+    --enable-prefix-caching
+

Multi-GPU Configuration

# For 4 GPUs
+python -m vllm.entrypoints.openai.api_server \
+    --model meta-llama/Llama-2-70b-chat-hf \
+    --host 0.0.0.0 \
+    --port 8001 \
+    --tensor-parallel-size 4 \
+    --gpu-memory-utilization 0.9
+

Using Docker

docker run --gpus all \
+    -v ~/.cache/huggingface:/root/.cache/huggingface \
+    -p 8001:8000 \
+    --env "HUGGING_FACE_HUB_TOKEN=<your_token>" \
+    vllm/vllm-openai:latest \
+    --model facebook/opt-125m \
+    --host 0.0.0.0 \
+    --port 8000
+

Step 4: Test vLLM Server

curl http://localhost:8001/v1/chat/completions \
+    -H "Content-Type: application/json" \
+    -d '{
+        "model": "facebook/opt-125m",
+        "messages": [
+            {"role": "user", "content": "Hello!"}
+        ]
+    }'
+

Step 5: Create systemd Service (Optional)

For production deployments, run vLLM as a system service:

sudo nano /etc/systemd/system/vllm.service
+

Add:

[Unit]
+Description=vLLM Inference Server
+After=network.target
+
+[Service]
+Type=simple
+User=your-username
+WorkingDirectory=/home/your-username
+Environment="PATH=/home/your-username/vllm-env/bin"
+ExecStart=/home/your-username/vllm-env/bin/python -m vllm.entrypoints.openai.api_server \
+    --model meta-llama/Meta-Llama-3-8B-Instruct \
+    --host 0.0.0.0 \
+    --port 8001 \
+    --tensor-parallel-size 1 \
+    --gpu-memory-utilization 0.9
+Restart=always
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
+

Enable and start:

sudo systemctl daemon-reload
+sudo systemctl enable vllm
+sudo systemctl start vllm
+sudo systemctl status vllm
+

Step 6: Configure Gateway

Update Gateway Environment

Edit your gateway's .env:

# Add if using local vLLM without Globus Compute
+LOCAL_VLLM_URL="http://localhost:8001"
+

Create Endpoint Fixture

Create or edit fixtures/endpoints.json in your gateway directory:

[
+    {
+        "model": "resource_server.endpoint",
+        "pk": 1,
+        "fields": {
+            "endpoint_slug": "local-vllm-opt-125m",
+            "cluster": "local",
+            "framework": "vllm",
+            "model": "facebook/opt-125m",
+            "api_port": 8001,
+            "endpoint_uuid": "",
+            "function_uuid": "",
+            "batch_endpoint_uuid": "",
+            "batch_function_uuid": "",
+            "allowed_globus_groups": ""
+        }
+    }
+]
+

For a Llama model:

[
+    {
+        "model": "resource_server.endpoint",
+        "pk": 2,
+        "fields": {
+            "endpoint_slug": "local-vllm-llama3-8b",
+            "cluster": "local",
+            "framework": "vllm",
+            "model": "meta-llama/Meta-Llama-3-8B-Instruct",
+            "api_port": 8001,
+            "endpoint_uuid": "",
+            "function_uuid": "",
+            "batch_endpoint_uuid": "",
+            "batch_function_uuid": "",
+            "allowed_globus_groups": ""
+        }
+    }
+]
+

Load Fixture

# Docker
+docker-compose exec inference-gateway python manage.py loaddata fixtures/endpoints.json
+
+# Bare metal
+python manage.py loaddata fixtures/endpoints.json
+

Step 7: Test End-to-End

Get a Globus token:

export TOKEN=$(python inference-auth-token.py get_access_token)
+

Test via gateway:

curl -X POST http://localhost:8000/resource_server/local/vllm/v1/chat/completions \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "model": "facebook/opt-125m",
+    "messages": [{"role": "user", "content": "Explain machine learning in one sentence"}],
+    "max_tokens": 50
+  }'
+

Performance Tuning

GPU Memory Optimization

# Use less GPU memory (if OOM errors)
+--gpu-memory-utilization 0.8
+
+# Use more GPU memory (if you have headroom)
+--gpu-memory-utilization 0.95
+

Batch Processing

# Increase batch size for throughput
+--max-num-batched-tokens 8192
+--max-num-seqs 256
+

Context Length

# Reduce for better throughput
+--max-model-len 2048
+
+# Increase for longer contexts
+--max-model-len 8192
+

Quantization

# Use 4-bit quantization (AWQ)
+--quantization awq
+--model TheBloke/Llama-2-7B-Chat-AWQ
+
+# Use 8-bit quantization (GPTQ)
+--quantization gptq
+--model TheBloke/Llama-2-7B-Chat-GPTQ
+

Monitoring

vLLM Metrics

vLLM exposes Prometheus metrics at http://localhost:8001/metrics:

curl http://localhost:8001/metrics
+

GPU Monitoring

# Watch GPU usage
+watch -n 1 nvidia-smi
+
+# More detailed stats
+nvidia-smi dmon
+

Log Monitoring

# systemd service logs
+sudo journalctl -u vllm -f
+
+# Or direct output if running in terminal
+python -m vllm.entrypoints.openai.api_server ... | tee vllm.log
+

Troubleshooting

Out of Memory (OOM) Errors

# Reduce GPU memory usage
+--gpu-memory-utilization 0.7
+
+# Reduce context length
+--max-model-len 2048
+
+# Use quantization
+--quantization awq
+
+# Use smaller model
+

Slow Response Times

  • Check GPU utilization with nvidia-smi
  • Increase --gpu-memory-utilization if GPU memory is underutilized
  • Enable --enable-prefix-caching for repeated prompts
  • Use tensor parallelism for multi-GPU setups

Model Not Found

# Pre-download model
+huggingface-cli download meta-llama/Meta-Llama-3-8B-Instruct
+
+# Or set cache directory
+export HF_HOME=/path/to/cache
+

CUDA Errors

# Check CUDA version
+nvidia-smi
+
+# Install matching vLLM version
+pip install vllm-cu118  # For CUDA 11.8
+

Connection Refused from Gateway

  • Verify vLLM is running: curl http://localhost:8001/health
  • Check firewall settings
  • Ensure correct port in fixture configuration
  • Verify host is 0.0.0.0 not localhost

Running Multiple Models

You can run multiple vLLM instances on different ports:

# Terminal 1 - OPT-125M
+python -m vllm.entrypoints.openai.api_server \
+    --model facebook/opt-125m \
+    --port 8001
+
+# Terminal 2 - Llama-2-7B
+python -m vllm.entrypoints.openai.api_server \
+    --model meta-llama/Llama-2-7b-chat-hf \
+    --port 8002
+

Add both to your fixtures:

[
+    {
+        "model": "resource_server.endpoint",
+        "pk": 1,
+        "fields": {
+            "endpoint_slug": "local-vllm-opt-125m",
+            "cluster": "local",
+            "framework": "vllm",
+            "model": "facebook/opt-125m",
+            "api_port": 8001,
+            ...
+        }
+    },
+    {
+        "model": "resource_server.endpoint",
+        "pk": 2,
+        "fields": {
+            "endpoint_slug": "local-vllm-llama2-7b",
+            "cluster": "local",
+            "framework": "vllm",
+            "model": "meta-llama/Llama-2-7b-chat-hf",
+            "api_port": 8002,
+            ...
+        }
+    }
+]
+

Next Steps

Additional Resources

\ No newline at end of file diff --git a/site/admin-guide/monitoring/index.html b/site/admin-guide/monitoring/index.html new file mode 100644 index 00000000..fbbce0b4 --- /dev/null +++ b/site/admin-guide/monitoring/index.html @@ -0,0 +1,133 @@ + Monitoring & Troubleshooting - FIRST Inference Gateway Documentation

Monitoring & Troubleshooting

This guide covers monitoring the FIRST Inference Gateway and troubleshooting common issues.

Monitoring

Application Logs

Docker Deployment

# View all logs
+docker-compose logs -f
+
+# View gateway logs only
+docker-compose logs -f inference-gateway
+
+# Last 100 lines
+docker-compose logs --tail=100 inference-gateway
+

Bare Metal Deployment

# Application logs
+tail -f logs/django_info.log
+
+# Gunicorn logs
+tail -f logs/backend_gateway.error.log
+tail -f logs/backend_gateway.access.log
+
+# Systemd service logs
+sudo journalctl -u inference-gateway -f
+

Database Monitoring

# Connection stats
+psql -h localhost -U inferencedev -d inferencegateway -c "SELECT * FROM pg_stat_activity;"
+
+# Table sizes
+psql -h localhost -U inferencedev -d inferencegateway -c "
+SELECT schemaname,tablename,pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename))
+FROM pg_tables WHERE schemaname='public' ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;"
+

Redis Monitoring

# Connect to Redis CLI
+redis-cli
+
+# In Redis CLI:
+INFO
+DBSIZE
+MONITOR  # Watch commands in real-time
+

Globus Compute Endpoints

# List endpoints
+globus-compute-endpoint list
+
+# Check status
+globus-compute-endpoint status my-endpoint
+
+# View logs
+globus-compute-endpoint log my-endpoint -n 100
+
+# Follow logs
+tail -f ~/.globus_compute/my-endpoint/endpoint.log
+

Common Issues

Gateway Won't Start

Symptoms: Container/service fails to start

Check:

# Docker
+docker-compose logs inference-gateway
+
+# Bare metal
+sudo journalctl -u inference-gateway -n 50
+python manage.py check
+

Common Causes:

  • Missing environment variables
  • Database connection failure
  • Port already in use
  • Syntax error in settings

Database Connection Errors

Symptoms: OperationalError: could not connect to server

Solutions:

# Verify PostgreSQL is running
+sudo systemctl status postgresql
+docker-compose ps postgres
+
+# Test connection
+psql -h localhost -U inferencedev -d inferencegateway
+
+# Check pg_hba.conf
+sudo nano /etc/postgresql/*/main/pg_hba.conf
+
+# Restart PostgreSQL
+sudo systemctl restart postgresql
+

Authentication Failures

Symptoms: 401 Unauthorized, Globus token errors

Solutions:

  1. Verify Globus application credentials in .env
  2. Check scope was created successfully:
    curl -s --user $CLIENT_ID:$CLIENT_SECRET \
    +    https://auth.globus.org/v2/api/clients/$CLIENT_ID
    +
  3. Force re-authentication:
    python inference-auth-token.py authenticate --force
    +
  4. Verify redirect URIs match in Globus app settings

Globus Compute Errors

Symptoms: Function execution failures, timeout errors

Solutions:

# Check endpoint is running
+globus-compute-endpoint list
+
+# Restart endpoint
+globus-compute-endpoint restart my-endpoint
+
+# View detailed logs
+globus-compute-endpoint log my-endpoint -n 200
+
+# Verify function UUID is allowed
+cat ~/.globus_compute/my-endpoint/config.yaml
+

Model Not Found

Symptoms: Model 'xxx' not found errors

Solutions:

  1. Verify fixture was loaded:
    python manage.py dumpdata resource_server.endpoint
    +
  2. Check model name matches exactly in fixture
  3. Reload fixtures:
    python manage.py loaddata fixtures/endpoints.json
    +

Slow Response Times

Causes:

  • Cold start (first request to endpoint)
  • GPU not available
  • Model loading time
  • Network latency

Solutions:

  1. Enable hot nodes (min_blocks > 0 in Globus Compute config)
  2. Monitor GPU usage: nvidia-smi
  3. Check vLLM logs for bottlenecks
  4. Increase Gunicorn timeout:
    timeout = 300
    +

Out of Memory Errors

Symptoms: OOM kills, CUDA out of memory

Solutions:

# vLLM: Reduce GPU memory usage
+--gpu-memory-utilization 0.7
+
+# vLLM: Use quantization
+--quantization awq
+
+# vLLM: Reduce context length
+--max-model-len 2048
+
+# System: Add swap space
+sudo fallocate -l 32G /swapfile
+sudo chmod 600 /swapfile
+sudo mkswap /swapfile
+sudo swapon /swapfile
+

Health Checks

Manual Health Checks

# Gateway health
+curl http://localhost:8000/
+
+# vLLM health
+curl http://localhost:8001/health
+
+# Database connectivity
+python manage.py dbshell
+
+# Redis connectivity
+redis-cli ping
+

Automated Health Monitoring

Create a health check script:

#!/bin/bash
+# health_check.sh
+
+# Check gateway
+if curl -s http://localhost:8000/ > /dev/null; then
+    echo "✓ Gateway is healthy"
+else
+    echo "✗ Gateway is down"
+    systemctl restart inference-gateway
+fi
+
+# Check database
+if psql -h localhost -U inferencedev -d inferencegateway -c "SELECT 1;" > /dev/null 2>&1; then
+    echo "✓ Database is healthy"
+else
+    echo "✗ Database is down"
+fi
+

Add to crontab:

*/5 * * * * /path/to/health_check.sh >> /var/log/health_check.log 2>&1
+

Performance Metrics

Key Metrics to Monitor

  1. Request Rate: Requests per second
  2. Latency: Response time (p50, p95, p99)
  3. Error Rate: Percentage of failed requests
  4. Queue Depth: Pending Globus Compute tasks
  5. GPU Utilization: GPU memory and compute usage
  6. Database Connections: Active connections
  7. Cache Hit Rate: Redis cache effectiveness

Prometheus Metrics

If using Prometheus, key metrics to track:

# Request metrics
+http_requests_total
+http_request_duration_seconds
+
+# Globus Compute metrics
+globus_compute_tasks_submitted
+globus_compute_tasks_completed
+globus_compute_tasks_failed
+
+# System metrics
+process_cpu_seconds_total
+process_resident_memory_bytes
+

Troubleshooting Checklist

When issues occur, work through this checklist:

  • [ ] Check application logs
  • [ ] Verify all services are running
  • [ ] Test database connectivity
  • [ ] Check Redis connectivity
  • [ ] Verify Globus Compute endpoints are online
  • [ ] Test authentication flow
  • [ ] Check network connectivity
  • [ ] Review recent configuration changes
  • [ ] Check disk space
  • [ ] Monitor resource usage (CPU, RAM, GPU)

Getting Help

If you're still stuck:

  1. Check documentation: Review the relevant setup guides
  2. Search issues: Look for similar issues on GitHub
  3. Enable debug logging: Set DEBUG=True temporarily
  4. Collect information:
  5. Version information
  6. Error messages and stack traces
  7. Configuration (sanitize secrets!)
  8. Relevant log excerpts
  9. Open an issue: Provide all collected information

Additional Resources

\ No newline at end of file diff --git a/site/assets/images/favicon.png b/site/assets/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1cf13b9f9d978896599290a74f77d5dbe7d1655c GIT binary patch literal 1870 zcmV-U2eJ5xP)Gc)JR9QMau)O=X#!i9;T z37kk-upj^(fsR36MHs_+1RCI)NNu9}lD0S{B^g8PN?Ww(5|~L#Ng*g{WsqleV}|#l zz8@ri&cTzw_h33bHI+12+kK6WN$h#n5cD8OQt`5kw6p~9H3()bUQ8OS4Q4HTQ=1Ol z_JAocz`fLbT2^{`8n~UAo=#AUOf=SOq4pYkt;XbC&f#7lb$*7=$na!mWCQ`dBQsO0 zLFBSPj*N?#u5&pf2t4XjEGH|=pPQ8xh7tpx;US5Cx_Ju;!O`ya-yF`)b%TEt5>eP1ZX~}sjjA%FJF?h7cX8=b!DZl<6%Cv z*G0uvvU+vmnpLZ2paivG-(cd*y3$hCIcsZcYOGh{$&)A6*XX&kXZd3G8m)G$Zz-LV z^GF3VAW^Mdv!)4OM8EgqRiz~*Cji;uzl2uC9^=8I84vNp;ltJ|q-*uQwGp2ma6cY7 z;`%`!9UXO@fr&Ebapfs34OmS9^u6$)bJxrucutf>`dKPKT%%*d3XlFVKunp9 zasduxjrjs>f8V=D|J=XNZp;_Zy^WgQ$9WDjgY=z@stwiEBm9u5*|34&1Na8BMjjgf3+SHcr`5~>oz1Y?SW^=K z^bTyO6>Gar#P_W2gEMwq)ot3; zREHn~U&Dp0l6YT0&k-wLwYjb?5zGK`W6S2v+K>AM(95m2C20L|3m~rN8dprPr@t)5lsk9Hu*W z?pS990s;Ez=+Rj{x7p``4>+c0G5^pYnB1^!TL=(?HLHZ+HicG{~4F1d^5Awl_2!1jICM-!9eoLhbbT^;yHcefyTAaqRcY zmuctDopPT!%k+}x%lZRKnzykr2}}XfG_ne?nRQO~?%hkzo;@RN{P6o`&mMUWBYMTe z6i8ChtjX&gXl`nvrU>jah)2iNM%JdjqoaeaU%yVn!^70x-flljp6Q5tK}5}&X8&&G zX3fpb3E(!rH=zVI_9Gjl45w@{(ITqngWFe7@9{mX;tO25Z_8 zQHEpI+FkTU#4xu>RkN>b3Tnc3UpWzPXWm#o55GKF09j^Mh~)K7{QqbO_~(@CVq! zS<8954|P8mXN2MRs86xZ&Q4EfM@JB94b=(YGuk)s&^jiSF=t3*oNK3`rD{H`yQ?d; ztE=laAUoZx5?RC8*WKOj`%LXEkgDd>&^Q4M^z`%u0rg-It=hLCVsq!Z%^6eB-OvOT zFZ28TN&cRmgU}Elrnk43)!>Z1FCPL2K$7}gwzIc48NX}#!A1BpJP?#v5wkNprhV** z?Cpalt1oH&{r!o3eSKc&ap)iz2BTn_VV`4>9M^b3;(YY}4>#ML6{~(4mH+?%07*qo IM6N<$f(jP3KmY&$ literal 0 HcmV?d00001 diff --git a/site/assets/javascripts/bundle.e71a0d61.min.js b/site/assets/javascripts/bundle.e71a0d61.min.js new file mode 100644 index 00000000..c76b3b2b --- /dev/null +++ b/site/assets/javascripts/bundle.e71a0d61.min.js @@ -0,0 +1,16 @@ +"use strict";(()=>{var Zi=Object.create;var _r=Object.defineProperty;var ea=Object.getOwnPropertyDescriptor;var ta=Object.getOwnPropertyNames,Bt=Object.getOwnPropertySymbols,ra=Object.getPrototypeOf,Ar=Object.prototype.hasOwnProperty,bo=Object.prototype.propertyIsEnumerable;var ho=(e,t,r)=>t in e?_r(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))Ar.call(t,r)&&ho(e,r,t[r]);if(Bt)for(var r of Bt(t))bo.call(t,r)&&ho(e,r,t[r]);return e};var vo=(e,t)=>{var r={};for(var o in e)Ar.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Bt)for(var o of Bt(e))t.indexOf(o)<0&&bo.call(e,o)&&(r[o]=e[o]);return r};var Cr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var oa=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of ta(t))!Ar.call(e,n)&&n!==r&&_r(e,n,{get:()=>t[n],enumerable:!(o=ea(t,n))||o.enumerable});return e};var $t=(e,t,r)=>(r=e!=null?Zi(ra(e)):{},oa(t||!e||!e.__esModule?_r(r,"default",{value:e,enumerable:!0}):r,e));var go=(e,t,r)=>new Promise((o,n)=>{var i=c=>{try{a(r.next(c))}catch(p){n(p)}},s=c=>{try{a(r.throw(c))}catch(p){n(p)}},a=c=>c.done?o(c.value):Promise.resolve(c.value).then(i,s);a((r=r.apply(e,t)).next())});var xo=Cr((kr,yo)=>{(function(e,t){typeof kr=="object"&&typeof yo!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(kr,(function(){"use strict";function e(r){var o=!0,n=!1,i=null,s={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function a(k){return!!(k&&k!==document&&k.nodeName!=="HTML"&&k.nodeName!=="BODY"&&"classList"in k&&"contains"in k.classList)}function c(k){var ut=k.type,je=k.tagName;return!!(je==="INPUT"&&s[ut]&&!k.readOnly||je==="TEXTAREA"&&!k.readOnly||k.isContentEditable)}function p(k){k.classList.contains("focus-visible")||(k.classList.add("focus-visible"),k.setAttribute("data-focus-visible-added",""))}function l(k){k.hasAttribute("data-focus-visible-added")&&(k.classList.remove("focus-visible"),k.removeAttribute("data-focus-visible-added"))}function f(k){k.metaKey||k.altKey||k.ctrlKey||(a(r.activeElement)&&p(r.activeElement),o=!0)}function u(k){o=!1}function d(k){a(k.target)&&(o||c(k.target))&&p(k.target)}function v(k){a(k.target)&&(k.target.classList.contains("focus-visible")||k.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(k.target))}function S(k){document.visibilityState==="hidden"&&(n&&(o=!0),X())}function X(){document.addEventListener("mousemove",ee),document.addEventListener("mousedown",ee),document.addEventListener("mouseup",ee),document.addEventListener("pointermove",ee),document.addEventListener("pointerdown",ee),document.addEventListener("pointerup",ee),document.addEventListener("touchmove",ee),document.addEventListener("touchstart",ee),document.addEventListener("touchend",ee)}function re(){document.removeEventListener("mousemove",ee),document.removeEventListener("mousedown",ee),document.removeEventListener("mouseup",ee),document.removeEventListener("pointermove",ee),document.removeEventListener("pointerdown",ee),document.removeEventListener("pointerup",ee),document.removeEventListener("touchmove",ee),document.removeEventListener("touchstart",ee),document.removeEventListener("touchend",ee)}function ee(k){k.target.nodeName&&k.target.nodeName.toLowerCase()==="html"||(o=!1,re())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",S,!0),X(),r.addEventListener("focus",d,!0),r.addEventListener("blur",v,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)}))});var ro=Cr((jy,Rn)=>{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var qa=/["'&<>]/;Rn.exports=Ka;function Ka(e){var t=""+e,r=qa.exec(t);if(!r)return t;var o,n="",i=0,s=0;for(i=r.index;i{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Nt=="object"&&typeof io=="object"?io.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Nt=="object"?Nt.ClipboardJS=r():t.ClipboardJS=r()})(Nt,function(){return(function(){var e={686:(function(o,n,i){"use strict";i.d(n,{default:function(){return Xi}});var s=i(279),a=i.n(s),c=i(370),p=i.n(c),l=i(817),f=i.n(l);function u(q){try{return document.execCommand(q)}catch(C){return!1}}var d=function(C){var _=f()(C);return u("cut"),_},v=d;function S(q){var C=document.documentElement.getAttribute("dir")==="rtl",_=document.createElement("textarea");_.style.fontSize="12pt",_.style.border="0",_.style.padding="0",_.style.margin="0",_.style.position="absolute",_.style[C?"right":"left"]="-9999px";var D=window.pageYOffset||document.documentElement.scrollTop;return _.style.top="".concat(D,"px"),_.setAttribute("readonly",""),_.value=q,_}var X=function(C,_){var D=S(C);_.container.appendChild(D);var N=f()(D);return u("copy"),D.remove(),N},re=function(C){var _=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},D="";return typeof C=="string"?D=X(C,_):C instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(C==null?void 0:C.type)?D=X(C.value,_):(D=f()(C),u("copy")),D},ee=re;function k(q){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?k=function(_){return typeof _}:k=function(_){return _&&typeof Symbol=="function"&&_.constructor===Symbol&&_!==Symbol.prototype?"symbol":typeof _},k(q)}var ut=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},_=C.action,D=_===void 0?"copy":_,N=C.container,G=C.target,We=C.text;if(D!=="copy"&&D!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(G!==void 0)if(G&&k(G)==="object"&&G.nodeType===1){if(D==="copy"&&G.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(D==="cut"&&(G.hasAttribute("readonly")||G.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(We)return ee(We,{container:N});if(G)return D==="cut"?v(G):ee(G,{container:N})},je=ut;function R(q){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?R=function(_){return typeof _}:R=function(_){return _&&typeof Symbol=="function"&&_.constructor===Symbol&&_!==Symbol.prototype?"symbol":typeof _},R(q)}function se(q,C){if(!(q instanceof C))throw new TypeError("Cannot call a class as a function")}function ce(q,C){for(var _=0;_0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof N.action=="function"?N.action:this.defaultAction,this.target=typeof N.target=="function"?N.target:this.defaultTarget,this.text=typeof N.text=="function"?N.text:this.defaultText,this.container=R(N.container)==="object"?N.container:document.body}},{key:"listenClick",value:function(N){var G=this;this.listener=p()(N,"click",function(We){return G.onClick(We)})}},{key:"onClick",value:function(N){var G=N.delegateTarget||N.currentTarget,We=this.action(G)||"copy",Yt=je({action:We,container:this.container,target:this.target(G),text:this.text(G)});this.emit(Yt?"success":"error",{action:We,text:Yt,trigger:G,clearSelection:function(){G&&G.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(N){return Mr("action",N)}},{key:"defaultTarget",value:function(N){var G=Mr("target",N);if(G)return document.querySelector(G)}},{key:"defaultText",value:function(N){return Mr("text",N)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(N){var G=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return ee(N,G)}},{key:"cut",value:function(N){return v(N)}},{key:"isSupported",value:function(){var N=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],G=typeof N=="string"?[N]:N,We=!!document.queryCommandSupported;return G.forEach(function(Yt){We=We&&!!document.queryCommandSupported(Yt)}),We}}]),_})(a()),Xi=Ji}),828:(function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function s(a,c){for(;a&&a.nodeType!==n;){if(typeof a.matches=="function"&&a.matches(c))return a;a=a.parentNode}}o.exports=s}),438:(function(o,n,i){var s=i(828);function a(l,f,u,d,v){var S=p.apply(this,arguments);return l.addEventListener(u,S,v),{destroy:function(){l.removeEventListener(u,S,v)}}}function c(l,f,u,d,v){return typeof l.addEventListener=="function"?a.apply(null,arguments):typeof u=="function"?a.bind(null,document).apply(null,arguments):(typeof l=="string"&&(l=document.querySelectorAll(l)),Array.prototype.map.call(l,function(S){return a(S,f,u,d,v)}))}function p(l,f,u,d){return function(v){v.delegateTarget=s(v.target,f),v.delegateTarget&&d.call(l,v)}}o.exports=c}),879:(function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var s=Object.prototype.toString.call(i);return i!==void 0&&(s==="[object NodeList]"||s==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var s=Object.prototype.toString.call(i);return s==="[object Function]"}}),370:(function(o,n,i){var s=i(879),a=i(438);function c(u,d,v){if(!u&&!d&&!v)throw new Error("Missing required arguments");if(!s.string(d))throw new TypeError("Second argument must be a String");if(!s.fn(v))throw new TypeError("Third argument must be a Function");if(s.node(u))return p(u,d,v);if(s.nodeList(u))return l(u,d,v);if(s.string(u))return f(u,d,v);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function p(u,d,v){return u.addEventListener(d,v),{destroy:function(){u.removeEventListener(d,v)}}}function l(u,d,v){return Array.prototype.forEach.call(u,function(S){S.addEventListener(d,v)}),{destroy:function(){Array.prototype.forEach.call(u,function(S){S.removeEventListener(d,v)})}}}function f(u,d,v){return a(document.body,u,d,v)}o.exports=c}),817:(function(o){function n(i){var s;if(i.nodeName==="SELECT")i.focus(),s=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var a=i.hasAttribute("readonly");a||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),a||i.removeAttribute("readonly"),s=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),p=document.createRange();p.selectNodeContents(i),c.removeAllRanges(),c.addRange(p),s=c.toString()}return s}o.exports=n}),279:(function(o){function n(){}n.prototype={on:function(i,s,a){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:s,ctx:a}),this},once:function(i,s,a){var c=this;function p(){c.off(i,p),s.apply(a,arguments)}return p._=s,this.on(i,p,a)},emit:function(i){var s=[].slice.call(arguments,1),a=((this.e||(this.e={}))[i]||[]).slice(),c=0,p=a.length;for(c;c0&&i[i.length-1])&&(p[0]===6||p[0]===2)){r=0;continue}if(p[0]===3&&(!i||p[1]>i[0]&&p[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function K(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],s;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(a){s={error:a}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(s)throw s.error}}return i}function B(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||c(d,S)})},v&&(n[d]=v(n[d])))}function c(d,v){try{p(o[d](v))}catch(S){u(i[0][3],S)}}function p(d){d.value instanceof dt?Promise.resolve(d.value.v).then(l,f):u(i[0][2],d)}function l(d){c("next",d)}function f(d){c("throw",d)}function u(d,v){d(v),i.shift(),i.length&&c(i[0][0],i[0][1])}}function To(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof Oe=="function"?Oe(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(s){return new Promise(function(a,c){s=e[i](s),n(a,c,s.done,s.value)})}}function n(i,s,a,c){Promise.resolve(c).then(function(p){i({value:p,done:a})},s)}}function I(e){return typeof e=="function"}function yt(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var Jt=yt(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Ze(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var qe=(function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var s=this._parentage;if(s)if(this._parentage=null,Array.isArray(s))try{for(var a=Oe(s),c=a.next();!c.done;c=a.next()){var p=c.value;p.remove(this)}}catch(S){t={error:S}}finally{try{c&&!c.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}else s.remove(this);var l=this.initialTeardown;if(I(l))try{l()}catch(S){i=S instanceof Jt?S.errors:[S]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=Oe(f),d=u.next();!d.done;d=u.next()){var v=d.value;try{So(v)}catch(S){i=i!=null?i:[],S instanceof Jt?i=B(B([],K(i)),K(S.errors)):i.push(S)}}}catch(S){o={error:S}}finally{try{d&&!d.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new Jt(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)So(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Ze(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Ze(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=(function(){var t=new e;return t.closed=!0,t})(),e})();var $r=qe.EMPTY;function Xt(e){return e instanceof qe||e&&"closed"in e&&I(e.remove)&&I(e.add)&&I(e.unsubscribe)}function So(e){I(e)?e():e.unsubscribe()}var De={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var xt={setTimeout:function(e,t){for(var r=[],o=2;o0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=this,i=n.hasError,s=n.isStopped,a=n.observers;return i||s?$r:(this.currentObservers=null,a.push(r),new qe(function(){o.currentObservers=null,Ze(a,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,s=o.isStopped;n?r.error(i):s&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,o){return new Ho(r,o)},t})(F);var Ho=(function(e){ie(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:$r},t})(T);var jr=(function(e){ie(t,e);function t(r){var o=e.call(this)||this;return o._value=r,o}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var o=e.prototype._subscribe.call(this,r);return!o.closed&&r.next(this._value),o},t.prototype.getValue=function(){var r=this,o=r.hasError,n=r.thrownError,i=r._value;if(o)throw n;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t})(T);var Rt={now:function(){return(Rt.delegate||Date).now()},delegate:void 0};var It=(function(e){ie(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=Rt);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,s=o._infiniteTimeWindow,a=o._timestampProvider,c=o._windowTime;n||(i.push(r),!s&&i.push(a.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,s=n._buffer,a=s.slice(),c=0;c0?e.prototype.schedule.call(this,r,o):(this.delay=o,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,o){return o>0||this.closed?e.prototype.execute.call(this,r,o):this._execute(r,o)},t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!=null&&n>0||n==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.flush(this),0)},t})(St);var Ro=(function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t})(Ot);var Dr=new Ro(Po);var Io=(function(e){ie(t,e);function t(r,o){var n=e.call(this,r,o)||this;return n.scheduler=r,n.work=o,n}return t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!==null&&n>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=Tt.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var s=r.actions;o!=null&&o===r._scheduled&&((i=s[s.length-1])===null||i===void 0?void 0:i.id)!==o&&(Tt.cancelAnimationFrame(o),r._scheduled=void 0)},t})(St);var Fo=(function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o;r?o=r.id:(o=this._scheduled,this._scheduled=void 0);var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t})(Ot);var ye=new Fo(Io);var y=new F(function(e){return e.complete()});function tr(e){return e&&I(e.schedule)}function Vr(e){return e[e.length-1]}function pt(e){return I(Vr(e))?e.pop():void 0}function Fe(e){return tr(Vr(e))?e.pop():void 0}function rr(e,t){return typeof Vr(e)=="number"?e.pop():t}var Lt=(function(e){return e&&typeof e.length=="number"&&typeof e!="function"});function or(e){return I(e==null?void 0:e.then)}function nr(e){return I(e[wt])}function ir(e){return Symbol.asyncIterator&&I(e==null?void 0:e[Symbol.asyncIterator])}function ar(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function fa(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var sr=fa();function cr(e){return I(e==null?void 0:e[sr])}function pr(e){return wo(this,arguments,function(){var r,o,n,i;return Gt(this,function(s){switch(s.label){case 0:r=e.getReader(),s.label=1;case 1:s.trys.push([1,,9,10]),s.label=2;case 2:return[4,dt(r.read())];case 3:return o=s.sent(),n=o.value,i=o.done,i?[4,dt(void 0)]:[3,5];case 4:return[2,s.sent()];case 5:return[4,dt(n)];case 6:return[4,s.sent()];case 7:return s.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function lr(e){return I(e==null?void 0:e.getReader)}function U(e){if(e instanceof F)return e;if(e!=null){if(nr(e))return ua(e);if(Lt(e))return da(e);if(or(e))return ha(e);if(ir(e))return jo(e);if(cr(e))return ba(e);if(lr(e))return va(e)}throw ar(e)}function ua(e){return new F(function(t){var r=e[wt]();if(I(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function da(e){return new F(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?g(function(n,i){return e(n,i,o)}):be,Ee(1),r?Qe(t):tn(function(){return new fr}))}}function Yr(e){return e<=0?function(){return y}:E(function(t,r){var o=[];t.subscribe(w(r,function(n){o.push(n),e=2,!0))}function le(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new T}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,s=i===void 0?!0:i,a=e.resetOnRefCountZero,c=a===void 0?!0:a;return function(p){var l,f,u,d=0,v=!1,S=!1,X=function(){f==null||f.unsubscribe(),f=void 0},re=function(){X(),l=u=void 0,v=S=!1},ee=function(){var k=l;re(),k==null||k.unsubscribe()};return E(function(k,ut){d++,!S&&!v&&X();var je=u=u!=null?u:r();ut.add(function(){d--,d===0&&!S&&!v&&(f=Br(ee,c))}),je.subscribe(ut),!l&&d>0&&(l=new bt({next:function(R){return je.next(R)},error:function(R){S=!0,X(),f=Br(re,n,R),je.error(R)},complete:function(){v=!0,X(),f=Br(re,s),je.complete()}}),U(k).subscribe(l))})(p)}}function Br(e,t){for(var r=[],o=2;oe.next(document)),e}function M(e,t=document){return Array.from(t.querySelectorAll(e))}function j(e,t=document){let r=ue(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function ue(e,t=document){return t.querySelector(e)||void 0}function Ne(){var e,t,r,o;return(o=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?o:void 0}var Ra=L(h(document.body,"focusin"),h(document.body,"focusout")).pipe(Ae(1),Q(void 0),m(()=>Ne()||document.body),Z(1));function Ye(e){return Ra.pipe(m(t=>e.contains(t)),Y())}function it(e,t){return H(()=>L(h(e,"mouseenter").pipe(m(()=>!0)),h(e,"mouseleave").pipe(m(()=>!1))).pipe(t?jt(r=>He(+!r*t)):be,Q(e.matches(":hover"))))}function sn(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)sn(e,r)}function x(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)sn(o,n);return o}function br(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function _t(e){let t=x("script",{src:e});return H(()=>(document.head.appendChild(t),L(h(t,"load"),h(t,"error").pipe(b(()=>Nr(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),A(()=>document.head.removeChild(t)),Ee(1))))}var cn=new T,Ia=H(()=>typeof ResizeObserver=="undefined"?_t("https://unpkg.com/resize-observer-polyfill"):$(void 0)).pipe(m(()=>new ResizeObserver(e=>e.forEach(t=>cn.next(t)))),b(e=>L(tt,$(e)).pipe(A(()=>e.disconnect()))),Z(1));function de(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Le(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return Ia.pipe(O(r=>r.observe(t)),b(r=>cn.pipe(g(o=>o.target===t),A(()=>r.unobserve(t)))),m(()=>de(e)),Q(de(e)))}function At(e){return{width:e.scrollWidth,height:e.scrollHeight}}function vr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function pn(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function Be(e){return{x:e.offsetLeft,y:e.offsetTop}}function ln(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function mn(e){return L(h(window,"load"),h(window,"resize")).pipe($e(0,ye),m(()=>Be(e)),Q(Be(e)))}function gr(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ge(e){return L(h(e,"scroll"),h(window,"scroll"),h(window,"resize")).pipe($e(0,ye),m(()=>gr(e)),Q(gr(e)))}var fn=new T,Fa=H(()=>$(new IntersectionObserver(e=>{for(let t of e)fn.next(t)},{threshold:0}))).pipe(b(e=>L(tt,$(e)).pipe(A(()=>e.disconnect()))),Z(1));function mt(e){return Fa.pipe(O(t=>t.observe(e)),b(t=>fn.pipe(g(({target:r})=>r===e),A(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function un(e,t=16){return Ge(e).pipe(m(({y:r})=>{let o=de(e),n=At(e);return r>=n.height-o.height-t}),Y())}var yr={drawer:j("[data-md-toggle=drawer]"),search:j("[data-md-toggle=search]")};function dn(e){return yr[e].checked}function at(e,t){yr[e].checked!==t&&yr[e].click()}function Je(e){let t=yr[e];return h(t,"change").pipe(m(()=>t.checked),Q(t.checked))}function ja(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ua(){return L(h(window,"compositionstart").pipe(m(()=>!0)),h(window,"compositionend").pipe(m(()=>!1))).pipe(Q(!1))}function hn(){let e=h(window,"keydown").pipe(g(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:dn("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),g(({mode:t,type:r})=>{if(t==="global"){let o=Ne();if(typeof o!="undefined")return!ja(o,r)}return!0}),le());return Ua().pipe(b(t=>t?y:e))}function we(){return new URL(location.href)}function st(e,t=!1){if(V("navigation.instant")&&!t){let r=x("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function bn(){return new T}function vn(){return location.hash.slice(1)}function gn(e){let t=x("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Zr(e){return L(h(window,"hashchange"),e).pipe(m(vn),Q(vn()),g(t=>t.length>0),Z(1))}function yn(e){return Zr(e).pipe(m(t=>ue(`[id="${t}"]`)),g(t=>typeof t!="undefined"))}function Wt(e){let t=matchMedia(e);return ur(r=>t.addListener(()=>r(t.matches))).pipe(Q(t.matches))}function xn(){let e=matchMedia("print");return L(h(window,"beforeprint").pipe(m(()=>!0)),h(window,"afterprint").pipe(m(()=>!1))).pipe(Q(e.matches))}function eo(e,t){return e.pipe(b(r=>r?t():y))}function to(e,t){return new F(r=>{let o=new XMLHttpRequest;return o.open("GET",`${e}`),o.responseType="blob",o.addEventListener("load",()=>{o.status>=200&&o.status<300?(r.next(o.response),r.complete()):r.error(new Error(o.statusText))}),o.addEventListener("error",()=>{r.error(new Error("Network error"))}),o.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(o.addEventListener("progress",n=>{var i;if(n.lengthComputable)t.progress$.next(n.loaded/n.total*100);else{let s=(i=o.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(n.loaded/+s*100)}}),t.progress$.next(5)),o.send(),()=>o.abort()})}function ze(e,t){return to(e,t).pipe(b(r=>r.text()),m(r=>JSON.parse(r)),Z(1))}function xr(e,t){let r=new DOMParser;return to(e,t).pipe(b(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),Z(1))}function En(e,t){let r=new DOMParser;return to(e,t).pipe(b(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),Z(1))}function wn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function Tn(){return L(h(window,"scroll",{passive:!0}),h(window,"resize",{passive:!0})).pipe(m(wn),Q(wn()))}function Sn(){return{width:innerWidth,height:innerHeight}}function On(){return h(window,"resize",{passive:!0}).pipe(m(Sn),Q(Sn()))}function Ln(){return z([Tn(),On()]).pipe(m(([e,t])=>({offset:e,size:t})),Z(1))}function Er(e,{viewport$:t,header$:r}){let o=t.pipe(ne("size")),n=z([o,r]).pipe(m(()=>Be(e)));return z([r,t,n]).pipe(m(([{height:i},{offset:s,size:a},{x:c,y:p}])=>({offset:{x:s.x-c,y:s.y-p+i},size:a})))}function Wa(e){return h(e,"message",t=>t.data)}function Da(e){let t=new T;return t.subscribe(r=>e.postMessage(r)),t}function Mn(e,t=new Worker(e)){let r=Wa(t),o=Da(t),n=new T;n.subscribe(o);let i=o.pipe(oe(),ae(!0));return n.pipe(oe(),Ve(r.pipe(W(i))),le())}var Va=j("#__config"),Ct=JSON.parse(Va.textContent);Ct.base=`${new URL(Ct.base,we())}`;function Te(){return Ct}function V(e){return Ct.features.includes(e)}function Me(e,t){return typeof t!="undefined"?Ct.translations[e].replace("#",t.toString()):Ct.translations[e]}function Ce(e,t=document){return j(`[data-md-component=${e}]`,t)}function me(e,t=document){return M(`[data-md-component=${e}]`,t)}function Na(e){let t=j(".md-typeset > :first-child",e);return h(t,"click",{once:!0}).pipe(m(()=>j(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function _n(e){if(!V("announce.dismiss")||!e.childElementCount)return y;if(!e.hidden){let t=j(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return H(()=>{let t=new T;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),Na(e).pipe(O(r=>t.next(r)),A(()=>t.complete()),m(r=>P({ref:e},r)))})}function za(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function An(e,t){let r=new T;return r.subscribe(({hidden:o})=>{e.hidden=o}),za(e,t).pipe(O(o=>r.next(o)),A(()=>r.complete()),m(o=>P({ref:e},o)))}function Dt(e,t){return t==="inline"?x("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"})):x("div",{class:"md-tooltip",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"}))}function wr(...e){return x("div",{class:"md-tooltip2",role:"dialog"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function Cn(...e){return x("div",{class:"md-tooltip2",role:"tooltip"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function kn(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return x("aside",{class:"md-annotation",tabIndex:0},Dt(t),x("a",{href:r,class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}else return x("aside",{class:"md-annotation",tabIndex:0},Dt(t),x("span",{class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}function Hn(e){return x("button",{class:"md-code__button",title:Me("clipboard.copy"),"data-clipboard-target":`#${e} > code`,"data-md-type":"copy"})}function $n(){return x("button",{class:"md-code__button",title:"Toggle line selection","data-md-type":"select"})}function Pn(){return x("nav",{class:"md-code__nav"})}var In=$t(ro());function oo(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(c=>!e.terms[c]).reduce((c,p)=>[...c,x("del",null,(0,In.default)(p))," "],[]).slice(0,-1),i=Te(),s=new URL(e.location,i.base);V("search.highlight")&&s.searchParams.set("h",Object.entries(e.terms).filter(([,c])=>c).reduce((c,[p])=>`${c} ${p}`.trim(),""));let{tags:a}=Te();return x("a",{href:`${s}`,class:"md-search-result__link",tabIndex:-1},x("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&x("div",{class:"md-search-result__icon md-icon"}),r>0&&x("h1",null,e.title),r<=0&&x("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&x("nav",{class:"md-tags"},e.tags.map(c=>{let p=a?c in a?`md-tag-icon md-tag--${a[c]}`:"md-tag-icon":"";return x("span",{class:`md-tag ${p}`},c)})),o>0&&n.length>0&&x("p",{class:"md-search-result__terms"},Me("search.result.term.missing"),": ",...n)))}function Fn(e){let t=e[0].score,r=[...e],o=Te(),n=r.findIndex(l=>!`${new URL(l.location,o.base)}`.includes("#")),[i]=r.splice(n,1),s=r.findIndex(l=>l.scoreoo(l,1)),...c.length?[x("details",{class:"md-search-result__more"},x("summary",{tabIndex:-1},x("div",null,c.length>0&&c.length===1?Me("search.result.more.one"):Me("search.result.more.other",c.length))),...c.map(l=>oo(l,1)))]:[]];return x("li",{class:"md-search-result__item"},p)}function jn(e){return x("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>x("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?br(r):r)))}function no(e){let t=`tabbed-control tabbed-control--${e}`;return x("div",{class:t,hidden:!0},x("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function Un(e){return x("div",{class:"md-typeset__scrollwrap"},x("div",{class:"md-typeset__table"},e))}function Qa(e){var o;let t=Te(),r=new URL(`../${e.version}/`,t.base);return x("li",{class:"md-version__item"},x("a",{href:`${r}`,class:"md-version__link"},e.title,((o=t.version)==null?void 0:o.alias)&&e.aliases.length>0&&x("span",{class:"md-version__alias"},e.aliases[0])))}function Wn(e,t){var o;let r=Te();return e=e.filter(n=>{var i;return!((i=n.properties)!=null&&i.hidden)}),x("div",{class:"md-version"},x("button",{class:"md-version__current","aria-label":Me("select.version")},t.title,((o=r.version)==null?void 0:o.alias)&&t.aliases.length>0&&x("span",{class:"md-version__alias"},t.aliases[0])),x("ul",{class:"md-version__list"},e.map(Qa)))}var Ya=0;function Ba(e,t=250){let r=z([Ye(e),it(e,t)]).pipe(m(([n,i])=>n||i),Y()),o=H(()=>pn(e)).pipe(J(Ge),gt(1),Pe(r),m(()=>ln(e)));return r.pipe(Re(n=>n),b(()=>z([r,o])),m(([n,i])=>({active:n,offset:i})),le())}function Vt(e,t,r=250){let{content$:o,viewport$:n}=t,i=`__tooltip2_${Ya++}`;return H(()=>{let s=new T,a=new jr(!1);s.pipe(oe(),ae(!1)).subscribe(a);let c=a.pipe(jt(l=>He(+!l*250,Dr)),Y(),b(l=>l?o:y),O(l=>l.id=i),le());z([s.pipe(m(({active:l})=>l)),c.pipe(b(l=>it(l,250)),Q(!1))]).pipe(m(l=>l.some(f=>f))).subscribe(a);let p=a.pipe(g(l=>l),te(c,n),m(([l,f,{size:u}])=>{let d=e.getBoundingClientRect(),v=d.width/2;if(f.role==="tooltip")return{x:v,y:8+d.height};if(d.y>=u.height/2){let{height:S}=de(f);return{x:v,y:-16-S}}else return{x:v,y:16+d.height}}));return z([c,s,p]).subscribe(([l,{offset:f},u])=>{l.style.setProperty("--md-tooltip-host-x",`${f.x}px`),l.style.setProperty("--md-tooltip-host-y",`${f.y}px`),l.style.setProperty("--md-tooltip-x",`${u.x}px`),l.style.setProperty("--md-tooltip-y",`${u.y}px`),l.classList.toggle("md-tooltip2--top",u.y<0),l.classList.toggle("md-tooltip2--bottom",u.y>=0)}),a.pipe(g(l=>l),te(c,(l,f)=>f),g(l=>l.role==="tooltip")).subscribe(l=>{let f=de(j(":scope > *",l));l.style.setProperty("--md-tooltip-width",`${f.width}px`),l.style.setProperty("--md-tooltip-tail","0px")}),a.pipe(Y(),xe(ye),te(c)).subscribe(([l,f])=>{f.classList.toggle("md-tooltip2--active",l)}),z([a.pipe(g(l=>l)),c]).subscribe(([l,f])=>{f.role==="dialog"?(e.setAttribute("aria-controls",i),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",i)}),a.pipe(g(l=>!l)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),Ba(e,r).pipe(O(l=>s.next(l)),A(()=>s.complete()),m(l=>P({ref:e},l)))})}function Xe(e,{viewport$:t},r=document.body){return Vt(e,{content$:new F(o=>{let n=e.title,i=Cn(n);return o.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",n)}}),viewport$:t},0)}function Ga(e,t){let r=H(()=>z([mn(e),Ge(t)])).pipe(m(([{x:o,y:n},i])=>{let{width:s,height:a}=de(e);return{x:o-i.x+s/2,y:n-i.y+a/2}}));return Ye(e).pipe(b(o=>r.pipe(m(n=>({active:o,offset:n})),Ee(+!o||1/0))))}function Dn(e,t,{target$:r}){let[o,n]=Array.from(e.children);return H(()=>{let i=new T,s=i.pipe(oe(),ae(!0));return i.subscribe({next({offset:a}){e.style.setProperty("--md-tooltip-x",`${a.x}px`),e.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),mt(e).pipe(W(s)).subscribe(a=>{e.toggleAttribute("data-md-visible",a)}),L(i.pipe(g(({active:a})=>a)),i.pipe(Ae(250),g(({active:a})=>!a))).subscribe({next({active:a}){a?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe($e(16,ye)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(gt(125,ye),g(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:a})=>a)).subscribe({next(a){a?e.style.setProperty("--md-tooltip-0",`${-a}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),h(n,"click").pipe(W(s),g(a=>!(a.metaKey||a.ctrlKey))).subscribe(a=>{a.stopPropagation(),a.preventDefault()}),h(n,"mousedown").pipe(W(s),te(i)).subscribe(([a,{active:c}])=>{var p;if(a.button!==0||a.metaKey||a.ctrlKey)a.preventDefault();else if(c){a.preventDefault();let l=e.parentElement.closest(".md-annotation");l instanceof HTMLElement?l.focus():(p=Ne())==null||p.blur()}}),r.pipe(W(s),g(a=>a===o),nt(125)).subscribe(()=>e.focus()),Ga(e,t).pipe(O(a=>i.next(a)),A(()=>i.complete()),m(a=>P({ref:e},a)))})}function Ja(e){let t=Te();if(e.tagName!=="CODE")return[e];let r=[".c",".c1",".cm"];if(t.annotate&&typeof t.annotate=="object"){let o=e.closest("[class|=language]");if(o)for(let n of Array.from(o.classList)){if(!n.startsWith("language-"))continue;let[,i]=n.split("-");i in t.annotate&&r.push(...t.annotate[i])}}return M(r.join(", "),e)}function Xa(e){let t=[];for(let r of Ja(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let s;for(;s=/(\(\d+\))(!)?/.exec(i.textContent);){let[,a,c]=s;if(typeof c=="undefined"){let p=i.splitText(s.index);i=p.splitText(a.length),t.push(p)}else{i.textContent=a,t.push(i);break}}}}return t}function Vn(e,t){t.append(...Array.from(e.childNodes))}function Tr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,s=new Map;for(let a of Xa(t)){let[,c]=a.textContent.match(/\((\d+)\)/);ue(`:scope > li:nth-child(${c})`,e)&&(s.set(c,kn(c,i)),a.replaceWith(s.get(c)))}return s.size===0?y:H(()=>{let a=new T,c=a.pipe(oe(),ae(!0)),p=[];for(let[l,f]of s)p.push([j(".md-typeset",f),j(`:scope > li:nth-child(${l})`,e)]);return o.pipe(W(c)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of p)l?Vn(f,u):Vn(u,f)}),L(...[...s].map(([,l])=>Dn(l,t,{target$:r}))).pipe(A(()=>a.complete()),le())})}function Nn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Nn(t)}}function zn(e,t){return H(()=>{let r=Nn(e);return typeof r!="undefined"?Tr(r,e,t):y})}var Kn=$t(ao());var Za=0,qn=L(h(window,"keydown").pipe(m(()=>!0)),L(h(window,"keyup"),h(window,"contextmenu")).pipe(m(()=>!1))).pipe(Q(!1),Z(1));function Qn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Qn(t)}}function es(e){return Le(e).pipe(m(({width:t})=>({scrollable:At(e).width>t})),ne("scrollable"))}function Yn(e,t){let{matches:r}=matchMedia("(hover)"),o=H(()=>{let n=new T,i=n.pipe(Yr(1));n.subscribe(({scrollable:d})=>{d&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let s=[],a=e.closest("pre"),c=a.closest("[id]"),p=c?c.id:Za++;a.id=`__code_${p}`;let l=[],f=e.closest(".highlight");if(f instanceof HTMLElement){let d=Qn(f);if(typeof d!="undefined"&&(f.classList.contains("annotate")||V("content.code.annotate"))){let v=Tr(d,e,t);l.push(Le(f).pipe(W(i),m(({width:S,height:X})=>S&&X),Y(),b(S=>S?v:y)))}}let u=M(":scope > span[id]",e);if(u.length&&(e.classList.add("md-code__content"),e.closest(".select")||V("content.code.select")&&!e.closest(".no-select"))){let d=+u[0].id.split("-").pop(),v=$n();s.push(v),V("content.tooltips")&&l.push(Xe(v,{viewport$}));let S=h(v,"click").pipe(Ut(R=>!R,!1),O(()=>v.blur()),le());S.subscribe(R=>{v.classList.toggle("md-code__button--active",R)});let X=fe(u).pipe(J(R=>it(R).pipe(m(se=>[R,se]))));S.pipe(b(R=>R?X:y)).subscribe(([R,se])=>{let ce=ue(".hll.select",R);if(ce&&!se)ce.replaceWith(...Array.from(ce.childNodes));else if(!ce&&se){let he=document.createElement("span");he.className="hll select",he.append(...Array.from(R.childNodes).slice(1)),R.append(he)}});let re=fe(u).pipe(J(R=>h(R,"mousedown").pipe(O(se=>se.preventDefault()),m(()=>R)))),ee=S.pipe(b(R=>R?re:y),te(qn),m(([R,se])=>{var he;let ce=u.indexOf(R)+d;if(se===!1)return[ce,ce];{let Se=M(".hll",e).map(Ue=>u.indexOf(Ue.parentElement)+d);return(he=window.getSelection())==null||he.removeAllRanges(),[Math.min(ce,...Se),Math.max(ce,...Se)]}})),k=Zr(y).pipe(g(R=>R.startsWith(`__codelineno-${p}-`)));k.subscribe(R=>{let[,,se]=R.split("-"),ce=se.split(":").map(Se=>+Se-d+1);ce.length===1&&ce.push(ce[0]);for(let Se of M(".hll:not(.select)",e))Se.replaceWith(...Array.from(Se.childNodes));let he=u.slice(ce[0]-1,ce[1]);for(let Se of he){let Ue=document.createElement("span");Ue.className="hll",Ue.append(...Array.from(Se.childNodes).slice(1)),Se.append(Ue)}}),k.pipe(Ee(1),xe(pe)).subscribe(R=>{if(R.includes(":")){let se=document.getElementById(R.split(":")[0]);se&&setTimeout(()=>{let ce=se,he=-64;for(;ce!==document.body;)he+=ce.offsetTop,ce=ce.offsetParent;window.scrollTo({top:he})},1)}});let je=fe(M('a[href^="#__codelineno"]',f)).pipe(J(R=>h(R,"click").pipe(O(se=>se.preventDefault()),m(()=>R)))).pipe(W(i),te(qn),m(([R,se])=>{let he=+j(`[id="${R.hash.slice(1)}"]`).parentElement.id.split("-").pop();if(se===!1)return[he,he];{let Se=M(".hll",e).map(Ue=>+Ue.parentElement.id.split("-").pop());return[Math.min(he,...Se),Math.max(he,...Se)]}}));L(ee,je).subscribe(R=>{let se=`#__codelineno-${p}-`;R[0]===R[1]?se+=R[0]:se+=`${R[0]}:${R[1]}`,history.replaceState({},"",se),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+se,oldURL:window.location.href}))})}if(Kn.default.isSupported()&&(e.closest(".copy")||V("content.code.copy")&&!e.closest(".no-copy"))){let d=Hn(a.id);s.push(d),V("content.tooltips")&&l.push(Xe(d,{viewport$}))}if(s.length){let d=Pn();d.append(...s),a.insertBefore(d,e)}return es(e).pipe(O(d=>n.next(d)),A(()=>n.complete()),m(d=>P({ref:e},d)),Ve(L(...l).pipe(W(i))))});return V("content.lazy")?mt(e).pipe(g(n=>n),Ee(1),b(()=>o)):o}function ts(e,{target$:t,print$:r}){let o=!0;return L(t.pipe(m(n=>n.closest("details:not([open])")),g(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(g(n=>n||!o),O(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Bn(e,t){return H(()=>{let r=new T;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),ts(e,t).pipe(O(o=>r.next(o)),A(()=>r.complete()),m(o=>P({ref:e},o)))})}var Gn=0;function rs(e){let t=document.createElement("h3");t.innerHTML=e.innerHTML;let r=[t],o=e.nextElementSibling;for(;o&&!(o instanceof HTMLHeadingElement);)r.push(o),o=o.nextElementSibling;return r}function os(e,t){for(let r of M("[href], [src]",e))for(let o of["href","src"]){let n=r.getAttribute(o);if(n&&!/^(?:[a-z]+:)?\/\//i.test(n)){r[o]=new URL(r.getAttribute(o),t).toString();break}}for(let r of M("[name^=__], [for]",e))for(let o of["id","for","name"]){let n=r.getAttribute(o);n&&r.setAttribute(o,`${n}$preview_${Gn}`)}return Gn++,$(e)}function Jn(e,t){let{sitemap$:r}=t;if(!(e instanceof HTMLAnchorElement))return y;if(!(V("navigation.instant.preview")||e.hasAttribute("data-preview")))return y;e.removeAttribute("title");let o=z([Ye(e),it(e)]).pipe(m(([i,s])=>i||s),Y(),g(i=>i));return rt([r,o]).pipe(b(([i])=>{let s=new URL(e.href);return s.search=s.hash="",i.has(`${s}`)?$(s):y}),b(i=>xr(i).pipe(b(s=>os(s,i)))),b(i=>{let s=e.hash?`article [id="${e.hash.slice(1)}"]`:"article h1",a=ue(s,i);return typeof a=="undefined"?y:$(rs(a))})).pipe(b(i=>{let s=new F(a=>{let c=wr(...i);return a.next(c),document.body.append(c),()=>c.remove()});return Vt(e,P({content$:s},t))}))}var Xn=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}[id^=entity] path,[id^=entity] rect{fill:var(--md-default-bg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs .marker.oneOrMore.er *,defs .marker.onlyOne.er *,defs .marker.zeroOrMore.er *,defs .marker.zeroOrOne.er *{stroke:var(--md-mermaid-edge-color)!important}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var so,is=0;function as(){return typeof mermaid=="undefined"||mermaid instanceof Element?_t("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):$(void 0)}function Zn(e){return e.classList.remove("mermaid"),so||(so=as().pipe(O(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Xn,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),Z(1))),so.subscribe(()=>go(null,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${is++}`,r=x("div",{class:"mermaid"}),o=e.textContent,{svg:n,fn:i}=yield mermaid.render(t,o),s=r.attachShadow({mode:"closed"});s.innerHTML=n,e.replaceWith(r),i==null||i(s)})),so.pipe(m(()=>({ref:e})))}var ei=x("table");function ti(e){return e.replaceWith(ei),ei.replaceWith(Un(e)),$({ref:e})}function ss(e){let t=e.find(r=>r.checked)||e[0];return L(...e.map(r=>h(r,"change").pipe(m(()=>j(`label[for="${r.id}"]`))))).pipe(Q(j(`label[for="${t.id}"]`)),m(r=>({active:r})))}function ri(e,{viewport$:t,target$:r}){let o=j(".tabbed-labels",e),n=M(":scope > input",e),i=no("prev");e.append(i);let s=no("next");return e.append(s),H(()=>{let a=new T,c=a.pipe(oe(),ae(!0));z([a,Le(e),mt(e)]).pipe(W(c),$e(1,ye)).subscribe({next([{active:p},l]){let f=Be(p),{width:u}=de(p);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let d=gr(o);(f.xd.x+l.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),z([Ge(o),Le(o)]).pipe(W(c)).subscribe(([p,l])=>{let f=At(o);i.hidden=p.x<16,s.hidden=p.x>f.width-l.width-16}),L(h(i,"click").pipe(m(()=>-1)),h(s,"click").pipe(m(()=>1))).pipe(W(c)).subscribe(p=>{let{width:l}=de(o);o.scrollBy({left:l*p,behavior:"smooth"})}),r.pipe(W(c),g(p=>n.includes(p))).subscribe(p=>p.click()),o.classList.add("tabbed-labels--linked");for(let p of n){let l=j(`label[for="${p.id}"]`);l.replaceChildren(x("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),h(l.firstElementChild,"click").pipe(W(c),g(f=>!(f.metaKey||f.ctrlKey)),O(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return V("content.tabs.link")&&a.pipe(Ie(1),te(t)).subscribe(([{active:p},{offset:l}])=>{let f=p.innerText.trim();if(p.hasAttribute("data-md-switching"))p.removeAttribute("data-md-switching");else{let u=e.offsetTop-l.y;for(let v of M("[data-tabs]"))for(let S of M(":scope > input",v)){let X=j(`label[for="${S.id}"]`);if(X!==p&&X.innerText.trim()===f){X.setAttribute("data-md-switching",""),S.click();break}}window.scrollTo({top:e.offsetTop-u});let d=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...d])])}}),a.pipe(W(c)).subscribe(()=>{for(let p of M("audio, video",e))p.offsetWidth&&p.autoplay?p.play().catch(()=>{}):p.pause()}),ss(n).pipe(O(p=>a.next(p)),A(()=>a.complete()),m(p=>P({ref:e},p)))}).pipe(et(pe))}function oi(e,t){let{viewport$:r,target$:o,print$:n}=t;return L(...M(".annotate:not(.highlight)",e).map(i=>zn(i,{target$:o,print$:n})),...M("pre:not(.mermaid) > code",e).map(i=>Yn(i,{target$:o,print$:n})),...M("a",e).map(i=>Jn(i,t)),...M("pre.mermaid",e).map(i=>Zn(i)),...M("table:not([class])",e).map(i=>ti(i)),...M("details",e).map(i=>Bn(i,{target$:o,print$:n})),...M("[data-tabs]",e).map(i=>ri(i,{viewport$:r,target$:o})),...M("[title]:not([data-preview])",e).filter(()=>V("content.tooltips")).map(i=>Xe(i,{viewport$:r})),...M(".footnote-ref",e).filter(()=>V("content.footnote.tooltips")).map(i=>Vt(i,{content$:new F(s=>{let a=new URL(i.href).hash.slice(1),c=Array.from(document.getElementById(a).cloneNode(!0).children),p=wr(...c);return s.next(p),document.body.append(p),()=>p.remove()}),viewport$:r})))}function cs(e,{alert$:t}){return t.pipe(b(r=>L($(!0),$(!1).pipe(nt(2e3))).pipe(m(o=>({message:r,active:o})))))}function ni(e,t){let r=j(".md-typeset",e);return H(()=>{let o=new T;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),cs(e,t).pipe(O(n=>o.next(n)),A(()=>o.complete()),m(n=>P({ref:e},n)))})}var ps=0;function ls(e,t){document.body.append(e);let{width:r}=de(e);e.style.setProperty("--md-tooltip-width",`${r}px`),e.remove();let o=vr(t),n=typeof o!="undefined"?Ge(o):$({x:0,y:0}),i=L(Ye(t),it(t)).pipe(Y());return z([i,n]).pipe(m(([s,a])=>{let{x:c,y:p}=Be(t),l=de(t),f=t.closest("table");return f&&t.parentElement&&(c+=f.offsetLeft+t.parentElement.offsetLeft,p+=f.offsetTop+t.parentElement.offsetTop),{active:s,offset:{x:c-a.x+l.width/2-r/2,y:p-a.y+l.height+8}}}))}function ii(e){let t=e.title;if(!t.length)return y;let r=`__tooltip_${ps++}`,o=Dt(r,"inline"),n=j(".md-typeset",o);return n.innerHTML=t,H(()=>{let i=new T;return i.subscribe({next({offset:s}){o.style.setProperty("--md-tooltip-x",`${s.x}px`),o.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){o.style.removeProperty("--md-tooltip-x"),o.style.removeProperty("--md-tooltip-y")}}),L(i.pipe(g(({active:s})=>s)),i.pipe(Ae(250),g(({active:s})=>!s))).subscribe({next({active:s}){s?(e.insertAdjacentElement("afterend",o),e.setAttribute("aria-describedby",r),e.removeAttribute("title")):(o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t))},complete(){o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t)}}),i.pipe($e(16,ye)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(gt(125,ye),g(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:s})=>s)).subscribe({next(s){s?o.style.setProperty("--md-tooltip-0",`${-s}px`):o.style.removeProperty("--md-tooltip-0")},complete(){o.style.removeProperty("--md-tooltip-0")}}),ls(o,e).pipe(O(s=>i.next(s)),A(()=>i.complete()),m(s=>P({ref:e},s)))}).pipe(et(pe))}function ms({viewport$:e}){if(!V("header.autohide"))return $(!1);let t=e.pipe(m(({offset:{y:n}})=>n),ot(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),Y()),o=Je("search");return z([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),Y(),b(n=>n?r:$(!1)),Q(!1))}function ai(e,t){return H(()=>z([Le(e),ms(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),Y((r,o)=>r.height===o.height&&r.hidden===o.hidden),Z(1))}function si(e,{header$:t,main$:r}){return H(()=>{let o=new T,n=o.pipe(oe(),ae(!0));o.pipe(ne("active"),Pe(t)).subscribe(([{active:s},{hidden:a}])=>{e.classList.toggle("md-header--shadow",s&&!a),e.hidden=a});let i=fe(M("[title]",e)).pipe(g(()=>V("content.tooltips")),J(s=>ii(s)));return r.subscribe(o),t.pipe(W(n),m(s=>P({ref:e},s)),Ve(i.pipe(W(n))))})}function fs(e,{viewport$:t,header$:r}){return Er(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:o}})=>{let{height:n}=de(e);return{active:n>0&&o>=n}}),ne("active"))}function ci(e,t){return H(()=>{let r=new T;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=ue(".md-content h1");return typeof o=="undefined"?y:fs(o,t).pipe(O(n=>r.next(n)),A(()=>r.complete()),m(n=>P({ref:e},n)))})}function pi(e,{viewport$:t,header$:r}){let o=r.pipe(m(({height:i})=>i),Y()),n=o.pipe(b(()=>Le(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),ne("bottom"))));return z([o,n,t]).pipe(m(([i,{top:s,bottom:a},{offset:{y:c},size:{height:p}}])=>(p=Math.max(0,p-Math.max(0,s-c,i)-Math.max(0,p+c-a)),{offset:s-i,height:p,active:s-i<=c})),Y((i,s)=>i.offset===s.offset&&i.height===s.height&&i.active===s.active))}function us(e){let t=__md_get("__palette")||{index:e.findIndex(o=>matchMedia(o.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return $(...e).pipe(J(o=>h(o,"change").pipe(m(()=>o))),Q(e[r]),m(o=>({index:e.indexOf(o),color:{media:o.getAttribute("data-md-color-media"),scheme:o.getAttribute("data-md-color-scheme"),primary:o.getAttribute("data-md-color-primary"),accent:o.getAttribute("data-md-color-accent")}})),Z(1))}function li(e){let t=M("input",e),r=x("meta",{name:"theme-color"});document.head.appendChild(r);let o=x("meta",{name:"color-scheme"});document.head.appendChild(o);let n=Wt("(prefers-color-scheme: light)");return H(()=>{let i=new T;return i.subscribe(s=>{if(document.body.setAttribute("data-md-color-switching",""),s.color.media==="(prefers-color-scheme)"){let a=matchMedia("(prefers-color-scheme: light)"),c=document.querySelector(a.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");s.color.scheme=c.getAttribute("data-md-color-scheme"),s.color.primary=c.getAttribute("data-md-color-primary"),s.color.accent=c.getAttribute("data-md-color-accent")}for(let[a,c]of Object.entries(s.color))document.body.setAttribute(`data-md-color-${a}`,c);for(let a=0;as.key==="Enter"),te(i,(s,a)=>a)).subscribe(({index:s})=>{s=(s+1)%t.length,t[s].click(),t[s].focus()}),i.pipe(m(()=>{let s=Ce("header"),a=window.getComputedStyle(s);return o.content=a.colorScheme,a.backgroundColor.match(/\d+/g).map(c=>(+c).toString(16).padStart(2,"0")).join("")})).subscribe(s=>r.content=`#${s}`),i.pipe(xe(pe)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),us(t).pipe(W(n.pipe(Ie(1))),vt(),O(s=>i.next(s)),A(()=>i.complete()),m(s=>P({ref:e},s)))})}function mi(e,{progress$:t}){return H(()=>{let r=new T;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(O(o=>r.next({value:o})),A(()=>r.complete()),m(o=>({ref:e,value:o})))})}function fi(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function ds(e,t){let r=new Map;for(let o of M("url",e)){let n=j("loc",o),i=[fi(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let s of M("[rel=alternate]",o)){let a=s.getAttribute("href");a!=null&&i.push(fi(new URL(a),t))}}return r}function kt(e){return En(new URL("sitemap.xml",e)).pipe(m(t=>ds(t,new URL(e))),ve(()=>$(new Map)),le())}function ui({document$:e}){let t=new Map;e.pipe(b(()=>M("link[rel=alternate]")),m(r=>new URL(r.href)),g(r=>!t.has(r.toString())),J(r=>kt(r).pipe(m(o=>[r,o]),ve(()=>y)))).subscribe(([r,o])=>{t.set(r.toString().replace(/\/$/,""),o)}),h(document.body,"click").pipe(g(r=>!r.metaKey&&!r.ctrlKey),b(r=>{if(r.target instanceof Element){let o=r.target.closest("a");if(o&&!o.target){let n=[...t].find(([f])=>o.href.startsWith(`${f}/`));if(typeof n=="undefined")return y;let[i,s]=n,a=we();if(a.href.startsWith(i))return y;let c=Te(),p=a.href.replace(c.base,"");p=`${i}/${p}`;let l=s.has(p.split("#")[0])?new URL(p,c.base):new URL(i);return r.preventDefault(),$(l)}}return y})).subscribe(r=>st(r,!0))}var co=$t(ao());function hs(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function di({alert$:e}){co.default.isSupported()&&new F(t=>{new co.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||hs(j(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(O(t=>{t.trigger.focus()}),m(()=>Me("clipboard.copied"))).subscribe(e)}function hi(e,t){if(!(e.target instanceof Element))return y;let r=e.target.closest("a");if(r===null)return y;if(r.target||e.metaKey||e.ctrlKey)return y;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),$(r)):y}function bi(e){let t=new Map;for(let r of M(":scope > *",e.head))t.set(r.outerHTML,r);return t}function vi(e){for(let t of M("[href], [src]",e))for(let r of["href","src"]){let o=t.getAttribute(r);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){t[r]=t[r];break}}return $(e)}function bs(e){for(let o of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...V("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let n=ue(o),i=ue(o,e);typeof n!="undefined"&&typeof i!="undefined"&&n.replaceWith(i)}let t=bi(document);for(let[o,n]of bi(e))t.has(o)?t.delete(o):document.head.appendChild(n);for(let o of t.values()){let n=o.getAttribute("name");n!=="theme-color"&&n!=="color-scheme"&&o.remove()}let r=Ce("container");return Ke(M("script",r)).pipe(b(o=>{let n=e.createElement("script");if(o.src){for(let i of o.getAttributeNames())n.setAttribute(i,o.getAttribute(i));return o.replaceWith(n),new F(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),y}),oe(),ae(document))}function gi({sitemap$:e,location$:t,viewport$:r,progress$:o}){if(location.protocol==="file:")return y;$(document).subscribe(vi);let n=h(document.body,"click").pipe(Pe(e),b(([a,c])=>hi(a,c)),m(({href:a})=>new URL(a)),le()),i=h(window,"popstate").pipe(m(we),le());n.pipe(te(r)).subscribe(([a,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",a)}),L(n,i).subscribe(t);let s=t.pipe(ne("pathname"),b(a=>xr(a,{progress$:o}).pipe(ve(()=>(st(a,!0),y)))),b(vi),b(bs),le());return L(s.pipe(te(t,(a,c)=>c)),s.pipe(b(()=>t),ne("hash")),t.pipe(Y((a,c)=>a.pathname===c.pathname&&a.hash===c.hash),b(()=>n),O(()=>history.back()))).subscribe(a=>{var c,p;history.state!==null||!a.hash?window.scrollTo(0,(p=(c=history.state)==null?void 0:c.y)!=null?p:0):(history.scrollRestoration="auto",gn(a.hash),history.scrollRestoration="manual")}),t.subscribe(()=>{history.scrollRestoration="manual"}),h(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),r.pipe(ne("offset"),Ae(100)).subscribe(({offset:a})=>{history.replaceState(a,"")}),V("navigation.instant.prefetch")&&L(h(document.body,"mousemove"),h(document.body,"focusin")).pipe(Pe(e),b(([a,c])=>hi(a,c)),Ae(25),Qr(({href:a})=>a),hr(a=>{let c=document.createElement("link");return c.rel="prefetch",c.href=a.toString(),document.head.appendChild(c),h(c,"load").pipe(m(()=>c),Ee(1))})).subscribe(a=>a.remove()),s}var yi=$t(ro());function xi(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,s)=>`${i}${s}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").replace(/&/g,"&").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return s=>(0,yi.default)(s).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function zt(e){return e.type===1}function Sr(e){return e.type===3}function Ei(e,t){let r=Mn(e);return L($(location.protocol!=="file:"),Je("search")).pipe(Re(o=>o),b(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:V("search.suggest")}}})),r}function wi(e){var l;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:o,currentBaseURL:n}=e,i=(l=po(n))==null?void 0:l.pathname;if(i===void 0)return;let s=ys(o.pathname,i);if(s===void 0)return;let a=Es(t.keys());if(!t.has(a))return;let c=po(s,a);if(!c||!t.has(c.href))return;let p=po(s,r);if(p)return p.hash=o.hash,p.search=o.search,p}function po(e,t){try{return new URL(e,t)}catch(r){return}}function ys(e,t){if(e.startsWith(t))return e.slice(t.length)}function xs(e,t){let r=Math.min(e.length,t.length),o;for(o=0;oy)),o=r.pipe(m(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:s,aliases:a})=>s===i||a.includes(i))||n[0]}));r.pipe(m(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),b(n=>h(document.body,"click").pipe(g(i=>!i.metaKey&&!i.ctrlKey),te(o),b(([i,s])=>{if(i.target instanceof Element){let a=i.target.closest("a");if(a&&!a.target&&n.has(a.href)){let c=a.href;return!i.target.closest(".md-version")&&n.get(c)===s?y:(i.preventDefault(),$(new URL(c)))}}return y}),b(i=>kt(i).pipe(m(s=>{var a;return(a=wi({selectedVersionSitemap:s,selectedVersionBaseURL:i,currentLocation:we(),currentBaseURL:t.base}))!=null?a:i})))))).subscribe(n=>st(n,!0)),z([r,o]).subscribe(([n,i])=>{j(".md-header__topic").appendChild(Wn(n,i))}),e.pipe(b(()=>o)).subscribe(n=>{var a;let i=new URL(t.base),s=__md_get("__outdated",sessionStorage,i);if(s===null){s=!0;let c=((a=t.version)==null?void 0:a.default)||"latest";Array.isArray(c)||(c=[c]);e:for(let p of c)for(let l of n.aliases.concat(n.version))if(new RegExp(p,"i").test(l)){s=!1;break e}__md_set("__outdated",s,sessionStorage,i)}if(s)for(let c of me("outdated"))c.hidden=!1})}function ws(e,{worker$:t}){let{searchParams:r}=we();r.has("q")&&(at("search",!0),e.value=r.get("q"),e.focus(),Je("search").pipe(Re(i=>!i)).subscribe(()=>{let i=we();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=Ye(e),n=L(t.pipe(Re(zt)),h(e,"keyup"),o).pipe(m(()=>e.value),Y());return z([n,o]).pipe(m(([i,s])=>({value:i,focus:s})),Z(1))}function Si(e,{worker$:t}){let r=new T,o=r.pipe(oe(),ae(!0));z([t.pipe(Re(zt)),r],(i,s)=>s).pipe(ne("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(ne("focus")).subscribe(({focus:i})=>{i&&at("search",i)}),h(e.form,"reset").pipe(W(o)).subscribe(()=>e.focus());let n=j("header [for=__search]");return h(n,"click").subscribe(()=>e.focus()),ws(e,{worker$:t}).pipe(O(i=>r.next(i)),A(()=>r.complete()),m(i=>P({ref:e},i)),Z(1))}function Oi(e,{worker$:t,query$:r}){let o=new T,n=un(e.parentElement).pipe(g(Boolean)),i=e.parentElement,s=j(":scope > :first-child",e),a=j(":scope > :last-child",e);Je("search").subscribe(l=>{a.setAttribute("role",l?"list":"presentation"),a.hidden=!l}),o.pipe(te(r),Gr(t.pipe(Re(zt)))).subscribe(([{items:l},{value:f}])=>{switch(l.length){case 0:s.textContent=f.length?Me("search.result.none"):Me("search.result.placeholder");break;case 1:s.textContent=Me("search.result.one");break;default:let u=br(l.length);s.textContent=Me("search.result.other",u)}});let c=o.pipe(O(()=>a.innerHTML=""),b(({items:l})=>L($(...l.slice(0,10)),$(...l.slice(10)).pipe(ot(4),Xr(n),b(([f])=>f)))),m(Fn),le());return c.subscribe(l=>a.appendChild(l)),c.pipe(J(l=>{let f=ue("details",l);return typeof f=="undefined"?y:h(f,"toggle").pipe(W(o),m(()=>f))})).subscribe(l=>{l.open===!1&&l.offsetTop<=i.scrollTop&&i.scrollTo({top:l.offsetTop})}),t.pipe(g(Sr),m(({data:l})=>l)).pipe(O(l=>o.next(l)),A(()=>o.complete()),m(l=>P({ref:e},l)))}function Ts(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=we();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function Li(e,t){let r=new T,o=r.pipe(oe(),ae(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),h(e,"click").pipe(W(o)).subscribe(n=>n.preventDefault()),Ts(e,t).pipe(O(n=>r.next(n)),A(()=>r.complete()),m(n=>P({ref:e},n)))}function Mi(e,{worker$:t,keyboard$:r}){let o=new T,n=Ce("search-query"),i=L(h(n,"keydown"),h(n,"focus")).pipe(xe(pe),m(()=>n.value),Y());return o.pipe(Pe(i),m(([{suggest:a},c])=>{let p=c.split(/([\s-]+)/);if(a!=null&&a.length&&p[p.length-1]){let l=a[a.length-1];l.startsWith(p[p.length-1])&&(p[p.length-1]=l)}else p.length=0;return p})).subscribe(a=>e.innerHTML=a.join("").replace(/\s/g," ")),r.pipe(g(({mode:a})=>a==="search")).subscribe(a=>{switch(a.type){case"ArrowRight":e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText);break}}),t.pipe(g(Sr),m(({data:a})=>a)).pipe(O(a=>o.next(a)),A(()=>o.complete()),m(()=>({ref:e})))}function _i(e,{index$:t,keyboard$:r}){let o=Te();try{let n=Ei(o.search,t),i=Ce("search-query",e),s=Ce("search-result",e);h(e,"click").pipe(g(({target:c})=>c instanceof Element&&!!c.closest("a"))).subscribe(()=>at("search",!1)),r.pipe(g(({mode:c})=>c==="search")).subscribe(c=>{let p=Ne();switch(c.type){case"Enter":if(p===i){let l=new Map;for(let f of M(":first-child [href]",s)){let u=f.firstElementChild;l.set(f,parseFloat(u.getAttribute("data-md-score")))}if(l.size){let[[f]]=[...l].sort(([,u],[,d])=>d-u);f.click()}c.claim()}break;case"Escape":case"Tab":at("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof p=="undefined")i.focus();else{let l=[i,...M(":not(details) > [href], summary, details[open] [href]",s)],f=Math.max(0,(Math.max(0,l.indexOf(p))+l.length+(c.type==="ArrowUp"?-1:1))%l.length);l[f].focus()}c.claim();break;default:i!==Ne()&&i.focus()}}),r.pipe(g(({mode:c})=>c==="global")).subscribe(c=>{switch(c.type){case"f":case"s":case"/":i.focus(),i.select(),c.claim();break}});let a=Si(i,{worker$:n});return L(a,Oi(s,{worker$:n,query$:a})).pipe(Ve(...me("search-share",e).map(c=>Li(c,{query$:a})),...me("search-suggest",e).map(c=>Mi(c,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,tt}}function Ai(e,{index$:t,location$:r}){return z([t,r.pipe(Q(we()),g(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>xi(o.config)(n.searchParams.get("h"))),m(o=>{var s;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let a=i.nextNode();a;a=i.nextNode())if((s=a.parentElement)!=null&&s.offsetHeight){let c=a.textContent,p=o(c);p.length>c.length&&n.set(a,p)}for(let[a,c]of n){let{childNodes:p}=x("span",null,c);a.replaceWith(...Array.from(p))}return{ref:e,nodes:n}}))}function Ss(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return z([r,t]).pipe(m(([{offset:i,height:s},{offset:{y:a}}])=>(s=s+Math.min(n,Math.max(0,a-i))-n,{height:s,locked:a>=i+n})),Y((i,s)=>i.height===s.height&&i.locked===s.locked))}function lo(e,o){var n=o,{header$:t}=n,r=vo(n,["header$"]);let i=j(".md-sidebar__scrollwrap",e),{y:s}=Be(i);return H(()=>{let a=new T,c=a.pipe(oe(),ae(!0)),p=a.pipe($e(0,ye));return p.pipe(te(t)).subscribe({next([{height:l},{height:f}]){i.style.height=`${l-2*s}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),p.pipe(Re()).subscribe(()=>{for(let l of M(".md-nav__link--active[href]",e)){if(!l.clientHeight)continue;let f=l.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=de(f);f.scrollTo({top:u-d/2})}}}),fe(M("label[tabindex]",e)).pipe(J(l=>h(l,"click").pipe(xe(pe),m(()=>l),W(c)))).subscribe(l=>{let f=j(`[id="${l.htmlFor}"]`);j(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),V("content.tooltips")&&fe(M("abbr[title]",e)).pipe(J(l=>Xe(l,{viewport$})),W(c)).subscribe(),Ss(e,r).pipe(O(l=>a.next(l)),A(()=>a.complete()),m(l=>P({ref:e},l)))})}function Ci(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return rt(ze(`${r}/releases/latest`).pipe(ve(()=>y),m(o=>({version:o.tag_name})),Qe({})),ze(r).pipe(ve(()=>y),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),Qe({}))).pipe(m(([o,n])=>P(P({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return ze(r).pipe(m(o=>({repositories:o.public_repos})),Qe({}))}}function ki(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return rt(ze(`${r}/releases/permalink/latest`).pipe(ve(()=>y),m(({tag_name:o})=>({version:o})),Qe({})),ze(r).pipe(ve(()=>y),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),Qe({}))).pipe(m(([o,n])=>P(P({},o),n)))}function Hi(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return Ci(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return ki(r,o)}return y}var Os;function Ls(e){return Os||(Os=H(()=>{let t=__md_get("__source",sessionStorage);if(t)return $(t);if(me("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return y}return Hi(e.href).pipe(O(o=>__md_set("__source",o,sessionStorage)))}).pipe(ve(()=>y),g(t=>Object.keys(t).length>0),m(t=>({facts:t})),Z(1)))}function $i(e){let t=j(":scope > :last-child",e);return H(()=>{let r=new T;return r.subscribe(({facts:o})=>{t.appendChild(jn(o)),t.classList.add("md-source__repository--active")}),Ls(e).pipe(O(o=>r.next(o)),A(()=>r.complete()),m(o=>P({ref:e},o)))})}function Ms(e,{viewport$:t,header$:r}){return Le(document.body).pipe(b(()=>Er(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),ne("hidden"))}function Pi(e,t){return H(()=>{let r=new T;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(V("navigation.tabs.sticky")?$({hidden:!1}):Ms(e,t)).pipe(O(o=>r.next(o)),A(()=>r.complete()),m(o=>P({ref:e},o)))})}function _s(e,{viewport$:t,header$:r}){let o=new Map,n=M(".md-nav__link",e);for(let a of n){let c=decodeURIComponent(a.hash.substring(1)),p=ue(`[id="${c}"]`);typeof p!="undefined"&&o.set(a,p)}let i=r.pipe(ne("height"),m(({height:a})=>{let c=Ce("main"),p=j(":scope > :first-child",c);return a+.8*(p.offsetTop-c.offsetTop)}),le());return Le(document.body).pipe(ne("height"),b(a=>H(()=>{let c=[];return $([...o].reduce((p,[l,f])=>{for(;c.length&&o.get(c[c.length-1]).tagName>=f.tagName;)c.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let d=f.offsetParent;for(;d;d=d.offsetParent)u+=d.offsetTop;return p.set([...c=[...c,l]].reverse(),u)},new Map))}).pipe(m(c=>new Map([...c].sort(([,p],[,l])=>p-l))),Pe(i),b(([c,p])=>t.pipe(Ut(([l,f],{offset:{y:u},size:d})=>{let v=u+d.height>=Math.floor(a.height);for(;f.length;){let[,S]=f[0];if(S-p=u&&!v)f=[l.pop(),...f];else break}return[l,f]},[[],[...c]]),Y((l,f)=>l[0]===f[0]&&l[1]===f[1])))))).pipe(m(([a,c])=>({prev:a.map(([p])=>p),next:c.map(([p])=>p)})),Q({prev:[],next:[]}),ot(2,1),m(([a,c])=>a.prev.length{let i=new T,s=i.pipe(oe(),ae(!0));if(i.subscribe(({prev:a,next:c})=>{for(let[p]of c)p.classList.remove("md-nav__link--passed"),p.classList.remove("md-nav__link--active");for(let[p,[l]]of a.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle("md-nav__link--active",p===a.length-1)}),V("toc.follow")){let a=L(t.pipe(Ae(1),m(()=>{})),t.pipe(Ae(250),m(()=>"smooth")));i.pipe(g(({prev:c})=>c.length>0),Pe(o.pipe(xe(pe))),te(a)).subscribe(([[{prev:c}],p])=>{let[l]=c[c.length-1];if(l.offsetHeight){let f=vr(l);if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=de(f);f.scrollTo({top:u-d/2,behavior:p})}}})}return V("navigation.tracking")&&t.pipe(W(s),ne("offset"),Ae(250),Ie(1),W(n.pipe(Ie(1))),vt({delay:250}),te(i)).subscribe(([,{prev:a}])=>{let c=we(),p=a[a.length-1];if(p&&p.length){let[l]=p,{hash:f}=new URL(l.href);c.hash!==f&&(c.hash=f,history.replaceState({},"",`${c}`))}else c.hash="",history.replaceState({},"",`${c}`)}),_s(e,{viewport$:t,header$:r}).pipe(O(a=>i.next(a)),A(()=>i.complete()),m(a=>P({ref:e},a)))})}function As(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:s}})=>s),ot(2,1),m(([s,a])=>s>a&&a>0),Y()),i=r.pipe(m(({active:s})=>s));return z([i,n]).pipe(m(([s,a])=>!(s&&a)),Y(),W(o.pipe(Ie(1))),ae(!0),vt({delay:250}),m(s=>({hidden:s})))}function Ii(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new T,s=i.pipe(oe(),ae(!0));return i.subscribe({next({hidden:a}){e.hidden=a,a?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(W(s),ne("height")).subscribe(({height:a})=>{e.style.top=`${a+16}px`}),h(e,"click").subscribe(a=>{a.preventDefault(),window.scrollTo({top:0})}),As(e,{viewport$:t,main$:o,target$:n}).pipe(O(a=>i.next(a)),A(()=>i.complete()),m(a=>P({ref:e},a)))}function Fi({document$:e,viewport$:t}){e.pipe(b(()=>M(".md-ellipsis")),J(r=>mt(r).pipe(W(e.pipe(Ie(1))),g(o=>o),m(()=>r),Ee(1))),g(r=>r.offsetWidth{let o=r.innerText,n=r.closest("a")||r;return n.title=o,V("content.tooltips")?Xe(n,{viewport$:t}).pipe(W(e.pipe(Ie(1))),A(()=>n.removeAttribute("title"))):y})).subscribe(),V("content.tooltips")&&e.pipe(b(()=>M(".md-status")),J(r=>Xe(r,{viewport$:t}))).subscribe()}function ji({document$:e,tablet$:t}){e.pipe(b(()=>M(".md-toggle--indeterminate")),O(r=>{r.indeterminate=!0,r.checked=!1}),J(r=>h(r,"change").pipe(Jr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),te(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function Cs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Ui({document$:e}){e.pipe(b(()=>M("[data-md-scrollfix]")),O(t=>t.removeAttribute("data-md-scrollfix")),g(Cs),J(t=>h(t,"touchstart").pipe(m(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function Wi({viewport$:e,tablet$:t}){z([Je("search"),t]).pipe(m(([r,o])=>r&&!o),b(r=>$(r).pipe(nt(r?400:100))),te(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function ks(){return location.protocol==="file:"?_t(`${new URL("search/search_index.js",Or.base)}`).pipe(m(()=>__index),Z(1)):ze(new URL("search/search_index.json",Or.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var ct=an(),Kt=bn(),Ht=yn(Kt),mo=hn(),ke=Ln(),Lr=Wt("(min-width: 60em)"),Vi=Wt("(min-width: 76.25em)"),Ni=xn(),Or=Te(),zi=document.forms.namedItem("search")?ks():tt,fo=new T;di({alert$:fo});ui({document$:ct});var uo=new T,qi=kt(Or.base);V("navigation.instant")&&gi({sitemap$:qi,location$:Kt,viewport$:ke,progress$:uo}).subscribe(ct);var Di;((Di=Or.version)==null?void 0:Di.provider)==="mike"&&Ti({document$:ct});L(Kt,Ht).pipe(nt(125)).subscribe(()=>{at("drawer",!1),at("search",!1)});mo.pipe(g(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=ue("link[rel=prev]");typeof t!="undefined"&&st(t);break;case"n":case".":let r=ue("link[rel=next]");typeof r!="undefined"&&st(r);break;case"Enter":let o=Ne();o instanceof HTMLLabelElement&&o.click()}});Fi({viewport$:ke,document$:ct});ji({document$:ct,tablet$:Lr});Ui({document$:ct});Wi({viewport$:ke,tablet$:Lr});var ft=ai(Ce("header"),{viewport$:ke}),qt=ct.pipe(m(()=>Ce("main")),b(e=>pi(e,{viewport$:ke,header$:ft})),Z(1)),Hs=L(...me("consent").map(e=>An(e,{target$:Ht})),...me("dialog").map(e=>ni(e,{alert$:fo})),...me("palette").map(e=>li(e)),...me("progress").map(e=>mi(e,{progress$:uo})),...me("search").map(e=>_i(e,{index$:zi,keyboard$:mo})),...me("source").map(e=>$i(e))),$s=H(()=>L(...me("announce").map(e=>_n(e)),...me("content").map(e=>oi(e,{sitemap$:qi,viewport$:ke,target$:Ht,print$:Ni})),...me("content").map(e=>V("search.highlight")?Ai(e,{index$:zi,location$:Kt}):y),...me("header").map(e=>si(e,{viewport$:ke,header$:ft,main$:qt})),...me("header-title").map(e=>ci(e,{viewport$:ke,header$:ft})),...me("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?eo(Vi,()=>lo(e,{viewport$:ke,header$:ft,main$:qt})):eo(Lr,()=>lo(e,{viewport$:ke,header$:ft,main$:qt}))),...me("tabs").map(e=>Pi(e,{viewport$:ke,header$:ft})),...me("toc").map(e=>Ri(e,{viewport$:ke,header$:ft,main$:qt,target$:Ht})),...me("top").map(e=>Ii(e,{viewport$:ke,header$:ft,main$:qt,target$:Ht})))),Ki=ct.pipe(b(()=>$s),Ve(Hs),Z(1));Ki.subscribe();window.document$=ct;window.location$=Kt;window.target$=Ht;window.keyboard$=mo;window.viewport$=ke;window.tablet$=Lr;window.screen$=Vi;window.print$=Ni;window.alert$=fo;window.progress$=uo;window.component$=Ki;})(); +//# sourceMappingURL=bundle.e71a0d61.min.js.map + diff --git a/site/assets/javascripts/bundle.e71a0d61.min.js.map b/site/assets/javascripts/bundle.e71a0d61.min.js.map new file mode 100644 index 00000000..23451b54 --- /dev/null +++ b/site/assets/javascripts/bundle.e71a0d61.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/escape-html/index.js", "node_modules/clipboard/dist/clipboard.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/tslib/tslib.es6.mjs", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/BehaviorSubject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/QueueAction.ts", "node_modules/rxjs/src/internal/scheduler/QueueScheduler.ts", "node_modules/rxjs/src/internal/scheduler/queue.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/EmptyError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/debounce.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinct.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/throwIfEmpty.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/exhaustMap.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/first.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/templates/assets/javascripts/browser/document/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/browser/element/focus/index.ts", "src/templates/assets/javascripts/browser/element/hover/index.ts", "src/templates/assets/javascripts/utilities/h/index.ts", "src/templates/assets/javascripts/utilities/round/index.ts", "src/templates/assets/javascripts/browser/script/index.ts", "src/templates/assets/javascripts/browser/element/size/_/index.ts", "src/templates/assets/javascripts/browser/element/size/content/index.ts", "src/templates/assets/javascripts/browser/element/offset/_/index.ts", "src/templates/assets/javascripts/browser/element/offset/content/index.ts", "src/templates/assets/javascripts/browser/element/visibility/index.ts", "src/templates/assets/javascripts/browser/toggle/index.ts", "src/templates/assets/javascripts/browser/keyboard/index.ts", "src/templates/assets/javascripts/browser/location/_/index.ts", "src/templates/assets/javascripts/browser/location/hash/index.ts", "src/templates/assets/javascripts/browser/media/index.ts", "src/templates/assets/javascripts/browser/request/index.ts", "src/templates/assets/javascripts/browser/viewport/offset/index.ts", "src/templates/assets/javascripts/browser/viewport/size/index.ts", "src/templates/assets/javascripts/browser/viewport/_/index.ts", "src/templates/assets/javascripts/browser/viewport/at/index.ts", "src/templates/assets/javascripts/browser/worker/index.ts", "src/templates/assets/javascripts/_/index.ts", "src/templates/assets/javascripts/components/_/index.ts", "src/templates/assets/javascripts/components/announce/index.ts", "src/templates/assets/javascripts/components/consent/index.ts", "src/templates/assets/javascripts/templates/tooltip/index.tsx", "src/templates/assets/javascripts/templates/annotation/index.tsx", "src/templates/assets/javascripts/templates/clipboard/index.tsx", "src/templates/assets/javascripts/templates/search/index.tsx", "src/templates/assets/javascripts/templates/source/index.tsx", "src/templates/assets/javascripts/templates/tabbed/index.tsx", "src/templates/assets/javascripts/templates/table/index.tsx", "src/templates/assets/javascripts/templates/version/index.tsx", "src/templates/assets/javascripts/components/tooltip2/index.ts", "src/templates/assets/javascripts/components/content/annotation/_/index.ts", "src/templates/assets/javascripts/components/content/annotation/list/index.ts", "src/templates/assets/javascripts/components/content/annotation/block/index.ts", "src/templates/assets/javascripts/components/content/code/_/index.ts", "src/templates/assets/javascripts/components/content/details/index.ts", "src/templates/assets/javascripts/components/content/link/index.ts", "src/templates/assets/javascripts/components/content/mermaid/index.css", "src/templates/assets/javascripts/components/content/mermaid/index.ts", "src/templates/assets/javascripts/components/content/table/index.ts", "src/templates/assets/javascripts/components/content/tabs/index.ts", "src/templates/assets/javascripts/components/content/_/index.ts", "src/templates/assets/javascripts/components/dialog/index.ts", "src/templates/assets/javascripts/components/tooltip/index.ts", "src/templates/assets/javascripts/components/header/_/index.ts", "src/templates/assets/javascripts/components/header/title/index.ts", "src/templates/assets/javascripts/components/main/index.ts", "src/templates/assets/javascripts/components/palette/index.ts", "src/templates/assets/javascripts/components/progress/index.ts", "src/templates/assets/javascripts/integrations/sitemap/index.ts", "src/templates/assets/javascripts/integrations/alternate/index.ts", "src/templates/assets/javascripts/integrations/clipboard/index.ts", "src/templates/assets/javascripts/integrations/instant/index.ts", "src/templates/assets/javascripts/integrations/search/highlighter/index.ts", "src/templates/assets/javascripts/integrations/search/worker/message/index.ts", "src/templates/assets/javascripts/integrations/search/worker/_/index.ts", "src/templates/assets/javascripts/integrations/version/findurl/index.ts", "src/templates/assets/javascripts/integrations/version/index.ts", "src/templates/assets/javascripts/components/search/query/index.ts", "src/templates/assets/javascripts/components/search/result/index.ts", "src/templates/assets/javascripts/components/search/share/index.ts", "src/templates/assets/javascripts/components/search/suggest/index.ts", "src/templates/assets/javascripts/components/search/_/index.ts", "src/templates/assets/javascripts/components/search/highlight/index.ts", "src/templates/assets/javascripts/components/sidebar/index.ts", "src/templates/assets/javascripts/components/source/facts/github/index.ts", "src/templates/assets/javascripts/components/source/facts/gitlab/index.ts", "src/templates/assets/javascripts/components/source/facts/_/index.ts", "src/templates/assets/javascripts/components/source/_/index.ts", "src/templates/assets/javascripts/components/tabs/index.ts", "src/templates/assets/javascripts/components/toc/index.ts", "src/templates/assets/javascripts/components/top/index.ts", "src/templates/assets/javascripts/patches/ellipsis/index.ts", "src/templates/assets/javascripts/patches/indeterminate/index.ts", "src/templates/assets/javascripts/patches/scrollfix/index.ts", "src/templates/assets/javascripts/patches/scrolllock/index.ts", "src/templates/assets/javascripts/polyfills/index.ts"], + "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*!\n * clipboard.js v2.0.11\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Create fake copy action wrapper using a fake element.\n * @param {String} target\n * @param {Object} options\n * @return {String}\n */\n\nvar fakeCopyAction = function fakeCopyAction(value, options) {\n var fakeElement = createFakeElement(value);\n options.container.appendChild(fakeElement);\n var selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n return selectedText;\n};\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n selectedText = fakeCopyAction(target, options);\n } else if (target instanceof HTMLInputElement && !['text', 'search', 'url', 'tel', 'password'].includes(target === null || target === void 0 ? void 0 : target.type)) {\n // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange\n selectedText = fakeCopyAction(target.value, options);\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*\n * Copyright (c) 2016-2025 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"focus-visible\"\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getActiveElement,\n getOptionalElement,\n requestJSON,\n setLocation,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchScript,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountAnnounce,\n mountBackToTop,\n mountConsent,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountProgress,\n mountSearch,\n mountSearchHiglight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n fetchSitemap,\n setupAlternate,\n setupClipboardJS,\n setupInstantNavigation,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchEllipsis,\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Functions - @todo refactor\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch search index\n *\n * @returns Search index observable\n */\nfunction fetchSearchIndex(): Observable {\n if (location.protocol === \"file:\") {\n return watchScript(\n `${new URL(\"search/search_index.js\", config.base)}`\n )\n .pipe(\n // @ts-ignore - @todo fix typings\n map(() => __index),\n shareReplay(1)\n )\n } else {\n return requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget(location$)\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 60em)\")\nconst screen$ = watchMedia(\"(min-width: 76.25em)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? fetchSearchIndex()\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up language selector */\nsetupAlternate({ document$ })\n\n/* Set up progress indicator */\nconst progress$ = new Subject()\n\n/* Set up sitemap for instant navigation and previews */\nconst sitemap$ = fetchSitemap(config.base)\n\n/* Set up instant navigation, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantNavigation({ sitemap$, location$, viewport$, progress$ })\n .subscribe(document$)\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"link[rel=prev]\")\n if (typeof prev !== \"undefined\")\n setLocation(prev)\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"link[rel=next]\")\n if (typeof next !== \"undefined\")\n setLocation(next)\n break\n\n /* Expand navigation, see https://bit.ly/3ZjG5io */\n case \"Enter\":\n const active = getActiveElement()\n if (active instanceof HTMLLabelElement)\n active.click()\n }\n })\n\n/* Set up patches */\npatchEllipsis({ viewport$, document$ })\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Consent */\n ...getComponentElements(\"consent\")\n .map(el => mountConsent(el, { target$ })),\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Progress bar */\n ...getComponentElements(\"progress\")\n .map(el => mountProgress(el, { progress$ })),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Announcement bar */\n ...getComponentElements(\"announce\")\n .map(el => mountAnnounce(el)),\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { sitemap$, viewport$, target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHiglight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, {\n viewport$, header$, main$, target$\n })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.progress$ = progress$ /* Progress indicator subject */\nwindow.component$ = component$ /* Component observable */\n", "/******************************************************************************\nCopyright (c) Microsoft Corporation.\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\nPERFORMANCE OF THIS SOFTWARE.\n***************************************************************************** */\n/* global Reflect, Promise, SuppressedError, Symbol, Iterator */\n\nvar extendStatics = function(d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n};\n\nexport function __extends(d, b) {\n if (typeof b !== \"function\" && b !== null)\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n}\n\nexport var __assign = function() {\n __assign = Object.assign || function __assign(t) {\n for (var s, i = 1, n = arguments.length; i < n; i++) {\n s = arguments[i];\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\n }\n return t;\n }\n return __assign.apply(this, arguments);\n}\n\nexport function __rest(s, e) {\n var t = {};\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\n t[p] = s[p];\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\n t[p[i]] = s[p[i]];\n }\n return t;\n}\n\nexport function __decorate(decorators, target, key, desc) {\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\n return c > 3 && r && Object.defineProperty(target, key, r), r;\n}\n\nexport function __param(paramIndex, decorator) {\n return function (target, key) { decorator(target, key, paramIndex); }\n}\n\nexport function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {\n function accept(f) { if (f !== void 0 && typeof f !== \"function\") throw new TypeError(\"Function expected\"); return f; }\n var kind = contextIn.kind, key = kind === \"getter\" ? \"get\" : kind === \"setter\" ? \"set\" : \"value\";\n var target = !descriptorIn && ctor ? contextIn[\"static\"] ? ctor : ctor.prototype : null;\n var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});\n var _, done = false;\n for (var i = decorators.length - 1; i >= 0; i--) {\n var context = {};\n for (var p in contextIn) context[p] = p === \"access\" ? {} : contextIn[p];\n for (var p in contextIn.access) context.access[p] = contextIn.access[p];\n context.addInitializer = function (f) { if (done) throw new TypeError(\"Cannot add initializers after decoration has completed\"); extraInitializers.push(accept(f || null)); };\n var result = (0, decorators[i])(kind === \"accessor\" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);\n if (kind === \"accessor\") {\n if (result === void 0) continue;\n if (result === null || typeof result !== \"object\") throw new TypeError(\"Object expected\");\n if (_ = accept(result.get)) descriptor.get = _;\n if (_ = accept(result.set)) descriptor.set = _;\n if (_ = accept(result.init)) initializers.unshift(_);\n }\n else if (_ = accept(result)) {\n if (kind === \"field\") initializers.unshift(_);\n else descriptor[key] = _;\n }\n }\n if (target) Object.defineProperty(target, contextIn.name, descriptor);\n done = true;\n};\n\nexport function __runInitializers(thisArg, initializers, value) {\n var useValue = arguments.length > 2;\n for (var i = 0; i < initializers.length; i++) {\n value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);\n }\n return useValue ? value : void 0;\n};\n\nexport function __propKey(x) {\n return typeof x === \"symbol\" ? x : \"\".concat(x);\n};\n\nexport function __setFunctionName(f, name, prefix) {\n if (typeof name === \"symbol\") name = name.description ? \"[\".concat(name.description, \"]\") : \"\";\n return Object.defineProperty(f, \"name\", { configurable: true, value: prefix ? \"\".concat(prefix, \" \", name) : name });\n};\n\nexport function __metadata(metadataKey, metadataValue) {\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\n}\n\nexport function __awaiter(thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n}\n\nexport function __generator(thisArg, body) {\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === \"function\" ? Iterator : Object).prototype);\n return g.next = verb(0), g[\"throw\"] = verb(1), g[\"return\"] = verb(2), typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\n function verb(n) { return function (v) { return step([n, v]); }; }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0: case 1: t = op; break;\n case 4: _.label++; return { value: op[1], done: false };\n case 5: _.label++; y = op[1]; op = [0]; continue;\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\n if (t[2]) _.ops.pop();\n _.trys.pop(); continue;\n }\n op = body.call(thisArg, _);\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\n }\n}\n\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n var desc = Object.getOwnPropertyDescriptor(m, k);\n if (!desc || (\"get\" in desc ? !m.__esModule : desc.writable || desc.configurable)) {\n desc = { enumerable: true, get: function() { return m[k]; } };\n }\n Object.defineProperty(o, k2, desc);\n}) : (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n o[k2] = m[k];\n});\n\nexport function __exportStar(m, o) {\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\n}\n\nexport function __values(o) {\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\n if (m) return m.call(o);\n if (o && typeof o.length === \"number\") return {\n next: function () {\n if (o && i >= o.length) o = void 0;\n return { value: o && o[i++], done: !o };\n }\n };\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\n}\n\nexport function __read(o, n) {\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\n if (!m) return o;\n var i = m.call(o), r, ar = [], e;\n try {\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\n }\n catch (error) { e = { error: error }; }\n finally {\n try {\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\n }\n finally { if (e) throw e.error; }\n }\n return ar;\n}\n\n/** @deprecated */\nexport function __spread() {\n for (var ar = [], i = 0; i < arguments.length; i++)\n ar = ar.concat(__read(arguments[i]));\n return ar;\n}\n\n/** @deprecated */\nexport function __spreadArrays() {\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\n r[k] = a[j];\n return r;\n}\n\nexport function __spreadArray(to, from, pack) {\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\n if (ar || !(i in from)) {\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\n ar[i] = from[i];\n }\n }\n return to.concat(ar || Array.prototype.slice.call(from));\n}\n\nexport function __await(v) {\n return this instanceof __await ? (this.v = v, this) : new __await(v);\n}\n\nexport function __asyncGenerator(thisArg, _arguments, generator) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\n return i = Object.create((typeof AsyncIterator === \"function\" ? AsyncIterator : Object).prototype), verb(\"next\"), verb(\"throw\"), verb(\"return\", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;\n function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }\n function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\n function fulfill(value) { resume(\"next\", value); }\n function reject(value) { resume(\"throw\", value); }\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\n}\n\nexport function __asyncDelegator(o) {\n var i, p;\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }\n}\n\nexport function __asyncValues(o) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var m = o[Symbol.asyncIterator], i;\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\n}\n\nexport function __makeTemplateObject(cooked, raw) {\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\n return cooked;\n};\n\nvar __setModuleDefault = Object.create ? (function(o, v) {\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\n}) : function(o, v) {\n o[\"default\"] = v;\n};\n\nexport function __importStar(mod) {\n if (mod && mod.__esModule) return mod;\n var result = {};\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\n __setModuleDefault(result, mod);\n return result;\n}\n\nexport function __importDefault(mod) {\n return (mod && mod.__esModule) ? mod : { default: mod };\n}\n\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n}\n\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n}\n\nexport function __classPrivateFieldIn(state, receiver) {\n if (receiver === null || (typeof receiver !== \"object\" && typeof receiver !== \"function\")) throw new TypeError(\"Cannot use 'in' operator on non-object\");\n return typeof state === \"function\" ? receiver === state : state.has(receiver);\n}\n\nexport function __addDisposableResource(env, value, async) {\n if (value !== null && value !== void 0) {\n if (typeof value !== \"object\" && typeof value !== \"function\") throw new TypeError(\"Object expected.\");\n var dispose, inner;\n if (async) {\n if (!Symbol.asyncDispose) throw new TypeError(\"Symbol.asyncDispose is not defined.\");\n dispose = value[Symbol.asyncDispose];\n }\n if (dispose === void 0) {\n if (!Symbol.dispose) throw new TypeError(\"Symbol.dispose is not defined.\");\n dispose = value[Symbol.dispose];\n if (async) inner = dispose;\n }\n if (typeof dispose !== \"function\") throw new TypeError(\"Object not disposable.\");\n if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };\n env.stack.push({ value: value, dispose: dispose, async: async });\n }\n else if (async) {\n env.stack.push({ async: true });\n }\n return value;\n}\n\nvar _SuppressedError = typeof SuppressedError === \"function\" ? SuppressedError : function (error, suppressed, message) {\n var e = new Error(message);\n return e.name = \"SuppressedError\", e.error = error, e.suppressed = suppressed, e;\n};\n\nexport function __disposeResources(env) {\n function fail(e) {\n env.error = env.hasError ? new _SuppressedError(e, env.error, \"An error was suppressed during disposal.\") : e;\n env.hasError = true;\n }\n var r, s = 0;\n function next() {\n while (r = env.stack.pop()) {\n try {\n if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);\n if (r.dispose) {\n var result = r.dispose.call(r.value);\n if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });\n }\n else s |= 1;\n }\n catch (e) {\n fail(e);\n }\n }\n if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();\n if (env.hasError) throw env.error;\n }\n return next();\n}\n\nexport default {\n __extends,\n __assign,\n __rest,\n __decorate,\n __param,\n __metadata,\n __awaiter,\n __generator,\n __createBinding,\n __exportStar,\n __values,\n __read,\n __spread,\n __spreadArrays,\n __spreadArray,\n __await,\n __asyncGenerator,\n __asyncDelegator,\n __asyncValues,\n __makeTemplateObject,\n __importStar,\n __importDefault,\n __classPrivateFieldGet,\n __classPrivateFieldSet,\n __classPrivateFieldIn,\n __addDisposableResource,\n __disposeResources,\n};\n", "/**\n * Returns true if the object is a function.\n * @param value The value to check\n */\nexport function isFunction(value: any): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "/**\n * Used to create Error subclasses until the community moves away from ES5.\n *\n * This is because compiling from TypeScript down to ES5 has issues with subclassing Errors\n * as well as other built-in types: https://github.com/Microsoft/TypeScript/issues/12123\n *\n * @param createImpl A factory function to create the actual constructor implementation. The returned\n * function should be a named function that calls `_super` internally.\n */\nexport function createErrorClass(createImpl: (_super: any) => any): T {\n const _super = (instance: any) => {\n Error.call(instance);\n instance.stack = new Error().stack;\n };\n\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface UnsubscriptionError extends Error {\n readonly errors: any[];\n}\n\nexport interface UnsubscriptionErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (errors: any[]): UnsubscriptionError;\n}\n\n/**\n * An error thrown when one or more errors have occurred during the\n * `unsubscribe` of a {@link Subscription}.\n */\nexport const UnsubscriptionError: UnsubscriptionErrorCtor = createErrorClass(\n (_super) =>\n function UnsubscriptionErrorImpl(this: any, errors: (Error | string)[]) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n }\n);\n", "/**\n * Removes an item from an array, mutating it.\n * @param arr The array to remove the item from\n * @param item The item to remove\n */\nexport function arrRemove(arr: T[] | undefined | null, item: T) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';\nimport { arrRemove } from './util/arrRemove';\n\n/**\n * Represents a disposable resource, such as the execution of an Observable. A\n * Subscription has one important method, `unsubscribe`, that takes no argument\n * and just disposes the resource held by the subscription.\n *\n * Additionally, subscriptions may be grouped together through the `add()`\n * method, which will attach a child Subscription to the current Subscription.\n * When a Subscription is unsubscribed, all its children (and its grandchildren)\n * will be unsubscribed as well.\n */\nexport class Subscription implements SubscriptionLike {\n public static EMPTY = (() => {\n const empty = new Subscription();\n empty.closed = true;\n return empty;\n })();\n\n /**\n * A flag to indicate whether this Subscription has already been unsubscribed.\n */\n public closed = false;\n\n private _parentage: Subscription[] | Subscription | null = null;\n\n /**\n * The list of registered finalizers to execute upon unsubscription. Adding and removing from this\n * list occurs in the {@link #add} and {@link #remove} methods.\n */\n private _finalizers: Exclude[] | null = null;\n\n /**\n * @param initialTeardown A function executed first as part of the finalization\n * process that is kicked off when {@link #unsubscribe} is called.\n */\n constructor(private initialTeardown?: () => void) {}\n\n /**\n * Disposes the resources held by the subscription. May, for instance, cancel\n * an ongoing Observable execution or cancel any other type of work that\n * started when the Subscription was created.\n */\n unsubscribe(): void {\n let errors: any[] | undefined;\n\n if (!this.closed) {\n this.closed = true;\n\n // Remove this from it's parents.\n const { _parentage } = this;\n if (_parentage) {\n this._parentage = null;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n } else {\n _parentage.remove(this);\n }\n }\n\n const { initialTeardown: initialFinalizer } = this;\n if (isFunction(initialFinalizer)) {\n try {\n initialFinalizer();\n } catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n\n const { _finalizers } = this;\n if (_finalizers) {\n this._finalizers = null;\n for (const finalizer of _finalizers) {\n try {\n execFinalizer(finalizer);\n } catch (err) {\n errors = errors ?? [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n } else {\n errors.push(err);\n }\n }\n }\n }\n\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n\n /**\n * Adds a finalizer to this subscription, so that finalization will be unsubscribed/called\n * when this subscription is unsubscribed. If this subscription is already {@link #closed},\n * because it has already been unsubscribed, then whatever finalizer is passed to it\n * will automatically be executed (unless the finalizer itself is also a closed subscription).\n *\n * Closed Subscriptions cannot be added as finalizers to any subscription. Adding a closed\n * subscription to a any subscription will result in no operation. (A noop).\n *\n * Adding a subscription to itself, or adding `null` or `undefined` will not perform any\n * operation at all. (A noop).\n *\n * `Subscription` instances that are added to this instance will automatically remove themselves\n * if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove\n * will need to be removed manually with {@link #remove}\n *\n * @param teardown The finalization logic to add to this subscription.\n */\n add(teardown: TeardownLogic): void {\n // Only add the finalizer if it's not undefined\n // and don't add a subscription to itself.\n if (teardown && teardown !== this) {\n if (this.closed) {\n // If this subscription is already closed,\n // execute whatever finalizer is handed to it automatically.\n execFinalizer(teardown);\n } else {\n if (teardown instanceof Subscription) {\n // We don't add closed subscriptions, and we don't add the same subscription\n // twice. Subscription unsubscribe is idempotent.\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._finalizers = this._finalizers ?? []).push(teardown);\n }\n }\n }\n\n /**\n * Checks to see if a this subscription already has a particular parent.\n * This will signal that this subscription has already been added to the parent in question.\n * @param parent the parent to check for\n */\n private _hasParent(parent: Subscription) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n\n /**\n * Adds a parent to this subscription so it can be removed from the parent if it\n * unsubscribes on it's own.\n *\n * NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.\n * @param parent The parent subscription to add\n */\n private _addParent(parent: Subscription) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n\n /**\n * Called on a child when it is removed via {@link #remove}.\n * @param parent The parent to remove\n */\n private _removeParent(parent: Subscription) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n } else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n\n /**\n * Removes a finalizer from this subscription that was previously added with the {@link #add} method.\n *\n * Note that `Subscription` instances, when unsubscribed, will automatically remove themselves\n * from every other `Subscription` they have been added to. This means that using the `remove` method\n * is not a common thing and should be used thoughtfully.\n *\n * If you add the same finalizer instance of a function or an unsubscribable object to a `Subscription` instance\n * more than once, you will need to call `remove` the same number of times to remove all instances.\n *\n * All finalizer instances are removed to free up memory upon unsubscription.\n *\n * @param teardown The finalizer to remove from this subscription\n */\n remove(teardown: Exclude): void {\n const { _finalizers } = this;\n _finalizers && arrRemove(_finalizers, teardown);\n\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\n\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\n\nexport function isSubscription(value: any): value is Subscription {\n return (\n value instanceof Subscription ||\n (value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))\n );\n}\n\nfunction execFinalizer(finalizer: Unsubscribable | (() => void)) {\n if (isFunction(finalizer)) {\n finalizer();\n } else {\n finalizer.unsubscribe();\n }\n}\n", "import { Subscriber } from './Subscriber';\nimport { ObservableNotification } from './types';\n\n/**\n * The {@link GlobalConfig} object for RxJS. It is used to configure things\n * like how to react on unhandled errors.\n */\nexport const config: GlobalConfig = {\n onUnhandledError: null,\n onStoppedNotification: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n\n/**\n * The global configuration object for RxJS, used to configure things\n * like how to react on unhandled errors. Accessible via {@link config}\n * object.\n */\nexport interface GlobalConfig {\n /**\n * A registration point for unhandled errors from RxJS. These are errors that\n * cannot were not handled by consuming code in the usual subscription path. For\n * example, if you have this configured, and you subscribe to an observable without\n * providing an error handler, errors from that subscription will end up here. This\n * will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onUnhandledError: ((err: any) => void) | null;\n\n /**\n * A registration point for notifications that cannot be sent to subscribers because they\n * have completed, errored or have been explicitly unsubscribed. By default, next, complete\n * and error notifications sent to stopped subscribers are noops. However, sometimes callers\n * might want a different behavior. For example, with sources that attempt to report errors\n * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead.\n * This will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null;\n\n /**\n * The promise constructor used by default for {@link Observable#toPromise toPromise} and {@link Observable#forEach forEach}\n * methods.\n *\n * @deprecated As of version 8, RxJS will no longer support this sort of injection of a\n * Promise constructor. If you need a Promise implementation other than native promises,\n * please polyfill/patch Promise as you see appropriate. Will be removed in v8.\n */\n Promise?: PromiseConstructorLike;\n\n /**\n * If true, turns on synchronous error rethrowing, which is a deprecated behavior\n * in v6 and higher. This behavior enables bad patterns like wrapping a subscribe\n * call in a try/catch block. It also enables producer interference, a nasty bug\n * where a multicast can be broken for all observers by a downstream consumer with\n * an unhandled error. DO NOT USE THIS FLAG UNLESS IT'S NEEDED TO BUY TIME\n * FOR MIGRATION REASONS.\n *\n * @deprecated As of version 8, RxJS will no longer support synchronous throwing\n * of unhandled errors. All errors will be thrown on a separate call stack to prevent bad\n * behaviors described above. Will be removed in v8.\n */\n useDeprecatedSynchronousErrorHandling: boolean;\n\n /**\n * If true, enables an as-of-yet undocumented feature from v5: The ability to access\n * `unsubscribe()` via `this` context in `next` functions created in observers passed\n * to `subscribe`.\n *\n * This is being removed because the performance was severely problematic, and it could also cause\n * issues when types other than POJOs are passed to subscribe as subscribers, as they will likely have\n * their `this` context overwritten.\n *\n * @deprecated As of version 8, RxJS will no longer support altering the\n * context of next functions provided as part of an observer to Subscribe. Instead,\n * you will have access to a subscription or a signal or token that will allow you to do things like\n * unsubscribe and test closed status. Will be removed in v8.\n */\n useDeprecatedNextContext: boolean;\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearTimeoutFunction = (handle: TimerHandle) => void;\n\ninterface TimeoutProvider {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n delegate:\n | {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n }\n | undefined;\n}\n\nexport const timeoutProvider: TimeoutProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setTimeout(handler: () => void, timeout?: number, ...args) {\n const { delegate } = timeoutProvider;\n if (delegate?.setTimeout) {\n return delegate.setTimeout(handler, timeout, ...args);\n }\n return setTimeout(handler, timeout, ...args);\n },\n clearTimeout(handle) {\n const { delegate } = timeoutProvider;\n return (delegate?.clearTimeout || clearTimeout)(handle as any);\n },\n delegate: undefined,\n};\n", "import { config } from '../config';\nimport { timeoutProvider } from '../scheduler/timeoutProvider';\n\n/**\n * Handles an error on another job either with the user-configured {@link onUnhandledError},\n * or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc.\n *\n * This should be called whenever there is an error that is out-of-band with the subscription\n * or when an error hits a terminal boundary of the subscription and no error handler was provided.\n *\n * @param err the error to report\n */\nexport function reportUnhandledError(err: any) {\n timeoutProvider.setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n // Execute the user-configured error handler.\n onUnhandledError(err);\n } else {\n // Throw so it is picked up by the runtime's uncaught error mechanism.\n throw err;\n }\n });\n}\n", "/* tslint:disable:no-empty */\nexport function noop() { }\n", "import { CompleteNotification, NextNotification, ErrorNotification } from './types';\n\n/**\n * A completion object optimized for memory use and created to be the\n * same \"shape\" as other notifications in v8.\n * @internal\n */\nexport const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)();\n\n/**\n * Internal use only. Creates an optimized error notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function errorNotification(error: any): ErrorNotification {\n return createNotification('E', undefined, error) as any;\n}\n\n/**\n * Internal use only. Creates an optimized next notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function nextNotification(value: T) {\n return createNotification('N', value, undefined) as NextNotification;\n}\n\n/**\n * Ensures that all notifications created internally have the same \"shape\" in v8.\n *\n * TODO: This is only exported to support a crazy legacy test in `groupBy`.\n * @internal\n */\nexport function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) {\n return {\n kind,\n value,\n error,\n };\n}\n", "import { config } from '../config';\n\nlet context: { errorThrown: boolean; error: any } | null = null;\n\n/**\n * Handles dealing with errors for super-gross mode. Creates a context, in which\n * any synchronously thrown errors will be passed to {@link captureError}. Which\n * will record the error such that it will be rethrown after the call back is complete.\n * TODO: Remove in v8\n * @param cb An immediately executed function.\n */\nexport function errorContext(cb: () => void) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n const isRoot = !context;\n if (isRoot) {\n context = { errorThrown: false, error: null };\n }\n cb();\n if (isRoot) {\n const { errorThrown, error } = context!;\n context = null;\n if (errorThrown) {\n throw error;\n }\n }\n } else {\n // This is the general non-deprecated path for everyone that\n // isn't crazy enough to use super-gross mode (useDeprecatedSynchronousErrorHandling)\n cb();\n }\n}\n\n/**\n * Captures errors only in super-gross mode.\n * @param err the error to capture\n */\nexport function captureError(err: any) {\n if (config.useDeprecatedSynchronousErrorHandling && context) {\n context.errorThrown = true;\n context.error = err;\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { Observer, ObservableNotification } from './types';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nimport { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories';\nimport { timeoutProvider } from './scheduler/timeoutProvider';\nimport { captureError } from './util/errorContext';\n\n/**\n * Implements the {@link Observer} interface and extends the\n * {@link Subscription} class. While the {@link Observer} is the public API for\n * consuming the values of an {@link Observable}, all Observers get converted to\n * a Subscriber, in order to provide Subscription-like capabilities such as\n * `unsubscribe`. Subscriber is a common type in RxJS, and crucial for\n * implementing operators, but it is rarely used as a public API.\n */\nexport class Subscriber extends Subscription implements Observer {\n /**\n * A static factory for a Subscriber, given a (potentially partial) definition\n * of an Observer.\n * @param next The `next` callback of an Observer.\n * @param error The `error` callback of an\n * Observer.\n * @param complete The `complete` callback of an\n * Observer.\n * @return A Subscriber wrapping the (partially defined)\n * Observer represented by the given arguments.\n * @deprecated Do not use. Will be removed in v8. There is no replacement for this\n * method, and there is no reason to be creating instances of `Subscriber` directly.\n * If you have a specific use case, please file an issue.\n */\n static create(next?: (x?: T) => void, error?: (e?: any) => void, complete?: () => void): Subscriber {\n return new SafeSubscriber(next, error, complete);\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected isStopped: boolean = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected destination: Subscriber | Observer; // this `any` is the escape hatch to erase extra type param (e.g. R)\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * There is no reason to directly create an instance of Subscriber. This type is exported for typings reasons.\n */\n constructor(destination?: Subscriber | Observer) {\n super();\n if (destination) {\n this.destination = destination;\n // Automatically chain subscriptions together here.\n // if destination is a Subscription, then it is a Subscriber.\n if (isSubscription(destination)) {\n destination.add(this);\n }\n } else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `next` from\n * the Observable, with a value. The Observable may call this method 0 or more\n * times.\n * @param value The `next` value.\n */\n next(value: T): void {\n if (this.isStopped) {\n handleStoppedNotification(nextNotification(value), this);\n } else {\n this._next(value!);\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `error` from\n * the Observable, with an attached `Error`. Notifies the Observer that\n * the Observable has experienced an error condition.\n * @param err The `error` exception.\n */\n error(err?: any): void {\n if (this.isStopped) {\n handleStoppedNotification(errorNotification(err), this);\n } else {\n this.isStopped = true;\n this._error(err);\n }\n }\n\n /**\n * The {@link Observer} callback to receive a valueless notification of type\n * `complete` from the Observable. Notifies the Observer that the Observable\n * has finished sending push-based notifications.\n */\n complete(): void {\n if (this.isStopped) {\n handleStoppedNotification(COMPLETE_NOTIFICATION, this);\n } else {\n this.isStopped = true;\n this._complete();\n }\n }\n\n unsubscribe(): void {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n this.destination = null!;\n }\n }\n\n protected _next(value: T): void {\n this.destination.next(value);\n }\n\n protected _error(err: any): void {\n try {\n this.destination.error(err);\n } finally {\n this.unsubscribe();\n }\n }\n\n protected _complete(): void {\n try {\n this.destination.complete();\n } finally {\n this.unsubscribe();\n }\n }\n}\n\n/**\n * This bind is captured here because we want to be able to have\n * compatibility with monoid libraries that tend to use a method named\n * `bind`. In particular, a library called Monio requires this.\n */\nconst _bind = Function.prototype.bind;\n\nfunction bind any>(fn: Fn, thisArg: any): Fn {\n return _bind.call(fn, thisArg);\n}\n\n/**\n * Internal optimization only, DO NOT EXPOSE.\n * @internal\n */\nclass ConsumerObserver implements Observer {\n constructor(private partialObserver: Partial>) {}\n\n next(value: T): void {\n const { partialObserver } = this;\n if (partialObserver.next) {\n try {\n partialObserver.next(value);\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n\n error(err: any): void {\n const { partialObserver } = this;\n if (partialObserver.error) {\n try {\n partialObserver.error(err);\n } catch (error) {\n handleUnhandledError(error);\n }\n } else {\n handleUnhandledError(err);\n }\n }\n\n complete(): void {\n const { partialObserver } = this;\n if (partialObserver.complete) {\n try {\n partialObserver.complete();\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n}\n\nexport class SafeSubscriber extends Subscriber {\n constructor(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((e?: any) => void) | null,\n complete?: (() => void) | null\n ) {\n super();\n\n let partialObserver: Partial>;\n if (isFunction(observerOrNext) || !observerOrNext) {\n // The first argument is a function, not an observer. The next\n // two arguments *could* be observers, or they could be empty.\n partialObserver = {\n next: (observerOrNext ?? undefined) as ((value: T) => void) | undefined,\n error: error ?? undefined,\n complete: complete ?? undefined,\n };\n } else {\n // The first argument is a partial observer.\n let context: any;\n if (this && config.useDeprecatedNextContext) {\n // This is a deprecated path that made `this.unsubscribe()` available in\n // next handler functions passed to subscribe. This only exists behind a flag\n // now, as it is *very* slow.\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n partialObserver = {\n next: observerOrNext.next && bind(observerOrNext.next, context),\n error: observerOrNext.error && bind(observerOrNext.error, context),\n complete: observerOrNext.complete && bind(observerOrNext.complete, context),\n };\n } else {\n // The \"normal\" path. Just use the partial observer directly.\n partialObserver = observerOrNext;\n }\n }\n\n // Wrap the partial observer to ensure it's a full observer, and\n // make sure proper error handling is accounted for.\n this.destination = new ConsumerObserver(partialObserver);\n }\n}\n\nfunction handleUnhandledError(error: any) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n captureError(error);\n } else {\n // Ideal path, we report this as an unhandled error,\n // which is thrown on a new call stack.\n reportUnhandledError(error);\n }\n}\n\n/**\n * An error handler used when no error handler was supplied\n * to the SafeSubscriber -- meaning no error handler was supplied\n * do the `subscribe` call on our observable.\n * @param err The error to handle\n */\nfunction defaultErrorHandler(err: any) {\n throw err;\n}\n\n/**\n * A handler for notifications that cannot be sent to a stopped subscriber.\n * @param notification The notification being sent.\n * @param subscriber The stopped subscriber.\n */\nfunction handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) {\n const { onStoppedNotification } = config;\n onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber));\n}\n\n/**\n * The observer used as a stub for subscriptions where the user did not\n * pass any arguments to `subscribe`. Comes with the default error handling\n * behavior.\n */\nexport const EMPTY_OBSERVER: Readonly> & { closed: true } = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n", "/**\n * Symbol.observable or a string \"@@observable\". Used for interop\n *\n * @deprecated We will no longer be exporting this symbol in upcoming versions of RxJS.\n * Instead polyfill and use Symbol.observable directly *or* use https://www.npmjs.com/package/symbol-observable\n */\nexport const observable: string | symbol = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();\n", "/**\n * This function takes one parameter and just returns it. Simply put,\n * this is like `(x: T): T => x`.\n *\n * ## Examples\n *\n * This is useful in some cases when using things like `mergeMap`\n *\n * ```ts\n * import { interval, take, map, range, mergeMap, identity } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(5));\n *\n * const result$ = source$.pipe(\n * map(i => range(i)),\n * mergeMap(identity) // same as mergeMap(x => x)\n * );\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * Or when you want to selectively apply an operator\n *\n * ```ts\n * import { interval, take, identity } from 'rxjs';\n *\n * const shouldLimit = () => Math.random() < 0.5;\n *\n * const source$ = interval(1000);\n *\n * const result$ = source$.pipe(shouldLimit() ? take(5) : identity);\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * @param x Any value that is returned by this function\n * @returns The value passed as the first parameter to this function\n */\nexport function identity(x: T): T {\n return x;\n}\n", "import { identity } from './identity';\nimport { UnaryFunction } from '../types';\n\nexport function pipe(): typeof identity;\nexport function pipe(fn1: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction,\n ...fns: UnaryFunction[]\n): UnaryFunction;\n\n/**\n * pipe() can be called on one or more functions, each of which can take one argument (\"UnaryFunction\")\n * and uses it to return a value.\n * It returns a function that takes one argument, passes it to the first UnaryFunction, and then\n * passes the result to the next one, passes that result to the next one, and so on. \n */\nexport function pipe(...fns: Array>): UnaryFunction {\n return pipeFromArray(fns);\n}\n\n/** @internal */\nexport function pipeFromArray(fns: Array>): UnaryFunction {\n if (fns.length === 0) {\n return identity as UnaryFunction;\n }\n\n if (fns.length === 1) {\n return fns[0];\n }\n\n return function piped(input: T): R {\n return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input as any);\n };\n}\n", "import { Operator } from './Operator';\nimport { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription, Subscription } from './Subscription';\nimport { TeardownLogic, OperatorFunction, Subscribable, Observer } from './types';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { isFunction } from './util/isFunction';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A representation of any set of values over any amount of time. This is the most basic building block\n * of RxJS.\n */\nexport class Observable implements Subscribable {\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n source: Observable | undefined;\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n operator: Operator | undefined;\n\n /**\n * @param subscribe The function that is called when the Observable is\n * initially subscribed to. This function is given a Subscriber, to which new values\n * can be `next`ed, or an `error` method can be called to raise an error, or\n * `complete` can be called to notify of a successful completion.\n */\n constructor(subscribe?: (this: Observable, subscriber: Subscriber) => TeardownLogic) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n\n // HACK: Since TypeScript inherits static properties too, we have to\n // fight against TypeScript here so Subject can have a different static create signature\n /**\n * Creates a new Observable by calling the Observable constructor\n * @param subscribe the subscriber function to be passed to the Observable constructor\n * @return A new observable.\n * @deprecated Use `new Observable()` instead. Will be removed in v8.\n */\n static create: (...args: any[]) => any = (subscribe?: (subscriber: Subscriber) => TeardownLogic) => {\n return new Observable(subscribe);\n };\n\n /**\n * Creates a new Observable, with this Observable instance as the source, and the passed\n * operator defined as the new observable's operator.\n * @param operator the operator defining the operation to take on the observable\n * @return A new observable with the Operator applied.\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * If you have implemented an operator using `lift`, it is recommended that you create an\n * operator by simply returning `new Observable()` directly. See \"Creating new operators from\n * scratch\" section here: https://rxjs.dev/guide/operators\n */\n lift(operator?: Operator): Observable {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n\n subscribe(observerOrNext?: Partial> | ((value: T) => void)): Subscription;\n /** @deprecated Instead of passing separate callback arguments, use an observer argument. Signatures taking separate callback arguments will be removed in v8. Details: https://rxjs.dev/deprecations/subscribe-arguments */\n subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;\n /**\n * Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.\n *\n * Use it when you have all these Observables, but still nothing is happening.\n *\n * `subscribe` is not a regular operator, but a method that calls Observable's internal `subscribe` function. It\n * might be for example a function that you passed to Observable's constructor, but most of the time it is\n * a library implementation, which defines what will be emitted by an Observable, and when it be will emitted. This means\n * that calling `subscribe` is actually the moment when Observable starts its work, not when it is created, as it is often\n * the thought.\n *\n * Apart from starting the execution of an Observable, this method allows you to listen for values\n * that an Observable emits, as well as for when it completes or errors. You can achieve this in two\n * of the following ways.\n *\n * The first way is creating an object that implements {@link Observer} interface. It should have methods\n * defined by that interface, but note that it should be just a regular JavaScript object, which you can create\n * yourself in any way you want (ES6 class, classic function constructor, object literal etc.). In particular, do\n * not attempt to use any RxJS implementation details to create Observers - you don't need them. Remember also\n * that your object does not have to implement all methods. If you find yourself creating a method that doesn't\n * do anything, you can simply omit it. Note however, if the `error` method is not provided and an error happens,\n * it will be thrown asynchronously. Errors thrown asynchronously cannot be caught using `try`/`catch`. Instead,\n * use the {@link onUnhandledError} configuration option or use a runtime handler (like `window.onerror` or\n * `process.on('error)`) to be notified of unhandled errors. Because of this, it's recommended that you provide\n * an `error` method to avoid missing thrown errors.\n *\n * The second way is to give up on Observer object altogether and simply provide callback functions in place of its methods.\n * This means you can provide three functions as arguments to `subscribe`, where the first function is equivalent\n * of a `next` method, the second of an `error` method and the third of a `complete` method. Just as in case of an Observer,\n * if you do not need to listen for something, you can omit a function by passing `undefined` or `null`,\n * since `subscribe` recognizes these functions by where they were placed in function call. When it comes\n * to the `error` function, as with an Observer, if not provided, errors emitted by an Observable will be thrown asynchronously.\n *\n * You can, however, subscribe with no parameters at all. This may be the case where you're not interested in terminal events\n * and you also handled emissions internally by using operators (e.g. using `tap`).\n *\n * Whichever style of calling `subscribe` you use, in both cases it returns a Subscription object.\n * This object allows you to call `unsubscribe` on it, which in turn will stop the work that an Observable does and will clean\n * up all resources that an Observable used. Note that cancelling a subscription will not call `complete` callback\n * provided to `subscribe` function, which is reserved for a regular completion signal that comes from an Observable.\n *\n * Remember that callbacks provided to `subscribe` are not guaranteed to be called asynchronously.\n * It is an Observable itself that decides when these functions will be called. For example {@link of}\n * by default emits all its values synchronously. Always check documentation for how given Observable\n * will behave when subscribed and if its default behavior can be modified with a `scheduler`.\n *\n * #### Examples\n *\n * Subscribe with an {@link guide/observer Observer}\n *\n * ```ts\n * import { of } from 'rxjs';\n *\n * const sumObserver = {\n * sum: 0,\n * next(value) {\n * console.log('Adding: ' + value);\n * this.sum = this.sum + value;\n * },\n * error() {\n * // We actually could just remove this method,\n * // since we do not really care about errors right now.\n * },\n * complete() {\n * console.log('Sum equals: ' + this.sum);\n * }\n * };\n *\n * of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes.\n * .subscribe(sumObserver);\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Subscribe with functions ({@link deprecations/subscribe-arguments deprecated})\n *\n * ```ts\n * import { of } from 'rxjs'\n *\n * let sum = 0;\n *\n * of(1, 2, 3).subscribe(\n * value => {\n * console.log('Adding: ' + value);\n * sum = sum + value;\n * },\n * undefined,\n * () => console.log('Sum equals: ' + sum)\n * );\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Cancel a subscription\n *\n * ```ts\n * import { interval } from 'rxjs';\n *\n * const subscription = interval(1000).subscribe({\n * next(num) {\n * console.log(num)\n * },\n * complete() {\n * // Will not be called, even when cancelling subscription.\n * console.log('completed!');\n * }\n * });\n *\n * setTimeout(() => {\n * subscription.unsubscribe();\n * console.log('unsubscribed!');\n * }, 2500);\n *\n * // Logs:\n * // 0 after 1s\n * // 1 after 2s\n * // 'unsubscribed!' after 2.5s\n * ```\n *\n * @param observerOrNext Either an {@link Observer} with some or all callback methods,\n * or the `next` handler that is called for each value emitted from the subscribed Observable.\n * @param error A handler for a terminal event resulting from an error. If no error handler is provided,\n * the error will be thrown asynchronously as unhandled.\n * @param complete A handler for a terminal event resulting from successful completion.\n * @return A subscription reference to the registered handlers.\n */\n subscribe(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((error: any) => void) | null,\n complete?: (() => void) | null\n ): Subscription {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n\n errorContext(() => {\n const { operator, source } = this;\n subscriber.add(\n operator\n ? // We're dealing with a subscription in the\n // operator chain to one of our lifted operators.\n operator.call(subscriber, source)\n : source\n ? // If `source` has a value, but `operator` does not, something that\n // had intimate knowledge of our API, like our `Subject`, must have\n // set it. We're going to just call `_subscribe` directly.\n this._subscribe(subscriber)\n : // In all other cases, we're likely wrapping a user-provided initializer\n // function, so we need to catch errors and handle them appropriately.\n this._trySubscribe(subscriber)\n );\n });\n\n return subscriber;\n }\n\n /** @internal */\n protected _trySubscribe(sink: Subscriber): TeardownLogic {\n try {\n return this._subscribe(sink);\n } catch (err) {\n // We don't need to return anything in this case,\n // because it's just going to try to `add()` to a subscription\n // above.\n sink.error(err);\n }\n }\n\n /**\n * Used as a NON-CANCELLABLE means of subscribing to an observable, for use with\n * APIs that expect promises, like `async/await`. You cannot unsubscribe from this.\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * #### Example\n *\n * ```ts\n * import { interval, take } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(4));\n *\n * async function getTotal() {\n * let total = 0;\n *\n * await source$.forEach(value => {\n * total += value;\n * console.log('observable -> ' + value);\n * });\n *\n * return total;\n * }\n *\n * getTotal().then(\n * total => console.log('Total: ' + total)\n * );\n *\n * // Expected:\n * // 'observable -> 0'\n * // 'observable -> 1'\n * // 'observable -> 2'\n * // 'observable -> 3'\n * // 'Total: 6'\n * ```\n *\n * @param next A handler for each value emitted by the observable.\n * @return A promise that either resolves on observable completion or\n * rejects with the handled error.\n */\n forEach(next: (value: T) => void): Promise;\n\n /**\n * @param next a handler for each value emitted by the observable\n * @param promiseCtor a constructor function used to instantiate the Promise\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n * @deprecated Passing a Promise constructor will no longer be available\n * in upcoming versions of RxJS. This is because it adds weight to the library, for very\n * little benefit. If you need this functionality, it is recommended that you either\n * polyfill Promise, or you create an adapter to convert the returned native promise\n * to whatever promise implementation you wanted. Will be removed in v8.\n */\n forEach(next: (value: T) => void, promiseCtor: PromiseConstructorLike): Promise;\n\n forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n const subscriber = new SafeSubscriber({\n next: (value) => {\n try {\n next(value);\n } catch (err) {\n reject(err);\n subscriber.unsubscribe();\n }\n },\n error: reject,\n complete: resolve,\n });\n this.subscribe(subscriber);\n }) as Promise;\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): TeardownLogic {\n return this.source?.subscribe(subscriber);\n }\n\n /**\n * An interop point defined by the es7-observable spec https://github.com/zenparsing/es-observable\n * @return This instance of the observable.\n */\n [Symbol_observable]() {\n return this;\n }\n\n /* tslint:disable:max-line-length */\n pipe(): Observable;\n pipe(op1: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction,\n ...operations: OperatorFunction[]\n ): Observable;\n /* tslint:enable:max-line-length */\n\n /**\n * Used to stitch together functional operators into a chain.\n *\n * ## Example\n *\n * ```ts\n * import { interval, filter, map, scan } from 'rxjs';\n *\n * interval(1000)\n * .pipe(\n * filter(x => x % 2 === 0),\n * map(x => x + x),\n * scan((acc, x) => acc + x)\n * )\n * .subscribe(x => console.log(x));\n * ```\n *\n * @return The Observable result of all the operators having been called\n * in the order they were passed in.\n */\n pipe(...operations: OperatorFunction[]): Observable {\n return pipeFromArray(operations)(this);\n }\n\n /* tslint:disable:max-line-length */\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: typeof Promise): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: PromiseConstructorLike): Promise;\n /* tslint:enable:max-line-length */\n\n /**\n * Subscribe to this Observable and get a Promise resolving on\n * `complete` with the last emission (if any).\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * @param [promiseCtor] a constructor function used to instantiate\n * the Promise\n * @return A Promise that resolves with the last value emit, or\n * rejects on an error. If there were no emissions, Promise\n * resolves with undefined.\n * @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise\n */\n toPromise(promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n let value: T | undefined;\n this.subscribe(\n (x: T) => (value = x),\n (err: any) => reject(err),\n () => resolve(value)\n );\n }) as Promise;\n }\n}\n\n/**\n * Decides between a passed promise constructor from consuming code,\n * A default configured promise constructor, and the native promise\n * constructor and returns it. If nothing can be found, it will throw\n * an error.\n * @param promiseCtor The optional promise constructor to passed by consuming code\n */\nfunction getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) {\n return promiseCtor ?? config.Promise ?? Promise;\n}\n\nfunction isObserver(value: any): value is Observer {\n return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);\n}\n\nfunction isSubscriber(value: any): value is Subscriber {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n", "import { Observable } from '../Observable';\nimport { Subscriber } from '../Subscriber';\nimport { OperatorFunction } from '../types';\nimport { isFunction } from './isFunction';\n\n/**\n * Used to determine if an object is an Observable with a lift function.\n */\nexport function hasLift(source: any): source is { lift: InstanceType['lift'] } {\n return isFunction(source?.lift);\n}\n\n/**\n * Creates an `OperatorFunction`. Used to define operators throughout the library in a concise way.\n * @param init The logic to connect the liftedSource to the subscriber at the moment of subscription.\n */\nexport function operate(\n init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void\n): OperatorFunction {\n return (source: Observable) => {\n if (hasLift(source)) {\n return source.lift(function (this: Subscriber, liftedSource: Observable) {\n try {\n return init(liftedSource, this);\n } catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n", "import { Subscriber } from '../Subscriber';\n\n/**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional teardown logic here. This will only be called on teardown if the\n * subscriber itself is not already closed. This is called after all other teardown logic is executed.\n */\nexport function createOperatorSubscriber(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n onFinalize?: () => void\n): Subscriber {\n return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);\n}\n\n/**\n * A generic helper for allowing operators to be created with a Subscriber and\n * use closures to capture necessary state from the operator function itself.\n */\nexport class OperatorSubscriber extends Subscriber {\n /**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional finalization logic here. This will only be called on finalization if the\n * subscriber itself is not already closed. This is called after all other finalization logic is executed.\n * @param shouldUnsubscribe An optional check to see if an unsubscribe call should truly unsubscribe.\n * NOTE: This currently **ONLY** exists to support the strange behavior of {@link groupBy}, where unsubscription\n * to the resulting observable does not actually disconnect from the source if there are active subscriptions\n * to any grouped observable. (DO NOT EXPOSE OR USE EXTERNALLY!!!)\n */\n constructor(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n private onFinalize?: () => void,\n private shouldUnsubscribe?: () => boolean\n ) {\n // It's important - for performance reasons - that all of this class's\n // members are initialized and that they are always initialized in the same\n // order. This will ensure that all OperatorSubscriber instances have the\n // same hidden class in V8. This, in turn, will help keep the number of\n // hidden classes involved in property accesses within the base class as\n // low as possible. If the number of hidden classes involved exceeds four,\n // the property accesses will become megamorphic and performance penalties\n // will be incurred - i.e. inline caches won't be used.\n //\n // The reasons for ensuring all instances have the same hidden class are\n // further discussed in this blog post from Benedikt Meurer:\n // https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/\n super(destination);\n this._next = onNext\n ? function (this: OperatorSubscriber, value: T) {\n try {\n onNext(value);\n } catch (err) {\n destination.error(err);\n }\n }\n : super._next;\n this._error = onError\n ? function (this: OperatorSubscriber, err: any) {\n try {\n onError(err);\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._error;\n this._complete = onComplete\n ? function (this: OperatorSubscriber) {\n try {\n onComplete();\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._complete;\n }\n\n unsubscribe() {\n if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {\n const { closed } = this;\n super.unsubscribe();\n // Execute additional teardown if we have any and we didn't already do so.\n !closed && this.onFinalize?.();\n }\n }\n}\n", "import { Subscription } from '../Subscription';\n\ninterface AnimationFrameProvider {\n schedule(callback: FrameRequestCallback): Subscription;\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n delegate:\n | {\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n }\n | undefined;\n}\n\nexport const animationFrameProvider: AnimationFrameProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel: typeof cancelAnimationFrame | undefined = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n // Clear the cancel function. The request has been fulfilled, so\n // attempting to cancel the request upon unsubscription would be\n // pointless.\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel?.(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.requestAnimationFrame || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.cancelAnimationFrame || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface ObjectUnsubscribedError extends Error {}\n\nexport interface ObjectUnsubscribedErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (): ObjectUnsubscribedError;\n}\n\n/**\n * An error thrown when an action is invalid because the object has been\n * unsubscribed.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n *\n * @class ObjectUnsubscribedError\n */\nexport const ObjectUnsubscribedError: ObjectUnsubscribedErrorCtor = createErrorClass(\n (_super) =>\n function ObjectUnsubscribedErrorImpl(this: any) {\n _super(this);\n this.name = 'ObjectUnsubscribedError';\n this.message = 'object unsubscribed';\n }\n);\n", "import { Operator } from './Operator';\nimport { Observable } from './Observable';\nimport { Subscriber } from './Subscriber';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { Observer, SubscriptionLike, TeardownLogic } from './types';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A Subject is a special type of Observable that allows values to be\n * multicasted to many Observers. Subjects are like EventEmitters.\n *\n * Every Subject is an Observable and an Observer. You can subscribe to a\n * Subject, and you can call next to feed values as well as error and complete.\n */\nexport class Subject extends Observable implements SubscriptionLike {\n closed = false;\n\n private currentObservers: Observer[] | null = null;\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n observers: Observer[] = [];\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n isStopped = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n hasError = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n thrownError: any = null;\n\n /**\n * Creates a \"subject\" by basically gluing an observer to an observable.\n *\n * @deprecated Recommended you do not use. Will be removed at some point in the future. Plans for replacement still under discussion.\n */\n static create: (...args: any[]) => any = (destination: Observer, source: Observable): AnonymousSubject => {\n return new AnonymousSubject(destination, source);\n };\n\n constructor() {\n // NOTE: This must be here to obscure Observable's constructor.\n super();\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n lift(operator: Operator): Observable {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator as any;\n return subject as any;\n }\n\n /** @internal */\n protected _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n\n next(value: T) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n if (!this.currentObservers) {\n this.currentObservers = Array.from(this.observers);\n }\n for (const observer of this.currentObservers) {\n observer.next(value);\n }\n }\n });\n }\n\n error(err: any) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.error(err);\n }\n }\n });\n }\n\n complete() {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.complete();\n }\n }\n });\n }\n\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = this.currentObservers = null!;\n }\n\n get observed() {\n return this.observers?.length > 0;\n }\n\n /** @internal */\n protected _trySubscribe(subscriber: Subscriber): TeardownLogic {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n\n /** @internal */\n protected _innerSubscribe(subscriber: Subscriber) {\n const { hasError, isStopped, observers } = this;\n if (hasError || isStopped) {\n return EMPTY_SUBSCRIPTION;\n }\n this.currentObservers = null;\n observers.push(subscriber);\n return new Subscription(() => {\n this.currentObservers = null;\n arrRemove(observers, subscriber);\n });\n }\n\n /** @internal */\n protected _checkFinalizedStatuses(subscriber: Subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n } else if (isStopped) {\n subscriber.complete();\n }\n }\n\n /**\n * Creates a new Observable with this Subject as the source. You can do this\n * to create custom Observer-side logic of the Subject and conceal it from\n * code that uses the Observable.\n * @return Observable that this Subject casts to.\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\n\nexport class AnonymousSubject extends Subject {\n constructor(\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n public destination?: Observer,\n source?: Observable\n ) {\n super();\n this.source = source;\n }\n\n next(value: T) {\n this.destination?.next?.(value);\n }\n\n error(err: any) {\n this.destination?.error?.(err);\n }\n\n complete() {\n this.destination?.complete?.();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION;\n }\n}\n", "import { Subject } from './Subject';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\n\n/**\n * A variant of Subject that requires an initial value and emits its current\n * value whenever it is subscribed to.\n */\nexport class BehaviorSubject extends Subject {\n constructor(private _value: T) {\n super();\n }\n\n get value(): T {\n return this.getValue();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n const subscription = super._subscribe(subscriber);\n !subscription.closed && subscriber.next(this._value);\n return subscription;\n }\n\n getValue(): T {\n const { hasError, thrownError, _value } = this;\n if (hasError) {\n throw thrownError;\n }\n this._throwIfClosed();\n return _value;\n }\n\n next(value: T): void {\n super.next((this._value = value));\n }\n}\n", "import { TimestampProvider } from '../types';\n\ninterface DateTimestampProvider extends TimestampProvider {\n delegate: TimestampProvider | undefined;\n}\n\nexport const dateTimestampProvider: DateTimestampProvider = {\n now() {\n // Use the variable rather than `this` so that the function can be called\n // without being bound to the provider.\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n", "import { Subject } from './Subject';\nimport { TimestampProvider } from './types';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * A variant of {@link Subject} that \"replays\" old values to new subscribers by emitting them when they first subscribe.\n *\n * `ReplaySubject` has an internal buffer that will store a specified number of values that it has observed. Like `Subject`,\n * `ReplaySubject` \"observes\" values by having them passed to its `next` method. When it observes a value, it will store that\n * value for a time determined by the configuration of the `ReplaySubject`, as passed to its constructor.\n *\n * When a new subscriber subscribes to the `ReplaySubject` instance, it will synchronously emit all values in its buffer in\n * a First-In-First-Out (FIFO) manner. The `ReplaySubject` will also complete, if it has observed completion; and it will\n * error if it has observed an error.\n *\n * There are two main configuration items to be concerned with:\n *\n * 1. `bufferSize` - This will determine how many items are stored in the buffer, defaults to infinite.\n * 2. `windowTime` - The amount of time to hold a value in the buffer before removing it from the buffer.\n *\n * Both configurations may exist simultaneously. So if you would like to buffer a maximum of 3 values, as long as the values\n * are less than 2 seconds old, you could do so with a `new ReplaySubject(3, 2000)`.\n *\n * ### Differences with BehaviorSubject\n *\n * `BehaviorSubject` is similar to `new ReplaySubject(1)`, with a couple of exceptions:\n *\n * 1. `BehaviorSubject` comes \"primed\" with a single value upon construction.\n * 2. `ReplaySubject` will replay values, even after observing an error, where `BehaviorSubject` will not.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n * @see {@link shareReplay}\n */\nexport class ReplaySubject extends Subject {\n private _buffer: (T | number)[] = [];\n private _infiniteTimeWindow = true;\n\n /**\n * @param _bufferSize The size of the buffer to replay on subscription\n * @param _windowTime The amount of time the buffered items will stay buffered\n * @param _timestampProvider An object with a `now()` method that provides the current timestamp. This is used to\n * calculate the amount of time something has been buffered.\n */\n constructor(\n private _bufferSize = Infinity,\n private _windowTime = Infinity,\n private _timestampProvider: TimestampProvider = dateTimestampProvider\n ) {\n super();\n this._infiniteTimeWindow = _windowTime === Infinity;\n this._bufferSize = Math.max(1, _bufferSize);\n this._windowTime = Math.max(1, _windowTime);\n }\n\n next(value: T): void {\n const { isStopped, _buffer, _infiniteTimeWindow, _timestampProvider, _windowTime } = this;\n if (!isStopped) {\n _buffer.push(value);\n !_infiniteTimeWindow && _buffer.push(_timestampProvider.now() + _windowTime);\n }\n this._trimBuffer();\n super.next(value);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._trimBuffer();\n\n const subscription = this._innerSubscribe(subscriber);\n\n const { _infiniteTimeWindow, _buffer } = this;\n // We use a copy here, so reentrant code does not mutate our array while we're\n // emitting it to a new subscriber.\n const copy = _buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i] as T);\n }\n\n this._checkFinalizedStatuses(subscriber);\n\n return subscription;\n }\n\n private _trimBuffer() {\n const { _bufferSize, _timestampProvider, _buffer, _infiniteTimeWindow } = this;\n // If we don't have an infinite buffer size, and we're over the length,\n // use splice to truncate the old buffer values off. Note that we have to\n // double the size for instances where we're not using an infinite time window\n // because we're storing the values and the timestamps in the same array.\n const adjustedBufferSize = (_infiniteTimeWindow ? 1 : 2) * _bufferSize;\n _bufferSize < Infinity && adjustedBufferSize < _buffer.length && _buffer.splice(0, _buffer.length - adjustedBufferSize);\n\n // Now, if we're not in an infinite time window, remove all values where the time is\n // older than what is allowed.\n if (!_infiniteTimeWindow) {\n const now = _timestampProvider.now();\n let last = 0;\n // Search the array for the first timestamp that isn't expired and\n // truncate the buffer up to that point.\n for (let i = 1; i < _buffer.length && (_buffer[i] as number) <= now; i += 2) {\n last = i;\n }\n last && _buffer.splice(0, last + 1);\n }\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Subscription } from '../Subscription';\nimport { SchedulerAction } from '../types';\n\n/**\n * A unit of work to be executed in a `scheduler`. An action is typically\n * created from within a {@link SchedulerLike} and an RxJS user does not need to concern\n * themselves about creating and manipulating an Action.\n *\n * ```ts\n * class Action extends Subscription {\n * new (scheduler: Scheduler, work: (state?: T) => void);\n * schedule(state?: T, delay: number = 0): Subscription;\n * }\n * ```\n */\nexport class Action extends Subscription {\n constructor(scheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void) {\n super();\n }\n /**\n * Schedules this action on its parent {@link SchedulerLike} for execution. May be passed\n * some context object, `state`. May happen at some point in the future,\n * according to the `delay` parameter, if specified.\n * @param state Some contextual data that the `work` function uses when called by the\n * Scheduler.\n * @param delay Time to wait before executing the work, where the time unit is implicit\n * and defined by the Scheduler.\n * @return A subscription in order to be able to unsubscribe the scheduled work.\n */\n public schedule(state?: T, delay: number = 0): Subscription {\n return this;\n }\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetIntervalFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearIntervalFunction = (handle: TimerHandle) => void;\n\ninterface IntervalProvider {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n delegate:\n | {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n }\n | undefined;\n}\n\nexport const intervalProvider: IntervalProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setInterval(handler: () => void, timeout?: number, ...args) {\n const { delegate } = intervalProvider;\n if (delegate?.setInterval) {\n return delegate.setInterval(handler, timeout, ...args);\n }\n return setInterval(handler, timeout, ...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return (delegate?.clearInterval || clearInterval)(handle as any);\n },\n delegate: undefined,\n};\n", "import { Action } from './Action';\nimport { SchedulerAction } from '../types';\nimport { Subscription } from '../Subscription';\nimport { AsyncScheduler } from './AsyncScheduler';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncAction extends Action {\n public id: TimerHandle | undefined;\n public state?: T;\n // @ts-ignore: Property has no initializer and is not definitely assigned\n public delay: number;\n protected pending: boolean = false;\n\n constructor(protected scheduler: AsyncScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (this.closed) {\n return this;\n }\n\n // Always replace the current state with the new state.\n this.state = state;\n\n const id = this.id;\n const scheduler = this.scheduler;\n\n //\n // Important implementation note:\n //\n // Actions only execute once by default, unless rescheduled from within the\n // scheduled callback. This allows us to implement single and repeat\n // actions via the same code path, without adding API surface area, as well\n // as mimic traditional recursion but across asynchronous boundaries.\n //\n // However, JS runtimes and timers distinguish between intervals achieved by\n // serial `setTimeout` calls vs. a single `setInterval` call. An interval of\n // serial `setTimeout` calls can be individually delayed, which delays\n // scheduling the next `setTimeout`, and so on. `setInterval` attempts to\n // guarantee the interval callback will be invoked more precisely to the\n // interval period, regardless of load.\n //\n // Therefore, we use `setInterval` to schedule single and repeat actions.\n // If the action reschedules itself with the same delay, the interval is not\n // canceled. If the action doesn't reschedule, or reschedules with a\n // different delay, the interval will be canceled after scheduled callback\n // execution.\n //\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n\n // Set the pending flag indicating that this action has been scheduled, or\n // has recursively rescheduled itself.\n this.pending = true;\n\n this.delay = delay;\n // If this action has already an async Id, don't request a new one.\n this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);\n\n return this;\n }\n\n protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n\n protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {\n // If this action is rescheduled with the same delay time, don't clear the interval id.\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n // Otherwise, if the action's delay time is different from the current delay,\n // or the action has been rescheduled before it's executed, clear the interval id\n if (id != null) {\n intervalProvider.clearInterval(id);\n }\n\n return undefined;\n }\n\n /**\n * Immediately executes this action and the `work` it contains.\n */\n public execute(state: T, delay: number): any {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n } else if (this.pending === false && this.id != null) {\n // Dequeue if the action didn't reschedule itself. Don't call\n // unsubscribe(), because the action could reschedule later.\n // For example:\n // ```\n // scheduler.schedule(function doWork(counter) {\n // /* ... I'm a busy worker bee ... */\n // var originalAction = this;\n // /* wait 100ms before rescheduling the action */\n // setTimeout(function () {\n // originalAction.schedule(counter + 1);\n // }, 100);\n // }, 1000);\n // ```\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n\n protected _execute(state: T, _delay: number): any {\n let errored: boolean = false;\n let errorValue: any;\n try {\n this.work(state);\n } catch (e) {\n errored = true;\n // HACK: Since code elsewhere is relying on the \"truthiness\" of the\n // return here, we can't have it return \"\" or 0 or false.\n // TODO: Clean this up when we refactor schedulers mid-version-8 or so.\n errorValue = e ? e : new Error('Scheduled action threw falsy error');\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n\n this.work = this.state = this.scheduler = null!;\n this.pending = false;\n\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n\n this.delay = null!;\n super.unsubscribe();\n }\n }\n}\n", "import { Action } from './scheduler/Action';\nimport { Subscription } from './Subscription';\nimport { SchedulerLike, SchedulerAction } from './types';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * An execution context and a data structure to order tasks and schedule their\n * execution. Provides a notion of (potentially virtual) time, through the\n * `now()` getter method.\n *\n * Each unit of work in a Scheduler is called an `Action`.\n *\n * ```ts\n * class Scheduler {\n * now(): number;\n * schedule(work, delay?, state?): Subscription;\n * }\n * ```\n *\n * @deprecated Scheduler is an internal implementation detail of RxJS, and\n * should not be used directly. Rather, create your own class and implement\n * {@link SchedulerLike}. Will be made internal in v8.\n */\nexport class Scheduler implements SchedulerLike {\n public static now: () => number = dateTimestampProvider.now;\n\n constructor(private schedulerActionCtor: typeof Action, now: () => number = Scheduler.now) {\n this.now = now;\n }\n\n /**\n * A getter method that returns a number representing the current time\n * (at the time this function was called) according to the scheduler's own\n * internal clock.\n * @return A number that represents the current time. May or may not\n * have a relation to wall-clock time. May or may not refer to a time unit\n * (e.g. milliseconds).\n */\n public now: () => number;\n\n /**\n * Schedules a function, `work`, for execution. May happen at some point in\n * the future, according to the `delay` parameter, if specified. May be passed\n * some context object, `state`, which will be passed to the `work` function.\n *\n * The given arguments will be processed an stored as an Action object in a\n * queue of actions.\n *\n * @param work A function representing a task, or some unit of work to be\n * executed by the Scheduler.\n * @param delay Time to wait before executing the work, where the time unit is\n * implicit and defined by the Scheduler itself.\n * @param state Some contextual data that the `work` function uses when called\n * by the Scheduler.\n * @return A subscription in order to be able to unsubscribe the scheduled work.\n */\n public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription {\n return new this.schedulerActionCtor(this, work).schedule(state, delay);\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Action } from './Action';\nimport { AsyncAction } from './AsyncAction';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncScheduler extends Scheduler {\n public actions: Array> = [];\n /**\n * A flag to indicate whether the Scheduler is currently executing a batch of\n * queued actions.\n * @internal\n */\n public _active: boolean = false;\n /**\n * An internal ID used to track the latest asynchronous task such as those\n * coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and\n * others.\n * @internal\n */\n public _scheduled: TimerHandle | undefined;\n\n constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {\n super(SchedulerAction, now);\n }\n\n public flush(action: AsyncAction): void {\n const { actions } = this;\n\n if (this._active) {\n actions.push(action);\n return;\n }\n\n let error: any;\n this._active = true;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions.shift()!)); // exhaust the scheduler queue\n\n this._active = false;\n\n if (error) {\n while ((action = actions.shift()!)) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\n/**\n *\n * Async Scheduler\n *\n * Schedule task as if you used setTimeout(task, duration)\n *\n * `async` scheduler schedules tasks asynchronously, by putting them on the JavaScript\n * event loop queue. It is best used to delay tasks in time or to schedule tasks repeating\n * in intervals.\n *\n * If you just want to \"defer\" task, that is to perform it right after currently\n * executing synchronous code ends (commonly achieved by `setTimeout(deferredTask, 0)`),\n * better choice will be the {@link asapScheduler} scheduler.\n *\n * ## Examples\n * Use async scheduler to delay task\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * const task = () => console.log('it works!');\n *\n * asyncScheduler.schedule(task, 2000);\n *\n * // After 2 seconds logs:\n * // \"it works!\"\n * ```\n *\n * Use async scheduler to repeat task in intervals\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * function task(state) {\n * console.log(state);\n * this.schedule(state + 1, 1000); // `this` references currently executing Action,\n * // which we reschedule with new state and delay\n * }\n *\n * asyncScheduler.schedule(task, 3000, 0);\n *\n * // Logs:\n * // 0 after 3s\n * // 1 after 4s\n * // 2 after 5s\n * // 3 after 6s\n * ```\n */\n\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\n\n/**\n * @deprecated Renamed to {@link asyncScheduler}. Will be removed in v8.\n */\nexport const async = asyncScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { Subscription } from '../Subscription';\nimport { QueueScheduler } from './QueueScheduler';\nimport { SchedulerAction } from '../types';\nimport { TimerHandle } from './timerHandle';\n\nexport class QueueAction extends AsyncAction {\n constructor(protected scheduler: QueueScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (delay > 0) {\n return super.schedule(state, delay);\n }\n this.delay = delay;\n this.state = state;\n this.scheduler.flush(this);\n return this;\n }\n\n public execute(state: T, delay: number): any {\n return delay > 0 || this.closed ? super.execute(state, delay) : this._execute(state, delay);\n }\n\n protected requestAsyncId(scheduler: QueueScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n\n if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n\n // Otherwise flush the scheduler starting with this action.\n scheduler.flush(this);\n\n // HACK: In the past, this was returning `void`. However, `void` isn't a valid\n // `TimerHandle`, and generally the return value here isn't really used. So the\n // compromise is to return `0` which is both \"falsy\" and a valid `TimerHandle`,\n // as opposed to refactoring every other instanceo of `requestAsyncId`.\n return 0;\n }\n}\n", "import { AsyncScheduler } from './AsyncScheduler';\n\nexport class QueueScheduler extends AsyncScheduler {\n}\n", "import { QueueAction } from './QueueAction';\nimport { QueueScheduler } from './QueueScheduler';\n\n/**\n *\n * Queue Scheduler\n *\n * Put every next task on a queue, instead of executing it immediately\n *\n * `queue` scheduler, when used with delay, behaves the same as {@link asyncScheduler} scheduler.\n *\n * When used without delay, it schedules given task synchronously - executes it right when\n * it is scheduled. However when called recursively, that is when inside the scheduled task,\n * another task is scheduled with queue scheduler, instead of executing immediately as well,\n * that task will be put on a queue and wait for current one to finish.\n *\n * This means that when you execute task with `queue` scheduler, you are sure it will end\n * before any other task scheduled with that scheduler will start.\n *\n * ## Examples\n * Schedule recursively first, then do something\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(() => {\n * queueScheduler.schedule(() => console.log('second')); // will not happen now, but will be put on a queue\n *\n * console.log('first');\n * });\n *\n * // Logs:\n * // \"first\"\n * // \"second\"\n * ```\n *\n * Reschedule itself recursively\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(function(state) {\n * if (state !== 0) {\n * console.log('before', state);\n * this.schedule(state - 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * console.log('after', state);\n * }\n * }, 0, 3);\n *\n * // In scheduler that runs recursively, you would expect:\n * // \"before\", 3\n * // \"before\", 2\n * // \"before\", 1\n * // \"after\", 1\n * // \"after\", 2\n * // \"after\", 3\n *\n * // But with queue it logs:\n * // \"before\", 3\n * // \"after\", 3\n * // \"before\", 2\n * // \"after\", 2\n * // \"before\", 1\n * // \"after\", 1\n * ```\n */\n\nexport const queueScheduler = new QueueScheduler(QueueAction);\n\n/**\n * @deprecated Renamed to {@link queueScheduler}. Will be removed in v8.\n */\nexport const queue = queueScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nimport { SchedulerAction } from '../types';\nimport { animationFrameProvider } from './animationFrameProvider';\nimport { TimerHandle } from './timerHandle';\n\nexport class AnimationFrameAction extends AsyncAction {\n constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay is greater than 0, request as an async action.\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n // Push the action to the end of the scheduler queue.\n scheduler.actions.push(this);\n // If an animation frame has already been requested, don't request another\n // one. If an animation frame hasn't been requested yet, request one. Return\n // the current animation frame request id.\n return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n\n protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n if (delay != null ? delay > 0 : this.delay > 0) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n // If the scheduler queue has no remaining actions with the same async id,\n // cancel the requested animation frame and set the scheduled flag to\n // undefined so the next AnimationFrameAction will request its own.\n const { actions } = scheduler;\n if (id != null && id === scheduler._scheduled && actions[actions.length - 1]?.id !== id) {\n animationFrameProvider.cancelAnimationFrame(id as number);\n scheduler._scheduled = undefined;\n }\n // Return undefined so the action knows to request a new async id if it's rescheduled.\n return undefined;\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\nexport class AnimationFrameScheduler extends AsyncScheduler {\n public flush(action?: AsyncAction): void {\n this._active = true;\n // The async id that effects a call to flush is stored in _scheduled.\n // Before executing an action, it's necessary to check the action's async\n // id to determine whether it's supposed to be executed in the current\n // flush.\n // Previous implementations of this method used a count to determine this,\n // but that was unsound, as actions that are unsubscribed - i.e. cancelled -\n // are removed from the actions array and that can shift actions that are\n // scheduled to be executed in a subsequent flush into positions at which\n // they are executed within the current flush.\n let flushId;\n if (action) {\n flushId = action.id;\n } else {\n flushId = this._scheduled;\n this._scheduled = undefined;\n }\n\n const { actions } = this;\n let error: any;\n action = action || actions.shift()!;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions[0]) && action.id === flushId && actions.shift());\n\n this._active = false;\n\n if (error) {\n while ((action = actions[0]) && action.id === flushId && actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\n\n/**\n *\n * Animation Frame Scheduler\n *\n * Perform task when `window.requestAnimationFrame` would fire\n *\n * When `animationFrame` scheduler is used with delay, it will fall back to {@link asyncScheduler} scheduler\n * behaviour.\n *\n * Without delay, `animationFrame` scheduler can be used to create smooth browser animations.\n * It makes sure scheduled task will happen just before next browser content repaint,\n * thus performing animations as efficiently as possible.\n *\n * ## Example\n * Schedule div height animation\n * ```ts\n * // html:
\n * import { animationFrameScheduler } from 'rxjs';\n *\n * const div = document.querySelector('div');\n *\n * animationFrameScheduler.schedule(function(height) {\n * div.style.height = height + \"px\";\n *\n * this.schedule(height + 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * }, 0, 0);\n *\n * // You will see a div element growing in height\n * ```\n */\n\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\n\n/**\n * @deprecated Renamed to {@link animationFrameScheduler}. Will be removed in v8.\n */\nexport const animationFrame = animationFrameScheduler;\n", "import { Observable } from '../Observable';\nimport { SchedulerLike } from '../types';\n\n/**\n * A simple Observable that emits no items to the Observer and immediately\n * emits a complete notification.\n *\n * Just emits 'complete', and nothing else.\n *\n * ![](empty.png)\n *\n * A simple Observable that only emits the complete notification. It can be used\n * for composing with other Observables, such as in a {@link mergeMap}.\n *\n * ## Examples\n *\n * Log complete notification\n *\n * ```ts\n * import { EMPTY } from 'rxjs';\n *\n * EMPTY.subscribe({\n * next: () => console.log('Next'),\n * complete: () => console.log('Complete!')\n * });\n *\n * // Outputs\n * // Complete!\n * ```\n *\n * Emit the number 7, then complete\n *\n * ```ts\n * import { EMPTY, startWith } from 'rxjs';\n *\n * const result = EMPTY.pipe(startWith(7));\n * result.subscribe(x => console.log(x));\n *\n * // Outputs\n * // 7\n * ```\n *\n * Map and flatten only odd numbers to the sequence `'a'`, `'b'`, `'c'`\n *\n * ```ts\n * import { interval, mergeMap, of, EMPTY } from 'rxjs';\n *\n * const interval$ = interval(1000);\n * const result = interval$.pipe(\n * mergeMap(x => x % 2 === 1 ? of('a', 'b', 'c') : EMPTY),\n * );\n * result.subscribe(x => console.log(x));\n *\n * // Results in the following to the console:\n * // x is equal to the count on the interval, e.g. (0, 1, 2, 3, ...)\n * // x will occur every 1000ms\n * // if x % 2 is equal to 1, print a, b, c (each on its own)\n * // if x % 2 is not equal to 1, nothing will be output\n * ```\n *\n * @see {@link Observable}\n * @see {@link NEVER}\n * @see {@link of}\n * @see {@link throwError}\n */\nexport const EMPTY = new Observable((subscriber) => subscriber.complete());\n\n/**\n * @param scheduler A {@link SchedulerLike} to use for scheduling\n * the emission of the complete notification.\n * @deprecated Replaced with the {@link EMPTY} constant or {@link scheduled} (e.g. `scheduled([], scheduler)`). Will be removed in v8.\n */\nexport function empty(scheduler?: SchedulerLike) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\n\nfunction emptyScheduled(scheduler: SchedulerLike) {\n return new Observable((subscriber) => scheduler.schedule(() => subscriber.complete()));\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport function isScheduler(value: any): value is SchedulerLike {\n return value && isFunction(value.schedule);\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\nimport { isScheduler } from './isScheduler';\n\nfunction last(arr: T[]): T | undefined {\n return arr[arr.length - 1];\n}\n\nexport function popResultSelector(args: any[]): ((...args: unknown[]) => unknown) | undefined {\n return isFunction(last(args)) ? args.pop() : undefined;\n}\n\nexport function popScheduler(args: any[]): SchedulerLike | undefined {\n return isScheduler(last(args)) ? args.pop() : undefined;\n}\n\nexport function popNumber(args: any[], defaultValue: number): number {\n return typeof last(args) === 'number' ? args.pop()! : defaultValue;\n}\n", "export const isArrayLike = ((x: any): x is ArrayLike => x && typeof x.length === 'number' && typeof x !== 'function');", "import { isFunction } from \"./isFunction\";\n\n/**\n * Tests to see if the object is \"thennable\".\n * @param value the object to test\n */\nexport function isPromise(value: any): value is PromiseLike {\n return isFunction(value?.then);\n}\n", "import { InteropObservable } from '../types';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being Observable (but not necessary an Rx Observable) */\nexport function isInteropObservable(input: any): input is InteropObservable {\n return isFunction(input[Symbol_observable]);\n}\n", "import { isFunction } from './isFunction';\n\nexport function isAsyncIterable(obj: any): obj is AsyncIterable {\n return Symbol.asyncIterator && isFunction(obj?.[Symbol.asyncIterator]);\n}\n", "/**\n * Creates the TypeError to throw if an invalid object is passed to `from` or `scheduled`.\n * @param input The object that was passed.\n */\nexport function createInvalidObservableTypeError(input: any) {\n // TODO: We should create error codes that can be looked up, so this can be less verbose.\n return new TypeError(\n `You provided ${\n input !== null && typeof input === 'object' ? 'an invalid object' : `'${input}'`\n } where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`\n );\n}\n", "export function getSymbolIterator(): symbol {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator' as any;\n }\n\n return Symbol.iterator;\n}\n\nexport const iterator = getSymbolIterator();\n", "import { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being an Iterable */\nexport function isIterable(input: any): input is Iterable {\n return isFunction(input?.[Symbol_iterator]);\n}\n", "import { ReadableStreamLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport async function* readableStreamLikeToAsyncGenerator(readableStream: ReadableStreamLike): AsyncGenerator {\n const reader = readableStream.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n return;\n }\n yield value!;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\nexport function isReadableStreamLike(obj: any): obj is ReadableStreamLike {\n // We don't want to use instanceof checks because they would return\n // false for instances from another Realm, like an