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
32 changes: 32 additions & 0 deletions agent/core/llm_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ def _resolve_hf_router_token(session_hf_token: str | None = None) -> str | None:
# ``extra_body`` field. The probe cascade walks down when a provider rejects
# an accepted-looking value, so this stays intentionally small and generic.
_HF_EFFORTS = {"low", "medium", "high"}
# Gemini 2.5+ thinking models. LiteLLM maps reasoning_effort → thinking
# budget for both the Google AI Studio (``gemini/``) and Vertex AI
# (``vertex_ai/``) routes; "disable" turns thinking off but we model that
# as "no effort" (None) rather than an effort level.
_GEMINI_EFFORTS = {"low", "medium", "high"}

# Prefixes routed directly through LiteLLM's Google Gemini adapters rather
# than the HuggingFace router catch-all.
_GEMINI_PREFIXES = ("gemini/", "vertex_ai/")


def _hf_router_effort_level(reasoning_effort: str) -> str:
Expand Down Expand Up @@ -96,6 +105,16 @@ def _resolve_llm_params(
"""
Build LiteLLM kwargs for a given model id.

• ``gemini/<model>`` / ``vertex_ai/<model>`` — Google Gemini via the AI
Studio API (``GEMINI_API_KEY``) or Vertex AI (GCP creds from
``VERTEX_PROJECT`` / ``VERTEX_LOCATION`` or application-default
credentials). Routed directly through LiteLLM's Google adapters rather
than the HF Router (which doesn't serve Gemini). ``reasoning_effort``
is forwarded as a top-level kwarg; LiteLLM's Gemini adapter translates
it into the thinking budget for 2.5+ thinking models. "minimal"
normalizes to "low". Models that don't support thinking reject it and
the probe cascade drops it.

• ``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 @@ -129,6 +148,19 @@ def _resolve_llm_params(
if local_model_provider(normalized_model) is not None:
return _resolve_local_model_params(normalized_model, reasoning_effort, strict)

if normalized_model.startswith(_GEMINI_PREFIXES):
params = {"model": normalized_model}
if reasoning_effort:
level = "low" if reasoning_effort == "minimal" else reasoning_effort
if level not in _GEMINI_EFFORTS:
if strict:
raise UnsupportedEffortError(
f"Gemini doesn't accept effort={level!r}"
)
else:
params["reasoning_effort"] = level
return params

hf_model = normalized_model
api_key = _resolve_hf_router_token(session_hf_token)
params = {
Expand Down
13 changes: 12 additions & 1 deletion agent/core/model_switcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from litellm import acompletion

from agent.core.effort_probe import ProbeInconclusive, probe_effort
from agent.core.llm_params import _resolve_llm_params
from agent.core.llm_params import _GEMINI_PREFIXES, _resolve_llm_params
from agent.core.local_models import (
LOCAL_MODEL_PREFIXES,
is_local_model_id,
Expand Down Expand Up @@ -48,6 +48,8 @@
{"id": KIMI_K27_CODE_MODEL_ID, "label": "Kimi K2.7 Code"},
{"id": GLM_52_MODEL_ID, "label": "GLM 5.2"},
{"id": DEEPSEEK_V4_PRO_MODEL_ID, "label": "DeepSeek V4 Pro"},
{"id": "gemini/gemini-2.5-pro", "label": "Gemini 2.5 Pro"},
{"id": "gemini/gemini-2.5-flash", "label": "Gemini 2.5 Flash"},
]


Expand All @@ -59,6 +61,7 @@ def is_valid_model_id(model_id: str) -> bool:
"""Loose format check — lets users pick any model id.

Accepts:
• gemini/<model>, vertex_ai/<model> (direct Google API)
• 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 Down Expand Up @@ -95,6 +98,12 @@ def _print_hf_routing_info(model_id: str, console) -> bool:
if is_local_model_id(model_id):
return True

# Gemini goes direct to Google, not through the HF router, so the
# catalog has nothing to say about it. The probe below covers "does
# this model exist".
if model_id.startswith(_GEMINI_PREFIXES):
return True

from agent.core import hf_router_catalog as cat

bare, _, tag = model_id.partition(":")
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 'gemini/<model>' or 'vertex_ai/<model>' for Google Gemini.\n"
"Use 'ollama/<model>', 'vllm/<model>', 'lm_studio/<model>', or "
"'llamacpp/<model>' for local OpenAI-compatible endpoints.[/dim]"
)
Expand All @@ -173,6 +183,7 @@ def print_invalid_id(arg: str, console) -> None:
console.print(
"[dim]Expected:\n"
" • <org>/<model>[:tag] (HF router — paste from huggingface.co)\n"
" • gemini/<model> | vertex_ai/<model>\n"
" • ollama/<model> | vllm/<model> | lm_studio/<model> | llamacpp/<model>[/dim]"
)

Expand Down
59 changes: 59 additions & 0 deletions tests/unit/test_llm_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,65 @@ def test_huggingface_prefix_is_stripped_for_router_calls():
assert params["api_base"] == HF_ROUTER_BASE_URL


def test_gemini_effort_is_forwarded_as_reasoning_effort():
params = _resolve_llm_params(
"gemini/gemini-2.5-pro",
reasoning_effort="high",
strict=True,
)

assert params == {"model": "gemini/gemini-2.5-pro", "reasoning_effort": "high"}


def test_vertex_ai_effort_is_forwarded_as_reasoning_effort():
params = _resolve_llm_params(
"vertex_ai/gemini-2.5-flash",
reasoning_effort="medium",
strict=True,
)

assert params == {
"model": "vertex_ai/gemini-2.5-flash",
"reasoning_effort": "medium",
}


def test_gemini_minimal_effort_normalizes_to_low():
params = _resolve_llm_params(
"gemini/gemini-2.5-pro",
reasoning_effort="minimal",
strict=True,
)

assert params["reasoning_effort"] == "low"


def test_gemini_max_effort_is_rejected_in_strict_mode():
with pytest.raises(UnsupportedEffortError, match="Gemini doesn't accept"):
_resolve_llm_params(
"gemini/gemini-2.5-pro",
reasoning_effort="max",
strict=True,
)


def test_gemini_unsupported_effort_is_dropped_in_non_strict_mode():
params = _resolve_llm_params(
"gemini/gemini-2.5-pro",
reasoning_effort="xhigh",
strict=False,
)

assert params == {"model": "gemini/gemini-2.5-pro"}


def test_gemini_is_not_routed_through_hf_router():
params = _resolve_llm_params("gemini/gemini-2.5-pro")

assert params["model"] == "gemini/gemini-2.5-pro"
assert "api_base" not in params


def test_resolve_ollama_params_adds_v1_and_uses_default_key(monkeypatch):
monkeypatch.delenv("OLLAMA_API_KEY", raising=False)
monkeypatch.setenv("OLLAMA_BASE_URL", "http://localhost:11434")
Expand Down
Loading