browser-use downgrade to 0.2.5
frdel committed
Aug 27, 2025 at 21:43 UTC
154b92cfd52950547556c675517f4f3c23d5f19b
3 files changed
+53
-219
models.py
+50
-215
@@ -23,7 +23,6 @@ from python.helpers.dotenv import load_dotenv
23
from python.helpers.providers import get_provider_config
24
from python.helpers.rate_limiter import RateLimiter
25
from python.helpers.tokens import approximate_tokens
26
-from python.helpers import dirty_json
26
27
from langchain_core.language_models.chat_models import SimpleChatModel
28
from langchain_core.outputs.chat_generation import ChatGenerationChunk
@@ -91,7 +90,6 @@ class ChatChunk(TypedDict):
90
rate_limiters: dict[str, RateLimiter] = {}
91
api_keys_round_robin: dict[str, int] = {}
92
94
-
93
def get_api_key(service: str) -> str:
94
# get api key for the service
95
key = (
@@ -118,14 +116,7 @@ def get_rate_limiter(
116
limiter.limits["output"] = output or 0
117
return limiter
118
121
-
122
-async def apply_rate_limiter(
123
- model_config: ModelConfig | None,
124
- input_text: str,
125
- rate_limiter_callback: (
126
- Callable[[str, str, int, int], Awaitable[bool]] | None
127
- ) = None,
128
-):
119
+async def apply_rate_limiter(model_config: ModelConfig|None, input_text: str, rate_limiter_callback: Callable[[str, str, int, int], Awaitable[bool]] | None = None):
120
if not model_config:
121
return
122
limiter = get_rate_limiter(
@@ -140,41 +131,25 @@ async def apply_rate_limiter(
131
await limiter.wait(rate_limiter_callback)
132
return limiter
133
143
-
144
-def apply_rate_limiter_sync(
145
- model_config: ModelConfig | None,
146
- input_text: str,
147
- rate_limiter_callback: (
148
- Callable[[str, str, int, int], Awaitable[bool]] | None
149
- ) = None,
150
-):
134
+def apply_rate_limiter_sync(model_config: ModelConfig|None, input_text: str, rate_limiter_callback: Callable[[str, str, int, int], Awaitable[bool]] | None = None):
135
if not model_config:
136
return
137
import asyncio, nest_asyncio
154
-
138
nest_asyncio.apply()
156
- return asyncio.run(
157
- apply_rate_limiter(model_config, input_text, rate_limiter_callback)
158
- )
139
+ return asyncio.run(apply_rate_limiter(model_config, input_text, rate_limiter_callback))
140
141
142
class LiteLLMChatWrapper(SimpleChatModel):
143
model_name: str
144
provider: str
145
kwargs: dict = {}
165
-
146
+
147
class Config:
148
arbitrary_types_allowed = True
149
extra = "allow" # Allow extra attributes
150
validate_assignment = False # Don't validate on assignment
151
171
- def __init__(
172
- self,
173
- model: str,
174
- provider: str,
175
- model_config: Optional[ModelConfig] = None,
176
- **kwargs: Any,
177
- ):
152
+ def __init__(self, model: str, provider: str, model_config: Optional[ModelConfig] = None, **kwargs: Any):
153
model_value = f"{provider}/{model}"
154
super().__init__(model_name=model_value, provider=provider, kwargs=kwargs) # type: ignore
155
# Set A0 model config as instance attribute after parent init
@@ -183,7 +158,7 @@ class LiteLLMChatWrapper(SimpleChatModel):
158
@property
159
def _llm_type(self) -> str:
160
return "litellm-chat"
186
-
161
+
162
def _convert_messages(self, messages: List[BaseMessage]) -> List[dict]:
163
result = []
164
# Map LangChain message types to LiteLLM roles
@@ -194,9 +169,7 @@ class LiteLLMChatWrapper(SimpleChatModel):
169
"tool": "tool",
170
}
171
for m in messages:
197
- m_type = getattr(m, "type", getattr(m, "role", ""))
198
- role = role_mapping.get(m_type, m_type)
199
- content = getattr(m, "content", getattr(m, "text", ""))
172
+ role = role_mapping.get(m.type, m.type)
173
message_dict = {"role": role, "content": m.content}
174
175
# Handle tool calls for AI messages
@@ -242,12 +215,12 @@ class LiteLLMChatWrapper(SimpleChatModel):
215
**kwargs: Any,
216
) -> str:
217
import asyncio
245
-
218
+
219
msgs = self._convert_messages(messages)
247
-
220
+
221
# Apply rate limiting if configured
222
apply_rate_limiter_sync(self.a0_model_conf, str(msgs))
250
-
223
+
224
# Call the model
225
resp = completion(
226
model=self.model_name, messages=msgs, stop=stop, **{**self.kwargs, **kwargs}
@@ -265,12 +238,12 @@ class LiteLLMChatWrapper(SimpleChatModel):
238
**kwargs: Any,
239
) -> Iterator[ChatGenerationChunk]:
240
import asyncio
268
-
241
+
242
msgs = self._convert_messages(messages)
270
-
243
+
244
# Apply rate limiting if configured
245
apply_rate_limiter_sync(self.a0_model_conf, str(msgs))
273
-
246
+
247
for chunk in completion(
248
model=self.model_name,
249
messages=msgs,
@@ -293,10 +266,11 @@ class LiteLLMChatWrapper(SimpleChatModel):
266
**kwargs: Any,
267
) -> AsyncIterator[ChatGenerationChunk]:
268
msgs = self._convert_messages(messages)
296
-
269
+
270
# Apply rate limiting if configured
271
await apply_rate_limiter(self.a0_model_conf, str(msgs))
299
-
272
+
273
+
274
response = await acompletion(
275
model=self.model_name,
276
messages=msgs,
@@ -320,9 +294,7 @@ class LiteLLMChatWrapper(SimpleChatModel):
294
response_callback: Callable[[str, str], Awaitable[None]] | None = None,
295
reasoning_callback: Callable[[str, str], Awaitable[None]] | None = None,
296
tokens_callback: Callable[[str, int], Awaitable[None]] | None = None,
323
- rate_limiter_callback: (
324
- Callable[[str, str, int, int], Awaitable[bool]] | None
325
- ) = None,
297
+ rate_limiter_callback: Callable[[str, str, int, int], Awaitable[bool]] | None = None,
298
**kwargs: Any,
299
) -> Tuple[str, str]:
300
@@ -340,9 +312,7 @@ class LiteLLMChatWrapper(SimpleChatModel):
312
msgs_conv = self._convert_messages(messages)
313
314
# Apply rate limiting if configured
343
- limiter = await apply_rate_limiter(
344
- self.a0_model_conf, str(msgs_conv), rate_limiter_callback
345
- )
315
+ limiter = await apply_rate_limiter(self.a0_model_conf, str(msgs_conv), rate_limiter_callback)
316
317
# call model
318
_completion = await acompletion(
@@ -390,27 +360,7 @@ class LiteLLMChatWrapper(SimpleChatModel):
360
return response, reasoning
361
362
393
-class AsyncAIChatReplacement:
394
- class _Completions:
395
- def __init__(self, wrapper):
396
- self._wrapper = wrapper
397
-
398
- async def create(self, *args, **kwargs):
399
- # call the async _acall method on the wrapper
400
- return await self._wrapper._acall(*args, **kwargs)
401
-
402
- class _Chat:
403
- def __init__(self, wrapper):
404
- self.completions = AsyncAIChatReplacement._Completions(wrapper)
405
-
406
- def __init__(self, wrapper, *args, **kwargs):
407
- self._wrapper = wrapper
408
- self.chat = AsyncAIChatReplacement._Chat(wrapper)
409
-
410
-
411
-from browser_use.llm import ChatOllama, ChatOpenRouter, ChatGoogle, ChatAnthropic, ChatGroq, ChatOpenAI
412
-
413
-class BrowserCompatibleChatWrapper(ChatOpenRouter):
363
+class BrowserCompatibleChatWrapper(LiteLLMChatWrapper):
364
"""
365
A wrapper for browser agent that can filter/sanitize messages
366
before sending them to the LLM.
@@ -418,118 +368,31 @@ class BrowserCompatibleChatWrapper(ChatOpenRouter):
368
369
def __init__(self, *args, **kwargs):
370
turn_off_logging()
421
- # Create the underlying LiteLLM wrapper
422
- self._wrapper = LiteLLMChatWrapper(*args, **kwargs)
371
+ super().__init__(*args, **kwargs)
372
# Browser-use may expect a 'model' attribute
424
- self.model = self._wrapper.model_name
425
- self.kwargs = self._wrapper.kwargs
426
-
427
- @property
428
- def model_name(self) -> str:
429
- return self._wrapper.model_name
373
+ self.model = self.model_name
374
431
- @property
432
- def provider(self) -> str:
433
- return self._wrapper.provider
434
-
435
- def get_client(self, *args, **kwargs): # type: ignore
436
- return AsyncAIChatReplacement(self, *args, **kwargs)
437
-
438
- # -- Gemini helper -----------------------------------------
439
- def _gemini_clean_and_conform(self, text: str):
440
- obj = None
441
- try:
442
- # dirty_json parser is robust enough to handle markdown fences
443
- obj = dirty_json.parse(text)
444
- except Exception:
445
- return None # return None if parsing fails
446
-
447
- if not isinstance(obj, dict):
448
- return None
449
-
450
- # Conform actions to browser-use expectations
451
- if isinstance(obj.get("action"), list):
452
- normalized_actions = []
453
- for item in obj["action"]:
454
- if not isinstance(item, dict):
455
- continue # Skip non-dict items
456
-
457
- action_key, action_value = next(iter(item.items()), (None, None))
458
- if not action_key:
459
- continue
460
-
461
- # Create a mutable copy of the value
462
- v = (action_value or {}).copy()
463
-
464
- if action_key in ("scroll_down", "scroll_up", "scroll"):
465
- is_down = action_key != "scroll_up"
466
- v.setdefault("down", is_down)
467
- v.setdefault("num_pages", 1.0)
468
- normalized_actions.append({"scroll": v})
469
- elif action_key == "go_to_url":
470
- v.setdefault("new_tab", False)
471
- normalized_actions.append({action_key: v})
472
- elif action_key == "done":
473
- if "text" in v and "data" not in v:
474
- t = v.pop("text", "")
475
- v["data"] = {"title": "Task result", "response": t, "page_summary": t}
476
- v.setdefault("success", True)
477
- normalized_actions.append({action_key: v})
478
- else:
479
- normalized_actions.append(item)
480
- obj["action"] = normalized_actions
481
-
482
- return dirty_json.stringify(obj)
483
-
484
- async def _acall(
375
+ def _call(
376
self,
377
messages: List[BaseMessage],
378
stop: Optional[List[str]] = None,
379
run_manager: Optional[CallbackManagerForLLMRun] = None,
380
**kwargs: Any,
490
- ):
491
- # Apply rate limiting if configured
492
- apply_rate_limiter_sync(self._wrapper.a0_model_conf, str(messages))
381
+ ) -> str:
382
+ turn_off_logging()
383
+ result = super()._call(messages, stop, run_manager, **kwargs)
384
+ return result
385
494
- # Call the model
495
- try:
496
- model = kwargs.pop("model", None)
497
- kwrgs = {**self._wrapper.kwargs, **kwargs}
498
-
499
- # hack from browser-use to fix json schema for gemini (additionalProperties, $defs, $ref)
500
- if "response_format" in kwrgs and "json_schema" in kwrgs["response_format"] and model.startswith("gemini/"):
501
- kwrgs["response_format"]["json_schema"] = ChatGoogle("")._fix_gemini_schema(self._wrapper.kwargs)
502
-
503
- resp = await acompletion(
504
- model=self._wrapper.model_name,
505
- messages=messages,
506
- stop=stop,
507
- **kwrgs,
508
- )
509
-
510
- # Gemini: strip triple backticks and conform schema
511
- try:
512
- msg = resp.choices[0].message # type: ignore
513
- if self.provider == "gemini" and isinstance(getattr(msg, "content", None), str):
514
- cleaned = self._gemini_clean_and_conform(msg.content) # type: ignore
515
- if cleaned:
516
- msg.content = cleaned
517
- except Exception:
518
- pass
519
-
520
- except Exception as e:
521
- raise e
522
-
523
- # another hack for browser-use post process invalid jsons
524
- try:
525
- if "response_format" in kwrgs and "json_schema" in kwrgs["response_format"] or "json_object" in kwrgs["response_format"]:
526
- if resp.choices[0].message.content is not None and not resp.choices[0].message.content.startswith("{"): # type: ignore
527
- js = dirty_json.parse(resp.choices[0].message.content) # type: ignore
528
- resp.choices[0].message.content = dirty_json.stringify(js) # type: ignore
529
- except Exception as e:
530
- pass
531
-
532
- return resp
386
+ async def _astream(
387
+ self,
388
+ messages: List[BaseMessage],
389
+ stop: Optional[List[str]] = None,
390
+ run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
391
+ **kwargs: Any,
392
+ ) -> AsyncIterator[ChatGenerationChunk]:
393
+ turn_off_logging()
394
+ async for chunk in super()._astream(messages, stop, run_manager, **kwargs):
395
+ yield chunk
396
397
398
class LiteLLMEmbeddingWrapper(Embeddings):
@@ -537,21 +400,15 @@ class LiteLLMEmbeddingWrapper(Embeddings):
400
kwargs: dict = {}
401
a0_model_conf: Optional[ModelConfig] = None
402
540
- def __init__(
541
- self,
542
- model: str,
543
- provider: str,
544
- model_config: Optional[ModelConfig] = None,
545
- **kwargs: Any,
546
- ):
403
+ def __init__(self, model: str, provider: str, model_config: Optional[ModelConfig] = None, **kwargs: Any):
404
self.model_name = f"{provider}/{model}" if provider != "openai" else model
405
self.kwargs = kwargs
406
self.a0_model_conf = model_config
550
-
407
+
408
def embed_documents(self, texts: List[str]) -> List[List[float]]:
409
# Apply rate limiting if configured
410
apply_rate_limiter_sync(self.a0_model_conf, " ".join(texts))
554
-
411
+
412
resp = embedding(model=self.model_name, input=texts, **self.kwargs)
413
return [
414
item.get("embedding") if isinstance(item, dict) else item.embedding # type: ignore
@@ -561,7 +418,7 @@ class LiteLLMEmbeddingWrapper(Embeddings):
418
def embed_query(self, text: str) -> List[float]:
419
# Apply rate limiting if configured
420
apply_rate_limiter_sync(self.a0_model_conf, text)
564
-
421
+
422
resp = embedding(model=self.model_name, input=[text], **self.kwargs)
423
item = resp.data[0] # type: ignore
424
return item.get("embedding") if isinstance(item, dict) else item.embedding # type: ignore
@@ -570,13 +427,7 @@ class LiteLLMEmbeddingWrapper(Embeddings):
427
class LocalSentenceTransformerWrapper(Embeddings):
428
"""Local wrapper for sentence-transformers models to avoid HuggingFace API calls"""
429
573
- def __init__(
574
- self,
575
- provider: str,
576
- model: str,
577
- model_config: Optional[ModelConfig] = None,
578
- **kwargs: Any,
579
- ):
430
+ def __init__(self, provider: str, model: str, model_config: Optional[ModelConfig] = None, **kwargs: Any):
431
# Clean common user-input mistakes
432
model = model.strip().strip('"').strip("'")
433
@@ -598,18 +449,18 @@ class LocalSentenceTransformerWrapper(Embeddings):
449
self.model = SentenceTransformer(model, **st_kwargs)
450
self.model_name = model
451
self.a0_model_conf = model_config
601
-
452
+
453
def embed_documents(self, texts: List[str]) -> List[List[float]]:
454
# Apply rate limiting if configured
455
apply_rate_limiter_sync(self.a0_model_conf, " ".join(texts))
605
-
456
+
457
embeddings = self.model.encode(texts, convert_to_tensor=False) # type: ignore
458
return embeddings.tolist() if hasattr(embeddings, "tolist") else embeddings # type: ignore
459
460
def embed_query(self, text: str) -> List[float]:
461
# Apply rate limiting if configured
462
apply_rate_limiter_sync(self.a0_model_conf, text)
612
-
463
+
464
embedding = self.model.encode([text], convert_to_tensor=False) # type: ignore
465
result = (
466
embedding[0].tolist() if hasattr(embedding[0], "tolist") else embedding[0]
@@ -634,17 +485,10 @@ def _get_litellm_chat(
485
provider_name, model_name, kwargs = _adjust_call_args(
486
provider_name, model_name, kwargs
487
)
637
- return cls(
638
- provider=provider_name, model=model_name, model_config=model_config, **kwargs
639
- )
488
+ return cls(provider=provider_name, model=model_name, model_config=model_config, **kwargs)
489
490
642
-def _get_litellm_embedding(
643
- model_name: str,
644
- provider_name: str,
645
- model_config: Optional[ModelConfig] = None,
646
- **kwargs: Any,
647
-):
491
+def _get_litellm_embedding(model_name: str, provider_name: str, model_config: Optional[ModelConfig] = None, **kwargs: Any):
492
# Check if this is a local sentence-transformers model
493
if provider_name == "huggingface" and model_name.startswith(
494
"sentence-transformers/"
@@ -654,10 +498,7 @@ def _get_litellm_embedding(
498
provider_name, model_name, kwargs
499
)
500
return LocalSentenceTransformerWrapper(
657
- provider=provider_name,
658
- model=model_name,
659
- model_config=model_config,
660
- **kwargs,
501
+ provider=provider_name, model=model_name, model_config=model_config, **kwargs
502
)
503
504
# use api key from kwargs or env
@@ -670,9 +511,7 @@ def _get_litellm_embedding(
511
provider_name, model_name, kwargs = _adjust_call_args(
512
provider_name, model_name, kwargs
513
)
673
- return LiteLLMEmbeddingWrapper(
674
- model=model_name, provider=provider_name, model_config=model_config, **kwargs
675
- )
514
+ return LiteLLMEmbeddingWrapper(model=model_name, provider=provider_name, model_config=model_config, **kwargs)
515
516
517
def _parse_chunk(chunk: Any) -> ChatChunk:
@@ -760,14 +599,10 @@ def _merge_provider_defaults(
599
return provider_name, kwargs
600
601
763
-def get_chat_model(
764
- provider: str, name: str, model_config: Optional[ModelConfig] = None, **kwargs: Any
765
-) -> LiteLLMChatWrapper:
602
+def get_chat_model(provider: str, name: str, model_config: Optional[ModelConfig] = None, **kwargs: Any) -> LiteLLMChatWrapper:
603
orig = provider.lower()
604
provider_name, kwargs = _merge_provider_defaults("chat", orig, kwargs)
768
- return _get_litellm_chat(
769
- LiteLLMChatWrapper, name, provider_name, model_config, **kwargs
770
- )
605
+ return _get_litellm_chat(LiteLLMChatWrapper, name, provider_name, model_config, **kwargs)
606
607
608
def get_browser_model(
python/tools/browser_agent.py
+1
-2
@@ -148,7 +148,6 @@ class State:
148
),
149
controller=controller,
150
enable_memory=False, # Disable memory to avoid state conflicts
151
- llm_timeout=3000, # TODO rem
151
sensitive_data=cast(dict[str, str | dict[str, str]] | None, secrets_dict or {}), # Pass secrets
152
)
153
except Exception as e:
@@ -388,7 +387,7 @@ class BrowserAgent(Tool):
387
def get_use_agent_log(use_agent: browser_use.Agent | None):
388
result = ["🚦 Starting task"]
389
if use_agent:
391
- action_results = use_agent.history.action_results() or []
390
+ action_results = use_agent.state.history.action_results() or []
391
short_log = []
392
for item in action_results:
393
# final results
requirements.txt
+2
-2
@@ -1,6 +1,6 @@
1
a2wsgi==1.10.8
2
ansio==0.0.1
3
-browser-use==0.5.11
3
+browser-use==0.2.5
4
docker==7.1.0
5
duckduckgo-search==6.1.12
6
faiss-cpu==1.11.0
@@ -19,7 +19,7 @@ langchain-unstructured[all-docs]==0.1.6
19
openai-whisper==20240930
20
lxml_html_clean==0.3.1
21
markdown==3.7
22
-mcp==1.13.1
22
+mcp==1.12.4
23
newspaper3k==0.2.8
24
paramiko==3.5.0
25
playwright==1.52.0