From d773ddc22fef6efe328b23c092e836dc51acfc08 Mon Sep 17 00:00:00 2001 From: Chirag Gupta <103719146+chiruu12@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:53:43 +0530 Subject: [PATCH 1/4] fix(providers): default openai to a model that supports json_schema (#6713) --- .../openai_provider/openai_provider.py | 2 +- .../test_openai_default_model.py | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 tests/providers/openai_provider/test_openai_default_model.py diff --git a/keep/providers/openai_provider/openai_provider.py b/keep/providers/openai_provider/openai_provider.py index e7f58a2ade..be7460b15f 100644 --- a/keep/providers/openai_provider/openai_provider.py +++ b/keep/providers/openai_provider/openai_provider.py @@ -52,7 +52,7 @@ def validate_scopes(self) -> dict[str, bool | str]: def _query( self, prompt, - model="gpt-3.5-turbo", + model="gpt-4o-mini", max_tokens=1024, structured_output_format=None, ): diff --git a/tests/providers/openai_provider/test_openai_default_model.py b/tests/providers/openai_provider/test_openai_default_model.py new file mode 100644 index 0000000000..3d0f0ba4d6 --- /dev/null +++ b/tests/providers/openai_provider/test_openai_default_model.py @@ -0,0 +1,51 @@ +"""The OpenAI provider default model must support the json_schema response format. + +_query passes response_format=structured_output_format (a json_schema) to +chat.completions.create. json_schema is only supported on gpt-4o-mini and later, so +gpt-3.5-turbo could not serve a structured-output call that relied on the default. +""" + +from unittest.mock import MagicMock, patch + +from keep.contextmanager.contextmanager import ContextManager +from keep.providers.models.provider_config import ProviderConfig +from keep.providers.openai_provider.openai_provider import OpenaiProvider + +# gpt-3.5-turbo does not support the json_schema response format the provider sends. +UNSUPPORTED_MODEL = "gpt-3.5-turbo" + + +def _build_provider() -> OpenaiProvider: + config = ProviderConfig( + description="OpenAI Provider", + authentication={"api_key": "test-key"}, + ) + return OpenaiProvider(ContextManager(tenant_id="test"), "openai-test", config) + + +def _patched_client(): + """Patch the OpenAI client so the model used in the request can be captured.""" + client = MagicMock() + message = MagicMock() + message.content = "ok" + client.chat.completions.create.return_value.choices = [MagicMock(message=message)] + return patch( + "keep.providers.openai_provider.openai_provider.OpenAI", + return_value=client, + ), client + + +def test_default_model_is_not_the_unsupported_one(): + provider = _build_provider() + p, client = _patched_client() + with p: + provider._query(prompt="hi") + assert client.chat.completions.create.call_args.kwargs["model"] != UNSUPPORTED_MODEL + + +def test_default_model_supports_json_schema(): + provider = _build_provider() + p, client = _patched_client() + with p: + provider._query(prompt="hi") + assert client.chat.completions.create.call_args.kwargs["model"] == "gpt-4o-mini" From f567a9367a114aab9660bea8f7e8fd6b77606d07 Mon Sep 17 00:00:00 2001 From: Chirag Gupta <103719146+chiruu12@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:54:28 +0530 Subject: [PATCH 2/4] fix(providers): default grok to a served model (#6711) --- keep/providers/grok_provider/grok_provider.py | 4 +- .../grok_provider/test_grok_default_model.py | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 tests/providers/grok_provider/test_grok_default_model.py diff --git a/keep/providers/grok_provider/grok_provider.py b/keep/providers/grok_provider/grok_provider.py index 2cd1760a30..d05fbcf095 100644 --- a/keep/providers/grok_provider/grok_provider.py +++ b/keep/providers/grok_provider/grok_provider.py @@ -45,7 +45,7 @@ def validate_scopes(self) -> dict[str, bool | str]: def _query( self, prompt, - model="grok-1", + model="grok-4.5", max_tokens=1024, structured_output_format=None, ): @@ -116,7 +116,7 @@ def _query( print( provider.query( prompt="Here is an alert, define environment for it: Clients are panicking, nothing works.", - model="grok-1", + model="grok-4.5", structured_output_format={ "type": "json_schema", "json_schema": { diff --git a/tests/providers/grok_provider/test_grok_default_model.py b/tests/providers/grok_provider/test_grok_default_model.py new file mode 100644 index 0000000000..f80ce26575 --- /dev/null +++ b/tests/providers/grok_provider/test_grok_default_model.py @@ -0,0 +1,46 @@ +"""The Grok provider default model must be one the xAI API still serves. + +grok-1 is xAI's open-weights release, not a model the api.x.ai chat completions +endpoint serves, so a call that relied on the default failed model-not-found. +""" + +from unittest.mock import MagicMock, patch + +from keep.contextmanager.contextmanager import ContextManager +from keep.providers.grok_provider.grok_provider import GrokProvider +from keep.providers.models.provider_config import ProviderConfig + +# grok-1 is the open-weights release; the API never served it as a chat model. +RETIRED_MODEL = "grok-1" + + +def _build_provider() -> GrokProvider: + config = ProviderConfig( + description="Grok Provider", + authentication={"api_key": "test-key"}, + ) + return GrokProvider(ContextManager(tenant_id="test"), "grok-test", config) + + +def _capture_post(): + """Patch requests.post and return the mock so the sent payload can be read.""" + response = MagicMock() + response.raise_for_status = MagicMock() + response.json = MagicMock( + return_value={"choices": [{"message": {"content": "ok"}}]} + ) + return patch("requests.post", return_value=response) + + +def test_default_model_is_not_the_retired_one(): + provider = _build_provider() + with _capture_post() as post: + provider._query(prompt="hi") + assert post.call_args.kwargs["json"]["model"] != RETIRED_MODEL + + +def test_default_model_is_a_served_model(): + provider = _build_provider() + with _capture_post() as post: + provider._query(prompt="hi") + assert post.call_args.kwargs["json"]["model"] == "grok-4.5" From f35afddf1cc96884746a26a94b0a2db59a63312a Mon Sep 17 00:00:00 2001 From: Chirag Gupta <103719146+chiruu12@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:55:16 +0530 Subject: [PATCH 3/4] fix(providers): don't crash on empty choices in grok (#6710) --- keep/providers/grok_provider/grok_provider.py | 9 +++- .../test_grok_response_parsing.py | 46 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 tests/providers/grok_provider/test_grok_response_parsing.py diff --git a/keep/providers/grok_provider/grok_provider.py b/keep/providers/grok_provider/grok_provider.py index d05fbcf095..2801604038 100644 --- a/keep/providers/grok_provider/grok_provider.py +++ b/keep/providers/grok_provider/grok_provider.py @@ -71,7 +71,14 @@ def _query( json=payload ) response.raise_for_status() - content = response.json()["choices"][0]["message"]["content"] + result = response.json() + # An OpenAI-compatible backend can return HTTP 200 with an empty + # choices list (e.g. a content-filter block), so raise_for_status + # passes but choices[0] would raise. Degrade to an empty string. + try: + content = result["choices"][0]["message"]["content"] + except (KeyError, IndexError): + content = "" # Try to parse as JSON if structured output was requested if structured_output_format: diff --git a/tests/providers/grok_provider/test_grok_response_parsing.py b/tests/providers/grok_provider/test_grok_response_parsing.py new file mode 100644 index 0000000000..61aedc4e8d --- /dev/null +++ b/tests/providers/grok_provider/test_grok_response_parsing.py @@ -0,0 +1,46 @@ +"""Tests for how the Grok provider parses a chat completion response. + +The xAI API is OpenAI-compatible, and OpenAI-compatible backends return HTTP 200 +with an empty ``choices`` list on a content-filter block. ``raise_for_status`` +passes, so the provider still has to read ``choices[0]`` and must not crash. +""" + +from unittest.mock import MagicMock, patch + +from keep.contextmanager.contextmanager import ContextManager +from keep.providers.grok_provider.grok_provider import GrokProvider +from keep.providers.models.provider_config import ProviderConfig + + +def _build_provider() -> GrokProvider: + config = ProviderConfig( + description="Grok Provider", + authentication={"api_key": "test-key"}, + ) + return GrokProvider(ContextManager(tenant_id="test"), "grok-test", config) + + +def _response(payload) -> MagicMock: + response = MagicMock() + response.raise_for_status = MagicMock() + response.json = MagicMock(return_value=payload) + return response + + +def test_empty_choices_does_not_crash(): + provider = _build_provider() + with patch("requests.post", return_value=_response({"choices": []})): + assert provider._query(prompt="hi")["response"] == "" + + +def test_missing_keys_do_not_crash(): + provider = _build_provider() + with patch("requests.post", return_value=_response({})): + assert provider._query(prompt="hi")["response"] == "" + + +def test_normal_response_is_returned(): + provider = _build_provider() + payload = {"choices": [{"message": {"content": "hello"}}]} + with patch("requests.post", return_value=_response(payload)): + assert provider._query(prompt="hi")["response"] == "hello" From 4003992272eeddd649e23e21d63f09892a149291 Mon Sep 17 00:00:00 2001 From: kate1128 <130445305+kate1128@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:26:01 +0800 Subject: [PATCH 4/4] fix: prevent duplicate workflow runs on rapid manual run clicks (#6691) Co-authored-by: Claude Opus 4.7 --- .../ui/manual-run-workflow-modal.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/keep-ui/features/workflows/manual-run-workflow/ui/manual-run-workflow-modal.tsx b/keep-ui/features/workflows/manual-run-workflow/ui/manual-run-workflow-modal.tsx index 60e3f232d0..d8ddbe0e01 100644 --- a/keep-ui/features/workflows/manual-run-workflow/ui/manual-run-workflow-modal.tsx +++ b/keep-ui/features/workflows/manual-run-workflow/ui/manual-run-workflow-modal.tsx @@ -48,6 +48,7 @@ export function ManualRunWorkflowModal({ >(undefined); const [workflowInputs, setWorkflowInputs] = useState([]); const [inputValues, setInputValues] = useState>({}); + const [isRunning, setIsRunning] = useState(false); const { workflows } = useWorkflowsV2({ ...DEFAULT_WORKFLOWS_QUERY, limit: 100, // Fetch more workflows at once for the dropdown @@ -128,6 +129,10 @@ export function ManualRunWorkflowModal({ }; const handleRun = async () => { + if (isRunning) { + return; + } + setIsRunning(true); try { if (onSubmit) { // If onSubmit prop is provided, use it (for WorkflowDetailHeader usage) @@ -161,10 +166,12 @@ export function ManualRunWorkflowModal({ ); } + clearAndClose(); } catch (error) { showErrorToast(error, "Failed to start workflow"); + } finally { + setIsRunning(false); } - clearAndClose(); }; const WorkflowSelect = (props: any) => { @@ -293,13 +300,20 @@ export function ManualRunWorkflowModal({ ) : null}
-