Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function ManualRunWorkflowModal({
>(undefined);
const [workflowInputs, setWorkflowInputs] = useState<WorkflowInput[]>([]);
const [inputValues, setInputValues] = useState<Record<string, any>>({});
const [isRunning, setIsRunning] = useState(false);
const { workflows } = useWorkflowsV2({
...DEFAULT_WORKFLOWS_QUERY,
limit: 100, // Fetch more workflows at once for the dropdown
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -161,10 +166,12 @@ export function ManualRunWorkflowModal({
</div>
);
}
clearAndClose();
} catch (error) {
showErrorToast(error, "Failed to start workflow");
} finally {
setIsRunning(false);
}
clearAndClose();
};

const WorkflowSelect = (props: any) => {
Expand Down Expand Up @@ -293,13 +300,20 @@ export function ManualRunWorkflowModal({
) : null}

<div className="flex justify-end gap-2 mt-4">
<Button onClick={clearAndClose} color="orange" variant="secondary">
<Button
onClick={clearAndClose}
color="orange"
variant="secondary"
disabled={isRunning}
>
Cancel
</Button>
<Button
onClick={handleRun}
color="orange"
loading={isRunning}
disabled={
isRunning ||
!effectiveWorkflow ||
(workflowInputs.length > 0 &&
!areRequiredInputsFilled(workflowInputs, inputValues))
Expand Down
13 changes: 10 additions & 3 deletions keep/providers/grok_provider/grok_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
):
Expand All @@ -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:
Expand Down Expand Up @@ -116,7 +123,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": {
Expand Down
2 changes: 1 addition & 1 deletion keep/providers/openai_provider/openai_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
):
Expand Down
46 changes: 46 additions & 0 deletions tests/providers/grok_provider/test_grok_default_model.py
Original file line number Diff line number Diff line change
@@ -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"
46 changes: 46 additions & 0 deletions tests/providers/grok_provider/test_grok_response_parsing.py
Original file line number Diff line number Diff line change
@@ -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"
51 changes: 51 additions & 0 deletions tests/providers/openai_provider/test_openai_default_model.py
Original file line number Diff line number Diff line change
@@ -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"
Loading