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