mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
feat(proxy): add OpenCode remote config endpoint
This commit is contained in:
parent
30ce5c70b2
commit
8321489bbd
3 changed files with 189 additions and 7 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue