diff --git a/examples/04_llm_specific_tools/01_gpt5_apply_patch_preset.py b/examples/04_llm_specific_tools/01_gpt5_apply_patch_preset.py new file mode 100644 index 0000000000..e0a8957abe --- /dev/null +++ b/examples/04_llm_specific_tools/01_gpt5_apply_patch_preset.py @@ -0,0 +1,47 @@ +"""Example: Using GPT-5 preset with ApplyPatchTool for file editing. + +This example demonstrates how to enable the GPT-5 preset, which swaps the +standard claude-style FileEditorTool for ApplyPatchTool. + +Usage: + export OPENAI_API_KEY=... # or set LLM_API_KEY + # Optionally set a model (we recommend a mini variant if available): + # export LLM_MODEL=( + # "openai/gpt-5.2-mini" # or fallback: "openai/gpt-5.1-mini" or "openai/gpt-5.1" + # ) + + uv run python examples/04_llm_specific_tools/01_gpt5_apply_patch_preset.py +""" + +import os + +from openhands.sdk import LLM, Agent, Conversation +from openhands.tools.preset.gpt5 import get_gpt5_agent + + +# Resolve API key from env +api_key = os.getenv("LLM_API_KEY") or os.getenv("OPENAI_API_KEY") +if not api_key: + raise SystemExit("Please set OPENAI_API_KEY or LLM_API_KEY to run this example.") + +model = os.getenv("LLM_MODEL", "openai/gpt-5.1") +base_url = os.getenv("LLM_BASE_URL", None) + +llm = LLM(model=model, api_key=api_key, base_url=base_url) + +# Build an agent with the GPT-5 preset (ApplyPatchTool-based editing) +agent: Agent = get_gpt5_agent(llm) + +# Run in the current working directory +cwd = os.getcwd() +conversation = Conversation(agent=agent, workspace=cwd) + +conversation.send_message( + "Create (or update) a file named GPT5_DEMO.txt at the repo root with " + "two short lines describing this repository." +) +conversation.run() + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") diff --git a/examples/01_standalone_sdk/30_gemini_file_tools.py b/examples/04_llm_specific_tools/02_gemini_file_tools.py similarity index 89% rename from examples/01_standalone_sdk/30_gemini_file_tools.py rename to examples/04_llm_specific_tools/02_gemini_file_tools.py index 3637f62e2e..65274512fb 100644 --- a/examples/01_standalone_sdk/30_gemini_file_tools.py +++ b/examples/04_llm_specific_tools/02_gemini_file_tools.py @@ -20,8 +20,8 @@ from openhands.tools.terminal import TerminalTool -# Route logs based on whether we're using a proxy (Vertex route) or direct Gemini -_log_dir = "logs/vertex" if os.getenv("LLM_BASE_URL") else "logs/gemini" +# Route logs in their own directory for easy tracing +_log_dir = "logs/gemini" os.makedirs(_log_dir, exist_ok=True) llm = LLM( @@ -49,4 +49,7 @@ conversation.send_message("Now delete the FACTS.txt file you just created.") conversation.run() -print("All done!") + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") diff --git a/openhands-tools/openhands/tools/preset/__init__.py b/openhands-tools/openhands/tools/preset/__init__.py index ae2b82ea60..cc44e66ce4 100644 --- a/openhands-tools/openhands/tools/preset/__init__.py +++ b/openhands-tools/openhands/tools/preset/__init__.py @@ -20,6 +20,7 @@ from .default import get_default_agent from .gemini import get_gemini_agent, get_gemini_tools +from .gpt5 import get_gpt5_agent from .planning import get_planning_agent @@ -27,5 +28,6 @@ "get_default_agent", "get_gemini_agent", "get_gemini_tools", + "get_gpt5_agent", "get_planning_agent", ] diff --git a/openhands-tools/openhands/tools/preset/gpt5.py b/openhands-tools/openhands/tools/preset/gpt5.py new file mode 100644 index 0000000000..05ad2f0583 --- /dev/null +++ b/openhands-tools/openhands/tools/preset/gpt5.py @@ -0,0 +1,78 @@ +"""GPT-5 preset configuration for OpenHands agents. + +This preset uses ApplyPatchTool for file edits instead of the default +claude-style FileEditorTool. It mirrors the Gemini preset pattern by +providing optional helpers without changing global defaults. +""" + +from openhands.sdk import Agent +from openhands.sdk.context.condenser import LLMSummarizingCondenser +from openhands.sdk.context.condenser.base import CondenserBase +from openhands.sdk.llm.llm import LLM +from openhands.sdk.logger import get_logger +from openhands.sdk.tool import Tool + + +logger = get_logger(__name__) + + +def register_gpt5_tools(enable_browser: bool = True) -> None: + """Register the GPT-5 tool set (terminal, apply_patch, task_tracker, browser).""" + from openhands.tools.apply_patch import ApplyPatchTool + from openhands.tools.task_tracker import TaskTrackerTool + from openhands.tools.terminal import TerminalTool + + logger.debug(f"Tool: {TerminalTool.name} registered.") + logger.debug(f"Tool: {ApplyPatchTool.name} registered.") + logger.debug(f"Tool: {TaskTrackerTool.name} registered.") + + if enable_browser: + from openhands.tools.browser_use import BrowserToolSet + + logger.debug(f"Tool: {BrowserToolSet.name} registered.") + + +def get_gpt5_tools(enable_browser: bool = True) -> list[Tool]: + """Get the GPT-5 tool specifications using ApplyPatchTool for edits. + + Args: + enable_browser: Whether to include browser tools. + """ + register_gpt5_tools(enable_browser=enable_browser) + + from openhands.tools.apply_patch import ApplyPatchTool + from openhands.tools.task_tracker import TaskTrackerTool + from openhands.tools.terminal import TerminalTool + + tools: list[Tool] = [ + Tool(name=TerminalTool.name), + Tool(name=ApplyPatchTool.name), + Tool(name=TaskTrackerTool.name), + ] + if enable_browser: + from openhands.tools.browser_use import BrowserToolSet + + tools.append(Tool(name=BrowserToolSet.name)) + return tools + + +def get_gpt5_condenser(llm: LLM) -> CondenserBase: + """Get the default condenser for the GPT-5 preset.""" + return LLMSummarizingCondenser(llm=llm, max_size=80, keep_first=4) + + +def get_gpt5_agent(llm: LLM, cli_mode: bool = False) -> Agent: + """Get an Agent configured with ApplyPatchTool-based file editing. + + This does not change defaults globally; users can opt into it explicitly. + """ + tools = get_gpt5_tools(enable_browser=not cli_mode) + agent = Agent( + llm=llm, + tools=tools, + system_prompt_kwargs={"cli_mode": cli_mode}, + condenser=get_gpt5_condenser( + llm=llm.model_copy(update={"usage_id": "condenser"}) + ), + ) + return agent