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

-14
View File
@@ -57,14 +57,10 @@ from artemis.config.constants import (
ENV_ADB_HOST,
ENV_ADB_PORT,
ENV_ADB_SERVER_SOCKET,
ENV_ANTHROPIC_API_KEY,
ENV_ANTIGRAVITY_APP_DIR,
ENV_ANTIGRAVITY_LS_ADDRESS,
ENV_DATA_ENGINE_DB_PATH,
ENV_EVENTS_OUTPUT_PATH,
ENV_GCP_API_KEY,
ENV_GEMINI_API_KEY,
ENV_GOOGLE_API_KEY,
ENV_KEEP_VIDEOS,
ENV_ARTEMIS_APP_DIR,
ENV_ARTEMIS_CLOUD_GATEWAY_URL,
@@ -83,11 +79,9 @@ from artemis.config.constants import (
ENV_ARTEMIS_TRACES_DIR,
ENV_ARTEMIS_USE_FILE_API,
ENV_ARTEMIS_USE_USER_DIR,
ENV_OPEN_ROUTER_API_KEY,
ENV_OPENAI_API_KEY,
ENV_OPENAI_BASE_URL,
ENV_RESULTS_OUTPUT_PATH,
ENV_XAI_API_KEY,
IPC_PORT_FILENAME,
LLM_CONFIG_FILENAME,
LLM_CONFIG_OVERRIDE_FILENAME,
@@ -110,7 +104,6 @@ from artemis.config.llm import (
initialize_llm_config,
load_llm_config_override,
parse_llm_config,
validate_vertex_ai_credentials,
)
from artemis.config.output import (
OutputConfig,
@@ -206,7 +199,6 @@ __all__ = [
"get_default_llm_config",
"deep_merge_llm_config",
"load_llm_config_override",
"validate_vertex_ai_credentials",
# Agent & Tool Config
"AgentGlobalConfig",
"CheckerConfig",
@@ -290,14 +282,8 @@ __all__ = [
"ENV_ADB_HOST",
"ENV_ADB_PORT",
"ENV_ADB_SERVER_SOCKET",
"ENV_GOOGLE_API_KEY",
"ENV_GEMINI_API_KEY",
"ENV_GCP_API_KEY",
"ENV_OPENAI_API_KEY",
"ENV_OPENAI_BASE_URL",
"ENV_ANTHROPIC_API_KEY",
"ENV_OPEN_ROUTER_API_KEY",
"ENV_XAI_API_KEY",
"ENV_ARTEMIS_EXPLORER_VERSION",
"ENV_ARTEMIS_DEFAULT_PROFILE",
"ENV_ARTEMIS_DEFAULT_MODEL",
+14 -38
View File
@@ -26,7 +26,8 @@ from artemis.config.constants import (
ExplorerVersion,
)
from artemis.config.paths import ROOT_DIR, get_config_path
from artemis.llm.google import VideoProcessing
from artemis.config.settings import settings
from artemis.config.core.interpolator import interpolate_config_value
from artemis.utils.file import load_jsonc
from artemis.utils.logger import get_logger
@@ -48,7 +49,7 @@ class VideoAnalyzerConfig(BaseModel):
description="Whether to enable video action ledger state tracking.",
)
chunk_size_seconds: float = Field(
default=60.0,
default_factory=lambda: settings.ARTEMIS_VIDEO_CHUNK_SECONDS,
ge=5.0,
le=300.0,
description="Maximum duration of an initial video-analysis chunk.",
@@ -95,44 +96,20 @@ class VideoAnalyzerConfig(BaseModel):
le=120,
description="Maximum additional action-proximal keyframes per chunk.",
)
native_max_retries: int = Field(
default=1,
max_retries: int = Field(
default_factory=lambda: settings.ARTEMIS_VIDEO_MAX_RETRIES,
ge=0,
le=5,
description=(
"Retries against the native video provider before switching to the "
"Retries against the video model before switching to the "
"configured universal fallback. A value of 1 means two total attempts."
),
)
model_call_timeout_seconds: float = Field(
default=120.0,
default_factory=lambda: settings.ARTEMIS_VIDEO_TIMEOUT_SECONDS,
ge=15.0,
le=600.0,
description="Hard timeout for one native or universal video-model response.",
)
processing: VideoProcessing = Field(
default="auto",
description=(
"How Gemini sub-agents watch a clip. 'agentic' lets the model search,"
" scan and re-watch the clip on demand (Gemini Interactions API);"
" 'static' samples it at a fixed frame rate; 'auto' picks agentic on"
" Gemini 3.6+ Flash models and static everywhere else."
),
)
agentic_chunk_size_seconds: float = Field(
default=600.0,
ge=30.0,
le=1800.0,
description=(
"Maximum clip length handed to one agentic sub-agent. Static mode keeps"
" using chunk_size_seconds."
),
)
agentic_call_timeout_seconds: float = Field(
default=300.0,
ge=30.0,
le=1200.0,
description="Hard timeout for one agentic sub-agent response.",
description="Hard timeout for one Responses video-model response.",
)
model_config = {"extra": "allow"}
@@ -214,12 +191,12 @@ class CheckerConfig(BaseModel):
description="Maximum concurrently running checkpoint attempts.",
)
checkpoint_timeout: float = Field(
default=180.0,
default_factory=lambda: settings.ARTEMIS_CHECKPOINT_TIMEOUT_SECONDS,
gt=0,
description="Per-attempt timeout (seconds) for one checkpoint check.",
)
settlement_timeout: float = Field(
default=120.0,
default_factory=lambda: settings.ARTEMIS_SETTLEMENT_TIMEOUT_SECONDS,
gt=0,
description="Aggregate timeout (seconds) for the exit settlement barrier.",
)
@@ -343,8 +320,7 @@ class ExplorerConfig(BaseModel):
caching: bool | None = Field(
default=None,
description=(
"Gemini explicit context caching for multi-turn Explorer tiers. ``None``"
" (default) uses the tier's own default: off for pro, on for ultra."
"Legacy compatibility field; Responses prompt caching is managed by the endpoint."
),
)
model_config = {"extra": "allow"}
@@ -391,7 +367,7 @@ class StepSummarizerConfig(BaseModel):
description="Whether to asynchronously summarize historical steps to replace pruned images.",
)
model: str = Field(
default="gemini-2.5-flash-lite",
default_factory=lambda: settings.OPENAI_MODEL,
description="Lightweight model used for background step state summarization.",
)
prune_history_xml: bool = Field(
@@ -688,7 +664,7 @@ class MemoryChunkingConfig(BaseModel):
),
)
model: str = Field(
default="gemini-3.8-flash",
default_factory=lambda: settings.OPENAI_MODEL,
description="Model used for the chunk-level StepCapsuleLens (bands ①+②).",
)
max_chunks: int = Field(
@@ -1089,7 +1065,7 @@ def load_agent_config(
try:
with open(resolved_path, encoding="utf-8") as f:
data = load_jsonc(f)
data = interpolate_config_value(load_jsonc(f))
if isinstance(data, dict) and "agent" in data:
return AgentGlobalConfig.model_validate(data["agent"])
return AgentGlobalConfig.model_validate(data)
+2 -10
View File
@@ -53,17 +53,11 @@ ENV_ARTEMIS_HIERARCHY_BACKEND = "ARTEMIS_HIERARCHY_BACKEND"
ENV_ARTEMIS_HELPER_AUTO_INSTALL = "ARTEMIS_HELPER_AUTO_INSTALL"
# LLM & Vision OCR API Keys
ENV_GOOGLE_API_KEY = "GOOGLE_API_KEY"
ENV_GEMINI_API_KEY = "GEMINI_API_KEY"
ENV_GCP_API_KEY = "GCP_API_KEY"
ENV_OCR_API_KEY = "OCR_API_KEY"
ENV_VISION_API_KEY = "VISION_API_KEY"
ENV_API_KEY = "API_KEY"
ENV_OPENAI_API_KEY = "OPENAI_API_KEY"
ENV_OPENAI_BASE_URL = "OPENAI_BASE_URL"
ENV_ANTHROPIC_API_KEY = "ANTHROPIC_API_KEY"
ENV_OPEN_ROUTER_API_KEY = "OPEN_ROUTER_API_KEY"
ENV_XAI_API_KEY = "XAI_API_KEY"
# Explorer & Agent Defaults
ENV_ARTEMIS_EXPLORER_VERSION = "ARTEMIS_EXPLORER_VERSION"
@@ -110,7 +104,7 @@ DEFAULT_ACTION_TIMEOUT = 30.0
DEFAULT_STREAM_PING_INTERVAL = 15.0
DEFAULT_PROFILE = "pro"
DEFAULT_MODEL = "gemini-2.5-flash"
DEFAULT_MODEL = "gpt-5-mini"
DEFAULT_EXPLORER_VERSION: Literal["flash", "pro", "ultra"] = "flash"
@@ -118,9 +112,7 @@ DEFAULT_EXPLORER_VERSION: Literal["flash", "pro", "ultra"] = "flash"
# Literals and Type Aliases
# ==============================================================================
LLMProvider = Literal[
"openai", "google", "openrouter", "xai", "vertexai", "anthropic", "ollama", "vllm", "custom"
]
LLMProvider = Literal["openai"]
ExplorerVersion = Literal["flash", "pro", "ultra"]
LLMUtilsNode = Literal[
+1 -1
View File
@@ -74,7 +74,7 @@ class CascadingConfigEngine:
# 3. Workspace .env
ws_env = self.workspace_root / ".env"
if ws_env.exists():
load_dotenv(dotenv_path=ws_env, override=True, encoding="utf-8")
load_dotenv(dotenv_path=ws_env, override=False, encoding="utf-8")
def load_raw_config(
self,
+19 -46
View File
@@ -15,12 +15,11 @@
"""LLM provider, model hierarchy, fallback chaining, and configuration loaders."""
import os
from artemis.config.core.interpolator import interpolate_config_value
from pathlib import Path
from typing import Any, Literal
import google.auth
from google.auth.exceptions import DefaultCredentialsError
from pydantic import BaseModel, ValidationError
from pydantic import Field, BaseModel, ValidationError
from artemis.config.constants import (
LLM_CONFIG_FILENAME,
@@ -36,18 +35,6 @@ from artemis.utils.logger import get_logger
logger = get_logger(__name__)
def validate_vertex_ai_credentials() -> None:
"""Validate Google Application Default Credentials for VertexAI provider."""
try:
_, project = google.auth.default()
if not project:
raise Exception("VertexAI requires a Google Cloud project to be set.")
except DefaultCredentialsError as e:
raise Exception(
f"VertexAI requires valid Google Application Default Credentials (ADC): {e}"
)
class _CyFunctionDetectorMeta(type):
def __instancecheck__(self, instance):
name = type(instance).__name__
@@ -72,13 +59,14 @@ class LLM(BaseModel):
"""Base model representing an LLM model provider and runtime parameters."""
model_config = {"ignored_types": (CyFunctionDetector,)}
provider: LLMProvider
model: str
provider: LLMProvider = "openai"
model: str = Field(default_factory=lambda: settings.OPENAI_MODEL)
api_base: str | None = None
timeout: float = Field(default_factory=lambda: settings.OPENAI_TIMEOUT_SECONDS, gt=0)
max_retries: int = Field(default_factory=lambda: settings.OPENAI_MAX_RETRIES, ge=0)
max_tokens: int | None = Field(default=None, gt=0)
temperature: float | None = None
thinking_budget: int | None = None
thinking_level: Literal["minimal", "low", "medium", "high"] | None = None
reasoning_effort: Literal["none", "low", "medium", "high"] | None = None
include_thoughts: bool | None = None
enable_grounding: bool | None = None
def validate_provider(self, name: str) -> None:
@@ -86,20 +74,6 @@ class LLM(BaseModel):
if self.provider == "openai":
if not settings.OPENAI_API_KEY:
raise Exception(f"{name} requires OPENAI_API_KEY in .env")
elif self.provider == "google":
if not settings.GOOGLE_API_KEY:
raise Exception(f"{name} requires GOOGLE_API_KEY in .env")
elif self.provider == "vertexai":
validate_vertex_ai_credentials()
elif self.provider == "anthropic":
if not (settings.ANTHROPIC_API_KEY or os.environ.get("ANTHROPIC_API_KEY")):
raise Exception(f"{name} requires ANTHROPIC_API_KEY in .env")
elif self.provider == "openrouter":
if not settings.OPEN_ROUTER_API_KEY:
raise Exception(f"{name} requires OPEN_ROUTER_API_KEY in .env")
elif self.provider == "xai":
if not settings.XAI_API_KEY:
raise Exception(f"{name} requires XAI_API_KEY in .env")
def __str__(self) -> str:
return f"{self.provider}/{self.model}"
@@ -110,7 +84,6 @@ class LLMWithFallback(LLM):
fallback: LLM
fix_model: str | None = None
timeout: float | None = None
def __str__(self) -> str:
return f"{self.provider}/{self.model} (fallback: {self.fallback})"
@@ -118,14 +91,14 @@ class LLMWithFallback(LLM):
def lightweight_judge_default() -> "LLMWithFallback":
"""Factory default for the lightweight judge nodes (pixel safety net and
planner validation): a flash-lite model at temperature 0."""
planner validation): the configured Responses model at temperature 0."""
return LLMWithFallback(
provider="google",
model="gemini-3.5-flash-lite",
provider="openai",
model=settings.OPENAI_MODEL,
temperature=0.0,
fallback=LLM(
provider="google",
model="gemini-3.1-flash-lite",
provider="openai",
model=settings.OPENAI_MODEL,
temperature=0.0,
),
)
@@ -236,11 +209,11 @@ def _expand_default_into_nodes(config_dict: dict) -> dict:
default_model_cfg = config_dict.get(
"default",
{
"provider": "google",
"model": "gemini-3.8-flash",
"provider": "openai",
"model": settings.OPENAI_MODEL,
"fallback": {
"provider": "google",
"model": "gemini-3.7-flash",
"provider": "openai",
"model": settings.OPENAI_MODEL,
},
},
)
@@ -310,7 +283,7 @@ def parse_llm_config() -> LLMConfig:
try:
with open(config_path, encoding="utf-8") as f:
config_dict = load_jsonc(f)
config_dict = interpolate_config_value(load_jsonc(f))
expanded_dict = _expand_default_into_nodes(config_dict)
return LLMConfig.model_validate(expanded_dict)
except Exception as e:
@@ -361,7 +334,7 @@ def load_llm_config_override(path: Path | str) -> LLMConfig:
if resolved_path.exists():
logger.info(f"Loading custom LLM config from {resolved_path.resolve()}...")
with open(resolved_path, encoding="utf-8") as f:
override_config_dict = load_jsonc(f)
override_config_dict = interpolate_config_value(load_jsonc(f))
else:
logger.warning(f"Custom LLM config not found at {path} - using default config")
+21 -71
View File
@@ -26,16 +26,10 @@ from artemis.config.constants import (
DEFAULT_ADB_PORT,
DEFAULT_MODEL,
DEFAULT_PROFILE,
ENV_ANTHROPIC_API_KEY,
ENV_DATA_ENGINE_DB_PATH,
ENV_GCP_API_KEY,
ENV_GEMINI_API_KEY,
ENV_GOOGLE_API_KEY,
ENV_OCR_API_KEY,
ENV_OPEN_ROUTER_API_KEY,
ENV_OPENAI_API_KEY,
ENV_VISION_API_KEY,
ENV_XAI_API_KEY,
)
from artemis.config.paths import (
GLOBAL_APP_DIR,
@@ -94,12 +88,6 @@ class Settings(BaseSettings):
# LLM Provider Authentication
OPENAI_API_KEY: SecretStr | None = None
GOOGLE_API_KEY: SecretStr | None = None
GEMINI_API_KEY: SecretStr | None = None
GCP_API_KEY: SecretStr | None = None
ANTHROPIC_API_KEY: SecretStr | None = None
XAI_API_KEY: SecretStr | None = None
OPEN_ROUTER_API_KEY: SecretStr | None = None
# Google Cloud Vision OCR Authentication
OCR_API_KEY: SecretStr | None = None
@@ -108,6 +96,19 @@ class Settings(BaseSettings):
# Custom Provider Endpoints
OPENAI_BASE_URL: str | None = None
OPENAI_MODEL: str = Field(default="gpt-5-mini", min_length=1)
OPENAI_TIMEOUT_SECONDS: float = Field(default=120.0, gt=0)
OPENAI_MAX_RETRIES: int = Field(default=0, ge=0)
ARTEMIS_CREDENTIAL_TIMEOUT_SECONDS: float = Field(default=12.0, gt=0)
ARTEMIS_MODEL_CACHE_TTL_SECONDS: float = Field(default=10.0, ge=0)
ARTEMIS_VIDEO_TIMEOUT_SECONDS: float = Field(default=120.0, gt=0)
ARTEMIS_VIDEO_MAX_RETRIES: int = Field(default=1, ge=0)
ARTEMIS_VIDEO_CHUNK_SECONDS: float = Field(default=60.0, gt=0)
ARTEMIS_CHECKPOINT_TIMEOUT_SECONDS: float = Field(default=180.0, gt=0)
ARTEMIS_SETTLEMENT_TIMEOUT_SECONDS: float = Field(default=120.0, gt=0)
ARTEMIS_VIDEO_ENABLED: bool = True
ARTEMIS_OUTPUTTER_ENABLED: bool = True
ARTEMIS_STEP_SUMMARIZER_ENABLED: bool = True
# Android ADB Connectivity
ADB_HOST: str | None = Field(default=DEFAULT_ADB_HOST)
@@ -125,16 +126,6 @@ class Settings(BaseSettings):
ARTEMIS_DEFAULT_PROFILE: str = Field(default=DEFAULT_PROFILE)
ARTEMIS_DEFAULT_MODEL: str = Field(default=DEFAULT_MODEL)
# Explorer Tool Settings (the tier itself is configured in artemis.jsonc or
# via ARTEMIS_EXPLORER_VERSION; see artemis.config.agent.ExplorerConfig)
EXPLORER_CACHING: bool | None = Field(
default=None,
description=(
"Environment-level override for Explorer context caching; unset"
" defers to the agent configuration and the tier default."
),
)
# LLM Reliability
LLM_PAUSE_TIMEOUT_SECONDS: float = Field(
default=900.0,
@@ -154,16 +145,10 @@ class Settings(BaseSettings):
@model_validator(mode="after")
def fallback_api_keys(self) -> "Settings":
"""Normalize Google / Gemini / GCP and OCR API keys and filter placeholders."""
"""Normalize OpenAI and OCR API keys and filter placeholders."""
# Sanitize any placeholder values loaded from environment or .env
for attr in (
"OPENAI_API_KEY",
"GOOGLE_API_KEY",
"GEMINI_API_KEY",
"GCP_API_KEY",
"ANTHROPIC_API_KEY",
"XAI_API_KEY",
"OPEN_ROUTER_API_KEY",
"OCR_API_KEY",
"VISION_API_KEY",
"API_KEY",
@@ -172,12 +157,6 @@ class Settings(BaseSettings):
if val and is_placeholder_key(val):
setattr(self, attr, None)
if not self.GOOGLE_API_KEY:
if self.GEMINI_API_KEY:
self.GOOGLE_API_KEY = self.GEMINI_API_KEY
elif self.GCP_API_KEY:
self.GOOGLE_API_KEY = self.GCP_API_KEY
# Fallback for OCR & Vision API keys
if not self.OCR_API_KEY and self.VISION_API_KEY:
self.OCR_API_KEY = self.VISION_API_KEY
@@ -187,27 +166,18 @@ class Settings(BaseSettings):
"""Get API key for a specified provider.
Args:
provider: Provider name (e.g., 'google', 'gemini', 'openai', 'ocr', 'vision').
provider: Provider name ('openai', 'ocr', or 'vision').
Returns:
SecretStr containing the API key or None if not configured.
"""
from artemis.llm.google.provider import is_google_family_provider
provider_lower = provider.lower()
key: SecretStr | None = None
if is_google_family_provider(provider_lower):
key = self.GOOGLE_API_KEY or self.GEMINI_API_KEY or self.GCP_API_KEY
elif provider_lower in ("ocr", "vision", "google_vision"):
if provider_lower in ("ocr", "vision", "google_vision"):
key = self.OCR_API_KEY or self.VISION_API_KEY
elif provider_lower == "openai":
key = self.OPENAI_API_KEY
elif provider_lower in ("anthropic", "claude"):
key = self.ANTHROPIC_API_KEY
elif provider_lower == "openrouter":
key = self.OPEN_ROUTER_API_KEY
elif provider_lower in ("xai", "grok"):
key = self.XAI_API_KEY
if key and not is_placeholder_key(key):
return key
@@ -222,35 +192,14 @@ class Settings(BaseSettings):
persist_to_env: Whether to save the key to the app directory's .env file.
"""
secret = SecretStr(key)
from artemis.llm.google.provider import is_google_family_provider
provider_lower = provider.lower()
env_key_name = None
if is_google_family_provider(provider_lower):
self.GOOGLE_API_KEY = secret
self.GEMINI_API_KEY = secret
self.GCP_API_KEY = secret
env_key_name = ENV_GEMINI_API_KEY
os.environ[ENV_GOOGLE_API_KEY] = key
os.environ[ENV_GEMINI_API_KEY] = key
os.environ[ENV_GCP_API_KEY] = key
elif provider_lower == "openai":
if provider_lower == "openai":
self.OPENAI_API_KEY = secret
env_key_name = ENV_OPENAI_API_KEY
os.environ[ENV_OPENAI_API_KEY] = key
elif provider_lower in ("anthropic", "claude"):
self.ANTHROPIC_API_KEY = secret
env_key_name = ENV_ANTHROPIC_API_KEY
os.environ[ENV_ANTHROPIC_API_KEY] = key
elif provider_lower == "openrouter":
self.OPEN_ROUTER_API_KEY = secret
env_key_name = ENV_OPEN_ROUTER_API_KEY
os.environ[ENV_OPEN_ROUTER_API_KEY] = key
elif provider_lower == "xai":
self.XAI_API_KEY = secret
env_key_name = ENV_XAI_API_KEY
os.environ[ENV_XAI_API_KEY] = key
elif provider_lower in ("ocr", "vision", "google_vision"):
self.OCR_API_KEY = secret
self.VISION_API_KEY = secret
@@ -258,6 +207,9 @@ class Settings(BaseSettings):
os.environ[ENV_OCR_API_KEY] = key
os.environ[ENV_VISION_API_KEY] = key
else:
raise ValueError(f"Unsupported credential provider: {provider}")
if persist_to_env and env_key_name:
target_env_files = [get_env_file()]
seen_paths = set()
@@ -275,9 +227,7 @@ class Settings(BaseSettings):
# Determine keys to update
keys_to_update = [env_key_name]
if is_google_family_provider(provider_lower):
keys_to_update = [ENV_GEMINI_API_KEY, ENV_GOOGLE_API_KEY, ENV_GCP_API_KEY]
elif provider_lower in ("ocr", "vision", "google_vision"):
if provider_lower in ("ocr", "vision", "google_vision"):
keys_to_update = [ENV_OCR_API_KEY, ENV_VISION_API_KEY]
new_lines = []