Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions artemis/agents/flash/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ def __init__(self, ctx: ArtemisContext, goal: str, max_turns: int | None = None)
ctx,
model_name=self.step_summarizer_cfg.model,
retry_limit=self.memory_runtime_cfg.retry_limit,
model_provider=self.step_summarizer_cfg.provider,
max_concurrency=self.memory_runtime_cfg.max_concurrency,
flush_timeout_s=self.memory_runtime_cfg.flush_timeout_s,
)
Expand Down
19 changes: 10 additions & 9 deletions artemis/agents/flash/summarizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

from artemis.context import ArtemisContext
from artemis.memory.step_memory import JobKey, StepMemoryService
from artemis.services.llm import RobustChatModelWrapper, get_google_llm, get_llm
from artemis.services.llm import RobustChatModelWrapper, get_lens_llm
from artemis.services.token_meter import record_llm_usage
from artemis.utils.task_tree import format_actions_clean
from artemis.utils.visualization import draw_action_overlay_on_image
Expand Down Expand Up @@ -151,6 +151,7 @@ def __init__(
model_name: str | None = None,
retry_limit: int = 3,
*,
model_provider: str | None = None,
max_concurrency: int = 1,
flush_timeout_s: float = 30.0,
):
Expand All @@ -161,16 +162,16 @@ def __init__(
flush_timeout_s=flush_timeout_s,
)

# Initialize lightweight VLM: prioritize explicit model_name
# Initialize lightweight VLM: prioritize explicit model_name. Routing
# is provider-aware: an explicit provider knob wins, Gemini names stay
# on Google, and anything else inherits the summarizer node's provider
# from the LLM config (see get_lens_llm). Construction errors surface
# instead of silently swapping in a different provider's model — a
# silent fallback is what previously hid provider misroutes behind
# per-step 404s.
target_model = model_name or "gemini-2.5-flash-lite"
self._model_name = target_model
try:
if model_name:
self._llm = get_google_llm(model_name=target_model, temperature=0.0)
else:
self._llm = get_llm(ctx, name="summarizer", is_utils=True)
except Exception:
self._llm = get_google_llm(model_name=target_model, temperature=0.0)
self._llm = get_lens_llm(ctx, target_model, model_provider, temperature=0.0)
try:
configured = getattr(self._llm, "model", None) or getattr(self._llm, "model_name", None)
if isinstance(configured, str) and configured:
Expand Down
53 changes: 50 additions & 3 deletions artemis/config/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from pathlib import Path
from typing import Any, Literal

from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, Field, field_validator, model_validator

from artemis.config.constants import (
AGENT_CONFIG_FILENAME,
Expand All @@ -27,6 +27,7 @@
)
from artemis.config.paths import ROOT_DIR, get_config_path
from artemis.llm.google import VideoProcessing
from artemis.llm.router import ModelProvider
from third_party.mobile_use.utils.file import load_jsonc
from third_party.mobile_use.utils.logger import get_logger

Expand Down Expand Up @@ -392,7 +393,21 @@ class StepSummarizerConfig(BaseModel):
)
model: str = Field(
default="gemini-2.5-flash-lite",
description="Lightweight model used for background step state summarization.",
description=(
"Lightweight model used for background step state summarization."
" Gemini names route to the Google provider automatically; any other"
" model ID needs 'provider' set explicitly or inherits the LLM"
" config's summarizer node provider (itself inheriting 'default')."
),
)
provider: str | None = Field(
default=None,
description=(
"Explicit provider for the step summarizer model ('google',"
" 'custom', 'openai', ...). None (default) auto-resolves: Gemini"
" model names use Google; other IDs inherit the summarizer node's"
" provider from the LLM config."
),
)
prune_history_xml: bool = Field(
default=True,
Expand All @@ -409,6 +424,14 @@ class StepSummarizerConfig(BaseModel):
)
model_config = {"extra": "allow"}

@field_validator("provider")
@classmethod
def _validate_provider(cls, v: str | None) -> str | None:
"""Fail fast on unknown provider names at config-load time."""
if v is not None and str(v).strip():
ModelProvider.from_string(v)
return v


class MemoryRuntimeConfig(BaseModel):
"""Scheduling options for the shared step-memory runtime (agent.memory.runtime)."""
Expand Down Expand Up @@ -689,7 +712,23 @@ class MemoryChunkingConfig(BaseModel):
)
model: str = Field(
default="gemini-3.8-flash",
description="Model used for the chunk-level StepCapsuleLens (bands ①+②).",
description=(
"Model used for the chunk-level StepCapsuleLens (bands ①+②)."
" Gemini names route to the Google provider automatically; any"
" other model ID needs 'provider' set explicitly or inherits the"
" LLM config's summarizer node provider (itself inheriting"
" 'default')."
),
)
provider: str | None = Field(
default=None,
description=(
"Explicit provider for the chunk capsule model ('google',"
" 'custom', 'openai', ...). None (default) auto-resolves: Gemini"
" model names use Google; other IDs inherit the summarizer node's"
" provider from the LLM config. Also applies to the capsule"
" fallback model resolved from the summarizer node's fallback."
),
)
max_chunks: int = Field(
default=8,
Expand Down Expand Up @@ -719,6 +758,14 @@ def _min_steps_within_cap(self) -> "MemoryChunkingConfig":
)
return self

@field_validator("provider")
@classmethod
def _validate_provider(cls, v: str | None) -> str | None:
"""Fail fast on unknown provider names at config-load time."""
if v is not None and str(v).strip():
ModelProvider.from_string(v)
return v

model_config = {"extra": "allow"}


Expand Down
6 changes: 5 additions & 1 deletion artemis/memory/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@ def ensure_step_memory(ctx):

kwargs: dict = {}
model_name = None
model_provider = None
try:
from artemis.config import load_agent_config

cfg = load_agent_config()
model_name = cfg.flash.step_summarizer.model
model_provider = cfg.flash.step_summarizer.provider
kwargs = {
"retry_limit": cfg.memory.runtime.retry_limit,
"max_concurrency": cfg.memory.runtime.max_concurrency,
Expand All @@ -65,7 +67,9 @@ def ensure_step_memory(ctx):
exc_info=True,
)

service = VisualStepSummarizer(ctx, model_name=model_name, **kwargs)
service = VisualStepSummarizer(
ctx, model_name=model_name, model_provider=model_provider, **kwargs
)
try:
ctx.step_memory = service
except (AttributeError, TypeError, ValueError):
Expand Down
49 changes: 34 additions & 15 deletions artemis/memory/chunking.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@

from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage

from artemis.llm.google import is_google_provider
from artemis.memory.step_memory import JobKey, StepLens, StepMemoryService
from artemis.memory.transcript import format_session_offset
from third_party.mobile_use.utils.logger import get_logger
Expand Down Expand Up @@ -295,10 +294,13 @@ def __init__(
llm: Any | None = None,
*,
ctx: Any = None,
model_provider: str | None = None,
fallback_model_name: str | None = None,
fallback_model_provider: str | None = None,
fallback_llm: Any | None = None,
):
self._model_name = model_name or "gemini-3.8-flash"
self._model_provider = model_provider
self._llm = llm
self._ctx = ctx
# Availability hardening: `chunking.model` is a dedicated model with no
Expand All @@ -309,6 +311,7 @@ def __init__(
self._fallback_model_name = (
fallback_model_name if fallback_model_name != self._model_name else None
)
self._fallback_model_provider = fallback_model_provider
self._fallback_llm = fallback_llm
try:
self._prompt = self._PROMPT_PATH.read_text(encoding="utf-8")
Expand All @@ -323,17 +326,22 @@ def __init__(

def _get_llm(self):
if self._llm is None:
from artemis.services.llm import get_google_llm
from artemis.services.llm import get_lens_llm

self._llm = get_google_llm(model_name=self._model_name, temperature=0.0)
self._llm = get_lens_llm(
self._ctx, self._model_name, self._model_provider, temperature=0.0
)
return self._llm

def _get_fallback_llm(self):
if self._fallback_llm is None and self._fallback_model_name:
from artemis.services.llm import get_google_llm
from artemis.services.llm import get_lens_llm

self._fallback_llm = get_google_llm(
model_name=self._fallback_model_name, temperature=0.0
self._fallback_llm = get_lens_llm(
self._ctx,
self._fallback_model_name,
self._fallback_model_provider,
temperature=0.0,
)
return self._fallback_llm

Expand Down Expand Up @@ -892,6 +900,7 @@ def __init__(
self._min_steps = max(1, min(self._max_steps, int(getattr(cc, "min_steps", 3) or 3)))
self._target_source_tokens = int(getattr(cc, "target_source_tokens", 2000) or 2000)
self._model_name = getattr(cc, "model", None) or "gemini-3.8-flash"
self._model_provider = getattr(cc, "provider", None)
self._max_chunks = int(getattr(cc, "max_chunks", 8) or 8)
# None uses max_chunks as the era cap.
self._max_eras = int(getattr(cc, "max_eras", None) or self._max_chunks)
Expand Down Expand Up @@ -942,19 +951,29 @@ def _build_capsule_service(self, ctx: Any) -> StepMemoryService:
f"Memory runtime config unavailable; using capsule service defaults: {exc}",
exc_info=True,
)
fallback_model, fallback_provider = self._resolve_capsule_fallback(ctx)
lens = StepCapsuleLens(
self._model_name,
ctx=ctx,
fallback_model_name=self._resolve_capsule_fallback_model(ctx),
model_provider=self._model_provider,
fallback_model_name=fallback_model,
# A fallback block without its own provider rides the chunking
# 'provider' knob; resolve eagerly so the lens stores what it
# will actually use.
fallback_model_provider=fallback_provider or self._model_provider,
)
return ChunkCapsuleService(ctx, lens, **kwargs)

def _resolve_capsule_fallback_model(self, ctx: Any) -> str | None:
"""Fallback model for capsule generation when `chunking.model` is down.
def _resolve_capsule_fallback(self, ctx: Any) -> tuple[str | None, str | None]:
"""Fallback model + provider for capsule generation when `chunking.model` is down.

Resolved from the LLM config's summarizer role (which inherits the
global default fallback unless overridden). Only same-provider (google)
fallbacks apply — the capsule lens rides the raw google model path.
global default fallback unless overridden). The fallback block carries
its own provider — that is how every other node's fallback resolves
(see ``_resolve_endpoint(use_fallback=True)``) — so it wins over the
chunking 'provider' knob. Any provider applies: the capsule lens rides
the provider-aware lens resolver, so a non-google summarizer fallback
(e.g. a gateway model) is now usable too.
"""
try:
llm_cfg = getattr(ctx, "llm_config", None) if ctx is not None else None
Expand All @@ -963,13 +982,13 @@ def _resolve_capsule_fallback_model(self, ctx: Any) -> str | None:

llm_cfg = get_default_llm_config()
fallback = getattr(getattr(llm_cfg, "summarizer", None), "fallback", None)
provider = str(getattr(fallback, "provider", "") or "")
model = getattr(fallback, "model", None)
if model and is_google_provider(provider) and model != self._model_name:
return str(model)
provider = getattr(fallback, "provider", None)
if model and model != self._model_name:
return str(model), (str(provider) if provider else None)
except Exception as exc:
logger.debug(f"Capsule fallback model resolution skipped: {exc}", exc_info=True)
return None
return None, None

@property
def capsule_service(self) -> StepMemoryService:
Expand Down
Loading
Loading