Skip to content
Merged
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
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.
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.lower().startswith("vertex_ai/gemini"):
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
62 changes: 61 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,60 @@ 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",
"vertex_ai/mistral-large",
"vertex_ai/meta/llama-3.3-70b-instruct-maas",
"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