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
+25
-193
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
Reference in new issue
Block a user