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
35 changes: 35 additions & 0 deletions agent/core/direct_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Helpers for direct OpenAI-compatible cloud model ids."""

DIRECT_MODEL_PROVIDERS: dict[str, dict[str, str]] = {
"atlas/": {
"base_url_env": "ATLASCLOUD_BASE_URL",
"base_url_default": "https://api.atlascloud.ai/v1",
"api_key_env": "ATLASCLOUD_API_KEY",
},
}

DIRECT_MODEL_PREFIXES = tuple(DIRECT_MODEL_PROVIDERS)


def direct_model_provider(model_id: str) -> dict[str, str] | None:
"""Return provider config for a direct model id, if supported."""
for prefix, config in DIRECT_MODEL_PROVIDERS.items():
if model_id.startswith(prefix):
return config
return None


def direct_model_name(model_id: str) -> str | None:
"""Return the upstream model name with the direct provider prefix removed."""
for prefix in DIRECT_MODEL_PREFIXES:
if model_id.startswith(prefix):
name = model_id[len(prefix) :]
return name or None
return None


def is_direct_model_id(model_id: str) -> bool:
"""Return True for non-empty, whitespace-free direct model ids."""
if not model_id or any(char.isspace() for char in model_id):
return False
return direct_model_name(model_id) is not None
45 changes: 43 additions & 2 deletions agent/core/llm_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

import os

from agent.core.direct_models import (
direct_model_name,
direct_model_provider,
)
from agent.core.hf_tokens import resolve_hf_router_token
from agent.core.local_models import (
LOCAL_MODEL_API_KEY_DEFAULT,
Expand Down Expand Up @@ -47,7 +51,7 @@ class UnsupportedEffortError(ValueError):
"""


def _local_api_base(base_url: str) -> str:
def _openai_api_base(base_url: str) -> str:
base = base_url.strip().rstrip("/")
if base.endswith("/v1"):
return base
Expand Down Expand Up @@ -82,7 +86,36 @@ def _resolve_local_model_params(
)
return {
"model": f"openai/{local_name}",
"api_base": _local_api_base(raw_base),
"api_base": _openai_api_base(raw_base),
"api_key": api_key,
}


def _resolve_direct_model_params(
model_name: str,
reasoning_effort: str | None = None,
strict: bool = False,
) -> dict:
if reasoning_effort and strict:
raise UnsupportedEffortError(
"Direct OpenAI-compatible endpoints don't accept reasoning_effort"
)

upstream_name = direct_model_name(model_name)
provider = direct_model_provider(model_name)
if upstream_name is None or provider is None:
raise ValueError(f"Unsupported direct model id: {model_name}")

raw_base = os.environ.get(provider["base_url_env"]) or provider[
"base_url_default"
]
api_key = os.environ.get(provider["api_key_env"], "").strip()
if not api_key:
raise ValueError(f"{provider['api_key_env']} is required for {model_name}")

return {
"model": f"openai/{upstream_name}",
"api_base": _openai_api_base(raw_base),
"api_key": api_key,
}

Expand All @@ -96,6 +129,9 @@ def _resolve_llm_params(
"""
Build LiteLLM kwargs for a given model id.

• ``atlas/<model>`` — Atlas Cloud's OpenAI-compatible endpoint, using
``ATLASCLOUD_API_KEY`` and optional ``ATLASCLOUD_BASE_URL``.

• ``ollama/<model>``, ``vllm/<model>``, ``lm_studio/<model>``, and
``llamacpp/<model>`` — local OpenAI-compatible endpoints. The id prefix
selects a configurable localhost base URL, and the model suffix is sent
Expand Down Expand Up @@ -126,6 +162,11 @@ def _resolve_llm_params(
if is_reserved_local_model_id(normalized_model):
raise ValueError(f"Unsupported local model id: {normalized_model}")

if direct_model_provider(normalized_model) is not None:
return _resolve_direct_model_params(
normalized_model, reasoning_effort, strict
)

if local_model_provider(normalized_model) is not None:
return _resolve_local_model_params(normalized_model, reasoning_effort, strict)

Expand Down
27 changes: 19 additions & 8 deletions agent/core/model_switcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@

from litellm import acompletion

from agent.core.direct_models import (
DIRECT_MODEL_PREFIXES,
is_direct_model_id,
)
from agent.core.effort_probe import ProbeInconclusive, probe_effort
from agent.core.llm_params import _resolve_llm_params
from agent.core.local_models import (
Expand Down Expand Up @@ -59,6 +63,7 @@ def is_valid_model_id(model_id: str) -> bool:
"""Loose format check — lets users pick any model id.

Accepts:
• atlas/<model> (Atlas Cloud)
• ollama/<model>, vllm/<model>, lm_studio/<model>, llamacpp/<model>
• <org>/<model>[:<tag>] (HF router; tag = provider or policy)
• huggingface/<org>/<model>[:<tag>] (same, optional LiteLLM prefix)
Expand All @@ -69,6 +74,10 @@ def is_valid_model_id(model_id: str) -> bool:
if not model_id:
return False
normalized_model_id = strip_huggingface_model_prefix(model_id) or model_id
if is_direct_model_id(normalized_model_id):
return True
if any(normalized_model_id.startswith(prefix) for prefix in DIRECT_MODEL_PREFIXES):
return False
if is_local_model_id(normalized_model_id):
return True
if is_reserved_local_model_id(normalized_model_id):
Expand All @@ -92,7 +101,7 @@ def _print_hf_routing_info(model_id: str, console) -> bool:
against the router catalog when possible; the probe below covers provider
availability for uncataloged ids.
"""
if is_local_model_id(model_id):
if is_direct_model_id(model_id) or is_local_model_id(model_id):
return True

from agent.core import hf_router_catalog as cat
Expand Down Expand Up @@ -163,6 +172,7 @@ def print_model_listing(config, console) -> None:
console.print(
"\n[dim]Paste any HF model id (e.g. 'MiniMaxAI/MiniMax-M3:novita').\n"
"Add ':fastest', ':cheapest', ':preferred', or ':<provider>' to override routing.\n"
"Use 'atlas/<model>' for Atlas Cloud.\n"
"Use 'ollama/<model>', 'vllm/<model>', 'lm_studio/<model>', or "
"'llamacpp/<model>' for local OpenAI-compatible endpoints.[/dim]"
)
Expand All @@ -173,11 +183,12 @@ def print_invalid_id(arg: str, console) -> None:
console.print(
"[dim]Expected:\n"
" • <org>/<model>[:tag] (HF router — paste from huggingface.co)\n"
" • atlas/<model> (Atlas Cloud)\n"
" • ollama/<model> | vllm/<model> | lm_studio/<model> | llamacpp/<model>[/dim]"
)


async def _probe_local_model(model_id: str) -> None:
async def _probe_direct_model(model_id: str) -> None:
params = _resolve_llm_params(model_id)
await asyncio.wait_for(
acompletion(
Expand Down Expand Up @@ -208,15 +219,15 @@ async def probe_and_switch_model(
* ✗ hard error (auth, model-not-found, quota) — we reject the switch
and keep the current model so the user isn't stranded

For non-local models, transient errors (5xx, timeout) complete the switch
For HF Router models, transient errors (5xx, timeout) complete the switch
with a yellow warning; the next real call re-surfaces the error if it's
persistent. Local models reject every probe error, including timeouts, and
keep the current model.
persistent. Direct and local models reject every probe error, including
timeouts, and keep the current model.
"""
if is_local_model_id(model_id):
console.print(f"[dim]checking local model {model_id}...[/dim]")
if is_direct_model_id(model_id) or is_local_model_id(model_id):
console.print(f"[dim]checking model {model_id}...[/dim]")
try:
await _probe_local_model(model_id)
await _probe_direct_model(model_id)
except Exception as e:
console.print(f"[bold red]Switch failed:[/bold red] {e}")
console.print(f"[dim]Keeping current model: {config.model_name}[/dim]")
Expand Down
13 changes: 10 additions & 3 deletions agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from agent.core import model_switcher
from agent.core.hf_access import fetch_whoami_v2, normalize_hf_user_plan
from agent.core.hf_tokens import resolve_hf_token
from agent.core.direct_models import is_direct_model_id
from agent.core.local_models import is_local_model_id
from agent.core.model_ids import strip_huggingface_model_prefix
from agent.core.session import OpType
Expand Down Expand Up @@ -1193,9 +1194,12 @@ async def main(model: str | None = None, sandbox_tools: bool = False):
local_mode = _is_local_tool_runtime(config)

# HF token — required for Hub-backed models/tools and sandbox tools, but
# not for local LLMs using only local filesystem tools.
# not for direct/local LLMs using only local filesystem tools.
hf_token = resolve_hf_token()
if not hf_token and (not is_local_model_id(config.model_name) or not local_mode):
non_router_model = is_direct_model_id(config.model_name) or is_local_model_id(
config.model_name
)
if not hf_token and (not non_router_model or not local_mode):
hf_token = await _prompt_and_save_hf_token(prompt_session)

# Resolve username and plan from one whoami-v2 request for banner and CTAs.
Expand Down Expand Up @@ -1447,7 +1451,10 @@ async def headless_main(
local_mode = _is_local_tool_runtime(config)

hf_token = resolve_hf_token()
if not hf_token and (not is_local_model_id(config.model_name) or not local_mode):
non_router_model = is_direct_model_id(config.model_name) or is_local_model_id(
config.model_name
)
if not hf_token and (not non_router_model or not local_mode):
print(
"ERROR: No HF token found. Set HF_TOKEN or run `hf auth login`.",
file=sys.stderr,
Expand Down
61 changes: 61 additions & 0 deletions tests/unit/test_cli_local_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from agent.config import load_config
from agent.core import model_switcher
from agent.core.direct_models import is_direct_model_id
from agent.core.local_models import is_local_model_id
from agent.main import CLI_CONFIG_PATH

Expand All @@ -13,11 +14,18 @@ def test_local_model_helper_accepts_supported_prefixes():
assert is_local_model_id("llamacpp/unsloth/Qwen3.5-2B")


def test_direct_model_helper_accepts_atlas_prefix():
assert is_direct_model_id("atlas/deepseek-ai/deepseek-v4-pro")
assert not is_direct_model_id("atlas/")
assert not is_direct_model_id("atlas/model with spaces")


def test_model_switcher_accepts_supported_local_prefixes():
assert model_switcher.is_valid_model_id("ollama/llama3.1:8b")
assert model_switcher.is_valid_model_id("vllm/meta-llama/Llama-3.1-8B")
assert model_switcher.is_valid_model_id("lm_studio/google/gemma-3-4b")
assert model_switcher.is_valid_model_id("llamacpp/llama-3.1-8b")
assert model_switcher.is_valid_model_id("atlas/deepseek-ai/deepseek-v4-pro")


def test_model_switcher_rejects_empty_or_whitespace_local_ids():
Expand All @@ -26,6 +34,8 @@ def test_model_switcher_rejects_empty_or_whitespace_local_ids():
assert not model_switcher.is_valid_model_id("lm_studio/")
assert not model_switcher.is_valid_model_id("llamacpp/")
assert not model_switcher.is_valid_model_id("ollama/llama 3.1")
assert not model_switcher.is_valid_model_id("atlas/")
assert not model_switcher.is_valid_model_id("atlas/model with spaces")


def test_openai_compat_prefix_is_not_supported():
Expand Down Expand Up @@ -104,6 +114,57 @@ def print(self, *args, **kwargs):
assert "extra_body" not in calls[0]


@pytest.mark.asyncio
async def test_probe_and_switch_atlas_model_uses_direct_endpoint(monkeypatch):
calls = []

async def fake_acompletion(**kwargs):
calls.append(kwargs)
return object()

monkeypatch.setenv("ATLASCLOUD_API_KEY", "atlas-secret")
monkeypatch.setattr(model_switcher, "acompletion", fake_acompletion)

class Config:
model_name = "anthropic/claude-opus-4.8:fal-ai"
reasoning_effort = "high"

class Session:
def __init__(self):
self.model_id = None
self.model_effective_effort = {}

def update_model(self, model_id):
self.model_id = model_id

class Console:
def print(self, *args, **kwargs):
pass

session = Session()
model_id = "atlas/deepseek-ai/deepseek-v4-pro"
await model_switcher.probe_and_switch_model(
model_id,
Config(),
session,
Console(),
hf_token=None,
)

assert session.model_id == model_id
assert session.model_effective_effort[model_id] is None
assert calls == [
{
"messages": [{"role": "user", "content": "ping"}],
"max_tokens": 1,
"stream": False,
"model": "openai/deepseek-ai/deepseek-v4-pro",
"api_base": "https://api.atlascloud.ai/v1",
"api_key": "atlas-secret",
}
]


@pytest.mark.asyncio
async def test_probe_and_switch_local_model_rejects_probe_errors(monkeypatch):
async def failing_acompletion(**kwargs):
Expand Down
33 changes: 33 additions & 0 deletions tests/unit/test_cli_rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,39 @@ def fake_banner(*, model=None, hf_user=None, tool_runtime=None):
await main_mod.main()


@pytest.mark.asyncio
async def test_atlas_model_local_runtime_skips_hf_token_prompt(monkeypatch):
class StopAfterBanner(Exception):
pass

async def fail_prompt(_prompt_session):
raise AssertionError("Atlas direct model should not prompt for an HF token")

def fake_banner(*, model=None, hf_user=None, tool_runtime=None):
assert model == "atlas/deepseek-ai/deepseek-v4-pro"
assert hf_user is None
assert tool_runtime == "local filesystem"
raise StopAfterBanner

monkeypatch.setattr(main_mod.os, "system", lambda *_args, **_kwargs: 0)
monkeypatch.setattr(main_mod, "PromptSession", lambda: object())
monkeypatch.setattr(main_mod, "resolve_hf_token", lambda: None)
monkeypatch.setattr(main_mod, "_prompt_and_save_hf_token", fail_prompt)
monkeypatch.setattr(
main_mod,
"load_config",
lambda _path, **_kwargs: SimpleNamespace(
model_name="atlas/deepseek-ai/deepseek-v4-pro",
mcpServers={},
tool_runtime="local",
),
)
monkeypatch.setattr(main_mod, "print_banner", fake_banner)

with pytest.raises(StopAfterBanner):
await main_mod.main()


@pytest.mark.asyncio
async def test_local_model_sandbox_runtime_prompts_for_hf_token(monkeypatch):
class StopAfterBanner(Exception):
Expand Down
Loading
Loading