feat: add Chinese UI, Responses models, and mirrored Windows setup
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

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:
chenmingxuan committed 2026-09-22 19:16:17 +08:00
1 parent 371aa6df56
commit 2ddec53954
183 files changed
+3427 -11857

No files matched your search

+25 -193
View File
@@ -45,7 +45,6 @@ from artemis.config import (
)
from artemis.context import ArtemisContext
from artemis.data_engine.trace import CURRENT_TRACE_ID, DataEngineCallbackHandler
from artemis.llm.google import is_google_chat_model, is_google_provider
from artemis.llm.reliability import (
CircuitBreaker,
FailureCategory,
@@ -241,62 +240,6 @@ def _record_llm_event(name: str, payload: dict[str, Any], *, status: str = "retr
llm_logger.warning("Failed to publish LLM event %s: %s", name, publish_error)
class _ProviderRetryTelemetryHandler(logging.Handler):
"""Turn provider SDK retry log records into structured Artemis telemetry."""
_DELAY_PATTERN = re.compile(r"\bin\s+(?P<delay>\d+(?:\.\d+)?)\s+seconds?\b", re.IGNORECASE)
_ERROR_PATTERN = re.compile(r"\bas it raised\s+(?P<error>.+)$", re.IGNORECASE)
_RETRYABLE_MARKERS = (
"503",
"429",
"unavailable",
"resource_exhausted",
"high demand",
"quota",
"throttled",
"overloaded",
)
def emit(self, record: logging.LogRecord) -> None:
try:
request = _ACTIVE_LLM_REQUEST.get()
if not request or request.get("provider") != ModelProvider.GOOGLE.value:
return
message = record.getMessage()
lowered = message.lower()
if "retrying" not in lowered or not any(
marker in lowered for marker in self._RETRYABLE_MARKERS
):
return
delay_match = self._DELAY_PATTERN.search(message)
error_match = self._ERROR_PATTERN.search(message)
delay = float(delay_match.group("delay")) if delay_match else 0.0
error = error_match.group("error").strip() if error_match else message
_record_llm_retry(
error,
delay,
provider=request["provider"],
source="provider_sdk",
)
except Exception:
# Telemetry must never interfere with the provider's own retry.
self.handleError(record)
def _install_provider_retry_telemetry() -> None:
provider_logger = logging.getLogger("google_genai._api_client")
if any(
isinstance(handler, _ProviderRetryTelemetryHandler) for handler in provider_logger.handlers
):
return
provider_logger.addHandler(_ProviderRetryTelemetryHandler(level=logging.INFO))
_install_provider_retry_telemetry()
def _handle_llm_pause_and_resume(last_error: Exception) -> Path:
"""Handles pausing task execution upon persistent failure until resumed."""
err_msg = str(last_error)
@@ -550,40 +493,12 @@ class RobustChatModelWrapper:
processed_tools = list(tools_list) if tools_list is not None else []
# Determine if the underlying model provider is Google / Gemini
if self.endpoint is not None:
google_backed = is_google_provider(self.endpoint.provider)
else:
google_backed = is_google_chat_model(self.base_model)
has_explicit_google_search = any(
isinstance(t, dict) and "google_search" in t for t in processed_tools
)
should_ground = (
self.endpoint is not None and self.endpoint.enable_grounding
) or has_explicit_google_search
if google_backed:
if should_ground:
# Add native Google Search Grounding tool if not already present
if not has_explicit_google_search:
processed_tools.append({"google_search": {}})
# Ensure include_server_side_tool_invocations is True in tool_config
# to satisfy Gemini API requirements when mixing built-in tools and function calling
tool_config = kwargs.get("tool_config")
if tool_config is None:
kwargs["tool_config"] = {"include_server_side_tool_invocations": True}
elif isinstance(tool_config, dict):
tool_config = dict(tool_config)
tool_config.setdefault("include_server_side_tool_invocations", True)
kwargs["tool_config"] = tool_config
else:
# If not Gemini/Google (e.g. OpenAI, Anthropic, Ollama, OpenRouter, XAI):
# Ignore grounding and filter out any Gemini-specific built-in dict tools to avoid provider errors
processed_tools = [
t for t in processed_tools if not (isinstance(t, dict) and "google_search" in t)
]
if self.endpoint is not None and self.endpoint.enable_grounding:
if not any(
isinstance(t, dict) and t.get("type") in ("web_search", "web_search_preview")
for t in processed_tools
):
processed_tools.append({"type": "web_search"})
bound = self.base_model.bind_tools(processed_tools, *other_args, **kwargs)
return RobustChatModelWrapper(bound, self.ctx, endpoint=self.endpoint)
@@ -597,6 +512,9 @@ class RobustChatModelWrapper:
def with_structured_output(self, *args, **kwargs):
if hasattr(self.base_model, "with_structured_output"):
# Compatible Responses endpoints may support functions but not text.format=json_schema.
# Keep schema validation in LangChain/Pydantic without changing the API transport.
kwargs.setdefault("method", "function_calling")
return RobustChatModelWrapper(
self.base_model.with_structured_output(*args, **kwargs),
self.ctx,
@@ -907,100 +825,18 @@ async def invoke_llm_with_timeout_message[T](
# Backward compatible factory functions delegating to ModelFactory
def get_google_llm(
model_name: str = "gemini-3.8-flash",
temperature: float | None = None,
timeout: float | None = None,
thinking_budget: int | None = None,
thinking_level: str | None = "medium",
include_thoughts: bool | None = None,
enable_grounding: bool = False,
) -> BaseChatModel:
ep = ModelEndpoint(
provider=ModelProvider.GOOGLE,
model_name=model_name,
temperature=temperature or 0.0,
timeout_seconds=timeout or 60.0,
thinking_budget=thinking_budget,
thinking_level=thinking_level,
include_thoughts=include_thoughts,
enable_grounding=enable_grounding,
)
return ModelFactory.create_model(ep)
def get_vertex_llm(
model_name: str = "gemini-3.8-flash",
temperature: float | None = None,
timeout: float | None = None,
thinking_budget: int | None = None,
) -> BaseChatModel:
ep = ModelEndpoint(
provider=ModelProvider.VERTEX_AI,
model_name=model_name,
temperature=temperature or 0.0,
timeout_seconds=timeout or 60.0,
thinking_budget=thinking_budget,
)
return ModelFactory.create_model(ep)
def get_openai_llm(
model_name: str = "o3",
model_name: str | None = None,
temperature: float | None = None,
timeout: float | None = None,
) -> BaseChatModel:
ep = ModelEndpoint(
provider=ModelProvider.OPENAI,
model_name=model_name,
temperature=temperature or 0.0,
timeout_seconds=timeout or 60.0,
)
return ModelFactory.create_model(ep)
def get_openrouter_llm(
model_name: str,
temperature: float | None = None,
timeout: float | None = None,
) -> BaseChatModel:
ep = ModelEndpoint(
provider=ModelProvider.OPENROUTER,
model_name=model_name,
temperature=temperature or 0.0,
timeout_seconds=timeout or 60.0,
)
return ModelFactory.create_model(ep)
def get_grok_llm(
model_name: str,
temperature: float | None = None,
timeout: float | None = None,
) -> BaseChatModel:
ep = ModelEndpoint(
provider=ModelProvider.XAI,
model_name=model_name,
temperature=temperature or 0.0,
timeout_seconds=timeout or 60.0,
)
return ModelFactory.create_model(ep)
def get_anthropic_llm(
model_name: str,
temperature: float | None = None,
timeout: float | None = None,
thinking_budget: int | None = None,
reasoning_effort: str | None = None,
) -> BaseChatModel:
ep = ModelEndpoint(
provider=ModelProvider.ANTHROPIC,
model_name=model_name,
temperature=temperature or 0.0,
timeout_seconds=timeout or 60.0,
thinking_budget=thinking_budget,
reasoning_effort=reasoning_effort,
model_name=model_name or settings.OPENAI_MODEL,
temperature=temperature,
timeout_seconds=timeout if timeout is not None else settings.OPENAI_TIMEOUT_SECONDS,
)
return ModelFactory.create_model(ep)
@@ -1010,21 +846,15 @@ def get_cached_raw_model(
model_name: str,
temperature: float | None = None,
timeout: float | None = None,
thinking_budget: int | None = None,
thinking_level: str | None = None,
include_thoughts: bool | None = None,
reasoning_effort: str | None = None,
enable_grounding: bool = False,
) -> BaseChatModel:
"""Retrieves or instantiates a cached raw LangChain chat model."""
ep = ModelEndpoint(
provider=ModelProvider.from_string(provider),
model_name=model_name,
temperature=temperature or 0.0,
timeout_seconds=timeout or 60.0,
thinking_budget=thinking_budget,
thinking_level=thinking_level,
include_thoughts=include_thoughts,
model_name=model_name or settings.OPENAI_MODEL,
temperature=temperature,
timeout_seconds=timeout if timeout is not None else settings.OPENAI_TIMEOUT_SECONDS,
reasoning_effort=reasoning_effort,
enable_grounding=enable_grounding,
)
@@ -1061,18 +891,20 @@ def _resolve_endpoint(
val = getattr(obj, attr, None)
return val if isinstance(val, expected_type) else None
provider_val = getattr(cfg, "provider", "google")
model_val = getattr(cfg, "model", "gemini-2.5-flash")
provider_val = getattr(cfg, "provider", "openai")
model_val = getattr(cfg, "model", settings.OPENAI_MODEL)
return ModelEndpoint(
provider=ModelProvider.from_string(provider_val),
model_name=str(model_val),
temperature=_get_val(cfg, "temperature", (int, float)) or 0.0,
timeout_seconds=_get_val(cfg, "timeout", (int, float)) or 60.0,
thinking_budget=_get_val(cfg, "thinking_budget", int),
thinking_level=_get_val(cfg, "thinking_level", str),
temperature=_get_val(cfg, "temperature", (int, float)),
timeout_seconds=_get_val(cfg, "timeout", (int, float)) or settings.OPENAI_TIMEOUT_SECONDS,
max_retries=_get_val(cfg, "max_retries", int)
if _get_val(cfg, "max_retries", int) is not None
else settings.OPENAI_MAX_RETRIES,
max_tokens=_get_val(cfg, "max_tokens", int),
api_base=_get_val(cfg, "api_base", str),
reasoning_effort=_get_val(cfg, "reasoning_effort", str),
include_thoughts=_get_val(cfg, "include_thoughts", bool),
enable_grounding=_get_val(cfg, "enable_grounding", bool) or False,
)
+2 -2
View File
@@ -40,7 +40,7 @@ import threading
from typing import Any
from artemis.data_engine.context_vars import CURRENT_NODE_NAME
from artemis.llm.google import usage_from_message
from artemis.llm.usage import usage_from_message
from artemis.utils.logger import get_logger
logger = get_logger(__name__)
@@ -62,7 +62,7 @@ OPERATOR_NODE_NAMES = frozenset({"operator", "flashrunner"})
def extract_usage(response: Any) -> dict[str, int] | None:
"""Extracts unified token usage from an LLM response message, best-effort.
Thin wrapper over :func:`artemis.llm.google.usage.usage_from_message`
Thin wrapper over :func:`artemis.llm.usage.usage_from_message`
that keeps the meter's historical four-key shape. Returns None when the
response carries no usable usage numbers.
"""