feat: expand module workflows, dynamic config, notifications and console API
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from gyxx_flow.modules.product_commerce.direct_llm import (
|
||||
DirectLLMConfig,
|
||||
DirectLLMConfigurationError,
|
||||
DirectLLMResponseError,
|
||||
DirectLLMTransientError,
|
||||
chat_completion,
|
||||
load_direct_llm_config,
|
||||
)
|
||||
|
||||
|
||||
def _config() -> DirectLLMConfig:
|
||||
return DirectLLMConfig(
|
||||
endpoint="https://api.minimaxi.com/v1/chat/completions",
|
||||
credential="test-only-secret",
|
||||
title_model="MiniMax-M3",
|
||||
vision_model="MiniMax-M3",
|
||||
)
|
||||
|
||||
|
||||
def _response(content: str) -> bytes:
|
||||
return json.dumps(
|
||||
{
|
||||
"choices": [{"message": {"content": content}}],
|
||||
"usage": {"prompt_tokens": 3, "completion_tokens": 4},
|
||||
}
|
||||
).encode()
|
||||
|
||||
|
||||
def test_direct_chat_uses_provider_endpoint_and_model_without_echoing_secret() -> None:
|
||||
observed: dict[str, object] = {}
|
||||
|
||||
def transport(url, headers, body, timeout):
|
||||
payload = json.loads(body)
|
||||
observed.update(
|
||||
url=url,
|
||||
model=payload["model"],
|
||||
messages=payload["messages"],
|
||||
reasoning_split=payload["reasoning_split"],
|
||||
timeout=timeout,
|
||||
authorization=headers["Authorization"],
|
||||
)
|
||||
return _response('{"ok":true}')
|
||||
|
||||
content, usage = chat_completion(
|
||||
[{"role": "user", "content": "hello"}],
|
||||
model="MiniMax-M3",
|
||||
timeout=17,
|
||||
transport=transport,
|
||||
config=_config(),
|
||||
)
|
||||
|
||||
assert content == '{"ok":true}'
|
||||
assert usage == {"prompt_tokens": 3, "completion_tokens": 4}
|
||||
assert observed["url"] == "https://api.minimaxi.com/v1/chat/completions"
|
||||
assert observed["model"] == "MiniMax-M3"
|
||||
assert observed["reasoning_split"] is True
|
||||
assert observed["timeout"] == 17.0
|
||||
assert observed["authorization"] == "Bearer test-only-secret"
|
||||
|
||||
|
||||
def test_direct_chat_can_disable_minimax_thinking_for_structured_video_tasks() -> None:
|
||||
observed: dict[str, object] = {}
|
||||
|
||||
def transport(_url, _headers, body, _timeout):
|
||||
observed.update(json.loads(body))
|
||||
return _response("ok")
|
||||
|
||||
chat_completion(
|
||||
[{"role": "user", "content": "return one token"}],
|
||||
thinking_mode="disabled",
|
||||
transport=transport,
|
||||
config=_config(),
|
||||
)
|
||||
|
||||
assert observed["thinking"] == {"type": "disabled"}
|
||||
|
||||
|
||||
def test_direct_chat_rejects_unknown_thinking_mode() -> None:
|
||||
with pytest.raises(DirectLLMConfigurationError, match="thinking mode"):
|
||||
chat_completion(
|
||||
[{"role": "user", "content": "hello"}],
|
||||
thinking_mode="sometimes",
|
||||
transport=lambda *_args: _response("unused"),
|
||||
config=_config(),
|
||||
)
|
||||
|
||||
|
||||
def test_config_reads_shared_minimax_environment(monkeypatch) -> None:
|
||||
monkeypatch.delenv("GYXX_DIRECT_LLM_BASE_URL", raising=False)
|
||||
monkeypatch.delenv("GYXX_DIRECT_LLM_API_KEY", raising=False)
|
||||
monkeypatch.delenv("GYXX_DIRECT_LLM_MODEL", raising=False)
|
||||
monkeypatch.setenv("STYLE_ANALYSIS_LLM_BASE_URL", "https://api.minimaxi.com/v1")
|
||||
monkeypatch.setenv("STYLE_ANALYSIS_LLM_API_KEY", "test-only-secret")
|
||||
monkeypatch.setenv("STYLE_ANALYSIS_LLM_MODEL", "MiniMax-M3")
|
||||
|
||||
config = load_direct_llm_config()
|
||||
|
||||
assert config.endpoint == "https://api.minimaxi.com/v1/chat/completions"
|
||||
assert config.title_model == "MiniMax-M3"
|
||||
assert config.vision_model == "MiniMax-M3"
|
||||
assert "test-only-secret" not in repr(config)
|
||||
|
||||
|
||||
def test_config_rejects_legacy_local_gateway(monkeypatch) -> None:
|
||||
monkeypatch.setenv(
|
||||
"GYXX_DIRECT_LLM_BASE_URL",
|
||||
"http://127.0.0.1:8642/v1",
|
||||
)
|
||||
monkeypatch.setenv("GYXX_DIRECT_LLM_API_KEY", "test-only-secret")
|
||||
|
||||
with pytest.raises(DirectLLMConfigurationError, match="local gateway"):
|
||||
load_direct_llm_config()
|
||||
|
||||
|
||||
def test_invalid_response_is_fail_closed() -> None:
|
||||
with pytest.raises(DirectLLMResponseError, match="invalid JSON"):
|
||||
chat_completion(
|
||||
[{"role": "user", "content": "hello"}],
|
||||
transport=lambda *_args: b"not-json",
|
||||
config=_config(),
|
||||
)
|
||||
|
||||
|
||||
def test_direct_chat_retries_transient_provider_overload(monkeypatch) -> None:
|
||||
calls = 0
|
||||
|
||||
def transport(_url, _headers, _body, _timeout):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
if calls < 3:
|
||||
raise DirectLLMTransientError(
|
||||
"direct model request failed with HTTP 529"
|
||||
)
|
||||
return _response("recovered")
|
||||
|
||||
delays: list[float] = []
|
||||
monkeypatch.setattr(
|
||||
"gyxx_flow.modules.product_commerce.direct_llm.time.sleep",
|
||||
delays.append,
|
||||
)
|
||||
|
||||
content, _usage = chat_completion(
|
||||
[{"role": "user", "content": "hello"}],
|
||||
transport=transport,
|
||||
config=_config(),
|
||||
)
|
||||
|
||||
assert content == "recovered"
|
||||
assert calls == 3
|
||||
assert delays == [1.0, 3.0]
|
||||
|
||||
|
||||
def test_direct_chat_preserves_transient_error_after_bounded_retries(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
calls = 0
|
||||
|
||||
def transport(_url, _headers, _body, _timeout):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
raise DirectLLMTransientError(
|
||||
"direct model request failed with HTTP 529"
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"gyxx_flow.modules.product_commerce.direct_llm.time.sleep",
|
||||
lambda _seconds: None,
|
||||
)
|
||||
|
||||
with pytest.raises(DirectLLMTransientError, match="HTTP 529"):
|
||||
chat_completion(
|
||||
[{"role": "user", "content": "hello"}],
|
||||
transport=transport,
|
||||
config=_config(),
|
||||
)
|
||||
|
||||
assert calls == 3
|
||||
Reference in New Issue
Block a user