diff --git a/docs/src/generated/settings.json b/docs/src/generated/settings.json index fcb47dbfb23..ccdb1bb04ae 100644 --- a/docs/src/generated/settings.json +++ b/docs/src/generated/settings.json @@ -92,6 +92,28 @@ "type": "typing.Optional[pathlib.Path]", "validation": {} }, + { + "category": "WEB", + "default": null, + "description": "Public base path when running behind a reverse proxy under a sub-path, e.g. `/invoke`. Required when the proxy PRESERVES the sub-path (the backend receives `/invoke/api/...`); optional when the proxy strips it (set it anyway so openapi/docs URLs are correct). Leave unset when serving at the domain root. Normalized to a single leading slash with no trailing slash.", + "env_var": "INVOKEAI_BASE_URL", + "literal_values": [], + "name": "base_url", + "required": false, + "type": "typing.Optional[str]", + "validation": {} + }, + { + "category": "WEB", + "default": "127.0.0.1", + "description": "Comma-separated list of IPs (or `*`) allowed to set X-Forwarded-* headers. Set to the reverse proxy's IP. Only used when `base_url` is set.", + "env_var": "INVOKEAI_FORWARDED_ALLOW_IPS", + "literal_values": [], + "name": "forwarded_allow_ips", + "required": false, + "type": "", + "validation": {} + }, { "category": "MISC FEATURES", "default": false, diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index 4b79e1eeb0c..236280b3866 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -11,6 +11,7 @@ from fastapi_events.handlers.local import local_handler from fastapi_events.middleware import EventHandlerASGIMiddleware from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.types import ASGIApp, Receive, Scope, Send import invokeai.frontend.web as web_dir from invokeai.app.api.dependencies import ApiDependencies @@ -143,6 +144,41 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint): return response +class SubPathASGIMiddleware: + """Make the app work behind a reverse proxy that serves it under a sub-path (e.g. `/invoke`). + + Handles both common proxy styles: + - The proxy STRIPS the sub-path: the incoming path is already `/api/...`. We only advertise the + public prefix via `root_path` so generated openapi/docs URLs include it. + - The proxy PRESERVES the sub-path: the incoming path is `/invoke/api/...`. We strip the prefix + so routing matches, and advertise it via `root_path`. + + Note: we deliberately do NOT use uvicorn's `root_path`, because uvicorn PREPENDS it to the request + path, which breaks the preserve-style proxy (the prefix would appear twice). Doing the path + rewrite here covers both styles. + """ + + def __init__(self, app: ASGIApp, base_path: str) -> None: + self.app = app + self.base_path = base_path + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] in ("http", "websocket"): + scope = dict(scope) + path: str = scope.get("path", "") + # Preserve-style proxy: strip the prefix from the path so routing matches. + if path == self.base_path or path.startswith(self.base_path + "/"): + scope["path"] = path[len(self.base_path) :] or "/" + raw_path = scope.get("raw_path") + if raw_path is not None: + base_bytes = self.base_path.encode("latin-1") + if raw_path.startswith(base_bytes): + scope["raw_path"] = raw_path[len(base_bytes) :] or b"/" + # Advertise the public prefix for both styles (openapi servers, /docs, redirects). + scope["root_path"] = self.base_path + await self.app(scope, receive, send) + + # Add the middleware app.add_middleware(RedirectRootWithQueryStringMiddleware) app.add_middleware(SlidingWindowTokenMiddleware) @@ -193,18 +229,22 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint): @app.get("/docs", include_in_schema=False) -def overridden_swagger() -> HTMLResponse: +def overridden_swagger(request: Request) -> HTMLResponse: + # Prefix with the proxy root_path (if any) so the schema resolves under a sub-path deployment. + root_path = request.scope.get("root_path", "") return get_swagger_ui_html( - openapi_url=app.openapi_url, # type: ignore [arg-type] # this is always a string + openapi_url=f"{root_path}{app.openapi_url}", # type: ignore [arg-type] # this is always a string title=f"{app.title} - Swagger UI", swagger_favicon_url="static/docs/invoke-favicon-docs.svg", ) @app.get("/redoc", include_in_schema=False) -def overridden_redoc() -> HTMLResponse: +def overridden_redoc(request: Request) -> HTMLResponse: + # Prefix with the proxy root_path (if any) so the schema resolves under a sub-path deployment. + root_path = request.scope.get("root_path", "") return get_redoc_html( - openapi_url=app.openapi_url, # type: ignore [arg-type] # this is always a string + openapi_url=f"{root_path}{app.openapi_url}", # type: ignore [arg-type] # this is always a string title=f"{app.title} - Redoc", redoc_favicon_url="static/docs/invoke-favicon-docs.svg", ) diff --git a/invokeai/app/run_app.py b/invokeai/app/run_app.py index febd4f4d4b1..51e0ff57ea4 100644 --- a/invokeai/app/run_app.py +++ b/invokeai/app/run_app.py @@ -87,15 +87,33 @@ def run_app() -> None: # imported. enable_dev_reload(custom_nodes_path=app_config.custom_nodes_path) + # When running behind a reverse proxy that serves the app under a sub-path (e.g. `/invoke`), + # wrap the app so route matching and openapi/docs work for both proxy styles (strip & preserve). + # Also honor X-Forwarded-* headers. Only enabled when `base_url` is set, so default installations + # are unaffected. + from typing import Any + + from invokeai.app.api_app import SubPathASGIMiddleware + + asgi_app: Any = app + proxy_kwargs = {} + if app_config.base_url: + asgi_app = SubPathASGIMiddleware(app, app_config.base_url) + proxy_kwargs = { + "proxy_headers": True, + "forwarded_allow_ips": app_config.forwarded_allow_ips, + } + # Start the server. config = uvicorn.Config( - app=app, + app=asgi_app, host=app_config.host, port=app_config.port, loop="asyncio", log_level=app_config.log_level_network, ssl_certfile=app_config.ssl_certfile, ssl_keyfile=app_config.ssl_keyfile, + **proxy_kwargs, ) server = uvicorn.Server(config) diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py index e6cc7c2798c..9c8b1c96a0d 100644 --- a/invokeai/app/services/config/config_default.py +++ b/invokeai/app/services/config/config_default.py @@ -134,6 +134,8 @@ class InvokeAIAppConfig(BaseSettings): external_openai_base_url: Base URL override for OpenAI image generation. external_seedream_api_key: API key for Seedream image generation. external_seedream_base_url: Base URL override for Seedream image generation. + base_url: Public base path when running behind a reverse proxy under a sub-path, e.g. `/invoke`. Set only when the proxy PRESERVES the sub-path (the backend receives `/invoke/api/...`). Leave unset when the proxy strips the sub-path or when serving at the domain root. + forwarded_allow_ips: Comma-separated list of IPs (or `*`) allowed to set X-Forwarded-* headers. Set to the reverse proxy's IP. Only used when `base_url` is set. """ _root: Optional[Path] = PrivateAttr(default=None) @@ -155,6 +157,8 @@ class InvokeAIAppConfig(BaseSettings): allow_headers: list[str] = Field(default=["*"], description="Headers allowed for CORS.") ssl_certfile: Optional[Path] = Field(default=None, description="SSL certificate file for HTTPS. See https://www.uvicorn.dev/settings/#https.") ssl_keyfile: Optional[Path] = Field(default=None, description="SSL key file for HTTPS. See https://www.uvicorn.dev/settings/#https.") + base_url: Optional[str] = Field(default=None, description="Public base path when running behind a reverse proxy under a sub-path, e.g. `/invoke`. Required when the proxy PRESERVES the sub-path (the backend receives `/invoke/api/...`); optional when the proxy strips it (set it anyway so openapi/docs URLs are correct). Leave unset when serving at the domain root. Normalized to a single leading slash with no trailing slash.") + forwarded_allow_ips: str = Field(default="127.0.0.1", description="Comma-separated list of IPs (or `*`) allowed to set X-Forwarded-* headers. Set to the reverse proxy's IP. Only used when `base_url` is set.") # MISC FEATURES log_tokenization: bool = Field(default=False, description="Enable logging of parsed prompt tokens.") @@ -257,6 +261,20 @@ class InvokeAIAppConfig(BaseSettings): model_config = SettingsConfigDict(env_prefix="INVOKEAI_", env_ignore_empty=True) + @field_validator("base_url") + @classmethod + def validate_base_url(cls, v: Optional[str]) -> Optional[str]: + """Normalize the reverse-proxy base path: ensure a single leading slash, no trailing slash. + + Empty values and a bare `/` normalize to `None` (feature disabled). + """ + if v is None: + return None + v = v.strip().strip("/") + if not v: + return None + return f"/{v}" + def update_config(self, config: dict[str, Any] | InvokeAIAppConfig, clobber: bool = True) -> None: """Updates the config, overwriting existing values. diff --git a/invokeai/frontend/web/openapi.json b/invokeai/frontend/web/openapi.json index a1c10ed8ed3..a8807c81637 100644 --- a/invokeai/frontend/web/openapi.json +++ b/invokeai/frontend/web/openapi.json @@ -40878,6 +40878,24 @@ "title": "Ssl Keyfile", "description": "SSL key file for HTTPS. See https://www.uvicorn.dev/settings/#https." }, + "base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Url", + "description": "Public base path when running behind a reverse proxy under a sub-path, e.g. `/invoke`. Required when the proxy PRESERVES the sub-path (the backend receives `/invoke/api/...`); optional when the proxy strips it (set it anyway so openapi/docs URLs are correct). Leave unset when serving at the domain root. Normalized to a single leading slash with no trailing slash." + }, + "forwarded_allow_ips": { + "type": "string", + "title": "Forwarded Allow Ips", + "description": "Comma-separated list of IPs (or `*`) allowed to set X-Forwarded-* headers. Set to the reverse proxy's IP. Only used when `base_url` is set.", + "default": "127.0.0.1" + }, "log_tokenization": { "type": "boolean", "title": "Log Tokenization", @@ -41411,7 +41429,7 @@ "additionalProperties": false, "type": "object", "title": "InvokeAIAppConfig", - "description": "Invoke's global app configuration.\n\nTypically, you won't need to interact with this class directly. Instead, use the `get_config` function from `invokeai.app.services.config` to get a singleton config object.\n\nAttributes:\n host: IP address to bind to. Use `0.0.0.0` to serve to your local network.\n port: Port to bind to.\n allow_origins: Allowed CORS origins.\n allow_credentials: Allow CORS credentials.\n allow_methods: Methods allowed for CORS.\n allow_headers: Headers allowed for CORS.\n ssl_certfile: SSL certificate file for HTTPS. See https://www.uvicorn.dev/settings/#https.\n ssl_keyfile: SSL key file for HTTPS. See https://www.uvicorn.dev/settings/#https.\n log_tokenization: Enable logging of parsed prompt tokens.\n patchmatch: Enable patchmatch inpaint code.\n models_dir: Path to the models directory.\n convert_cache_dir: Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions).\n download_cache_dir: Path to the directory that contains dynamically downloaded models.\n legacy_conf_dir: Path to directory of legacy checkpoint config files.\n db_dir: Path to InvokeAI databases directory.\n outputs_dir: Path to directory for outputs.\n image_subfolder_strategy: Strategy for organizing images into subfolders. 'flat' stores all images in a single folder. 'date' organizes by YYYY/MM/DD. 'type' organizes by image category. 'hash' uses first 2 characters of UUID for filesystem performance.
Valid values: `flat`, `date`, `type`, `hash`\n custom_nodes_dir: Path to directory for custom nodes.\n style_presets_dir: Path to directory for style presets.\n workflow_thumbnails_dir: Path to directory for workflow thumbnails.\n log_handlers: Log handler. Valid options are \"console\", \"file=\", \"syslog=path|address:host:port\", \"http=\".\n log_format: Log format. Use \"plain\" for text-only, \"color\" for colorized output, \"legacy\" for 2.3-style logging and \"syslog\" for syslog-style.
Valid values: `plain`, `color`, `syslog`, `legacy`\n log_level: Emit logging messages at this level or higher.
Valid values: `debug`, `info`, `warning`, `error`, `critical`\n log_sql: Log SQL queries. `log_level` must be `debug` for this to do anything. Extremely verbose.\n log_level_network: Log level for network-related messages. 'info' and 'debug' are very verbose.
Valid values: `debug`, `info`, `warning`, `error`, `critical`\n use_memory_db: Use in-memory database. Useful for development.\n dev_reload: Automatically reload when Python sources are changed. Does not reload node definitions.\n profile_graphs: Enable graph profiling using `cProfile`.\n profile_prefix: An optional prefix for profile output files.\n profiles_dir: Path to profiles output directory.\n max_cache_ram_gb: The maximum amount of CPU RAM to use for model caching in GB. If unset, the limit will be configured based on the available RAM. In most cases, it is recommended to leave this unset.\n max_cache_vram_gb: The amount of VRAM to use for model caching in GB. If unset, the limit will be configured based on the available VRAM and the device_working_mem_gb. In most cases, it is recommended to leave this unset.\n log_memory_usage: If True, a memory snapshot will be captured before and after every model cache operation, and the result will be logged (at debug level). There is a time cost to capturing the memory snapshots, so it is recommended to only enable this feature if you are actively inspecting the model cache's behaviour.\n model_cache_keep_alive_min: How long to keep models in cache after last use, in minutes. A value of 0 (the default) means models are kept in cache indefinitely. If no model generations occur within the timeout period, the model cache is cleared using the same logic as the 'Clear Model Cache' button.\n device_working_mem_gb: The amount of working memory to keep available on the compute device (in GB). Has no effect if running on CPU. If you are experiencing OOM errors, try increasing this value.\n enable_partial_loading: Enable partial loading of models. This enables models to run with reduced VRAM requirements (at the cost of slower speed) by streaming the model from RAM to VRAM as its used. In some edge cases, partial loading can cause models to run more slowly if they were previously being fully loaded into VRAM.\n keep_ram_copy_of_weights: Whether to keep a full RAM copy of a model's weights when the model is loaded in VRAM. Keeping a RAM copy increases average RAM usage, but speeds up model switching and LoRA patching (assuming there is sufficient RAM). Set this to False if RAM pressure is consistently high.\n ram: DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_ram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.\n vram: DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_vram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.\n lazy_offload: DEPRECATED: This setting is no longer used. Lazy-offloading is enabled by default. This config setting will be removed once the new model cache behavior is stable.\n pytorch_cuda_alloc_conf: Configure the Torch CUDA memory allocator. This will impact peak reserved VRAM usage and performance. Setting to \"backend:cudaMallocAsync\" works well on many systems. The optimal configuration is highly dependent on the system configuration (device type, VRAM, CUDA driver version, etc.), so must be tuned experimentally.\n device: Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities.
Valid values: `auto`, `cpu`, `cuda`, `mps`, `cuda:N` (where N is a device number)\n precision: Floating point precision. `float16` will consume half the memory of `float32` but produce slightly lower-quality images. The `auto` setting will guess the proper precision based on your video card and operating system.
Valid values: `auto`, `float16`, `bfloat16`, `float32`\n sequential_guidance: Whether to calculate guidance in serial instead of in parallel, lowering memory requirements.\n attention_type: Attention type.
Valid values: `auto`, `normal`, `xformers`, `sliced`, `torch-sdp`\n attention_slice_size: Slice size, valid when attention_type==\"sliced\".
Valid values: `auto`, `balanced`, `max`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`\n force_tiled_decode: Whether to enable tiled VAE decode (reduces memory consumption with some performance penalty).\n pil_compress_level: The compress_level setting of PIL.Image.save(), used for PNG encoding. All settings are lossless. 0 = no compression, 1 = fastest with slightly larger filesize, 9 = slowest with smallest filesize. 1 is typically the best setting.\n max_queue_size: Maximum number of items in the session queue.\n clear_queue_on_startup: Empties session queue on startup. If true, disables `max_queue_history`.\n max_queue_history: Keep the last N completed, failed, and canceled queue items. Older items are deleted on startup. Set to 0 to prune all terminal items. Ignored if `clear_queue_on_startup` is true.\n allow_nodes: List of nodes to allow. Omit to allow all.\n deny_nodes: List of nodes to deny. Omit to deny none.\n node_cache_size: How many cached nodes to keep in memory.\n hashing_algorithm: Model hashing algorthim for model installs. 'blake3_multi' is best for SSDs. 'blake3_single' is best for spinning disk HDDs. 'random' disables hashing, instead assigning a UUID to models. Useful when using a memory db to reduce model installation time, or if you don't care about storing stable hashes for models. Alternatively, any other hashlib algorithm is accepted, though these are not nearly as performant as blake3.
Valid values: `blake3_multi`, `blake3_single`, `random`, `md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `blake2b`, `blake2s`, `sha3_224`, `sha3_256`, `sha3_384`, `sha3_512`, `shake_128`, `shake_256`\n remote_api_tokens: List of regular expression and token pairs used when downloading models from URLs. The download URL is tested against the regex, and if it matches, the token is provided in as a Bearer token.\n scan_models_on_startup: Scan the models directory on startup, registering orphaned models. This is typically only used in conjunction with `use_memory_db` for testing purposes.\n unsafe_disable_picklescan: UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.\n allow_unknown_models: Allow installation of models that we are unable to identify. If enabled, models will be marked as `unknown` in the database, and will not have any metadata associated with them. If disabled, unknown models will be rejected during installation.\n multiuser: Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.\n strict_password_checking: Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.\n external_alibabacloud_api_key: API key for Alibaba Cloud DashScope image generation.\n external_alibabacloud_base_url: Base URL override for Alibaba Cloud DashScope image generation.\n external_gemini_api_key: API key for Gemini image generation.\n external_openai_api_key: API key for OpenAI image generation.\n external_gemini_base_url: Base URL override for Gemini image generation.\n external_openai_base_url: Base URL override for OpenAI image generation.\n external_seedream_api_key: API key for Seedream image generation.\n external_seedream_base_url: Base URL override for Seedream image generation." + "description": "Invoke's global app configuration.\n\nTypically, you won't need to interact with this class directly. Instead, use the `get_config` function from `invokeai.app.services.config` to get a singleton config object.\n\nAttributes:\n host: IP address to bind to. Use `0.0.0.0` to serve to your local network.\n port: Port to bind to.\n allow_origins: Allowed CORS origins.\n allow_credentials: Allow CORS credentials.\n allow_methods: Methods allowed for CORS.\n allow_headers: Headers allowed for CORS.\n ssl_certfile: SSL certificate file for HTTPS. See https://www.uvicorn.dev/settings/#https.\n ssl_keyfile: SSL key file for HTTPS. See https://www.uvicorn.dev/settings/#https.\n log_tokenization: Enable logging of parsed prompt tokens.\n patchmatch: Enable patchmatch inpaint code.\n models_dir: Path to the models directory.\n convert_cache_dir: Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions).\n download_cache_dir: Path to the directory that contains dynamically downloaded models.\n legacy_conf_dir: Path to directory of legacy checkpoint config files.\n db_dir: Path to InvokeAI databases directory.\n outputs_dir: Path to directory for outputs.\n image_subfolder_strategy: Strategy for organizing images into subfolders. 'flat' stores all images in a single folder. 'date' organizes by YYYY/MM/DD. 'type' organizes by image category. 'hash' uses first 2 characters of UUID for filesystem performance.
Valid values: `flat`, `date`, `type`, `hash`\n custom_nodes_dir: Path to directory for custom nodes.\n style_presets_dir: Path to directory for style presets.\n workflow_thumbnails_dir: Path to directory for workflow thumbnails.\n log_handlers: Log handler. Valid options are \"console\", \"file=\", \"syslog=path|address:host:port\", \"http=\".\n log_format: Log format. Use \"plain\" for text-only, \"color\" for colorized output, \"legacy\" for 2.3-style logging and \"syslog\" for syslog-style.
Valid values: `plain`, `color`, `syslog`, `legacy`\n log_level: Emit logging messages at this level or higher.
Valid values: `debug`, `info`, `warning`, `error`, `critical`\n log_sql: Log SQL queries. `log_level` must be `debug` for this to do anything. Extremely verbose.\n log_level_network: Log level for network-related messages. 'info' and 'debug' are very verbose.
Valid values: `debug`, `info`, `warning`, `error`, `critical`\n use_memory_db: Use in-memory database. Useful for development.\n dev_reload: Automatically reload when Python sources are changed. Does not reload node definitions.\n profile_graphs: Enable graph profiling using `cProfile`.\n profile_prefix: An optional prefix for profile output files.\n profiles_dir: Path to profiles output directory.\n max_cache_ram_gb: The maximum amount of CPU RAM to use for model caching in GB. If unset, the limit will be configured based on the available RAM. In most cases, it is recommended to leave this unset.\n max_cache_vram_gb: The amount of VRAM to use for model caching in GB. If unset, the limit will be configured based on the available VRAM and the device_working_mem_gb. In most cases, it is recommended to leave this unset.\n log_memory_usage: If True, a memory snapshot will be captured before and after every model cache operation, and the result will be logged (at debug level). There is a time cost to capturing the memory snapshots, so it is recommended to only enable this feature if you are actively inspecting the model cache's behaviour.\n model_cache_keep_alive_min: How long to keep models in cache after last use, in minutes. A value of 0 (the default) means models are kept in cache indefinitely. If no model generations occur within the timeout period, the model cache is cleared using the same logic as the 'Clear Model Cache' button.\n device_working_mem_gb: The amount of working memory to keep available on the compute device (in GB). Has no effect if running on CPU. If you are experiencing OOM errors, try increasing this value.\n enable_partial_loading: Enable partial loading of models. This enables models to run with reduced VRAM requirements (at the cost of slower speed) by streaming the model from RAM to VRAM as its used. In some edge cases, partial loading can cause models to run more slowly if they were previously being fully loaded into VRAM.\n keep_ram_copy_of_weights: Whether to keep a full RAM copy of a model's weights when the model is loaded in VRAM. Keeping a RAM copy increases average RAM usage, but speeds up model switching and LoRA patching (assuming there is sufficient RAM). Set this to False if RAM pressure is consistently high.\n ram: DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_ram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.\n vram: DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_vram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.\n lazy_offload: DEPRECATED: This setting is no longer used. Lazy-offloading is enabled by default. This config setting will be removed once the new model cache behavior is stable.\n pytorch_cuda_alloc_conf: Configure the Torch CUDA memory allocator. This will impact peak reserved VRAM usage and performance. Setting to \"backend:cudaMallocAsync\" works well on many systems. The optimal configuration is highly dependent on the system configuration (device type, VRAM, CUDA driver version, etc.), so must be tuned experimentally.\n device: Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities.
Valid values: `auto`, `cpu`, `cuda`, `mps`, `cuda:N` (where N is a device number)\n precision: Floating point precision. `float16` will consume half the memory of `float32` but produce slightly lower-quality images. The `auto` setting will guess the proper precision based on your video card and operating system.
Valid values: `auto`, `float16`, `bfloat16`, `float32`\n sequential_guidance: Whether to calculate guidance in serial instead of in parallel, lowering memory requirements.\n attention_type: Attention type.
Valid values: `auto`, `normal`, `xformers`, `sliced`, `torch-sdp`\n attention_slice_size: Slice size, valid when attention_type==\"sliced\".
Valid values: `auto`, `balanced`, `max`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`\n force_tiled_decode: Whether to enable tiled VAE decode (reduces memory consumption with some performance penalty).\n pil_compress_level: The compress_level setting of PIL.Image.save(), used for PNG encoding. All settings are lossless. 0 = no compression, 1 = fastest with slightly larger filesize, 9 = slowest with smallest filesize. 1 is typically the best setting.\n max_queue_size: Maximum number of items in the session queue.\n clear_queue_on_startup: Empties session queue on startup. If true, disables `max_queue_history`.\n max_queue_history: Keep the last N completed, failed, and canceled queue items. Older items are deleted on startup. Set to 0 to prune all terminal items. Ignored if `clear_queue_on_startup` is true.\n allow_nodes: List of nodes to allow. Omit to allow all.\n deny_nodes: List of nodes to deny. Omit to deny none.\n node_cache_size: How many cached nodes to keep in memory.\n hashing_algorithm: Model hashing algorthim for model installs. 'blake3_multi' is best for SSDs. 'blake3_single' is best for spinning disk HDDs. 'random' disables hashing, instead assigning a UUID to models. Useful when using a memory db to reduce model installation time, or if you don't care about storing stable hashes for models. Alternatively, any other hashlib algorithm is accepted, though these are not nearly as performant as blake3.
Valid values: `blake3_multi`, `blake3_single`, `random`, `md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `blake2b`, `blake2s`, `sha3_224`, `sha3_256`, `sha3_384`, `sha3_512`, `shake_128`, `shake_256`\n remote_api_tokens: List of regular expression and token pairs used when downloading models from URLs. The download URL is tested against the regex, and if it matches, the token is provided in as a Bearer token.\n scan_models_on_startup: Scan the models directory on startup, registering orphaned models. This is typically only used in conjunction with `use_memory_db` for testing purposes.\n unsafe_disable_picklescan: UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.\n allow_unknown_models: Allow installation of models that we are unable to identify. If enabled, models will be marked as `unknown` in the database, and will not have any metadata associated with them. If disabled, unknown models will be rejected during installation.\n multiuser: Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.\n strict_password_checking: Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.\n external_alibabacloud_api_key: API key for Alibaba Cloud DashScope image generation.\n external_alibabacloud_base_url: Base URL override for Alibaba Cloud DashScope image generation.\n external_gemini_api_key: API key for Gemini image generation.\n external_openai_api_key: API key for OpenAI image generation.\n external_gemini_base_url: Base URL override for Gemini image generation.\n external_openai_base_url: Base URL override for OpenAI image generation.\n external_seedream_api_key: API key for Seedream image generation.\n external_seedream_base_url: Base URL override for Seedream image generation.\n base_url: Public base path when running behind a reverse proxy under a sub-path, e.g. `/invoke`. Set only when the proxy PRESERVES the sub-path (the backend receives `/invoke/api/...`). Leave unset when the proxy strips the sub-path or when serving at the domain root.\n forwarded_allow_ips: Comma-separated list of IPs (or `*`) allowed to set X-Forwarded-* headers. Set to the reverse proxy's IP. Only used when `base_url` is set." }, "InvokeAIAppConfigWithSetFields": { "properties": { diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx index 685e1ee3a91..cde63370e84 100644 --- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx +++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx @@ -5,6 +5,7 @@ import { addStorageListeners } from 'app/store/enhancers/reduxRemember/driver'; import { $store } from 'app/store/nanostores/store'; import { createStore } from 'app/store/store'; import Loading from 'common/components/Loading/Loading'; +import { getBasePath } from 'common/util/baseUrl'; import React, { lazy, useEffect, useState } from 'react'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; @@ -48,7 +49,7 @@ const InvokeAIUI = () => { return ( - + }> diff --git a/invokeai/frontend/web/src/common/util/baseUrl.test.ts b/invokeai/frontend/web/src/common/util/baseUrl.test.ts new file mode 100644 index 00000000000..04906956e01 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/baseUrl.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; + +import { deriveDeploymentBaseUrl } from './baseUrl'; + +describe('deriveDeploymentBaseUrl', () => { + const fallback = 'http://fallback.example'; + + it('returns origin + sub-path prefix when served from a sub-path', () => { + expect(deriveDeploymentBaseUrl('https://example.com/invoke/assets/index-abc.js', fallback)).toBe( + 'https://example.com/invoke' + ); + }); + + it('returns just the origin when served from the domain root', () => { + expect(deriveDeploymentBaseUrl('https://example.com/assets/index-abc.js', fallback)).toBe('https://example.com'); + }); + + it('handles nested sub-paths', () => { + expect(deriveDeploymentBaseUrl('https://example.com/a/b/c/assets/index-abc.js', fallback)).toBe( + 'https://example.com/a/b/c' + ); + }); + + it('preserves a non-default port', () => { + expect(deriveDeploymentBaseUrl('http://localhost:8080/invoke/assets/index-abc.js', fallback)).toBe( + 'http://localhost:8080/invoke' + ); + }); + + it('returns origin (no prefix) in dev mode where there is no /assets/ segment', () => { + expect(deriveDeploymentBaseUrl('http://localhost:5173/src/main.tsx', fallback)).toBe('http://localhost:5173'); + }); + + it('falls back to the provided origin for an unparseable module url', () => { + expect(deriveDeploymentBaseUrl('not-a-url', fallback)).toBe(fallback); + }); +}); diff --git a/invokeai/frontend/web/src/common/util/baseUrl.ts b/invokeai/frontend/web/src/common/util/baseUrl.ts new file mode 100644 index 00000000000..58db4c24572 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/baseUrl.ts @@ -0,0 +1,49 @@ +/** + * Derives the deployment base URL (origin + optional sub-path prefix, WITHOUT a trailing slash) + * from the URL of the app's entry module and a fallback origin. + * + * Because Vite is configured with `base: './'`, the entry chunk is always emitted under + * `/assets/`, so the deployment root is everything before `/assets/`: + * + * https://example.com/invoke/assets/index-abc.js -> https://example.com/invoke + * https://example.com/assets/index-abc.js -> https://example.com + * + * In dev mode (Vite serves `/src/...`, no `/assets/` segment) and when served from the domain + * root, the prefix is empty, so the result is just the origin - identical to the previous + * behavior, leaving existing installations unaffected. + * + * Exported separately from {@link getDeploymentBaseUrl} so it can be unit-tested without relying + * on `import.meta.url`. + */ +export const deriveDeploymentBaseUrl = (moduleUrl: string, fallbackOrigin: string): string => { + try { + const url = new URL(moduleUrl); + const assetsIdx = url.pathname.indexOf('/assets/'); + const prefix = assetsIdx >= 0 ? url.pathname.slice(0, assetsIdx) : ''; + return `${url.origin}${prefix}`; + } catch { + return fallbackOrigin; + } +}; + +let _cachedBaseUrl: string | undefined; + +/** + * Returns the deployment base URL: origin + optional sub-path prefix, WITHOUT a trailing slash. + * See {@link deriveDeploymentBaseUrl} for how the prefix is determined. + */ +export const getDeploymentBaseUrl = (): string => { + if (_cachedBaseUrl === undefined) { + _cachedBaseUrl = deriveDeploymentBaseUrl(import.meta.url, window.location.origin); + } + return _cachedBaseUrl; +}; + +/** + * Returns just the normalized sub-path prefix: `''` at the domain root, or e.g. `'/invoke'` + * (leading slash, no trailing slash). Used for the React Router `basename` and the socket.io `path`. + */ +export const getBasePath = (): string => { + const { pathname } = new URL(getDeploymentBaseUrl()); + return pathname === '/' ? '' : pathname.replace(/\/$/, ''); +}; diff --git a/invokeai/frontend/web/src/i18n.ts b/invokeai/frontend/web/src/i18n.ts index adf53c0fd94..a6a867282ad 100644 --- a/invokeai/frontend/web/src/i18n.ts +++ b/invokeai/frontend/web/src/i18n.ts @@ -1,3 +1,4 @@ +import { getDeploymentBaseUrl } from 'common/util/baseUrl'; import i18n from 'i18next'; import Backend from 'i18next-http-backend'; import { initReactI18next } from 'react-i18next'; @@ -32,7 +33,7 @@ if (import.meta.env.MODE === 'package') { fallbackLng: 'en', debug: false, backend: { - loadPath: `${window.location.origin}/locales/{{lng}}.json`, + loadPath: `${getDeploymentBaseUrl()}/locales/{{lng}}.json`, }, interpolation: { escapeValue: false, diff --git a/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts b/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts index 653f458dde8..ed53508e0fe 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts @@ -8,7 +8,7 @@ import type { ExternalProviderStatus, } from 'services/api/types'; -import { api, buildV1Url } from '..'; +import { api, buildV1Url, getBaseUrl } from '..'; /** * Builds an endpoint URL for the app router @@ -138,7 +138,7 @@ export const appInfoApi = api.injectEndpoints({ invalidatesTags: ['InvocationCacheStatus'], }), getOpenAPISchema: build.query({ - query: () => `${window.location.origin}/openapi.json`, + query: () => `${getBaseUrl()}/openapi.json`, providesTags: ['Schema'], }), }), diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index a586273f3a7..e19c54e8469 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -7,6 +7,7 @@ import type { TagDescription, } from '@reduxjs/toolkit/query/react'; import { buildCreateApi, coreModule, fetchBaseQuery, reactHooksModule } from '@reduxjs/toolkit/query/react'; +import { getDeploymentBaseUrl } from 'common/util/baseUrl'; import { sessionExpiredLogout } from 'features/auth/store/authSlice'; import queryString from 'query-string'; import stableHash from 'stable-hash'; @@ -68,7 +69,7 @@ export const LIST_TAG = 'LIST'; export const LIST_ALL_TAG = 'LIST_ALL'; export const getBaseUrl = (): string => { - return window.location.origin; + return getDeploymentBaseUrl(); }; const dynamicBaseQuery: BaseQueryFn = async ( diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 822b3fd8ead..b775ace5328 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -16227,6 +16227,8 @@ export type components = { * external_openai_base_url: Base URL override for OpenAI image generation. * external_seedream_api_key: API key for Seedream image generation. * external_seedream_base_url: Base URL override for Seedream image generation. + * base_url: Public base path when running behind a reverse proxy under a sub-path, e.g. `/invoke`. Set only when the proxy PRESERVES the sub-path (the backend receives `/invoke/api/...`). Leave unset when the proxy strips the sub-path or when serving at the domain root. + * forwarded_allow_ips: Comma-separated list of IPs (or `*`) allowed to set X-Forwarded-* headers. Set to the reverse proxy's IP. Only used when `base_url` is set. */ InvokeAIAppConfig: { /** @@ -16290,6 +16292,17 @@ export type components = { * @description SSL key file for HTTPS. See https://www.uvicorn.dev/settings/#https. */ ssl_keyfile?: string | null; + /** + * Base Url + * @description Public base path when running behind a reverse proxy under a sub-path, e.g. `/invoke`. Required when the proxy PRESERVES the sub-path (the backend receives `/invoke/api/...`); optional when the proxy strips it (set it anyway so openapi/docs URLs are correct). Leave unset when serving at the domain root. Normalized to a single leading slash with no trailing slash. + */ + base_url?: string | null; + /** + * Forwarded Allow Ips + * @description Comma-separated list of IPs (or `*`) allowed to set X-Forwarded-* headers. Set to the reverse proxy's IP. Only used when `base_url` is set. + * @default 127.0.0.1 + */ + forwarded_allow_ips?: string; /** * Log Tokenization * @description Enable logging of parsed prompt tokens. diff --git a/invokeai/frontend/web/src/services/events/useSocketIO.ts b/invokeai/frontend/web/src/services/events/useSocketIO.ts index dcbe2501f3c..c8945e8df49 100644 --- a/invokeai/frontend/web/src/services/events/useSocketIO.ts +++ b/invokeai/frontend/web/src/services/events/useSocketIO.ts @@ -1,5 +1,6 @@ import { useAppStore } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { getBasePath, getDeploymentBaseUrl } from 'common/util/baseUrl'; import type { MapStore } from 'nanostores'; import { useEffect, useMemo } from 'react'; import { selectQueueStatus } from 'services/api/endpoints/queue'; @@ -25,15 +26,17 @@ export const useSocketIO = () => { const store = useAppStore(); const socketUrl = useMemo(() => { - const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; - return `${wsProtocol}://${window.location.host}`; + const base = new URL(getDeploymentBaseUrl()); + const wsProtocol = base.protocol === 'https:' ? 'wss' : 'ws'; + // Origin only - the sub-path prefix (if any) is passed via the socket.io `path` option below. + return `${wsProtocol}://${base.host}`; }, []); const socketOptions = useMemo(() => { const token = localStorage.getItem('auth_token'); const options: Partial = { timeout: 60000, - path: '/ws/socket.io', + path: `${getBasePath()}/ws/socket.io`, autoConnect: false, // achtung! removing this breaks the dynamic middleware forceNew: true, auth: token ? { token } : undefined,