Add community node protocol spec, Agent Skills, and make the failing tests hermetic
DevPlace CI / test (push) Failing after 31m42s
DevPlace CI / test (push) Failing after 31m42s
Community nodes (Phase 1 of plan.md): the protocol specification in services/nodes/spec.py as the single source of truth, GET /nodes/spec.json, a generated protocol docs page, the Community nodes API docs group, and the implementation plan plus the residential proxy research. The no-open-proxy rule is enforced in the spec (open_proxy cannot be set). Agent Skills: register the Skills API docs group, cascade skills on account deletion, add the nested CLAUDE.md, README and routes docs, and cover the parser, the fetcher (end to end against a mock origin), the data layer and the routes with 73 unit and 16 api tests. Tests made hermetic with the repo's existing patterns instead of ambient environment: embeddings tests set OPENROUTER_API_KEY via monkeypatch, the workspace tests install FakeBackend via runtime.set_backend, and the skills install tests use run_async. DockerCliBackend now returns 127 (command not found) when the CLI is missing instead of raising FileNotFoundError, so a host without docker answers with a clean error rather than a 500. The Providers tab empty state no longer names the Model routes tab. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -439,6 +439,7 @@ class FakeEmbedClient_openai_gateway:
|
||||
|
||||
def test_embeddings_remaps_alias_to_configured_model(local_db, monkeypatch):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeEmbedClient_openai_gateway)
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "test-embed-key")
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_force_model"] = False
|
||||
@@ -459,6 +460,7 @@ def test_embeddings_remaps_alias_to_configured_model(local_db, monkeypatch):
|
||||
|
||||
def test_embeddings_success_records_ledger(local_db, monkeypatch):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeEmbedClient_openai_gateway)
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "test-embed-key")
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_force_model"] = True
|
||||
@@ -740,6 +742,7 @@ def test_app_reference_defaults_when_not_passed(local_db, monkeypatch):
|
||||
|
||||
def test_app_reference_stored_in_ledger_for_embeddings(local_db, monkeypatch):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeEmbedClient_openai_gateway)
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "test-embed-key")
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_embed_enabled"] = True
|
||||
@@ -1228,6 +1231,7 @@ def test_chat_without_a_fallback_never_makes_a_second_call(local_db, monkeypatch
|
||||
|
||||
def test_embeddings_unknown_model_falls_back_to_default(local_db, monkeypatch):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeEmbedClient_openai_gateway)
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "test-embed-key")
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_force_model"] = False
|
||||
@@ -1834,6 +1838,7 @@ def test_upstream_x_gateway_model_header_is_forwarded_for_embeddings(
|
||||
monkeypatch.setattr(
|
||||
gwmod.httpx, "AsyncClient", FakeEmbedClientUpstreamModelHeader_openai_gateway
|
||||
)
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "test-embed-key")
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_embed_enabled"] = True
|
||||
@@ -2211,3 +2216,325 @@ def test_internal_bypass_allowed_requires_the_exact_internal_key(local_db):
|
||||
assert svc.internal_bypass_allowed(_make_request_openai_gateway(), cfg) is False
|
||||
finally:
|
||||
set_setting("gateway_internal_key", "")
|
||||
|
||||
|
||||
class FakeResponsesResp_openai_gateway(FakeResp_openai_gateway):
|
||||
def __init__(self, status=200, payload=None, events=None, **kw):
|
||||
super().__init__(status=status, payload=payload, **kw)
|
||||
self._events = events or []
|
||||
|
||||
async def aiter_lines(self):
|
||||
for event in self._events:
|
||||
yield f"data: {json.dumps(event)}"
|
||||
yield "data: [DONE]"
|
||||
|
||||
|
||||
def _resp_obj_openai_gateway(model):
|
||||
return {
|
||||
"id": "resp_1",
|
||||
"object": "response",
|
||||
"created_at": 7,
|
||||
"status": "completed",
|
||||
"model": model,
|
||||
"output": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [{"type": "output_text", "text": "hi there"}],
|
||||
}
|
||||
],
|
||||
"usage": {"input_tokens": 5, "output_tokens": 7, "total_tokens": 12},
|
||||
}
|
||||
|
||||
|
||||
def _resp_events_openai_gateway(model):
|
||||
return [
|
||||
{"type": "response.created", "response": {"id": "resp_1"}},
|
||||
{"type": "response.output_text.delta", "delta": "hi "},
|
||||
{"type": "response.output_text.delta", "delta": "there"},
|
||||
{"type": "response.completed", "response": _resp_obj_openai_gateway(model)},
|
||||
]
|
||||
|
||||
|
||||
class FakeResponsesClient_openai_gateway:
|
||||
behavior = "ok"
|
||||
|
||||
def __init__(self, *a, **k):
|
||||
self.calls = []
|
||||
|
||||
def build_request(self, method, url, headers=None, json=None, content=None):
|
||||
return FakeRequest(method, url, json)
|
||||
|
||||
async def send(self, request, stream=False):
|
||||
self.calls.append((request.url, request.json_body))
|
||||
body = request.json_body or {}
|
||||
if self.behavior == "error-envelope":
|
||||
return FakeResp_openai_gateway(
|
||||
status=429,
|
||||
payload={
|
||||
"error": {
|
||||
"message": "slow down",
|
||||
"type": "rate_limit",
|
||||
"code": "rate_limit_exceeded",
|
||||
}
|
||||
},
|
||||
)
|
||||
if self.behavior == "failed-object":
|
||||
return FakeResponsesResp_openai_gateway(
|
||||
payload={
|
||||
"object": "response",
|
||||
"status": "failed",
|
||||
"error": {"message": "boom"},
|
||||
}
|
||||
)
|
||||
model = body.get("model", "")
|
||||
return FakeResponsesResp_openai_gateway(
|
||||
payload=_resp_obj_openai_gateway(model),
|
||||
events=_resp_events_openai_gateway(model),
|
||||
)
|
||||
|
||||
async def post(self, url, headers=None, json=None, timeout=None):
|
||||
return await self.send(FakeRequest("POST", url, json), stream=False)
|
||||
|
||||
async def aclose(self):
|
||||
pass
|
||||
|
||||
|
||||
def _setup_responses_route_openai_gateway():
|
||||
from devplacepy.services.openai_gateway import routing
|
||||
|
||||
routing.provider_store.set(
|
||||
routing.ProviderIn(
|
||||
name="gwmeta",
|
||||
base_url="https://api.meta.ai/v1/responses",
|
||||
api_key="sk-test",
|
||||
protocol="responses",
|
||||
)
|
||||
)
|
||||
routing.model_store.set(
|
||||
routing.ModelRouteIn(
|
||||
source_model="meta-chat",
|
||||
provider="gwmeta",
|
||||
target_model="muse-spark-1.3-contributor",
|
||||
kind="chat",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _teardown_responses_route_openai_gateway():
|
||||
from devplacepy.services.openai_gateway import routing
|
||||
|
||||
routing.model_store.remove("meta-chat")
|
||||
routing.provider_store.remove("gwmeta")
|
||||
|
||||
|
||||
def _chat_body_openai_gateway(**over):
|
||||
body = {
|
||||
"model": "meta-chat",
|
||||
"messages": [
|
||||
{"role": "system", "content": "Be terse."},
|
||||
{"role": "user", "content": "hi"},
|
||||
],
|
||||
}
|
||||
body.update(over)
|
||||
return body
|
||||
|
||||
|
||||
def test_responses_protocol_transpiles_request_and_response(local_db, monkeypatch):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeResponsesClient_openai_gateway)
|
||||
FakeResponsesClient_openai_gateway.behavior = "ok"
|
||||
_setup_responses_route_openai_gateway()
|
||||
try:
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_vision_enabled"] = False
|
||||
rt = svc.runtime()
|
||||
resp = run_async(
|
||||
rt.handle_chat(
|
||||
_chat_body_openai_gateway(), cfg, ("guest", "test"), "test", "default"
|
||||
)
|
||||
)
|
||||
url, sent = rt._client.calls[-1]
|
||||
assert str(url) == "https://api.meta.ai/v1/responses"
|
||||
assert sent["model"] == "muse-spark-1.3-contributor"
|
||||
assert "messages" not in sent
|
||||
assert "Be terse." in sent["instructions"]
|
||||
assert sent["input"] == [
|
||||
{"type": "message", "role": "user", "content": "hi"}
|
||||
]
|
||||
assert sent["reasoning"] == {"effort": "minimal"}
|
||||
assert resp.status_code == 200
|
||||
payload = json.loads(bytes(resp.body).decode())
|
||||
assert payload["object"] == "chat.completion"
|
||||
assert payload["choices"][0]["message"]["content"] == "hi there"
|
||||
assert payload["choices"][0]["finish_reason"] == "stop"
|
||||
assert payload["usage"]["prompt_tokens"] == 5
|
||||
assert resp.headers["X-Gateway-Model"] == "muse-spark-1.3-contributor"
|
||||
finally:
|
||||
_teardown_responses_route_openai_gateway()
|
||||
|
||||
|
||||
def test_responses_protocol_stream_relays_chat_chunks(local_db, monkeypatch):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeResponsesClient_openai_gateway)
|
||||
FakeResponsesClient_openai_gateway.behavior = "ok"
|
||||
_setup_responses_route_openai_gateway()
|
||||
try:
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_vision_enabled"] = False
|
||||
rt = svc.runtime()
|
||||
resp = run_async(
|
||||
rt.handle_chat(
|
||||
_chat_body_openai_gateway(stream=True),
|
||||
cfg,
|
||||
("guest", "test"),
|
||||
"test",
|
||||
"default",
|
||||
)
|
||||
)
|
||||
|
||||
async def drain():
|
||||
out = []
|
||||
async for chunk in resp.body_iterator:
|
||||
out.append(chunk if isinstance(chunk, str) else chunk.decode())
|
||||
return "".join(out)
|
||||
|
||||
body = run_async(drain())
|
||||
assert "chat.completion.chunk" in body
|
||||
assert "hi " in body and "there" in body
|
||||
assert '"usage"' not in body
|
||||
assert "[DONE]" in body
|
||||
finally:
|
||||
_teardown_responses_route_openai_gateway()
|
||||
|
||||
|
||||
def test_responses_protocol_stream_relays_usage_when_asked(local_db, monkeypatch):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeResponsesClient_openai_gateway)
|
||||
FakeResponsesClient_openai_gateway.behavior = "ok"
|
||||
_setup_responses_route_openai_gateway()
|
||||
try:
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_vision_enabled"] = False
|
||||
rt = svc.runtime()
|
||||
resp = run_async(
|
||||
rt.handle_chat(
|
||||
_chat_body_openai_gateway(
|
||||
stream=True, stream_options={"include_usage": True}
|
||||
),
|
||||
cfg,
|
||||
("guest", "test"),
|
||||
"test",
|
||||
"default",
|
||||
)
|
||||
)
|
||||
_, sent = rt._client.calls[-1]
|
||||
assert "stream_options" not in sent
|
||||
|
||||
async def drain():
|
||||
out = []
|
||||
async for chunk in resp.body_iterator:
|
||||
out.append(chunk if isinstance(chunk, str) else chunk.decode())
|
||||
return "".join(out)
|
||||
|
||||
body = run_async(drain())
|
||||
assert '"usage"' in body
|
||||
assert '"prompt_tokens": 5' in body
|
||||
finally:
|
||||
_teardown_responses_route_openai_gateway()
|
||||
|
||||
|
||||
def test_responses_protocol_relays_error_envelope(local_db, monkeypatch):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeResponsesClient_openai_gateway)
|
||||
FakeResponsesClient_openai_gateway.behavior = "error-envelope"
|
||||
_setup_responses_route_openai_gateway()
|
||||
try:
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_vision_enabled"] = False
|
||||
rt = svc.runtime()
|
||||
resp = run_async(
|
||||
rt.handle_chat(
|
||||
_chat_body_openai_gateway(), cfg, ("guest", "test"), "test", "default"
|
||||
)
|
||||
)
|
||||
assert resp.status_code == 429
|
||||
payload = json.loads(bytes(resp.body).decode())
|
||||
assert payload["error"]["message"] == "slow down"
|
||||
assert payload["error"]["code"] == "rate_limit_exceeded"
|
||||
finally:
|
||||
FakeResponsesClient_openai_gateway.behavior = "ok"
|
||||
_teardown_responses_route_openai_gateway()
|
||||
|
||||
|
||||
def test_responses_protocol_failed_object_is_502(local_db, monkeypatch):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeResponsesClient_openai_gateway)
|
||||
FakeResponsesClient_openai_gateway.behavior = "failed-object"
|
||||
_setup_responses_route_openai_gateway()
|
||||
try:
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_vision_enabled"] = False
|
||||
rt = svc.runtime()
|
||||
resp = run_async(
|
||||
rt.handle_chat(
|
||||
_chat_body_openai_gateway(), cfg, ("guest", "test"), "test", "default"
|
||||
)
|
||||
)
|
||||
assert resp.status_code == 502
|
||||
finally:
|
||||
FakeResponsesClient_openai_gateway.behavior = "ok"
|
||||
_teardown_responses_route_openai_gateway()
|
||||
|
||||
|
||||
def test_responses_protocol_client_reasoning_effort_passes_through(
|
||||
local_db, monkeypatch
|
||||
):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeResponsesClient_openai_gateway)
|
||||
FakeResponsesClient_openai_gateway.behavior = "ok"
|
||||
_setup_responses_route_openai_gateway()
|
||||
try:
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_vision_enabled"] = False
|
||||
rt = svc.runtime()
|
||||
run_async(
|
||||
rt.handle_chat(
|
||||
_chat_body_openai_gateway(reasoning_effort="high"),
|
||||
cfg,
|
||||
("guest", "test"),
|
||||
"test",
|
||||
"default",
|
||||
)
|
||||
)
|
||||
_, sent = rt._client.calls[-1]
|
||||
assert sent["reasoning"] == {"effort": "high"}
|
||||
finally:
|
||||
_teardown_responses_route_openai_gateway()
|
||||
|
||||
|
||||
def test_responses_protocol_default_provider(local_db, monkeypatch):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeResponsesClient_openai_gateway)
|
||||
FakeResponsesClient_openai_gateway.behavior = "ok"
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_vision_enabled"] = False
|
||||
cfg["gateway_upstream_url"] = "https://api.meta.ai/v1/responses"
|
||||
cfg["gateway_upstream_protocol"] = "responses"
|
||||
cfg["gateway_model"] = "muse-spark-1.3-contributor"
|
||||
rt = svc.runtime()
|
||||
resp = run_async(
|
||||
rt.handle_chat(
|
||||
{"messages": [{"role": "user", "content": "hi"}]},
|
||||
cfg,
|
||||
("guest", "test"),
|
||||
"test",
|
||||
"default",
|
||||
)
|
||||
)
|
||||
url, sent = rt._client.calls[-1]
|
||||
assert str(url) == "https://api.meta.ai/v1/responses"
|
||||
assert sent["input"] == [{"type": "message", "role": "user", "content": "hi"}]
|
||||
assert resp.status_code == 200
|
||||
payload = json.loads(bytes(resp.body).decode())
|
||||
assert payload["object"] == "chat.completion"
|
||||
|
||||
Reference in New Issue
Block a user