diff --git a/README.md b/README.md index 0104cec4..8f012385 100644 --- a/README.md +++ b/README.md @@ -426,7 +426,7 @@ codex -p oss ### Browser > [!WARNING] -> This implementation is purely for educational purposes and should not be used in production. You should implement your own equivalent of the [`YouComBackend`](gpt_oss/tools/simple_browser/backend.py) class with your own browsing environment. Currently we have available `YouComBackend` and `ExaBackend`. +> This implementation is purely for educational purposes and should not be used in production. You should implement your own equivalent of the [`ParallelBackend`](gpt_oss/tools/simple_browser/backend.py) class with your own browsing environment. Currently we have available `ParallelBackend`, `YouComBackend`, and `ExaBackend`. Both gpt-oss models were trained with the capability to browse using the `browser` tool that exposes the following three methods: @@ -441,19 +441,23 @@ To enable the browser tool, you'll have to place the definition into the `system ```python import datetime from gpt_oss.tools.simple_browser import SimpleBrowserTool -from gpt_oss.tools.simple_browser.backend import YouComBackend +from gpt_oss.tools.simple_browser.backend import ParallelBackend from openai_harmony import SystemContent, Message, Conversation, Role, load_harmony_encoding, HarmonyEncodingName encoding = load_harmony_encoding(HarmonyEncodingName.HARMONY_GPT_OSS) # Depending on the choice of the browser backend you need corresponding env variables setup -# In case you use You.com backend requires you to have set the YDC_API_KEY environment variable, -# while for Exa you might need EXA_API_KEY environment variable set -backend = YouComBackend( +# - Parallel backend requires PARALLEL_API_KEY environment variable +# - You.com backend requires YDC_API_KEY environment variable +# - Exa backend requires EXA_API_KEY environment variable +backend = ParallelBackend( source="web", ) +# backend = YouComBackend( +# source="web", +# ) # backend = ExaBackend( -# source="web", +# source="web", # ) browser_tool = SimpleBrowserTool(backend=backend) diff --git a/gpt-oss-mcp-server/browser_server.py b/gpt-oss-mcp-server/browser_server.py index b37a63a6..38b014e9 100644 --- a/gpt-oss-mcp-server/browser_server.py +++ b/gpt-oss-mcp-server/browser_server.py @@ -6,7 +6,7 @@ from mcp.server.fastmcp import Context, FastMCP from gpt_oss.tools.simple_browser import SimpleBrowserTool -from gpt_oss.tools.simple_browser.backend import YouComBackend, ExaBackend +from gpt_oss.tools.simple_browser.backend import YouComBackend, ExaBackend, ParallelBackend @dataclass class AppContext: @@ -15,7 +15,9 @@ class AppContext: def create_or_get_browser(self, session_id: str) -> SimpleBrowserTool: if session_id not in self.browsers: tool_backend = os.getenv("BROWSER_BACKEND", "exa") - if tool_backend == "youcom": + if tool_backend == "parallel": + backend = ParallelBackend(source="web") + elif tool_backend == "youcom": backend = YouComBackend(source="web") elif tool_backend == "exa": backend = ExaBackend(source="web") diff --git a/gpt-oss-mcp-server/reference-system-prompt.py b/gpt-oss-mcp-server/reference-system-prompt.py index 6ddbf7c9..ca31a46a 100644 --- a/gpt-oss-mcp-server/reference-system-prompt.py +++ b/gpt-oss-mcp-server/reference-system-prompt.py @@ -1,7 +1,7 @@ import datetime from gpt_oss.tools.simple_browser import SimpleBrowserTool -from gpt_oss.tools.simple_browser.backend import YouComBackend +from gpt_oss.tools.simple_browser.backend import ParallelBackend from gpt_oss.tools.python_docker.docker_tool import PythonTool from gpt_oss.tokenizer import tokenizer @@ -22,7 +22,7 @@ ReasoningEffort.LOW).with_conversation_start_date( datetime.datetime.now().strftime("%Y-%m-%d"))) -backend = YouComBackend(source="web") +backend = ParallelBackend(source="web") browser_tool = SimpleBrowserTool(backend=backend) system_message_content = system_message_content.with_tools( browser_tool.tool_config) diff --git a/gpt_oss/chat.py b/gpt_oss/chat.py index 4856a397..491dea2f 100644 --- a/gpt_oss/chat.py +++ b/gpt_oss/chat.py @@ -19,7 +19,7 @@ from gpt_oss.tools import apply_patch from gpt_oss.tools.simple_browser import SimpleBrowserTool -from gpt_oss.tools.simple_browser.backend import YouComBackend +from gpt_oss.tools.simple_browser.backend import YouComBackend, ExaBackend, ParallelBackend from gpt_oss.tools.python_docker.docker_tool import PythonTool from openai_harmony import ( @@ -85,9 +85,15 @@ def main(args): ) if args.browser: - backend = YouComBackend( - source="web", - ) + tool_backend = os.getenv("BROWSER_BACKEND", "exa") + if tool_backend == "parallel": + backend = ParallelBackend(source="web") + elif tool_backend == "youcom": + backend = YouComBackend(source="web") + elif tool_backend == "exa": + backend = ExaBackend(source="web") + else: + raise ValueError(f"Invalid browser backend: {tool_backend}") browser_tool = SimpleBrowserTool(backend=backend) system_message_content = system_message_content.with_tools(browser_tool.tool_config) diff --git a/gpt_oss/responses_api/api_server.py b/gpt_oss/responses_api/api_server.py index 009fa8d8..9b4a608a 100644 --- a/gpt_oss/responses_api/api_server.py +++ b/gpt_oss/responses_api/api_server.py @@ -23,7 +23,7 @@ from gpt_oss.tools.python_docker.docker_tool import PythonTool from gpt_oss.tools.simple_browser import SimpleBrowserTool -from gpt_oss.tools.simple_browser.backend import YouComBackend, ExaBackend +from gpt_oss.tools.simple_browser.backend import YouComBackend, ExaBackend, ParallelBackend from .events import ( ResponseCodeInterpreterCallCodeDelta, @@ -1148,7 +1148,9 @@ async def generate(body: ResponsesRequest, request: Request): if use_browser_tool: tool_backend = os.getenv("BROWSER_BACKEND", "exa") - if tool_backend == "youcom": + if tool_backend == "parallel": + backend = ParallelBackend(source="web") + elif tool_backend == "youcom": backend = YouComBackend(source="web") elif tool_backend == "exa": backend = ExaBackend(source="web") diff --git a/gpt_oss/tools/simple_browser/__init__.py b/gpt_oss/tools/simple_browser/__init__.py index da3ff280..a07dcf0d 100644 --- a/gpt_oss/tools/simple_browser/__init__.py +++ b/gpt_oss/tools/simple_browser/__init__.py @@ -1,8 +1,9 @@ from .simple_browser_tool import SimpleBrowserTool -from .backend import ExaBackend, YouComBackend +from .backend import ExaBackend, YouComBackend, ParallelBackend __all__ = [ "SimpleBrowserTool", "ExaBackend", "YouComBackend", + "ParallelBackend", ] diff --git a/gpt_oss/tools/simple_browser/backend.py b/gpt_oss/tools/simple_browser/backend.py index 33daf8d6..6dc51946 100644 --- a/gpt_oss/tools/simple_browser/backend.py +++ b/gpt_oss/tools/simple_browser/backend.py @@ -190,7 +190,7 @@ def _get_api_key(self) -> str: raise BackendError("You.com API key not provided") return key - + async def search( self, query: str, topn: int, session: ClientSession ) -> PageContents: @@ -250,3 +250,99 @@ async def fetch(self, url: str, session: ClientSession) -> PageContents: session=session, ) +@chz.chz(typecheck=True) +class ParallelBackend(Backend): + """Backend that uses the Parallel Search and Extract API.""" + + source: str = chz.field(doc="Description of the backend source") + + BASE_URL: str = "https://api.parallel.ai" + + def _get_api_key(self) -> str: + key = os.environ.get("PARALLEL_API_KEY") + if not key: + raise BackendError("Parallel API key not provided") + return key + + async def _post_parallel(self, session: ClientSession, endpoint: str, payload: dict) -> dict: + """Custom post method for Parallel API which uses different auth header.""" + headers = { + "x-api-key": self._get_api_key(), + "Content-Type": "application/json" + } + # Add parallel-beta header for beta endpoints + if endpoint.startswith("/v1beta/"): + headers["parallel-beta"] = "search-extract-2025-10-10" + + async with session.post(f"{self.BASE_URL}{endpoint}", json=payload, headers=headers) as resp: + if resp.status != 200: + raise BackendError( + f"{self.__class__.__name__} error {resp.status}: {await resp.text()}" + ) + return await resp.json() + + async def search( + self, query: str, topn: int, session: ClientSession + ) -> PageContents: + data = await self._post_parallel( + session, + "/v1beta/search", + { + "objective": query, + "max_results": topn, + }, + ) + # make a simple HTML page to work with browser format + titles_and_urls = [] + if "results" in data: + titles_and_urls = [ + (result["title"], result["url"], " ".join(result.get("excerpts", []) or [])) + for result in data["results"] + ] + html_page = f""" +
+