openhands-agent-server 1.45.0__tar.gz → 1.47.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/PKG-INFO +1 -1
  2. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/canvas_extensions_router.py +24 -5
  3. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/event_service.py +42 -0
  4. openhands_agent_server-1.47.0/openhands/agent_server/openai/models.py +133 -0
  5. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/openai/router.py +37 -0
  6. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/openai/service.py +327 -58
  7. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/profiles_router.py +17 -0
  8. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/session_protocol.py +10 -0
  9. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/session_socket.py +64 -7
  10. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands_agent_server.egg-info/PKG-INFO +1 -1
  11. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/pyproject.toml +1 -1
  12. openhands_agent_server-1.45.0/openhands/agent_server/openai/models.py +0 -59
  13. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/__init__.py +0 -0
  14. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/__main__.py +0 -0
  15. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/_secret_redaction.py +0 -0
  16. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/_secrets_exposure.py +0 -0
  17. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/agent_profiles_router.py +0 -0
  18. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/api.py +0 -0
  19. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/auth_router.py +0 -0
  20. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/bash_router.py +0 -0
  21. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/bash_service.py +0 -0
  22. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/canvas_extensions/__init__.py +0 -0
  23. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/canvas_extensions/installed.py +0 -0
  24. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/canvas_extensions/manifest.py +0 -0
  25. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/config.py +0 -0
  26. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/conversation_lease.py +0 -0
  27. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/conversation_router.py +0 -0
  28. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/conversation_service.py +0 -0
  29. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/credential_binding.py +0 -0
  30. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/dependencies.py +0 -0
  31. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/desktop_router.py +0 -0
  32. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/docker/Dockerfile +0 -0
  33. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/docker/build.py +0 -0
  34. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/env_parser.py +0 -0
  35. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/event_router.py +0 -0
  36. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/file_router.py +0 -0
  37. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/git_router.py +0 -0
  38. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/hooks_router.py +0 -0
  39. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/hooks_service.py +0 -0
  40. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/init_router.py +0 -0
  41. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/llm_router.py +0 -0
  42. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/logging_config.py +0 -0
  43. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/mcp_oauth_store.py +0 -0
  44. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/mcp_router.py +0 -0
  45. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/middleware.py +0 -0
  46. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/models.py +0 -0
  47. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/openai/__init__.py +0 -0
  48. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/openapi.py +0 -0
  49. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/persistence/__init__.py +0 -0
  50. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/persistence/models.py +0 -0
  51. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/persistence/store.py +0 -0
  52. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/plugins_router.py +0 -0
  53. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/plugins_service.py +0 -0
  54. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/provider_connections_router.py +0 -0
  55. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/pub_sub.py +0 -0
  56. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/py.typed +0 -0
  57. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/server_details_router.py +0 -0
  58. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/settings_router.py +0 -0
  59. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/skills_router.py +0 -0
  60. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/skills_service.py +0 -0
  61. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/sockets.py +0 -0
  62. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/sub_agents_router.py +0 -0
  63. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/telemetry/__init__.py +0 -0
  64. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/telemetry/factory.py +0 -0
  65. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/telemetry/http_exporter.py +0 -0
  66. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/telemetry/models.py +0 -0
  67. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/telemetry/policy.py +0 -0
  68. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/telemetry/posthog_exporter.py +0 -0
  69. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/telemetry/sanitizer.py +0 -0
  70. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/telemetry/service.py +0 -0
  71. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/telemetry/sink.py +0 -0
  72. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/telemetry/subscriber.py +0 -0
  73. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/telemetry_types.py +0 -0
  74. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/tool_preload_service.py +0 -0
  75. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/tool_router.py +0 -0
  76. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/utils.py +0 -0
  77. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
  78. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
  79. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/vscode_router.py +0 -0
  80. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/vscode_service.py +0 -0
  81. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/workspace_router.py +0 -0
  82. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands/agent_server/workspaces_router.py +0 -0
  83. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands_agent_server.egg-info/SOURCES.txt +0 -0
  84. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
  85. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands_agent_server.egg-info/entry_points.txt +0 -0
  86. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands_agent_server.egg-info/requires.txt +0 -0
  87. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/openhands_agent_server.egg-info/top_level.txt +0 -0
  88. {openhands_agent_server-1.45.0 → openhands_agent_server-1.47.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-agent-server
3
- Version: 1.45.0
3
+ Version: 1.47.0
4
4
  Summary: OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -24,8 +24,12 @@ from openhands.agent_server.canvas_extensions.installed import (
24
24
  list_installed_canvas_extensions,
25
25
  uninstall_canvas_extension,
26
26
  )
27
- from openhands.agent_server.canvas_extensions.manifest import CanvasExtensionManifest
27
+ from openhands.agent_server.canvas_extensions.manifest import (
28
+ MANIFEST_FILENAME,
29
+ CanvasExtensionManifest,
30
+ )
28
31
  from openhands.sdk.extensions.fetch import ExtensionFetchError
32
+ from openhands.sdk.utils.redact import redact_url_credentials
29
33
 
30
34
 
31
35
  canvas_extensions_router = APIRouter(
@@ -144,7 +148,7 @@ class UninstallCanvasExtensionResponse(BaseModel):
144
148
  "/install",
145
149
  response_model=InstalledCanvasExtensionResponse,
146
150
  responses={
147
- 400: {"description": "Failed to fetch canvas extension source"},
151
+ 400: {"description": "Could not read canvas extension source"},
148
152
  409: {"description": "Canvas extension already installed (use force=true)"},
149
153
  422: {"description": "Invalid canvas extension (bad manifest, etc.)"},
150
154
  },
@@ -173,12 +177,27 @@ def install_canvas_extension_endpoint(
173
177
  status_code=409,
174
178
  detail="Canvas extension already installed. Use force=true to overwrite.",
175
179
  )
176
- except ExtensionFetchError:
180
+ except ExtensionFetchError as e:
181
+ # Pass the specific reason through -- which of source/ref/repo_path is
182
+ # wrong is the whole diagnostic, and a generic message misattributes a
183
+ # bad repo_path to the source. Redacted in case a URL carried a token.
184
+ # Deliberately not worded "failed to fetch": the canvas client treats
185
+ # that phrase as a network outage and hides the reason behind a
186
+ # "Disconnected, check your network" toast.
177
187
  raise HTTPException(
178
188
  status_code=400,
179
189
  detail=(
180
- "Failed to fetch canvas extension source. "
181
- "Check that the source is valid."
190
+ "Could not read canvas extension source: "
191
+ f"{redact_url_credentials(str(e))}"
192
+ ),
193
+ )
194
+ except FileNotFoundError:
195
+ raise HTTPException(
196
+ status_code=422,
197
+ detail=(
198
+ f"No {MANIFEST_FILENAME} found at the resolved location. "
199
+ "Source plus Path must point at the extension's own "
200
+ "directory, not the repository root."
182
201
  ),
183
202
  )
184
203
  except (ValidationError, ValueError, OSError):
@@ -31,6 +31,7 @@ from openhands.sdk.agent.acp_file_credentials import (
31
31
  CODEX_AUTH_SECRET_NAME,
32
32
  is_valid_codex_auth,
33
33
  )
34
+ from openhands.sdk.agent.stream_context import StreamProgress
34
35
  from openhands.sdk.conversation.base import BaseConversation
35
36
  from openhands.sdk.conversation.events_list_base import EventsListBase
36
37
  from openhands.sdk.conversation.exceptions import ConversationRunError
@@ -135,6 +136,11 @@ class EventService:
135
136
  _pub_sub: PubSub[Event] = field(
136
137
  default_factory=lambda: PubSub[Event](max_subscribers=50), init=False
137
138
  )
139
+ # Its own fan-out, not the event bus: frames are not events, and only the
140
+ # session socket consumes them.
141
+ _stream_pub_sub: PubSub[StreamProgress] = field(
142
+ default_factory=lambda: PubSub[StreamProgress](max_subscribers=50), init=False
143
+ )
138
144
  _run_task: asyncio.Task | None = field(default=None, init=False)
139
145
  # Set when a send_message(run=True) is rejected because a run is still
140
146
  # wrapping up; consumed by _run_and_publish to re-run the stranded message.
@@ -848,6 +854,19 @@ class EventService:
848
854
  async def unsubscribe_from_events(self, subscriber_id: UUID) -> bool:
849
855
  return self._pub_sub.unsubscribe(subscriber_id)
850
856
 
857
+ async def subscribe_to_stream_progress(
858
+ self, subscriber: Subscriber[StreamProgress]
859
+ ) -> UUID:
860
+ """Register for stream-progress frames.
861
+
862
+ No initial push, unlike :meth:`subscribe_to_events`: a client that
863
+ connects mid-stream gets the real text with the durable event.
864
+ """
865
+ return self._stream_pub_sub.subscribe(subscriber)
866
+
867
+ async def unsubscribe_from_stream_progress(self, subscriber_id: UUID) -> bool:
868
+ return self._stream_pub_sub.unsubscribe(subscriber_id)
869
+
851
870
  def _emit_event_from_thread(self, event: Event) -> None:
852
871
  """Helper to safely emit events from non-async contexts (e.g., callbacks).
853
872
 
@@ -1075,6 +1094,16 @@ class EventService:
1075
1094
  with suppress(RuntimeError): # main loop already closed during teardown
1076
1095
  asyncio.run_coroutine_threadsafe(self._pub_sub(event), self._main_loop)
1077
1096
 
1097
+ def _publish_stream_progress(frame: StreamProgress) -> None:
1098
+ # Same cross-thread hop as _publish_stream_delta: called from the
1099
+ # run thread, or the ACP portal thread.
1100
+ if not self._main_loop or not self._main_loop.is_running():
1101
+ return
1102
+ with suppress(RuntimeError): # main loop already closed during teardown
1103
+ asyncio.run_coroutine_threadsafe(
1104
+ self._stream_pub_sub(frame), self._main_loop
1105
+ )
1106
+
1078
1107
  def _token_streaming_callback(chunk: LLMStreamChunk | str) -> None:
1079
1108
  if isinstance(chunk, str):
1080
1109
  _publish_stream_delta(content=chunk)
@@ -1099,6 +1128,7 @@ class EventService:
1099
1128
  conversation_id=self.stored.id,
1100
1129
  callbacks=[self._callback_wrapper],
1101
1130
  token_callbacks=([_token_streaming_callback] if streaming_enabled else []),
1131
+ stream_callbacks=[_publish_stream_progress],
1102
1132
  max_iteration_per_run=self.stored.max_iterations,
1103
1133
  stuck_detection=self.stored.stuck_detection,
1104
1134
  visualizer=None,
@@ -1357,6 +1387,17 @@ class EventService:
1357
1387
  # Create task but don't await it - runs in background
1358
1388
  self._run_task = asyncio.create_task(_run_and_publish())
1359
1389
 
1390
+ async def wait_for_run_completion(
1391
+ self, timeout: float | None = None
1392
+ ) -> ConversationExecutionStatus:
1393
+ """Wait for the active run task without cancelling it on timeout."""
1394
+ run_task = self._run_task
1395
+ if run_task is not None:
1396
+ done, _ = await asyncio.wait({run_task}, timeout=timeout)
1397
+ if not done:
1398
+ raise TimeoutError("Conversation run timed out")
1399
+ return await self._get_execution_status()
1400
+
1360
1401
  async def start_goal_loop(
1361
1402
  self,
1362
1403
  objective: str,
@@ -1774,6 +1815,7 @@ class EventService:
1774
1815
  self._run_task = None
1775
1816
 
1776
1817
  await self._pub_sub.close()
1818
+ await self._stream_pub_sub.close()
1777
1819
  if self._conversation:
1778
1820
  loop = asyncio.get_running_loop()
1779
1821
  await loop.run_in_executor(None, self._conversation.close)
@@ -0,0 +1,133 @@
1
+ """Models for the OpenAI-compatible agent-server gateway."""
2
+
3
+ from typing import Literal
4
+
5
+ from openai.types import CompletionUsage, Model
6
+ from openai.types.chat import ChatCompletion, ChatCompletionChunk
7
+ from openai.types.chat.chat_completion import Choice
8
+ from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice, ChoiceDelta
9
+ from openai.types.chat.chat_completion_message import ChatCompletionMessage
10
+ from pydantic import BaseModel, ConfigDict
11
+
12
+
13
+ OpenAIChatCompletionChoice = Choice
14
+ OpenAIChatCompletionChunk = ChatCompletionChunk
15
+ OpenAIChatCompletionChunkChoice = ChunkChoice
16
+ OpenAIChatCompletionChunkChoiceDelta = ChoiceDelta
17
+ OpenAIChatCompletionResponse = ChatCompletion
18
+ OpenAIModel = Model
19
+ OpenAIResponseMessage = ChatCompletionMessage
20
+ OpenAIUsage = CompletionUsage
21
+
22
+
23
+ class OpenAIResponseInputTokensDetails(BaseModel):
24
+ cached_tokens: int
25
+
26
+
27
+ class OpenAIResponseOutputTokensDetails(BaseModel):
28
+ reasoning_tokens: int
29
+
30
+
31
+ class OpenAIResponseUsage(BaseModel):
32
+ input_tokens: int
33
+ input_tokens_details: OpenAIResponseInputTokensDetails
34
+ output_tokens: int
35
+ output_tokens_details: OpenAIResponseOutputTokensDetails
36
+ total_tokens: int
37
+
38
+
39
+ class OpenAIResponseOutputText(BaseModel):
40
+ annotations: list[dict[str, object]]
41
+ text: str
42
+ type: Literal["output_text"] = "output_text"
43
+
44
+
45
+ class OpenAIResponseOutputMessage(BaseModel):
46
+ id: str
47
+ content: list[OpenAIResponseOutputText]
48
+ role: Literal["assistant"] = "assistant"
49
+ status: Literal["completed"] = "completed"
50
+ type: Literal["message"] = "message"
51
+
52
+
53
+ class OpenAIResponse(BaseModel):
54
+ id: str
55
+ created_at: float
56
+ completed_at: float
57
+ instructions: str | None = None
58
+ metadata: dict[str, str] | None = None
59
+ model: str
60
+ object: Literal["response"] = "response"
61
+ output: list[OpenAIResponseOutputMessage]
62
+ parallel_tool_calls: bool = False
63
+ previous_response_id: str | None = None
64
+ status: Literal["completed"] = "completed"
65
+ tool_choice: Literal["none"] = "none"
66
+ tools: list[dict[str, str]]
67
+ usage: OpenAIResponseUsage
68
+
69
+
70
+ class OpenAIImageURL(BaseModel):
71
+ url: str
72
+
73
+
74
+ class OpenAIContentPart(BaseModel):
75
+ type: str
76
+ text: str | None = None
77
+ image_url: OpenAIImageURL | str | None = None
78
+
79
+ model_config = ConfigDict(extra="ignore")
80
+
81
+
82
+ class OpenAIChatMessage(BaseModel):
83
+ role: Literal["system", "developer", "user", "assistant", "tool"]
84
+ content: str | list[OpenAIContentPart] | None = None
85
+
86
+ model_config = ConfigDict(extra="ignore")
87
+
88
+
89
+ class OpenAIStreamOptions(BaseModel):
90
+ include_usage: bool = False
91
+
92
+ model_config = ConfigDict(extra="ignore")
93
+
94
+
95
+ class OpenAIChatCompletionRequest(BaseModel):
96
+ model: str
97
+ messages: list[OpenAIChatMessage]
98
+ stream: bool = False
99
+ stream_options: OpenAIStreamOptions | None = None
100
+
101
+ model_config = ConfigDict(extra="ignore")
102
+
103
+
104
+ class OpenAIResponseInputContentPart(BaseModel):
105
+ type: str
106
+ text: str | None = None
107
+ image_url: str | None = None
108
+
109
+ model_config = ConfigDict(extra="ignore")
110
+
111
+
112
+ class OpenAIResponseInputMessage(BaseModel):
113
+ role: Literal["system", "developer", "user", "assistant"]
114
+ content: str | list[OpenAIResponseInputContentPart]
115
+
116
+ model_config = ConfigDict(extra="ignore")
117
+
118
+
119
+ class OpenAIResponseRequest(BaseModel):
120
+ model: str
121
+ input: str | list[OpenAIResponseInputMessage]
122
+ instructions: str | None = None
123
+ previous_response_id: str | None = None
124
+ store: bool = False
125
+ stream: bool = False
126
+ metadata: dict[str, str] | None = None
127
+
128
+ model_config = ConfigDict(extra="ignore")
129
+
130
+
131
+ class OpenAIModelListResponse(BaseModel):
132
+ object: Literal["list"] = "list"
133
+ data: list[OpenAIModel]
@@ -16,11 +16,14 @@ from openhands.agent_server.openai.models import (
16
16
  OpenAIChatCompletionRequest,
17
17
  OpenAIChatCompletionResponse,
18
18
  OpenAIModelListResponse,
19
+ OpenAIResponse,
20
+ OpenAIResponseRequest,
19
21
  )
20
22
  from openhands.agent_server.openai.service import (
21
23
  iter_openai_chat_completion_sse,
22
24
  list_openai_models,
23
25
  run_chat_completion,
26
+ run_response,
24
27
  )
25
28
  from openhands.sdk.conversation.types import (
26
29
  ConversationObservabilityMetadata,
@@ -167,3 +170,37 @@ async def create_chat_completion(
167
170
 
168
171
  response.headers["X-OpenHands-ServerConversation-ID"] = conversation_id
169
172
  return result.response
173
+
174
+
175
+ @openai_router.post(
176
+ "/v1/responses",
177
+ response_model=OpenAIResponse,
178
+ response_model_exclude_none=True,
179
+ )
180
+ async def create_response(
181
+ body: OpenAIResponseRequest,
182
+ request: Request,
183
+ response: Response,
184
+ x_openhands_observability_span_name: Annotated[
185
+ str | None, Header(alias="X-OpenHands-Observability-Span-Name")
186
+ ] = None,
187
+ x_openhands_observability_tags: Annotated[
188
+ str | None, Header(alias="X-OpenHands-Observability-Tags")
189
+ ] = None,
190
+ x_openhands_observability_metadata: Annotated[
191
+ str | None, Header(alias="X-OpenHands-Observability-Metadata")
192
+ ] = None,
193
+ conversation_service: ConversationService = Depends(get_conversation_service),
194
+ ) -> OpenAIResponse:
195
+ result = await run_response(
196
+ request=body,
197
+ config=_get_config(request),
198
+ conversation_service=conversation_service,
199
+ observability_overrides=_parse_observability_overrides(
200
+ span_name=x_openhands_observability_span_name,
201
+ tags=x_openhands_observability_tags,
202
+ metadata=x_openhands_observability_metadata,
203
+ ),
204
+ )
205
+ response.headers["X-OpenHands-ServerConversation-ID"] = str(result.conversation_id)
206
+ return result.response