Files
Artemis-Eapil/tests/unit/test_openai_responses.py
T
chenmingxuan 2ddec53954
CI / Python 3.12 / ubuntu-latest (push) Canceled after 0s
CI / Python 3.12 / windows-latest (push) Canceled after 0s
CI / Frontend tests and production build (push) Canceled after 0s
CI / Build and smoke-test Python packages (push) Canceled after 0s
feat: add Chinese UI, Responses models, and mirrored Windows setup
Localize the web console and persist custom model and capability settings.
Use OpenAI Responses exclusively and move runtime limits into environment configuration.
Bootstrap uv and the virtual environment on Windows, support optional China mirrors,
and package source distributions without environments, build output, or credentials.

Update regression tests and repository quality checks for the new behavior.
2026-09-22 19:16:17 +08:00

216 lines
7.2 KiB
Python

"""Transport-level checks: every model call must use /responses."""
import json
from unittest.mock import MagicMock
import httpx
import pytest
from langchain_core.messages import HumanMessage, ToolMessage
from pydantic import BaseModel, SecretStr, ValidationError
from artemis.config.settings import settings
from artemis.llm.router import ModelEndpoint, ModelFactory, ModelProvider
from artemis.services.llm import RobustChatModelWrapper
def response_body(output):
return {
"id": "resp_test",
"object": "response",
"created_at": 1,
"status": "completed",
"model": "test-model",
"output": output,
"usage": {"input_tokens": 10, "output_tokens": 3, "total_tokens": 13},
}
def text_output(text):
return [
{
"id": "msg_test",
"type": "message",
"role": "assistant",
"status": "completed",
"content": [{"type": "output_text", "text": text, "annotations": []}],
}
]
@pytest.fixture
def transport_model(monkeypatch):
import langchain_openai
original = langchain_openai.ChatOpenAI
requests = []
outputs = []
def handle(request):
requests.append(request)
return httpx.Response(
200, json=response_body(outputs.pop(0) if outputs else text_output("OK"))
)
client = httpx.AsyncClient(transport=httpx.MockTransport(handle))
monkeypatch.setattr(
langchain_openai,
"ChatOpenAI",
lambda **kwargs: original(**kwargs, http_async_client=client),
)
monkeypatch.setattr(settings, "OPENAI_API_KEY", SecretStr("test-secret"))
monkeypatch.setattr(settings, "OPENAI_BASE_URL", "https://responses.example/v1")
monkeypatch.delenv("ARTEMIS_FAKE_LLM", raising=False)
model = ModelFactory.create_model(ModelEndpoint(model_name="test-model", timeout_seconds=23))
return model, requests, outputs
@pytest.mark.asyncio
async def test_text_and_image_use_responses_transport(transport_model):
model, requests, _ = transport_model
result = await model.ainvoke(
[
HumanMessage(
content=[
{"type": "text", "text": "Describe"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,AA=="}},
]
)
]
)
assert requests[0].url == "https://responses.example/v1/responses"
payload = json.loads(requests[0].content)
assert payload["store"] is False
assert payload["input"][0]["content"][1]["type"] == "input_image"
assert "messages" not in payload
assert result.usage_metadata["total_tokens"] == 13
@pytest.mark.asyncio
async def test_function_call_roundtrip_uses_call_id(transport_model):
model, requests, outputs = transport_model
outputs.append(
[
{
"type": "function_call",
"name": "lookup",
"call_id": "call_1",
"id": "fc_1",
"arguments": '{"query":"hello"}',
"status": "completed",
}
]
)
tool = {
"type": "function",
"function": {
"name": "lookup",
"description": "Look up a value",
"parameters": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
},
}
messages = [HumanMessage(content="Look up hello")]
answer = await model.bind_tools([tool]).ainvoke(messages)
assert answer.tool_calls[0]["id"] == "call_1"
await model.ainvoke([*messages, answer, ToolMessage(content="found", tool_call_id="call_1")])
payload = json.loads(requests[1].content)
assert any(
item.get("type") == "function_call_output" and item["call_id"] == "call_1"
for item in payload["input"]
)
assert all(r.url.path == "/v1/responses" for r in requests)
@pytest.mark.asyncio
async def test_structured_output_uses_responses_text_format(transport_model):
class Verdict(BaseModel):
ok: bool
model, requests, outputs = transport_model
outputs.append(text_output('{"ok":true}'))
result = await model.with_structured_output(Verdict, method="json_schema", strict=True).ainvoke(
"Check"
)
assert result.ok
payload = json.loads(requests[0].content)
assert payload["text"]["format"]["type"] == "json_schema"
assert "response_format" not in payload
@pytest.mark.parametrize(
"provider", ["google", "gemini", "anthropic", "openrouter", "ollama", "custom"]
)
def test_other_provider_transports_are_rejected(provider):
with pytest.raises(ValueError, match="only OpenAI Responses"):
ModelProvider.from_string(provider)
with pytest.raises(ValidationError):
ModelEndpoint(provider=provider)
@pytest.mark.parametrize("max_retries", [0, 3])
def test_client_settings_and_cache_credentials(monkeypatch, max_retries):
monkeypatch.setattr(settings, "OPENAI_API_KEY", SecretStr("first"))
monkeypatch.setattr(settings, "OPENAI_MAX_RETRIES", max_retries)
endpoint = ModelEndpoint(api_key="test", model_name="gpt-5-mini", reasoning_effort="low")
model = ModelFactory.create_model(endpoint)
assert model.use_responses_api is True
assert model.max_retries == max_retries
assert model.reasoning_effort == "low"
dynamic = ModelEndpoint()
key = dynamic.cache_key()
monkeypatch.setattr(settings, "OPENAI_API_KEY", SecretStr("second"))
assert dynamic.cache_key() != key
assert "first" not in repr(key)
def test_web_search_uses_openai_tool():
base = MagicMock()
wrapper = RobustChatModelWrapper(base, endpoint=ModelEndpoint(enable_grounding=True))
wrapper.bind_tools([])
base.bind_tools.assert_called_once_with([{"type": "web_search"}])
def test_video_text_fallback_extracts_responses_blocks():
from langchain_core.messages import AIMessage
from artemis.agents.video_analyzer.universal_engine import _extract_submit_payload
result = AIMessage(
content=[
{"type": "reasoning", "summary": [], "id": "rs_1"},
{"type": "text", "text": "已完成视频画面检查。", "annotations": []},
]
)
summary, analysis, events = _extract_submit_payload(result)
assert summary == analysis == "已完成视频画面检查。"
assert events == []
@pytest.mark.asyncio
async def test_wrapper_structured_output_uses_responses_function_call(transport_model):
class Verdict(BaseModel):
ok: bool
model, requests, outputs = transport_model
outputs.append(
[
{
"type": "function_call",
"name": "Verdict",
"call_id": "call_verdict",
"id": "fc_verdict",
"arguments": '{"ok":true}',
"status": "completed",
}
]
)
wrapper = RobustChatModelWrapper(model, endpoint=ModelEndpoint(model_name="test-model"))
result = await wrapper.with_structured_output(Verdict).ainvoke("Check")
assert result.ok is True
payload = json.loads(requests[0].content)
assert requests[0].url.path == "/v1/responses"
assert payload["tools"][0]["name"] == "Verdict"
assert payload.get("text", {}).get("format", {}).get("type") != "json_schema"