Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
9d2442a
feat(tools): add ApplyPatchTool implementing OpenAI cookbook apply_pa…
enyst Nov 14, 2025
c5a0836
chore(example): enable native tool calling (Responses API) for GPT-5.…
enyst Nov 14, 2025
fc86fa7
ApplyPatch: Responses API name-only tool spec; accept 'patch' alias; …
enyst Nov 14, 2025
3c429e1
ApplyPatch: accept 'patch' and expose minimal schema for Responses; f…
enyst Nov 14, 2025
772ce28
Responses: do not echo assistant function_call or reasoning items in …
enyst Nov 14, 2025
f08a048
Responses: include assistant function_call items but omit reasoning p…
enyst Nov 14, 2025
9ae743f
Docs(dev): ApplyPatch + OpenAI Responses integration notes
enyst Nov 14, 2025
02e67d3
Responses serialization: restore reasoning passthrough and keep assis…
enyst Nov 14, 2025
a00128b
Tests: add Responses pairing test for function_call and function_call…
enyst Nov 14, 2025
35e3be3
Examples+Docs: add FileEditor GPT-5.1 example and update ApplyPatch R…
enyst Nov 14, 2025
e910dbf
ApplyPatch: include text output so Responses gets function_call_outpu…
enyst Nov 14, 2025
6cd9639
Tests: add ApplyPatchExecutor tests for create/append/delete and path…
enyst Nov 14, 2025
e772cbb
Docs: update ApplyPatch Responses notes with paired tool output examp…
enyst Nov 14, 2025
0149040
Remove mistakenly committed FACTS.txt test artifact.
enyst Nov 14, 2025
e8978ad
ApplyPatch: drop patch_text alias; use canonical 'patch' name only ac…
enyst Nov 14, 2025
73f7f81
Docs: update ApplyPatch notes to canonicalize on 'patch' only; remove…
enyst Nov 14, 2025
fab4f3b
Revert openhands.sdk.llm.message to main branch version (comments-onl…
enyst Nov 14, 2025
db5b177
Example: simplify tool registration; add ApplyPatch like other tools …
enyst Nov 14, 2025
74f0d15
ApplyPatch: align create() return type to Sequence; add docstrings an…
enyst Nov 15, 2025
a7eed40
Merge branch 'main' into feat/apply-patch-tool-gpt5-1
enyst Nov 22, 2025
d1ce8f3
Delete docs/dev/apply_patch_responses_notes.md
enyst Nov 22, 2025
3078a95
Update examples/01_standalone_sdk/28_apply_patch_with_gpt5_1.py
enyst Nov 22, 2025
8691cec
Restore telemetry.py from main
enyst Nov 22, 2025
1f7e0af
Add apply_patch executor fuzz and error-path tests
enyst Nov 22, 2025
f4c54d2
Harden apply_patch missing-delete handling to DiffError
enyst Nov 22, 2025
dd6c7df
Clarify delete-missing-file test comment
enyst Nov 22, 2025
9dff45e
Add apply_patch multi-hunk and multi-file success tests
enyst Nov 22, 2025
99213f1
Enrich apply_patch gpt-5.1 example with multi-file/multi-hunk flow
enyst Nov 22, 2025
e59b3c4
Delete examples/01_standalone_sdk/29_file_editor_with_gpt5_1.py
enyst Nov 22, 2025
76c21e3
test: clarify apply_patch tmp workspace comment
enyst Nov 24, 2025
7fa1aab
test: move apply_patch executor tests into subpackage
enyst Nov 24, 2025
6c89fd4
feat(preset): use apply_patch by default for GPT-5 models
enyst Nov 24, 2025
5d99105
Merge branch 'main' into feat/apply-patch-tool-gpt5-1
enyst Nov 25, 2025
53f0301
Update allowed-model-stubs.json
enyst Nov 25, 2025
24da42f
Merge branch 'main' into feat/apply-patch-tool-gpt5-1
enyst Nov 25, 2025
9362ad9
Update .github/run-eval/allowed-model-stubs.json
enyst Nov 25, 2025
bd4d338
Update examples/01_standalone_sdk/28_apply_patch_with_gpt5_1.py
enyst Nov 28, 2025
a0e1070
chore(split): keep only ApplyPatch tool, LLM Responses formatter, and…
enyst Nov 29, 2025
adcb46d
Merge branch 'main' into feat/apply-patch-tool-gpt5-1
enyst Nov 29, 2025
be8e7c7
Delete examples/01_standalone_sdk/28_apply_patch_with_gpt5_1.py
enyst Nov 30, 2025
adffe6a
Merge branch 'main' into feat/apply-patch-tool-gpt5-1
enyst Nov 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions examples/01_standalone_sdk/28_apply_patch_with_gpt5_1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Example: Using ApplyPatch tool with GPT-5.1 models via direct OpenAI API.

This demonstrates adding a new ApplyPatch tool to the agent and guiding the
model to create, modify, and delete a FACTS.txt file using 'apply_patch' text.

Notes:
- Works with any GPT-5.1 family model (names start with "gpt-5.1").
- Uses direct OpenAI API through LiteLLM's LLM wrapper with no base_url.
- Requires OPENAI_API_KEY in the environment (or LLM_API_KEY fallback).
"""

from __future__ import annotations

import os

from pydantic import SecretStr

from openhands.sdk import LLM, Agent, Conversation, get_logger
from openhands.sdk.tool import Tool
from openhands.tools.apply_patch import ApplyPatchTool
from openhands.tools.task_tracker import TaskTrackerTool

# from openhands.tools.preset.default import register_default_tools
from openhands.tools.terminal import TerminalTool


logger = get_logger(__name__)

api_key = os.getenv("OPENAI_API_KEY") or os.getenv("LLM_API_KEY")
assert api_key, "Set OPENAI_API_KEY (or LLM_API_KEY) in your environment."

# Choose a GPT-5.1 model; mini is cost-effective for examples
default_model = "openai/gpt-5.1-codex-mini"
model = os.getenv("LLM_MODEL", default_model)
assert model.startswith("openai/gpt-5.1"), "Model must be an openai gpt-5.1 variant"

# Force Chat Completions path by using a non-Responses model alias if needed
if model.startswith("openai/gpt-5.1"):
# Litellm treats 'openai/gpt-5.1' via Responses; to avoid the Responses tool-output
# coupling for this example, we can strip the provider prefix for chat path.
# However, leave as-is to try Responses first; if it errors, instruct user below.
pass

llm = LLM(
model=model,
api_key=SecretStr(api_key),
native_tool_calling=True, # enable native tool calling (Responses API)
reasoning_summary=None, # avoid OpenAI org verification requirement
log_completions=True, # enable telemetry to log input/output payloads
)

# Explicitly register tool classes so Tool(name=...) can resolve
# They self-register into the global registry on import
_ = (TerminalTool, TaskTrackerTool, ApplyPatchTool)

agent = Agent(
llm=llm,
tools=[
Tool(name="terminal"),
Tool(name="task_tracker"),
Tool(name="apply_patch"),
],
system_prompt_kwargs={"cli_mode": True},
)

conversation = Conversation(agent=agent, workspace=os.getcwd())

# Compose instructions guiding the model to use the new tool
prompt = (
"Use the ApplyPatch tool to: "
"1) create a FACTS.txt with a single line 'OpenHands SDK integrates tools.'; "
"2) modify FACTS.txt by appending a second line 'ApplyPatch works.'; "
"3) delete FACTS.txt. "
"Only use the apply_patch format between '*** Begin Patch' and '*** End Patch' "
"when calling the tool."
)

conversation.send_message(prompt)
conversation.run()

print("Conversation finished.")
print(f"EXAMPLE_COST: {llm.metrics.accumulated_cost}")
63 changes: 63 additions & 0 deletions examples/01_standalone_sdk/29_file_editor_with_gpt5_1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Example: Using FileEditor tool with GPT-5.1 models via direct OpenAI API.

This mirrors the ApplyPatch example but uses FileEditor to create/modify/delete
FACTS.txt. Useful for comparing Responses input/output behavior and logs.

Requirements:
- OPENAI_API_KEY in the environment (or LLM_API_KEY)
- Model: any openai/gpt-5.1* variant; default uses openai/gpt-5.1-codex-mini
"""

from __future__ import annotations

import os

from pydantic import SecretStr

from openhands.sdk import LLM, Agent, Conversation, get_logger
from openhands.sdk.tool import Tool
from openhands.tools.file_editor import FileEditorTool
from openhands.tools.task_tracker import TaskTrackerTool
from openhands.tools.terminal import TerminalTool


logger = get_logger(__name__)

api_key = os.getenv("OPENAI_API_KEY") or os.getenv("LLM_API_KEY")
assert api_key, "Set OPENAI_API_KEY (or LLM_API_KEY) in your environment."

model = os.getenv("LLM_MODEL", "openai/gpt-5.1-codex-mini")
assert model.startswith("openai/gpt-5.1"), "Model must be an openai gpt-5.1 variant"

llm = LLM(
model=model,
api_key=SecretStr(api_key),
native_tool_calling=True,
reasoning_summary=None,
log_completions=True,
)

# Ensure registration
_ = (TerminalTool, TaskTrackerTool, FileEditorTool)

agent = Agent(
llm=llm,
tools=[Tool(name="terminal"), Tool(name="task_tracker"), Tool(name="file_editor")],
system_prompt_kwargs={"cli_mode": True},
)

conversation = Conversation(agent=agent, workspace=os.getcwd())

prompt = (
"You must use tools to perform all actions. Do not merely describe actions. "
"Use the FileEditor tool to: "
"1) create a FACTS.txt with a single line 'OpenHands SDK integrates tools.'; "
"2) modify FACTS.txt by appending a second line 'FileEditor works.'; "
"3) delete FACTS.txt using the terminal tool with: rm FACTS.txt."
)

conversation.send_message(prompt)
conversation.run()

print("Conversation finished.")
print(f"EXAMPLE_COST: {llm.metrics.accumulated_cost}")
31 changes: 30 additions & 1 deletion openhands-sdk/openhands/sdk/llm/utils/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,24 @@ class Telemetry(BaseModel):
# ---------- Lifecycle ----------
def on_request(self, log_ctx: dict | None) -> None:
self._req_start = time.time()
self._req_ctx = log_ctx or {}
# Trim heavy fields in request context for readability
ctx = log_ctx or {}
# Compact tools into minimal metadata if present
tools = ctx.get("tools")
if isinstance(tools, (list, tuple)):
compact_tools = []
for t in tools:
try:
compact_tools.append(
{
"name": getattr(t, "name", getattr(t, "title", "")),
"kind": t.__class__.__name__,
}
)
except Exception:
compact_tools.append(str(t))
ctx["tools"] = compact_tools
self._req_ctx = ctx

def on_response(
self,
Expand Down Expand Up @@ -239,6 +256,18 @@ def log_llm_call(
resp # ModelResponse | ResponsesAPIResponse;
# serialized via _safe_json
)
# Omit extremely large system instructions from logs for readability
try:
if (
isinstance(data["response"], dict)
and "instructions" in data["response"]
):
# Replace with trimmed preview and length
instr = data["response"].get("instructions") or ""
data["response"]["instructions_len"] = len(instr)
data["response"]["instructions"] = "[omitted]"
except Exception:
pass
data["cost"] = float(cost or 0.0)
data["timestamp"] = time.time()
data["latency_sec"] = self._last_latency
Expand Down
4 changes: 4 additions & 0 deletions openhands-tools/openhands/tools/apply_patch/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .definition import ApplyPatchTool


__all__ = ["ApplyPatchTool"]
Loading
Loading