From bfc2646b1f57c448b07d3a45a22c4872326a87d9 Mon Sep 17 00:00:00 2001 From: enyst Date: Sat, 6 Jun 2026 08:30:17 +0000 Subject: [PATCH 01/16] feat(agent-server): add OpenAI chat completions gateway Co-authored-by: openhands --- .pr/live-litellm-haiku.json | 26 + .pr/live-models.json | 17 + .pr/live-openai-nano.json | 26 + .pr/live-server.log | 55 +++ .pr/live-tests.md | 30 ++ .../openhands/agent_server/api.py | 9 + .../openhands/agent_server/openai_router.py | 454 ++++++++++++++++++ tests/agent_server/test_api_authentication.py | 23 + .../test_remote_conversation_live_server.py | 62 +++ 9 files changed, 702 insertions(+) create mode 100644 .pr/live-litellm-haiku.json create mode 100644 .pr/live-models.json create mode 100644 .pr/live-openai-nano.json create mode 100644 .pr/live-server.log create mode 100644 .pr/live-tests.md create mode 100644 openhands-agent-server/openhands/agent_server/openai_router.py diff --git a/.pr/live-litellm-haiku.json b/.pr/live-litellm-haiku.json new file mode 100644 index 0000000000..08b75836b8 --- /dev/null +++ b/.pr/live-litellm-haiku.json @@ -0,0 +1,26 @@ +{ + "conversation_id_header_present": true, + "elapsed_seconds": 2.759, + "response": { + "choices": [ + { + "finish_reason": "stop", + "index": 0, + "message": { + "content": "LITELLM_HAIKU_GATEWAY_OK", + "role": "assistant" + } + } + ], + "created": 1780734298, + "id": "chatcmpl-f1a6e8c772234b9b8f4f745ea156d366", + "model": "openhands_haiku_eval_proxy", + "object": "chat.completion", + "usage": { + "completion_tokens": 0, + "prompt_tokens": 0, + "total_tokens": 0 + } + }, + "status_code": 200 +} diff --git a/.pr/live-models.json b/.pr/live-models.json new file mode 100644 index 0000000000..9c93262bf8 --- /dev/null +++ b/.pr/live-models.json @@ -0,0 +1,17 @@ +{ + "data": [ + { + "created": 0, + "id": "openhands_haiku_eval_proxy", + "object": "model", + "owned_by": "openhands" + }, + { + "created": 0, + "id": "openhands_openai_nano", + "object": "model", + "owned_by": "openhands" + } + ], + "object": "list" +} diff --git a/.pr/live-openai-nano.json b/.pr/live-openai-nano.json new file mode 100644 index 0000000000..7c09f6aed6 --- /dev/null +++ b/.pr/live-openai-nano.json @@ -0,0 +1,26 @@ +{ + "conversation_id_header_present": true, + "elapsed_seconds": 3.752, + "response": { + "choices": [ + { + "finish_reason": "stop", + "index": 0, + "message": { + "content": "OPENAI_GATEWAY_OK", + "role": "assistant" + } + } + ], + "created": 1780734295, + "id": "chatcmpl-6e7acc4cc31d4ba589e426e87a95c43a", + "model": "openhands_openai_nano", + "object": "chat.completion", + "usage": { + "completion_tokens": 0, + "prompt_tokens": 0, + "total_tokens": 0 + } + }, + "status_code": 200 +} diff --git a/.pr/live-server.log b/.pr/live-server.log new file mode 100644 index 0000000000..55da639fe0 --- /dev/null +++ b/.pr/live-server.log @@ -0,0 +1,55 @@ +2026-06-06 08:24:48,929::lmnr.opentelemetry_lib.tracing.exporter::INFO: No path found in HTTP endpoint URL. Adding default path /v1/traces: https://laminar-api.app.all-hands.dev:443 -> https://laminar-api.app.all-hands.dev:443/v1/traces (exporter.py:121) +2026-06-06 08:24:48,929::lmnr.opentelemetry_lib.tracing.exporter::INFO: No path found in HTTP endpoint URL. Adding default path /v1/logs: https://laminar-api.app.all-hands.dev:443 -> https://laminar-api.app.all-hands.dev:443/v1/logs (exporter.py:121) +2026-06-06 08:24:50,411::lmnr.sdk.laminar::INFO: Laminar is already initialized. Skipping initialization. (laminar.py:249) +{"asctime": "2026-06-06 08:24:50,411", "levelname": "INFO", "name": "lmnr.sdk.laminar", "filename": "laminar.py", "lineno": 249, "message": "Laminar is already initialized. Skipping initialization."} ++----------------------------------------------------------------------+ +| OpenHands SDK v1.26.0 | +| | +| Report a bug: github.com/OpenHands/software-agent-sdk/issues | +| Get help: openhands.dev/joinslack | +| Scale up: openhands.dev/product/sdk | +| | +| Set OPENHANDS_SUPPRESS_BANNER=1 to hide this message | ++----------------------------------------------------------------------+ + +{"asctime": "2026-06-06 08:24:50,440", "levelname": "WARNING", "name": "uvicorn.error", "filename": "config.py", "lineno": 285, "message": "Current configuration will not reload as not all conditions are met, please refer to documentation."} +{"asctime": "2026-06-06 08:24:50,767", "levelname": "INFO", "name": "openhands.tools.preset.default", "filename": "default.py", "lineno": 134, "message": "Registered file-based agent 'bash-runner' from /workspace/project/software-agent-sdk/openhands-tools/openhands/tools/preset/subagents/bash_runner.md"} +{"asctime": "2026-06-06 08:24:50,767", "levelname": "INFO", "name": "openhands.tools.preset.default", "filename": "default.py", "lineno": 134, "message": "Registered file-based agent 'code-explorer' from /workspace/project/software-agent-sdk/openhands-tools/openhands/tools/preset/subagents/code_explorer.md"} +{"asctime": "2026-06-06 08:24:50,767", "levelname": "INFO", "name": "openhands.tools.preset.default", "filename": "default.py", "lineno": 134, "message": "Registered file-based agent 'general-purpose' from /workspace/project/software-agent-sdk/openhands-tools/openhands/tools/preset/subagents/default.md"} +{"asctime": "2026-06-06 08:24:50,767", "levelname": "INFO", "name": "openhands.tools.preset.default", "filename": "default.py", "lineno": 134, "message": "Registered file-based agent 'web-researcher' from /workspace/project/software-agent-sdk/openhands-tools/openhands/tools/preset/subagents/web_researcher.md"} +{"asctime": "2026-06-06 08:24:51,259", "levelname": "INFO", "name": "uvicorn.error", "filename": "server.py", "lineno": 84, "message": "Started server process [2045]", "color_message": "Started server process [\u001b[36m%d\u001b[0m]"} +{"asctime": "2026-06-06 08:24:51,259", "levelname": "INFO", "name": "uvicorn.error", "filename": "on.py", "lineno": 48, "message": "Waiting for application startup."} +{"asctime": "2026-06-06 08:24:51,264", "levelname": "INFO", "name": "openhands.agent_server.vscode_service", "filename": "vscode_service.py", "lineno": 234, "message": "VSCode is disabled in configuration"} +{"asctime": "2026-06-06 08:24:51,264", "levelname": "INFO", "name": "openhands.agent_server.desktop_service", "filename": "desktop_service.py", "lineno": 214, "message": "VNC desktop is disabled in configuration"} +{"asctime": "2026-06-06 08:24:51,264", "levelname": "INFO", "name": "openhands.agent_server.tool_preload_service", "filename": "tool_preload_service.py", "lineno": 64, "message": "Tool preload is disabled in configuration"} +{"asctime": "2026-06-06 08:24:51,265", "levelname": "INFO", "name": "openhands.agent_server.api", "filename": "api.py", "lineno": 143, "message": "VSCode service is disabled"} +{"asctime": "2026-06-06 08:24:51,265", "levelname": "INFO", "name": "openhands.agent_server.api", "filename": "api.py", "lineno": 155, "message": "Desktop service is disabled"} +{"asctime": "2026-06-06 08:24:51,265", "levelname": "INFO", "name": "openhands.agent_server.api", "filename": "api.py", "lineno": 165, "message": "Tool preload service is disabled"} +{"asctime": "2026-06-06 08:24:51,265", "levelname": "INFO", "name": "openhands.agent_server.api", "filename": "api.py", "lineno": 191, "message": "Server initialization complete - ready to serve requests"} +{"asctime": "2026-06-06 08:24:51,265", "levelname": "INFO", "name": "uvicorn.error", "filename": "on.py", "lineno": 62, "message": "Application startup complete."} +{"asctime": "2026-06-06 08:24:51,265", "levelname": "INFO", "name": "uvicorn.error", "filename": "server.py", "lineno": 216, "message": "Uvicorn running on http://127.0.0.1:60381 (Press CTRL+C to quit)", "color_message": "Uvicorn running on \u001b[1m%s://%s:%d\u001b[0m (Press CTRL+C to quit)"} +{"asctime": "2026-06-06 08:24:51,350", "levelname": "INFO", "name": "uvicorn.access", "message": "127.0.0.1:34934 - \"GET /health HTTP/1.1\" 200", "http.client_ip": "127.0.0.1:34934", "http.method": "GET", "http.url": "/health", "http.version": "1.1", "http.status_code": 200} +{"asctime": "2026-06-06 08:24:51,365", "levelname": "INFO", "name": "openhands.sdk.llm.llm_profile_store", "filename": "llm_profile_store.py", "lineno": 170, "message": "[Profile Store] Saved profile `openai_nano` at /tmp/oh-openai-gateway-live-v_8suzyn/home/.openhands/profiles/openai_nano.json"} +{"asctime": "2026-06-06 08:24:51,365", "levelname": "INFO", "name": "openhands.agent_server.profiles_router", "filename": "profiles_router.py", "lineno": 205, "message": "Saved profile 'openai_nano' (include_secrets=True)"} +{"asctime": "2026-06-06 08:24:51,365", "levelname": "INFO", "name": "uvicorn.access", "message": "127.0.0.1:34944 - \"POST /api/profiles/openai_nano HTTP/1.1\" 201", "http.client_ip": "127.0.0.1:34944", "http.method": "POST", "http.url": "/api/profiles/openai_nano", "http.version": "1.1", "http.status_code": 201} +{"asctime": "2026-06-06 08:24:51,778", "levelname": "INFO", "name": "openhands.sdk.llm.llm_profile_store", "filename": "llm_profile_store.py", "lineno": 170, "message": "[Profile Store] Saved profile `haiku_eval_proxy` at /tmp/oh-openai-gateway-live-v_8suzyn/home/.openhands/profiles/haiku_eval_proxy.json"} +{"asctime": "2026-06-06 08:24:51,778", "levelname": "INFO", "name": "openhands.agent_server.profiles_router", "filename": "profiles_router.py", "lineno": 205, "message": "Saved profile 'haiku_eval_proxy' (include_secrets=True)"} +{"asctime": "2026-06-06 08:24:51,778", "levelname": "INFO", "name": "uvicorn.access", "message": "127.0.0.1:34944 - \"POST /api/profiles/haiku_eval_proxy HTTP/1.1\" 201", "http.client_ip": "127.0.0.1:34944", "http.method": "POST", "http.url": "/api/profiles/haiku_eval_proxy", "http.version": "1.1", "http.status_code": 201} +{"asctime": "2026-06-06 08:24:51,781", "levelname": "INFO", "name": "uvicorn.access", "message": "127.0.0.1:34944 - \"GET /v1/models HTTP/1.1\" 200", "http.client_ip": "127.0.0.1:34944", "http.method": "GET", "http.url": "/v1/models", "http.version": "1.1", "http.status_code": 200} +{"asctime": "2026-06-06 08:24:51,783", "levelname": "INFO", "name": "openhands.sdk.llm.llm_profile_store", "filename": "llm_profile_store.py", "lineno": 208, "message": "[Profile Store] Loaded profile `openai_nano` from /tmp/oh-openai-gateway-live-v_8suzyn/home/.openhands/profiles/openai_nano.json"} +{"asctime": "2026-06-06 08:24:51,791", "levelname": "INFO", "name": "openhands.sdk.conversation.state", "filename": "state.py", "lineno": 468, "message": "Created new conversation 69de25a6-5b8b-47f4-8922-76fb46e597b1"} +{"asctime": "2026-06-06 08:24:51,793", "levelname": "INFO", "name": "openhands.sdk.conversation.impl.local_conversation", "filename": "local_conversation.py", "lineno": 1576, "message": "Confirmation policy set to: kind='NeverConfirm'"} +{"asctime": "2026-06-06 08:24:51,796", "levelname": "INFO", "name": "openhands.sdk.agent.base", "filename": "base.py", "lineno": 534, "message": "Loaded 0 tools from spec"} +{"asctime": "2026-06-06 08:24:55,532", "levelname": "INFO", "name": "uvicorn.access", "message": "127.0.0.1:34944 - \"POST /v1/chat/completions HTTP/1.1\" 200", "http.client_ip": "127.0.0.1:34944", "http.method": "POST", "http.url": "/v1/chat/completions", "http.version": "1.1", "http.status_code": 200} +{"asctime": "2026-06-06 08:24:55,536", "levelname": "INFO", "name": "openhands.agent_server.conversation_service", "filename": "conversation_service.py", "lineno": 772, "message": "Successfully deleted conversation 69de25a6-5b8b-47f4-8922-76fb46e597b1"} +{"asctime": "2026-06-06 08:24:55,537", "levelname": "INFO", "name": "openhands.sdk.llm.llm_profile_store", "filename": "llm_profile_store.py", "lineno": 208, "message": "[Profile Store] Loaded profile `haiku_eval_proxy` from /tmp/oh-openai-gateway-live-v_8suzyn/home/.openhands/profiles/haiku_eval_proxy.json"} +{"asctime": "2026-06-06 08:24:55,541", "levelname": "INFO", "name": "openhands.sdk.conversation.state", "filename": "state.py", "lineno": 468, "message": "Created new conversation 7a84a08b-b14a-4c62-b727-a4ae8b9e8df8"} +{"asctime": "2026-06-06 08:24:55,543", "levelname": "INFO", "name": "openhands.sdk.conversation.impl.local_conversation", "filename": "local_conversation.py", "lineno": 1576, "message": "Confirmation policy set to: kind='NeverConfirm'"} +{"asctime": "2026-06-06 08:24:55,546", "levelname": "INFO", "name": "openhands.sdk.agent.base", "filename": "base.py", "lineno": 534, "message": "Loaded 0 tools from spec"} +{"asctime": "2026-06-06 08:24:58,292", "levelname": "INFO", "name": "uvicorn.access", "message": "127.0.0.1:34944 - \"POST /v1/chat/completions HTTP/1.1\" 200", "http.client_ip": "127.0.0.1:34944", "http.method": "POST", "http.url": "/v1/chat/completions", "http.version": "1.1", "http.status_code": 200} +{"asctime": "2026-06-06 08:24:58,293", "levelname": "INFO", "name": "__main__", "filename": "__main__.py", "lineno": 159, "message": "Received signal SIGTERM (15), shutting down..."} +{"asctime": "2026-06-06 08:24:58,294", "levelname": "INFO", "name": "openhands.agent_server.conversation_service", "filename": "conversation_service.py", "lineno": 772, "message": "Successfully deleted conversation 7a84a08b-b14a-4c62-b727-a4ae8b9e8df8"} +{"asctime": "2026-06-06 08:24:58,312", "levelname": "INFO", "name": "uvicorn.error", "filename": "server.py", "lineno": 264, "message": "Shutting down"} +{"asctime": "2026-06-06 08:24:58,412", "levelname": "INFO", "name": "uvicorn.error", "filename": "on.py", "lineno": 67, "message": "Waiting for application shutdown."} +{"asctime": "2026-06-06 08:24:58,413", "levelname": "INFO", "name": "uvicorn.error", "filename": "on.py", "lineno": 76, "message": "Application shutdown complete."} +{"asctime": "2026-06-06 08:24:58,413", "levelname": "INFO", "name": "uvicorn.error", "filename": "server.py", "lineno": 94, "message": "Finished server process [2045]", "color_message": "Finished server process [\u001b[36m%d\u001b[0m]"} diff --git a/.pr/live-tests.md b/.pr/live-tests.md new file mode 100644 index 0000000000..4044437ec7 --- /dev/null +++ b/.pr/live-tests.md @@ -0,0 +1,30 @@ +# Live test artifacts for issue #3540 + +These artifacts were generated against this branch's OpenAI-compatible agent-server gateway. + +## Server setup + +- Started a real local `openhands.agent_server` process with: + - `OH_SESSION_API_KEYS_0=live-test-session-key` + - isolated temporary `HOME`, conversation directory, bash-events directory, and `TMUX_TMPDIR` + - `OH_WEBHOOKS=[]`, `OH_ENABLE_VSCODE=0`, `OH_ENABLE_VNC=0`, `OH_PRELOAD_TOOLS=0` +- Saved two LLM profiles through `POST /api/profiles/{name}` using `X-Session-API-Key`. +- Called OpenAI-compatible endpoints using `Authorization: Bearer live-test-session-key`. + +## Profiles exercised + +| Gateway model | Backing profile | Backing LLM config | +| --- | --- | --- | +| `openhands_openai_nano` | `openai_nano` | `model=gpt-5-nano`, OpenAI API key | +| `openhands_haiku_eval_proxy` | `haiku_eval_proxy` | `model=litellm_proxy/anthropic/claude-haiku-4-5-20251001`, `base_url=https://llm-proxy.eval.all-hands.dev`, LiteLLM proxy API key | + +## Results + +| Artifact | Request | Result | +| --- | --- | --- | +| `live-models.json` | `GET /v1/models` | Returned both profile-backed OpenAI model IDs. | +| `live-openai-nano.json` | `POST /v1/chat/completions` with `model=openhands_openai_nano` | HTTP 200, OpenAI-shaped `chat.completion`, assistant content `OPENAI_GATEWAY_OK`, conversation ID response header present. | +| `live-litellm-haiku.json` | `POST /v1/chat/completions` with `model=openhands_haiku_eval_proxy` | HTTP 200, OpenAI-shaped `chat.completion`, assistant content `LITELLM_HAIKU_GATEWAY_OK`, conversation ID response header present. | +| `live-server.log` | Server stdout/stderr | Shows server startup, profile creation, `/v1/models`, both `/v1/chat/completions` calls, and background cleanup of ephemeral conversations. | + +No API keys or bearer tokens are written into these artifacts. diff --git a/openhands-agent-server/openhands/agent_server/api.py b/openhands-agent-server/openhands/agent_server/api.py index 2040ae84c0..d1dc7d3139 100644 --- a/openhands-agent-server/openhands/agent_server/api.py +++ b/openhands-agent-server/openhands/agent_server/api.py @@ -41,6 +41,10 @@ from openhands.agent_server.llm_router import llm_router from openhands.agent_server.mcp_router import mcp_router from openhands.agent_server.middleware import CORSDispatcher +from openhands.agent_server.openai_router import ( + create_openai_api_key_dependency, + openai_router, +) from openhands.agent_server.profiles_router import profiles_router from openhands.agent_server.server_details_router import ( get_server_info, @@ -320,6 +324,11 @@ def _add_api_routes(app: FastAPI, config: Config) -> None: api_router.include_router(auth_router) app.include_router(api_router) + openai_dependencies = [] + if config.session_api_keys: + openai_dependencies.append(Depends(create_openai_api_key_dependency(config))) + app.include_router(openai_router, dependencies=openai_dependencies) + # Workspace static-file routes get their own auth group that accepts # EITHER the X-Session-API-Key header OR the workspace session cookie. # The cookie is required so that