From 036744e6395ffe98d809aef2b67e2a8658e8fca0 Mon Sep 17 00:00:00 2001 From: chaimaerachdi Date: Thu, 1 Oct 2026 16:33:54 +0200 Subject: [PATCH 1/3] Make the decision deadline configurable: S1A_DECISION_TIMEOUT_S JevModel cut every decision at 5 s, a deadline sized for Jev (0.3 s). A local System One server behind TYPESAFE_API_URL can be slower: on Google Flights, OneJev-27B on an A100 takes about 3.7 s a decision and more on the calendar page, so every run stopped at the eighth step. With 30 s it completed the task with the same 11 actions as Jev. S1A_DECISION_TIMEOUT_S sets the deadline; unset, it stays 5 s, so Jev is unchanged. A value that is not a positive number is a config error. An explicit timeout_s passed to JevModel.from_env still wins. Co-Authored-By: Claude Opus 5.5 --- .env.example | 1 + CHANGELOG.md | 4 ++++ docs/configuration.md | 1 + s1a/decision_models/jev.py | 10 ++++++---- s1a/decision_models/wire.py | 19 +++++++++++++++++++ tests/test_decision_models_jev.py | 29 +++++++++++++++++++++++++++++ 6 files changed, 60 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 58acac1..38dd90d 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,7 @@ MODEL_NAME=google/gemini-2.5-flash # Jev through OpenRouter's /api/alpha/decisions with OPENROUTER_API_KEY: TYPESAFE_API_URL selects that backend. # TYPESAFE_API_URL=https://openrouter.ai/api/alpha/decisions # TYPESAFE_MODEL=typesafe/jev-1.13 +# S1A_DECISION_TIMEOUT_S=5 # seconds per decision; raise it (e.g. 30) for a slower local System One server # Any OpenAI-compatible endpoint instead of OpenRouter: # OPENAI_API_KEY= # OPENAI_BASE_URL=https://api.openai.com/v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b1eca6..b92db68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); ver ### Added +- `S1A_DECISION_TIMEOUT_S`: the deadline of one decision on the `jev` backend, 5 s when unset. A local System One + server behind `TYPESAFE_API_URL` can be slower than Jev: on Google Flights, OneJev-27B on an A100 takes about 3.7 s + a decision and more on the calendar page, so the 5 s deadline stopped every run at the eighth step; with 30 s it + completed the task. `docs/configuration.md`. - The MCP `decide` tool accepts `model="jev"|"laya"|"cua"`, defaulting to `jev`. Local backends use their optional extras and need no Jev API key. - `docs/benchmarks.md`: the Google Flights driver comparison rerun on 2026-09-23 from Poland, every arm three times on diff --git a/docs/configuration.md b/docs/configuration.md index 7c71622..58a69dc 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,6 +11,7 @@ Variables can be exported in your shell or placed in a `.env` file at the root o | `TYPESAFE_API_KEY` | `jev` model | *(unset)* | API key for direct TypeSafe decisions endpoint (`https://api.typesafe.ai/v1/systemone`). | | `TYPESAFE_API_URL` | `jev` model | `https://openrouter.ai/api/alpha/decisions` | Endpoint URL for decisions; defaults to OpenRouter proxy, or can be overridden to a custom proxy URL. | | `TYPESAFE_MODEL` | `jev` model | `typesafe/jev-1.13` | Model identifier when proxying Jev decisions through OpenRouter. | +| `S1A_DECISION_TIMEOUT_S` | `jev` model | `5` | Seconds one decision may take, retries included. Raise it for a slower System One server behind `TYPESAFE_API_URL` (e.g. a local OneJev-27B or CLM-8B: `30`). | | `OPENROUTER_API_KEY` | `jev` model (proxy), chat model fallback | *(unset)* | OpenRouter API key, used for proxying Jev decisions or as a fallback for `LLM_API_KEY`. | | `OPENROUTER_BASE_URL` | chat model fallback | `https://openrouter.ai/api/v1` | Fallback base URL for the chat model when `LLM_BASE_URL` or `OPENAI_BASE_URL` is unset. | | `MODEL_NAME` | chat model (`llm` model, rethink planner, browser agent) | *(unset)* | Model identifier for the chat model (e.g. `google/gemini-2.5-flash` or `claude-fable-5-1`). | diff --git a/s1a/decision_models/jev.py b/s1a/decision_models/jev.py index fce6e39..af4a200 100644 --- a/s1a/decision_models/jev.py +++ b/s1a/decision_models/jev.py @@ -5,7 +5,7 @@ from s1a.decision_models.base import DecisionModel, JevTransport from s1a.decision_models.types import ChoiceQuestion, Json, NoulQuestion, Observation, Question, Reply, Usage -from s1a.decision_models.wire import DECISIONS_TIMEOUT_S, client_from_env, decisions_backend_from_env +from s1a.decision_models.wire import client_from_env, decisions_backend_from_env, decisions_timeout_from_env def jev_question(question: Question) -> Json: @@ -67,6 +67,8 @@ async def close(self) -> None: await self._transport.close() @classmethod - def from_env(cls, *, timeout_s: float = DECISIONS_TIMEOUT_S) -> "JevModel": - """TypeSafe directly or the OpenRouter proxy, whichever the environment names.""" - return cls(client_from_env(decisions_backend_from_env(), timeout_s=timeout_s)) + def from_env(cls, *, timeout_s: float | None = None) -> "JevModel": + """TypeSafe directly or the OpenRouter proxy, whichever the environment names; the decision deadline is + ``timeout_s`` when given, else ``S1A_DECISION_TIMEOUT_S``, else 5 s.""" + deadline = timeout_s if timeout_s is not None else decisions_timeout_from_env() + return cls(client_from_env(decisions_backend_from_env(), timeout_s=deadline)) diff --git a/s1a/decision_models/wire.py b/s1a/decision_models/wire.py index b561cf8..f3dcc80 100644 --- a/s1a/decision_models/wire.py +++ b/s1a/decision_models/wire.py @@ -31,6 +31,25 @@ _MAX_RETRY_AFTER_S = 5.0 +def decisions_timeout_from_env() -> float: + """The deadline for one decision, retries included: ``S1A_DECISION_TIMEOUT_S`` when set, else + ``DECISIONS_TIMEOUT_S``. A local System One server (OneJev, CLM) behind ``TYPESAFE_API_URL`` can take several + seconds on a page with many options, past the 5 s that fits Jev.""" + raw = (os.getenv("S1A_DECISION_TIMEOUT_S") or "").strip() + if not raw: + return DECISIONS_TIMEOUT_S + try: + value = float(raw) + except ValueError: + value = 0.0 + if value <= 0: + raise build_error( + StatusCode.MODEL_SERVICE_CONFIG_ERROR, + error_msg=f"S1A_DECISION_TIMEOUT_S must be a positive number of seconds, not {raw!r}", + ) + return value + + def decisions_backend_from_env() -> str: """``typesafe`` when a TypeSafe key is set and no proxy URL overrides it; otherwise the OpenRouter proxy.""" return "typesafe" if os.getenv("TYPESAFE_API_KEY") and not os.getenv("TYPESAFE_API_URL") else "openrouter" diff --git a/tests/test_decision_models_jev.py b/tests/test_decision_models_jev.py index 01e6930..8ef1c47 100644 --- a/tests/test_decision_models_jev.py +++ b/tests/test_decision_models_jev.py @@ -300,3 +300,32 @@ def test_no_key_is_a_config_error(self) -> None: with self.assertRaises(BaseError) as caught: JevModel.from_env() self.assertEqual(caught.exception.status, StatusCode.MODEL_SERVICE_CONFIG_ERROR) + + +class TestDecisionTimeout(TestCase): + """``S1A_DECISION_TIMEOUT_S`` sets the deadline of one decision; Jev keeps 5 s when it is not set.""" + + ENV = {"TYPESAFE_API_KEY": "", "OPENROUTER_API_KEY": "r", "TYPESAFE_MODEL": "", "TYPESAFE_API_URL": ""} + + def test_the_deadline_stays_5_s_without_the_variable(self) -> None: + with patch.dict(os.environ, {**self.ENV, "S1A_DECISION_TIMEOUT_S": ""}): + decision_model = JevModel.from_env() + self.assertEqual(decision_model._transport._timeout_s, wire.DECISIONS_TIMEOUT_S) + + def test_the_variable_sets_the_deadline(self) -> None: + with patch.dict(os.environ, {**self.ENV, "S1A_DECISION_TIMEOUT_S": "30"}): + decision_model = JevModel.from_env() + self.assertEqual(decision_model._transport._timeout_s, 30.0) + + def test_an_explicit_deadline_wins_over_the_variable(self) -> None: + with patch.dict(os.environ, {**self.ENV, "S1A_DECISION_TIMEOUT_S": "30"}): + decision_model = JevModel.from_env(timeout_s=2.0) + self.assertEqual(decision_model._transport._timeout_s, 2.0) + + def test_a_value_that_is_not_a_positive_number_is_a_config_error(self) -> None: + for raw in ("abc", "0", "-3"): + with self.subTest(raw=raw), patch.dict(os.environ, {**self.ENV, "S1A_DECISION_TIMEOUT_S": raw}): + with self.assertRaises(BaseError) as caught: + JevModel.from_env() + self.assertEqual(caught.exception.status, StatusCode.MODEL_SERVICE_CONFIG_ERROR) + self.assertIn("S1A_DECISION_TIMEOUT_S", str(caught.exception)) From 847db86a82a77f70882c6cd59cd35fc1f96e97de Mon Sep 17 00:00:00 2001 From: chaimaerachdi Date: Thu, 1 Oct 2026 17:53:40 +0200 Subject: [PATCH 2/3] Reject nan and infinite values for S1A_DECISION_TIMEOUT_S float() reads nan, inf and 1e9999; nan passed the `<= 0` guard and made the deadline nan, inf removed the bound. Co-Authored-By: Claude Opus 5.5 --- s1a/decision_models/wire.py | 6 ++++-- tests/test_decision_models_jev.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/s1a/decision_models/wire.py b/s1a/decision_models/wire.py index f3dcc80..e69284e 100644 --- a/s1a/decision_models/wire.py +++ b/s1a/decision_models/wire.py @@ -8,6 +8,7 @@ from __future__ import annotations import asyncio +import math import os import time from typing import Any @@ -42,10 +43,11 @@ def decisions_timeout_from_env() -> float: value = float(raw) except ValueError: value = 0.0 - if value <= 0: + # float() also reads nan, inf and 1e9999 (inf): nan passes `<= 0` and poisons the deadline, inf removes it + if not math.isfinite(value) or value <= 0: raise build_error( StatusCode.MODEL_SERVICE_CONFIG_ERROR, - error_msg=f"S1A_DECISION_TIMEOUT_S must be a positive number of seconds, not {raw!r}", + error_msg=f"S1A_DECISION_TIMEOUT_S must be a finite, positive number of seconds, not {raw!r}", ) return value diff --git a/tests/test_decision_models_jev.py b/tests/test_decision_models_jev.py index 8ef1c47..9db0aa3 100644 --- a/tests/test_decision_models_jev.py +++ b/tests/test_decision_models_jev.py @@ -322,8 +322,8 @@ def test_an_explicit_deadline_wins_over_the_variable(self) -> None: decision_model = JevModel.from_env(timeout_s=2.0) self.assertEqual(decision_model._transport._timeout_s, 2.0) - def test_a_value_that_is_not_a_positive_number_is_a_config_error(self) -> None: - for raw in ("abc", "0", "-3"): + def test_a_value_that_is_not_a_finite_positive_number_is_a_config_error(self) -> None: + for raw in ("abc", "0", "-3", "nan", "inf", "-inf", "1e9999"): with self.subTest(raw=raw), patch.dict(os.environ, {**self.ENV, "S1A_DECISION_TIMEOUT_S": raw}): with self.assertRaises(BaseError) as caught: JevModel.from_env() From de5f5d252ad6f8bbd328068343384f03c36d1f8d Mon Sep 17 00:00:00 2001 From: chaimaerachdi Date: Mon, 5 Oct 2026 01:20:05 +0200 Subject: [PATCH 3/3] Mention S1A_DECISION_TIMEOUT_S where browser-front.md lists DECISIONS_TIMEOUT_S Co-Authored-By: Claude Opus 5.5 --- docs/browser-front.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/browser-front.md b/docs/browser-front.md index 026ff92..a840936 100644 --- a/docs/browser-front.md +++ b/docs/browser-front.md @@ -85,7 +85,7 @@ browser-use/jev-ultrafast (MIT), whose observe-decide-act tick this policy follo | `MAX_PROBE_SETTLE_MS` | 1500 | `load(3 s) + settle + 1 s` last-resort must stay at least 1 s under the 30 s transport timeout | | `WAIT_SETTLE_BUDGET_MS` | 3000 | in-page wait one WAIT streak may spend before BLOCKED | | `ACTION_SETTLE_START_MS`, `ACTION_SETTLE_BUDGET_MS` | 250, 1000 | first and total post-action wait | - | `DECISIONS_TIMEOUT_S` | 5 | Jev answers in 0.4 to 1.3 s through the proxy; a dead connection must not stall a step; one transport retry | + | `DECISIONS_TIMEOUT_S` | 5 | Jev answers in 0.4 to 1.3 s through the proxy; a dead connection must not stall a step; one transport retry; `S1A_DECISION_TIMEOUT_S` overrides it for a slower System One server ([configuration.md](configuration.md)) | | `DECISION_ATTEMPTS` | 2 | one re-ask of the same request when an answer fails validation; a transport error is final | | `BATCH_ACTION_TIMEOUT_MS` | 2000 | a stamp lost to a re-render fails fast; the returned probe re-stamps |