feat(proxy): add OpenCode remote config endpoint

This commit is contained in:
Dávid Balatoni 2026-06-25 20:12:42 +02:00
parent 30ce5c70b2
commit 8321489bbd
No known key found for this signature in database
3 changed files with 189 additions and 7 deletions

View file

@ -1,6 +1,6 @@
import re
from fastapi import Depends, FastAPI, HTTPException, Response
from fastapi import Depends, FastAPI, HTTPException, Request, Response
from litellm.llms.litellm_proxy.skills.handler import LiteLLMSkillsHandler
from litellm.llms.litellm_proxy.skills.prompt_injection import (
@ -10,6 +10,8 @@ from litellm.proxy._types import LiteLLM_SkillsTable, UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
OPENCODE_SKILLS_DEFAULT_PATH = "/opencode/skills"
OPENCODE_REMOTE_CONFIG_DEFAULT_PATH = "/.well-known/opencode"
OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json"
AGENT_SKILLS_PATHS = ("/.well-known/agent-skills", "/.well-known/skills")
_MAX_SKILLS = 1000
@ -25,6 +27,14 @@ def _opencode_config_enabled(skills_gateway_config: dict | None) -> bool:
)
def _opencode_remote_config_enabled(skills_gateway_config: dict | None) -> bool:
if not _opencode_config_enabled(skills_gateway_config):
return False
opencode_config = skills_gateway_config.get("opencode", {})
remote_config = opencode_config.get("remote_config", {})
return isinstance(remote_config, dict) and remote_config.get("enabled") is True
def _agent_skills_config_enabled(skills_gateway_config: dict | None) -> bool:
if not isinstance(skills_gateway_config, dict):
return False
@ -36,17 +46,38 @@ def _agent_skills_config_enabled(skills_gateway_config: dict | None) -> bool:
)
def _normalized_route_path(path: object, default: str) -> str:
path = str(path or default).strip() or default
if not path.startswith("/"):
path = f"/{path}"
return path.rstrip("/") or default
def _opencode_path(skills_gateway_config: dict | None) -> str:
if not isinstance(skills_gateway_config, dict):
return OPENCODE_SKILLS_DEFAULT_PATH
opencode_config = skills_gateway_config.get("opencode", {})
if not isinstance(opencode_config, dict):
return OPENCODE_SKILLS_DEFAULT_PATH
path = opencode_config.get("path") or OPENCODE_SKILLS_DEFAULT_PATH
path = str(path).strip() or OPENCODE_SKILLS_DEFAULT_PATH
if not path.startswith("/"):
path = f"/{path}"
return path.rstrip("/") or OPENCODE_SKILLS_DEFAULT_PATH
return _normalized_route_path(
opencode_config.get("path"),
OPENCODE_SKILLS_DEFAULT_PATH,
)
def _opencode_remote_config_path(skills_gateway_config: dict | None) -> str:
if not isinstance(skills_gateway_config, dict):
return OPENCODE_REMOTE_CONFIG_DEFAULT_PATH
opencode_config = skills_gateway_config.get("opencode", {})
if not isinstance(opencode_config, dict):
return OPENCODE_REMOTE_CONFIG_DEFAULT_PATH
remote_config = opencode_config.get("remote_config", {})
if not isinstance(remote_config, dict):
return OPENCODE_REMOTE_CONFIG_DEFAULT_PATH
return _normalized_route_path(
remote_config.get("path"),
OPENCODE_REMOTE_CONFIG_DEFAULT_PATH,
)
def _skill_enabled(skill: LiteLLM_SkillsTable) -> bool:
@ -130,6 +161,24 @@ async def opencode_skills_index(
}
def _opencode_remote_config_response(
request: Request,
skills_gateway_config: dict | None,
) -> dict:
from litellm.proxy.utils import get_custom_url
skills_url = get_custom_url(
request_base_url=str(request.base_url),
route=_opencode_path(skills_gateway_config),
)
return {
"config": {
"$schema": OPENCODE_CONFIG_SCHEMA,
"skills": {"urls": [skills_url]},
}
}
async def opencode_skill_file(
skill_name: str,
file_path: str,
@ -210,6 +259,26 @@ def initialize_opencode_skills_endpoint(
_add_route(app, f"{path}/{{skill_name}}/{{file_path:path}}", opencode_skill_file)
def initialize_opencode_remote_config_endpoint(
app: FastAPI,
skills_gateway_config: dict | None,
) -> None:
if not _opencode_remote_config_enabled(skills_gateway_config):
return
async def opencode_remote_config(request: Request):
return _opencode_remote_config_response(
request=request,
skills_gateway_config=skills_gateway_config,
)
_add_route(
app,
_opencode_remote_config_path(skills_gateway_config),
opencode_remote_config,
)
def initialize_agent_skills_endpoint(
app: FastAPI,
skills_gateway_config: dict | None,

View file

@ -4553,6 +4553,7 @@ class ProxyConfig:
general_settings = {}
from litellm.proxy.opencode_endpoints.skills_endpoints import (
initialize_agent_skills_endpoint,
initialize_opencode_remote_config_endpoint,
initialize_opencode_skills_endpoint,
)
@ -4560,6 +4561,10 @@ class ProxyConfig:
app=app,
skills_gateway_config=config.get("skills_gateway"),
)
initialize_opencode_remote_config_endpoint(
app=app,
skills_gateway_config=config.get("skills_gateway"),
)
initialize_agent_skills_endpoint(
app=app,
skills_gateway_config=config.get("skills_gateway"),

View file

@ -13,6 +13,7 @@ from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
def _client(config: dict, auth: UserAPIKeyAuth | None = None) -> TestClient:
from litellm.proxy.opencode_endpoints.skills_endpoints import (
initialize_agent_skills_endpoint,
initialize_opencode_remote_config_endpoint,
initialize_opencode_skills_endpoint,
)
@ -20,6 +21,7 @@ def _client(config: dict, auth: UserAPIKeyAuth | None = None) -> TestClient:
if auth is not None:
app.dependency_overrides[user_api_key_auth] = lambda: auth
initialize_opencode_skills_endpoint(app=app, skills_gateway_config=config)
initialize_opencode_remote_config_endpoint(app=app, skills_gateway_config=config)
initialize_agent_skills_endpoint(app=app, skills_gateway_config=config)
return TestClient(app)
@ -32,6 +34,14 @@ def test_should_not_register_opencode_skills_endpoint_when_disabled():
assert response.status_code == 404
def test_should_not_register_opencode_remote_config_when_disabled():
client = _client({"enabled": True, "opencode": {"enabled": True}})
response = client.get("/.well-known/opencode")
assert response.status_code == 404
def test_should_not_register_agent_skills_endpoint_when_disabled():
client = _client({"enabled": True})
@ -87,6 +97,30 @@ def test_should_return_opencode_skills_index_when_enabled(monkeypatch):
list_skills.assert_awaited_once_with(limit=1000, offset=0, user_api_key_dict=auth)
def test_should_return_opencode_remote_config_when_enabled_without_auth():
client = _client(
{
"enabled": True,
"opencode": {
"enabled": True,
"remote_config": {"enabled": True},
},
}
)
response = client.get("/.well-known/opencode")
assert response.status_code == 200
assert response.json() == {
"config": {
"$schema": "https://opencode.ai/config.json",
"skills": {
"urls": ["http://testserver/opencode/skills"],
},
},
}
def test_should_normalize_opencode_path_without_leading_slash(monkeypatch):
from litellm.llms.litellm_proxy.skills.handler import LiteLLMSkillsHandler
@ -106,6 +140,32 @@ def test_should_normalize_opencode_path_without_leading_slash(monkeypatch):
assert response.json() == {"skills": []}
def test_should_return_opencode_remote_config_with_custom_paths_and_proxy_base_url(
monkeypatch,
):
monkeypatch.setenv("PROXY_BASE_URL", "https://litellm.example.com/root")
client = _client(
{
"enabled": True,
"opencode": {
"enabled": True,
"path": "native/skills",
"remote_config": {
"enabled": True,
"path": "/custom/opencode",
},
},
}
)
response = client.get("/custom/opencode")
assert response.status_code == 200
assert response.json()["config"]["skills"]["urls"] == [
"https://litellm.example.com/root/native/skills"
]
def test_should_return_agent_skills_well_known_index_when_enabled(monkeypatch):
from litellm.llms.litellm_proxy.skills.handler import LiteLLMSkillsHandler
@ -200,6 +260,25 @@ def test_should_return_opencode_404_when_skill_file_is_missing(monkeypatch):
assert response.json() == {"detail": "Skill file not found"}
def test_should_return_opencode_404_when_no_skills_exist(monkeypatch):
from litellm.llms.litellm_proxy.skills.handler import LiteLLMSkillsHandler
monkeypatch.setattr(
LiteLLMSkillsHandler,
"list_skills",
AsyncMock(return_value=[]),
)
client = _client(
{"enabled": True, "opencode": {"enabled": True}},
auth=UserAPIKeyAuth(user_id="user-1"),
)
response = client.get("/opencode/skills/missing_skill/SKILL.md")
assert response.status_code == 404
assert response.json() == {"detail": "Skill file not found"}
def test_should_return_agent_404_when_skill_file_is_missing(monkeypatch):
from litellm.llms.litellm_proxy.skills.handler import LiteLLMSkillsHandler
@ -224,21 +303,47 @@ def test_should_return_agent_404_when_skill_file_is_missing(monkeypatch):
assert response.json() == {"detail": "Skill file not found"}
def test_should_return_agent_404_when_no_skills_exist(monkeypatch):
from litellm.llms.litellm_proxy.skills.handler import LiteLLMSkillsHandler
monkeypatch.setattr(
LiteLLMSkillsHandler,
"list_skills",
AsyncMock(return_value=[]),
)
client = _client(
{"enabled": True, "agent_skills": {"enabled": True}},
auth=UserAPIKeyAuth(user_id="user-1"),
)
response = client.get("/.well-known/agent-skills/missing-skill/SKILL.md")
assert response.status_code == 404
assert response.json() == {"detail": "Skill file not found"}
def test_should_not_duplicate_routes_when_initialized_twice():
from litellm.proxy.opencode_endpoints.skills_endpoints import (
initialize_opencode_remote_config_endpoint,
initialize_opencode_skills_endpoint,
)
app = FastAPI()
config = {"enabled": True, "opencode": {"enabled": True}}
config = {
"enabled": True,
"opencode": {"enabled": True, "remote_config": {"enabled": True}},
}
initialize_opencode_skills_endpoint(app=app, skills_gateway_config=config)
initialize_opencode_remote_config_endpoint(app=app, skills_gateway_config=config)
initialize_opencode_skills_endpoint(app=app, skills_gateway_config=config)
initialize_opencode_remote_config_endpoint(app=app, skills_gateway_config=config)
route_paths = [getattr(route, "path", None) for route in app.routes]
assert route_paths.count("/opencode/skills") == 1
assert route_paths.count("/opencode/skills/index.json") == 1
assert route_paths.count("/opencode/skills/{skill_name}/{file_path:path}") == 1
assert route_paths.count("/.well-known/opencode") == 1
def test_should_serve_legacy_agent_skills_well_known_alias(monkeypatch):
@ -415,6 +520,8 @@ async def test_should_register_skills_endpoints_from_proxy_config_load(
" opencode:\n"
" enabled: true\n"
" path: /native/skills\n"
" remote_config:\n"
" enabled: true\n"
" agent_skills:\n"
" enabled: true\n"
)
@ -430,4 +537,5 @@ async def test_should_register_skills_endpoints_from_proxy_config_load(
route_paths = {getattr(route, "path", None) for route in app.routes}
assert "/native/skills" in route_paths
assert "/native/skills/index.json" in route_paths
assert "/.well-known/opencode" in route_paths
assert "/.well-known/agent-skills/index.json" in route_paths