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.
This commit is contained in:
1 parent
371aa6df56
commit
2ddec53954
183 files changed
+3427
-11857
No files matched your search
@@ -0,0 +1,215 @@
|
||||
"""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"
|
||||
Reference in new issue
Block a user