Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion .github/workflows/nightly-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ env:
NOTTE_API_KEY: ${{ secrets.NOTTE_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
# Use the Vertex credentials configured below for the local-agent example.
NOTTE_EXAMPLE_MODEL: "vertex_ai/gemini-2.5-flash"
NOTTE_EXAMPLE_MODEL: "vertex_ai/gemini-3.5-flash"
GITHUB_COM_EMAIL: ${{ secrets.BOT_GITHUB_COM_EMAIL }}
GITHUB_COM_PASSWORD: ${{ secrets.BOT_GITHUB_COM_PASSWORD }}
GITHUB_COM_MFA_SECRET: ${{ secrets.BOT_GITHUB_COM_MFA_SECRET }}
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ from dotenv import load_dotenv
load_dotenv()

with notte.Session(headless=False) as session:
model = os.getenv("NOTTE_EXAMPLE_MODEL", "gemini/gemini-2.5-flash")
model = os.getenv("NOTTE_EXAMPLE_MODEL", "gemini/gemini-3.5-flash")
agent = notte.Agent(session=session, reasoning_model=model, max_steps=10)
response = agent.run(task="Find three cat memes on Google Images and describe them")
```
Expand All @@ -70,7 +70,7 @@ import os
client = NotteClient(api_key=os.getenv("NOTTE_API_KEY"))

with client.Session(open_viewer=True) as session:
agent = client.Agent(session=session, reasoning_model='gemini/gemini-2.5-flash', max_steps=30)
agent = client.Agent(session=session, reasoning_model='gemini/gemini-3.5-flash', max_steps=30)
response = agent.run(task="doom scroll cat memes on google images")
```

Expand Down Expand Up @@ -108,7 +108,7 @@ class TopPosts(BaseModel):

client = NotteClient()
with client.Session(open_viewer=True, browser_type="chrome") as session:
agent = client.Agent(session=session, reasoning_model='gemini/gemini-2.5-flash', max_steps=15)
agent = client.Agent(session=session, reasoning_model='gemini/gemini-3.5-flash', max_steps=15)
response = agent.run(
task="Go to Hacker News (news.ycombinator.com) and extract the top 5 posts with their titles, URLs, points, authors, and comment counts.",
response_format=TopPosts,
Expand Down
2 changes: 1 addition & 1 deletion docs/src/snippets/integrations/mcp/google_adk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ toolset = McpToolset(

agent = LlmAgent(
name="notte_browser_agent",
model="gemini-2.5-flash",
model="gemini-3.5-flash",
instruction="You browse the web using the Notte tools.",
tools=[toolset],
)
Expand Down
2 changes: 1 addition & 1 deletion docs/src/testers/integrations/mcp/google_adk.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

agent = LlmAgent(
name="notte_browser_agent",
model="gemini-2.5-flash",
model="gemini-3.5-flash",
instruction="You browse the web using the Notte tools.",
tools=[toolset],
)
Expand Down
2 changes: 1 addition & 1 deletion node-sdk/test/agent.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe('Agent Integration Tests', () => {
await client.Session({ proxies: false, headless: true, idle_timeout_minutes: 2 }).use(async (session) => {
const agent = client.Agent({
session,
reasoning_model: 'gemini/gemini-2.5-flash',
reasoning_model: 'gemini/gemini-3.5-flash',
max_steps: 3
});

Expand Down
4 changes: 2 additions & 2 deletions packages/notte-core/src/notte_core/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ def has_apikey_in_env(self) -> bool:

class LlmModel(StrEnum):
openai = "openai/gpt-4o"
gemini = "gemini/gemini-2.5-flash"
gemini_vertex = "vertex_ai/gemini-2.5-flash"
gemini = "gemini/gemini-3.5-flash"
gemini_vertex = "vertex_ai/gemini-3.5-flash"
Comment thread
giordano-lucas marked this conversation as resolved.
Resolved
gemma = "openrouter/google/gemma-3-27b-it"
cerebras = "cerebras/gpt-oss-120b"
groq = "groq/gpt-oss-120b"
Expand Down
16 changes: 16 additions & 0 deletions packages/notte-llm/src/notte_llm/engine.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import os
import re
from collections.abc import Iterable
from dataclasses import dataclass
Expand Down Expand Up @@ -166,6 +167,18 @@ def is_gemini_model(model: str) -> bool:
return "gemini" in model_lower or "vertex_ai" in model_lower


def get_vertex_location(model: str) -> str | None:
"""Vertex AI location to use for a Gemini model, or None for other models.

litellm defaults to us-central1, but recent Gemini models (e.g. gemini-3.5-flash) are only
served from the global endpoint and a few regions. Default to global unless a location is
already configured.
"""
if not model.startswith("vertex_ai/") or not is_gemini_model(model):
Comment thread
giordano-lucas marked this conversation as resolved.
Resolved
return None
return litellm.vertex_location or os.getenv("VERTEXAI_LOCATION") or os.getenv("VERTEX_LOCATION") or "global"


def is_anthropic_model(model: str) -> bool:
"""Check if the model is an Anthropic model."""
model_lower = model.lower()
Expand Down Expand Up @@ -558,6 +571,8 @@ async def completion(
model = self._get_model(model)
# Apply model-specific temperature overrides
temperature = LlmModel.get_temperature(model, temperature)
vertex_location = get_vertex_location(model)
vertex_kwargs: dict[str, Any] = {"vertex_location": vertex_location} if vertex_location is not None else {}
try:
response = await litellm.acompletion( # pyright: ignore [reportUnknownMemberType]
model,
Expand All @@ -572,6 +587,7 @@ async def completion(
# indefinitely. Without this, httpx has no read timeout and silent server
# stalls hang the whole agent run.
timeout=60,
**vertex_kwargs,
)
# Cast to ModelResponse since we know it's not streaming in this case
return cast(ModelResponse, response)
Expand Down
12 changes: 11 additions & 1 deletion tests/config/test_default_config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pytest
from notte_core.common.config import NotteConfig, config
from notte_core.common.config import LlmModel, NotteConfig, config
from pydantic import ValidationError


Expand All @@ -25,6 +25,16 @@ def test_default_config():
assert config.evaluate_js_max_result_bytes == 16 * 1024 * 1024


def test_default_llm_model_uses_vertex_with_google_credentials(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("GOOGLE_APPLICATION_CREDENTIALS", "credentials.json")
assert LlmModel.default() == "vertex_ai/gemini-3.5-flash"


def test_default_llm_model_uses_gemini_without_google_credentials(monkeypatch: pytest.MonkeyPatch):
monkeypatch.delenv("GOOGLE_APPLICATION_CREDENTIALS", raising=False)
assert LlmModel.default() == "gemini/gemini-3.5-flash"


def test_default_is_headless():
assert config.headless, "headless should be true by default for tests"

Expand Down
10 changes: 5 additions & 5 deletions tests/config/test_openrouter_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def test_anthropic_model_returns_none(self) -> None:
assert LlmModel.get_openrouter_provider("anthropic/claude-sonnet-4-5-20250929") is None

def test_gemini_model_returns_none(self) -> None:
assert LlmModel.get_openrouter_provider("gemini/gemini-2.5-flash") is None
assert LlmModel.get_openrouter_provider("gemini/gemini-3.5-flash") is None


class TestGetOpenrouterModel:
Expand All @@ -82,12 +82,12 @@ def test_claude_sonnet_conversion(self) -> None:
assert result == "openrouter/anthropic/claude-sonnet-4-5"

def test_vertex_ai_conversion(self) -> None:
result = LlmModel.get_openrouter_model("vertex_ai/gemini-2.5-flash")
assert result == "openrouter/google/gemini-2.5-flash"
result = LlmModel.get_openrouter_model("vertex_ai/gemini-3.5-flash")
assert result == "openrouter/google/gemini-3.5-flash"

def test_gemini_prefix_conversion(self) -> None:
result = LlmModel.get_openrouter_model("gemini/gemini-2.5-flash")
assert result == "openrouter/google/gemini-2.5-flash"
result = LlmModel.get_openrouter_model("gemini/gemini-3.5-flash")
assert result == "openrouter/google/gemini-3.5-flash"

def test_kimi_conversion(self) -> None:
result = LlmModel.get_openrouter_model("moonshot/kimi-k2.5")
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/sdk/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def test_agent_gemini_form_fill_no_null_fields():
_ = load_dotenv()
client = NotteClient()
with client.Session(proxies=False) as session:
agent = client.Agent(session=session, max_steps=3, reasoning_model="vertex_ai/gemini-2.5-flash")
agent = client.Agent(session=session, max_steps=3, reasoning_model="vertex_ai/gemini-3.5-flash")
response = agent.run(
task="Ignore the web page. Simply return a form fill action with email='lucas@notte.cc' and password='123456'. Stop immediately after this",
url="https://github.com/login",
Expand All @@ -49,7 +49,7 @@ def test_local_agent_gemini_form_fill_no_null_fields():
"""Local agent: Gemini should only fill requested fields, not all fields with null."""
_ = load_dotenv()
with notte.Session(headless=True) as session:
agent = notte.Agent(session=session, max_steps=3, reasoning_model="vertex_ai/gemini-2.5-flash")
agent = notte.Agent(session=session, max_steps=3, reasoning_model="vertex_ai/gemini-3.5-flash")
response = agent.run(
task="Ignore the web page. Simply return a form fill action with email='lucas@notte.cc' and password='123456'. Stop immediately after this",
url="https://console.notte.cc/login",
Expand All @@ -65,7 +65,7 @@ def test_start_agent_with_gemini_reasoning():
_ = load_dotenv()
notte = NotteClient()
with notte.Session(proxies=False) as session:
agent = notte.Agent(session=session, reasoning_model="gemini/gemini-2.5-flash", max_steps=3)
agent = notte.Agent(session=session, reasoning_model="gemini/gemini-3.5-flash", max_steps=3)
_ = agent.run(task="Go notte.cc and describe the page")
resp = agent.status()
assert resp.status == "closed"
Expand Down
60 changes: 59 additions & 1 deletion tests/llms/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
import pytest
from litellm import Message
from notte_core.errors.base import ErrorConfig
from notte_llm.engine import LLMEngine, StructuredContent, fix_schema_for_gemini, fix_schema_for_openai
from notte_llm.engine import (
LLMEngine,
StructuredContent,
fix_schema_for_gemini,
fix_schema_for_openai,
get_vertex_location,
)


@pytest.fixture
Expand Down Expand Up @@ -43,6 +49,58 @@ async def test_completion_error(llm_engine: LLMEngine) -> None:
assert "API Error" in str(exc_info.value)


class TestGetVertexLocation:
@pytest.fixture(autouse=True)
def _no_configured_location(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr("litellm.vertex_location", None)
monkeypatch.delenv("VERTEXAI_LOCATION", raising=False)
monkeypatch.delenv("VERTEX_LOCATION", raising=False)

def test_vertex_gemini_defaults_to_global(self) -> None:
assert get_vertex_location("vertex_ai/gemini-3.5-flash") == "global"

def test_env_location_is_respected(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("VERTEXAI_LOCATION", "europe-west2")
assert get_vertex_location("vertex_ai/gemini-3.5-flash") == "europe-west2"

def test_litellm_location_is_respected(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr("litellm.vertex_location", "us")
assert get_vertex_location("vertex_ai/gemini-3.5-flash") == "us"

@pytest.mark.parametrize(
"model",
[
"gemini/gemini-3.5-flash",
"openrouter/google/gemini-3.5-flash",
"vertex_ai/claude-sonnet-4-5",
"openai/gpt-4o",
],
)
def test_other_models_have_no_location(self, model: str) -> None:
assert get_vertex_location(model) is None


@pytest.mark.asyncio
async def test_completion_passes_global_location_for_vertex_gemini(
llm_engine: LLMEngine, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr("litellm.vertex_location", None)
monkeypatch.delenv("VERTEXAI_LOCATION", raising=False)
monkeypatch.delenv("VERTEX_LOCATION", raising=False)
with patch("litellm.acompletion", return_value=Mock()) as acompletion:
await llm_engine.completion(
messages=[Message(role="user", content="Hello")], model="vertex_ai/gemini-3.5-flash"
)
assert acompletion.call_args.kwargs["vertex_location"] == "global"


@pytest.mark.asyncio
async def test_completion_omits_location_for_other_models(llm_engine: LLMEngine) -> None:
with patch("litellm.acompletion", return_value=Mock()) as acompletion:
await llm_engine.completion(messages=[Message(role="user", content="Hello")], model="gpt-3.5-turbo")
assert "vertex_location" not in acompletion.call_args.kwargs


class TestStructuredContent:
def test_extract_with_outer_tag(self):
structure = StructuredContent(outer_tag="response")
Expand Down
2 changes: 1 addition & 1 deletion tests/llms/test_openrouter_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# Update this list as new models become available
OPENROUTER_MODELS = [
"google/gemini-3-flash-preview",
"google/gemini-2.5-flash",
"google/gemini-3.5-flash",
"anthropic/claude-opus-4.6",
"anthropic/claude-sonnet-4.6",
"anthropic/claude-haiku-4.5",
Expand Down
Loading