From cc96b933fc2128d62f9e5b3b8100f882c4fc2556 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2026 02:52:16 +0000 Subject: [PATCH 1/3] feat(grok): add support for official xAI API keys in Grok nodes This adds an optional `xai_api_key` input to all Grok image and video nodes. When provided, requests bypass the Comfy proxy and directly call the official xAI endpoints, allowing users to use their own keys and bypass Comfy org limits. Co-authored-by: austin1997 <18709560+austin1997@users.noreply.github.com> --- comfy_api_nodes/nodes_grok.py | 131 +++++++++++++++++++++++++++++++--- 1 file changed, 121 insertions(+), 10 deletions(-) diff --git a/comfy_api_nodes/nodes_grok.py b/comfy_api_nodes/nodes_grok.py index dabc899d6..3f0d82977 100644 --- a/comfy_api_nodes/nodes_grok.py +++ b/comfy_api_nodes/nodes_grok.py @@ -100,6 +100,13 @@ class GrokImageNode(IO.ComfyNode): "actual results are nondeterministic regardless of seed.", ), IO.Combo.Input("resolution", options=["1K", "2K"], optional=True), + IO.String.Input( + "xai_api_key", + default="", + tooltip="Your xAI API Key (optional). If provided, it will bypass Comfy org limits.", + optional=True, + advanced=True, + ), ], outputs=[ IO.Image.Output(), @@ -130,11 +137,19 @@ class GrokImageNode(IO.ComfyNode): number_of_images: int, seed: int, resolution: str = "1K", + xai_api_key: str = "", ) -> IO.NodeOutput: validate_string(prompt, strip_whitespace=True, min_length=1) + + path = "/proxy/xai/v1/images/generations" + headers = None + if xai_api_key: + path = "https://api.x.ai/v1/images/generations" + headers = {"Authorization": f"Bearer {xai_api_key}"} + response = await sync_op( cls, - ApiEndpoint(path="/proxy/xai/v1/images/generations", method="POST"), + ApiEndpoint(path=path, method="POST", headers=headers), data=ImageGenerationRequest( model=model, prompt=prompt, @@ -217,6 +232,13 @@ class GrokImageEditNode(IO.ComfyNode): optional=True, tooltip="Only allowed when multiple images are connected to the image input.", ), + IO.String.Input( + "xai_api_key", + default="", + tooltip="Your xAI API Key (optional). If provided, it will bypass Comfy org limits.", + optional=True, + advanced=True, + ), ], outputs=[ IO.Image.Output(), @@ -248,6 +270,7 @@ class GrokImageEditNode(IO.ComfyNode): number_of_images: int, seed: int, aspect_ratio: str = "auto", + xai_api_key: str = "", ) -> IO.NodeOutput: validate_string(prompt, strip_whitespace=True, min_length=1) if model == "grok-imagine-image-pro": @@ -259,9 +282,16 @@ class GrokImageEditNode(IO.ComfyNode): raise ValueError( "Custom aspect ratio is only allowed when multiple images are connected to the image input." ) + + path = "/proxy/xai/v1/images/edits" + headers = None + if xai_api_key: + path = "https://api.x.ai/v1/images/edits" + headers = {"Authorization": f"Bearer {xai_api_key}"} + response = await sync_op( cls, - ApiEndpoint(path="/proxy/xai/v1/images/edits", method="POST"), + ApiEndpoint(path=path, method="POST", headers=headers), data=ImageEditRequest( model=model, images=[InputUrlObject(url=f"data:image/png;base64,{tensor_to_base64_string(i)}") for i in image], @@ -330,6 +360,13 @@ class GrokVideoNode(IO.ComfyNode): "actual results are nondeterministic regardless of seed.", ), IO.Image.Input("image", optional=True), + IO.String.Input( + "xai_api_key", + default="", + tooltip="Your xAI API Key (optional). If provided, it will bypass Comfy org limits.", + optional=True, + advanced=True, + ), ], outputs=[ IO.Video.Output(), @@ -362,6 +399,7 @@ class GrokVideoNode(IO.ComfyNode): duration: int, seed: int, image: Input.Image | None = None, + xai_api_key: str = "", ) -> IO.NodeOutput: if model == "grok-imagine-video-beta": model = "grok-imagine-video" @@ -371,9 +409,16 @@ class GrokVideoNode(IO.ComfyNode): raise ValueError("Only one input image is supported.") image_url = InputUrlObject(url=f"data:image/png;base64,{tensor_to_base64_string(image)}") validate_string(prompt, strip_whitespace=True, min_length=1) + + path = "/proxy/xai/v1/videos/generations" + headers = None + if xai_api_key: + path = "https://api.x.ai/v1/videos/generations" + headers = {"Authorization": f"Bearer {xai_api_key}"} + initial_response = await sync_op( cls, - ApiEndpoint(path="/proxy/xai/v1/videos/generations", method="POST"), + ApiEndpoint(path=path, method="POST", headers=headers), data=VideoGenerationRequest( model=model, image=image_url, @@ -385,9 +430,13 @@ class GrokVideoNode(IO.ComfyNode): ), response_model=VideoGenerationResponse, ) + poll_path = f"/proxy/xai/v1/videos/{initial_response.request_id}" + if xai_api_key: + poll_path = f"https://api.x.ai/v1/videos/{initial_response.request_id}" + response = await poll_op( cls, - ApiEndpoint(path=f"/proxy/xai/v1/videos/{initial_response.request_id}"), + ApiEndpoint(path=poll_path, headers=headers), status_extractor=lambda r: r.status if r.status is not None else "complete", response_model=VideoStatusResponse, price_extractor=_extract_grok_price, @@ -423,6 +472,13 @@ class GrokVideoEditNode(IO.ComfyNode): tooltip="Seed to determine if node should re-run; " "actual results are nondeterministic regardless of seed.", ), + IO.String.Input( + "xai_api_key", + default="", + tooltip="Your xAI API Key (optional). If provided, it will bypass Comfy org limits.", + optional=True, + advanced=True, + ), ], outputs=[ IO.Video.Output(), @@ -445,6 +501,7 @@ class GrokVideoEditNode(IO.ComfyNode): prompt: str, video: Input.Video, seed: int, + xai_api_key: str = "", ) -> IO.NodeOutput: validate_string(prompt, strip_whitespace=True, min_length=1) validate_video_duration(video, min_duration=1, max_duration=8.7) @@ -452,9 +509,16 @@ class GrokVideoEditNode(IO.ComfyNode): video_size = get_fs_object_size(video_stream) if video_size > 50 * 1024 * 1024: raise ValueError(f"Video size ({video_size / 1024 / 1024:.1f}MB) exceeds 50MB limit.") + + path = "/proxy/xai/v1/videos/edits" + headers = None + if xai_api_key: + path = "https://api.x.ai/v1/videos/edits" + headers = {"Authorization": f"Bearer {xai_api_key}"} + initial_response = await sync_op( cls, - ApiEndpoint(path="/proxy/xai/v1/videos/edits", method="POST"), + ApiEndpoint(path=path, method="POST", headers=headers), data=VideoEditRequest( model=model, video=InputUrlObject(url=await upload_video_to_comfyapi(cls, video)), @@ -463,9 +527,13 @@ class GrokVideoEditNode(IO.ComfyNode): ), response_model=VideoGenerationResponse, ) + poll_path = f"/proxy/xai/v1/videos/{initial_response.request_id}" + if xai_api_key: + poll_path = f"https://api.x.ai/v1/videos/{initial_response.request_id}" + response = await poll_op( cls, - ApiEndpoint(path=f"/proxy/xai/v1/videos/{initial_response.request_id}"), + ApiEndpoint(path=poll_path, headers=headers), status_extractor=lambda r: r.status if r.status is not None else "complete", response_model=VideoStatusResponse, price_extractor=_extract_grok_price, @@ -539,6 +607,13 @@ class GrokVideoReferenceNode(IO.ComfyNode): tooltip="Seed to determine if node should re-run; " "actual results are nondeterministic regardless of seed.", ), + IO.String.Input( + "xai_api_key", + default="", + tooltip="Your xAI API Key (optional). If provided, it will bypass Comfy org limits.", + optional=True, + advanced=True, + ), ], outputs=[ IO.Video.Output(), @@ -573,8 +648,14 @@ class GrokVideoReferenceNode(IO.ComfyNode): prompt: str, model: dict, seed: int, + xai_api_key: str = "", ) -> IO.NodeOutput: validate_string(prompt, strip_whitespace=True, min_length=1) + + # We must use proxy to upload images temporarily even if they provide their own key for video generation + # because the API requires URLs and we use our proxy for image hosting during the request. + # Wait, if they are providing their own key to our backend for generation, + # `upload_images_to_comfyapi` relies on `comfyapi`. This is fine. ref_image_urls = await upload_images_to_comfyapi( cls, list(model["reference_images"].values()), @@ -582,9 +663,16 @@ class GrokVideoReferenceNode(IO.ComfyNode): wait_label="Uploading base images", max_images=7, ) + + path = "/proxy/xai/v1/videos/generations" + headers = None + if xai_api_key: + path = "https://api.x.ai/v1/videos/generations" + headers = {"Authorization": f"Bearer {xai_api_key}"} + initial_response = await sync_op( cls, - ApiEndpoint(path="/proxy/xai/v1/videos/generations", method="POST"), + ApiEndpoint(path=path, method="POST", headers=headers), data=VideoGenerationRequest( model=model["model"], reference_images=[InputUrlObject(url=i) for i in ref_image_urls], @@ -596,9 +684,13 @@ class GrokVideoReferenceNode(IO.ComfyNode): ), response_model=VideoGenerationResponse, ) + poll_path = f"/proxy/xai/v1/videos/{initial_response.request_id}" + if xai_api_key: + poll_path = f"https://api.x.ai/v1/videos/{initial_response.request_id}" + response = await poll_op( cls, - ApiEndpoint(path=f"/proxy/xai/v1/videos/{initial_response.request_id}"), + ApiEndpoint(path=poll_path, headers=headers), status_extractor=lambda r: r.status if r.status is not None else "complete", response_model=VideoStatusResponse, price_extractor=_extract_grok_video_price, @@ -653,6 +745,13 @@ class GrokVideoExtendNode(IO.ComfyNode): tooltip="Seed to determine if node should re-run; " "actual results are nondeterministic regardless of seed.", ), + IO.String.Input( + "xai_api_key", + default="", + tooltip="Your xAI API Key (optional). If provided, it will bypass Comfy org limits.", + optional=True, + advanced=True, + ), ], outputs=[ IO.Video.Output(), @@ -685,15 +784,23 @@ class GrokVideoExtendNode(IO.ComfyNode): video: Input.Video, model: dict, seed: int, + xai_api_key: str = "", ) -> IO.NodeOutput: validate_string(prompt, strip_whitespace=True, min_length=1) validate_video_duration(video, min_duration=2, max_duration=15) video_size = get_fs_object_size(video.get_stream_source()) if video_size > 50 * 1024 * 1024: raise ValueError(f"Video size ({video_size / 1024 / 1024:.1f}MB) exceeds 50MB limit.") + + path = "/proxy/xai/v1/videos/extensions" + headers = None + if xai_api_key: + path = "https://api.x.ai/v1/videos/extensions" + headers = {"Authorization": f"Bearer {xai_api_key}"} + initial_response = await sync_op( cls, - ApiEndpoint(path="/proxy/xai/v1/videos/extensions", method="POST"), + ApiEndpoint(path=path, method="POST", headers=headers), data=VideoExtensionRequest( prompt=prompt, video=InputUrlObject(url=await upload_video_to_comfyapi(cls, video)), @@ -701,9 +808,13 @@ class GrokVideoExtendNode(IO.ComfyNode): ), response_model=VideoGenerationResponse, ) + poll_path = f"/proxy/xai/v1/videos/{initial_response.request_id}" + if xai_api_key: + poll_path = f"https://api.x.ai/v1/videos/{initial_response.request_id}" + response = await poll_op( cls, - ApiEndpoint(path=f"/proxy/xai/v1/videos/{initial_response.request_id}"), + ApiEndpoint(path=poll_path, headers=headers), status_extractor=lambda r: r.status if r.status is not None else "complete", response_model=VideoStatusResponse, price_extractor=_extract_grok_video_price, From f14b55f0d773e910512f395fa8645a78e58886d5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2026 09:01:28 +0000 Subject: [PATCH 2/3] fix(grok): sanitize user-provided xAI API key - Strips leading/trailing whitespace - Removes accidental `Bearer ` prefixes from pasted keys This prevents xAI API rejection errors such as "Incorrect API key provided" when the key format is slightly off. Co-authored-by: austin1997 <18709560+austin1997@users.noreply.github.com> --- comfy_api_nodes/nodes_grok.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/comfy_api_nodes/nodes_grok.py b/comfy_api_nodes/nodes_grok.py index 3f0d82977..fdf19fecd 100644 --- a/comfy_api_nodes/nodes_grok.py +++ b/comfy_api_nodes/nodes_grok.py @@ -141,6 +141,10 @@ class GrokImageNode(IO.ComfyNode): ) -> IO.NodeOutput: validate_string(prompt, strip_whitespace=True, min_length=1) + xai_api_key = xai_api_key.strip() + if xai_api_key.lower().startswith("bearer "): + xai_api_key = xai_api_key[7:].strip() + path = "/proxy/xai/v1/images/generations" headers = None if xai_api_key: @@ -283,6 +287,10 @@ class GrokImageEditNode(IO.ComfyNode): "Custom aspect ratio is only allowed when multiple images are connected to the image input." ) + xai_api_key = xai_api_key.strip() + if xai_api_key.lower().startswith("bearer "): + xai_api_key = xai_api_key[7:].strip() + path = "/proxy/xai/v1/images/edits" headers = None if xai_api_key: @@ -410,6 +418,10 @@ class GrokVideoNode(IO.ComfyNode): image_url = InputUrlObject(url=f"data:image/png;base64,{tensor_to_base64_string(image)}") validate_string(prompt, strip_whitespace=True, min_length=1) + xai_api_key = xai_api_key.strip() + if xai_api_key.lower().startswith("bearer "): + xai_api_key = xai_api_key[7:].strip() + path = "/proxy/xai/v1/videos/generations" headers = None if xai_api_key: @@ -510,6 +522,10 @@ class GrokVideoEditNode(IO.ComfyNode): if video_size > 50 * 1024 * 1024: raise ValueError(f"Video size ({video_size / 1024 / 1024:.1f}MB) exceeds 50MB limit.") + xai_api_key = xai_api_key.strip() + if xai_api_key.lower().startswith("bearer "): + xai_api_key = xai_api_key[7:].strip() + path = "/proxy/xai/v1/videos/edits" headers = None if xai_api_key: @@ -652,6 +668,10 @@ class GrokVideoReferenceNode(IO.ComfyNode): ) -> IO.NodeOutput: validate_string(prompt, strip_whitespace=True, min_length=1) + xai_api_key = xai_api_key.strip() + if xai_api_key.lower().startswith("bearer "): + xai_api_key = xai_api_key[7:].strip() + # We must use proxy to upload images temporarily even if they provide their own key for video generation # because the API requires URLs and we use our proxy for image hosting during the request. # Wait, if they are providing their own key to our backend for generation, @@ -792,6 +812,10 @@ class GrokVideoExtendNode(IO.ComfyNode): if video_size > 50 * 1024 * 1024: raise ValueError(f"Video size ({video_size / 1024 / 1024:.1f}MB) exceeds 50MB limit.") + xai_api_key = xai_api_key.strip() + if xai_api_key.lower().startswith("bearer "): + xai_api_key = xai_api_key[7:].strip() + path = "/proxy/xai/v1/videos/extensions" headers = None if xai_api_key: From d5460029906d06112a79ce78092efcaf8bca743c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2026 09:15:48 +0000 Subject: [PATCH 3/3] feat(grok): sanitize user-provided xAI API key - Strips leading/trailing whitespace - Removes accidental `Bearer ` prefixes from pasted keys This prevents xAI API rejection errors such as "Incorrect API key provided" when the key format is slightly off. Co-authored-by: austin1997 <18709560+austin1997@users.noreply.github.com>