Files
Mujoco_WASM/decision_server/server.py
T
chenlin f3a8a38acd
web-platform-ci / Standalone decision service (no cloud credentials) (push) Waiting to run
web-platform-ci / TypeScript, lint, unit, build (push) Waiting to run
web-platform-ci / Playwright E2E (push) Waiting to run
lekiwi-compatibility / cpu-compatibility (push) Waiting to run
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Waiting to run
web-platform-ci / TypeScript, lint, unit, build (pull_request) Waiting to run
web-platform-ci / Playwright E2E (pull_request) Waiting to run
lekiwi-compatibility / cpu-compatibility (pull_request) Waiting to run
feat: release v1.0.1 CADWorld 网站与 LeKiwi 智能抓放
集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
2026-09-24 09:57:41 +08:00

387 lines
14 KiB
Python

"""Loopback-only bounded model gateway. No physics, arbitrary URL proxy or arbitrary RPC."""
import asyncio
import hmac
import secrets
import time
from collections import deque
from pathlib import Path
import aiohttp
from aiohttp import web
from .connections import Connections
from .protocol import (
SCHEMA,
DecisionError,
candidates,
fields,
loads,
remaining_skills,
validate,
)
from .providers import jev, openai
from .providers.codex import CodexAccount
from .providers.http import post, usage
PREFIX = "/api/decision/v1"
STATE = web.AppKey("decision_state", object)
WEB_SERVICE = web.RequestKey("website_service", object)
class Service:
def __init__(self, directory, token, origins, port, *, connections=None):
self.connections = connections if connections is not None else Connections(directory)
self.token = token
self.origins = set(origins)
self.hosts = {f"127.0.0.1:{port}", f"localhost:{port}", f"[::1]:{port}"}
self.codex = CodexAccount(directory / "codex")
self.session = None
self.active = {}
self.runs = {}
self.calls = deque()
self.cancelled = {}
self.records = deque(maxlen=100)
self.epoch = 0
def invalidate(self):
self.epoch += 1
for task in self.active.values():
task.cancel()
def admit(self, role, stamp):
now = time.monotonic()
self.cancelled = {k: t for k, t in self.cancelled.items() if now - t < 3600}
if (stamp["runId"], stamp["requestId"]) in self.cancelled:
raise DecisionError("request_cancelled", 409)
while self.calls and now - self.calls[0] > 3600:
self.calls.popleft()
if len(self.calls) >= 120:
raise DecisionError("session_hourly_budget_exceeded", 429)
# Bounded tombstones prevent reused request IDs or cancelled calls resetting budgets.
self.runs = {k: v for k, v in self.runs.items() if now - v["start"] < 3600}
run_id = stamp["runId"]
run = self.runs.get(run_id)
if run is None:
if len(self.runs) >= 32:
raise DecisionError("too_many_runs", 429)
run = {
"start": now,
"llm": 0,
"jev": 0,
"ids": set(),
"scene": stamp["sceneRevision"],
"sequence": -1,
"revision": -1,
}
self.runs[run_id] = run
if now - run["start"] > 1200:
raise DecisionError("run_wall_deadline_exceeded", 408)
if (
stamp["requestId"] in run["ids"]
or stamp["sceneRevision"] != run["scene"]
or stamp["sequence"] < run["sequence"]
or stamp["planRevision"] < run["revision"]
):
raise DecisionError("stale_or_duplicate_request", 409)
if run[role] >= (60 if role == "jev" else 3):
raise DecisionError("run_request_budget_exceeded", 429)
run[role] += 1
run["ids"].add(stamp["requestId"])
run["sequence"], run["revision"] = stamp["sequence"], stamp["planRevision"]
self.calls.append(now)
async def request(self, role, data):
if self.active:
raise DecisionError("request_already_running", 409)
common = ["observation"]
fields(
data,
common + (["instruction", "remaining"] if role == "llm" else ["candidates"]),
[] if role == "llm" else ["failure"],
)
data = self.connections.redact(data)
obs = validate("Observation", data["observation"])
if role == "llm":
instruction = data["instruction"]
if not isinstance(instruction, str) or not 1 <= len(instruction) <= 2000:
raise DecisionError("invalid_instruction")
remaining_skills(data["remaining"])
else:
candidates(data["candidates"])
codes = SCHEMA["$defs"]["SkillResult"]["properties"]["code"]["enum"]
if data.get("failure", "none") not in codes:
raise DecisionError("invalid_failure_code")
conn = self.connections.get(role)
async def invoke():
if conn.protocol == "codex":
return await self.codex.plan(data, conn.model)
if role == "llm":
return await openai.plan(self.session, conn, data)
return await jev.decide(self.session, conn, data)
return await self.execute(role, obs["stamp"], conn, invoke)
async def execute(self, role, stamp, conn, invoke):
if self.active:
raise DecisionError("request_already_running", 409)
self.admit(role, stamp)
key = (stamp["runId"], stamp["requestId"])
epoch = self.epoch
start = time.monotonic()
code = "completed"
task = asyncio.create_task(invoke())
self.active[key] = task
try:
value, metrics = await asyncio.wait_for(task, 60)
if self.epoch != epoch:
raise DecisionError("connection_changed", 409)
# Summaries are the only free-form upstream strings; redact current credential values.
value = self.connections.redact(value)
return {
"stamp": stamp,
"value": value,
"provider": conn.protocol,
"model": conn.model,
"usage": metrics,
"elapsedMs": (time.monotonic() - start) * 1000,
}
except asyncio.CancelledError:
code = "cancelled"
raise DecisionError("request_cancelled", 409) from None
except TimeoutError:
code = "timeout"
raise DecisionError("request_timeout", 504) from None
except DecisionError as exc:
code = exc.code
raise
except Exception:
code = "internal_error"
raise
finally:
if not task.done():
task.cancel()
await asyncio.gather(task, return_exceptions=True)
self.active.pop(key, None)
self.records.append(
{
"role": role,
"status": code,
"elapsedMs": round((time.monotonic() - start) * 1000),
}
)
def service_for(request):
return request.get(WEB_SERVICE) or request.app[STATE]
@web.middleware
async def boundary(request, handler):
service = service_for(request)
# Exact Host and Origin checks precede authentication and even OPTIONS; no DNS wildcard.
if request.headers.get("Host", "") not in service.hosts:
return web.json_response({"error": "host_forbidden"}, status=403)
origin = request.headers.get("Origin")
if origin and origin not in service.origins:
return web.json_response({"error": "origin_forbidden"}, status=403)
if request.method == "OPTIONS":
response = web.Response(status=204)
elif not hmac.compare_digest(
request.headers.get("Authorization", "").encode(), ("Bearer " + service.token).encode()
):
response = web.json_response({"error": "token_required"}, status=401)
else:
try:
response = await handler(request)
except DecisionError as exc:
response = web.json_response({"error": exc.code}, status=exc.status)
except web.HTTPException as exc:
response = web.json_response({"error": "http_request_rejected"}, status=exc.status)
except Exception:
# Never echo raw provider responses, config, tracebacks, account URLs or headers.
response = web.json_response({"error": "internal_error"}, status=500)
response.headers.update({"Cache-Control": "no-store", "X-Content-Type-Options": "nosniff"})
if origin:
response.headers.update(
{
"Access-Control-Allow-Origin": origin,
"Vary": "Origin",
"Access-Control-Allow-Headers": "Authorization,Content-Type",
"Access-Control-Allow-Methods": "GET,POST,PUT,OPTIONS",
}
)
return response
async def body(request):
if request.content_type != "application/json":
raise DecisionError("json_content_type_required", 415)
try:
return loads(await request.text())
except UnicodeError:
raise DecisionError("invalid_encoding") from None
async def status(request):
s = service_for(request)
return web.json_response(
{
"version": "lekiwi-agent-v1",
"keyStorage": "memory-only",
"connections": {k: v.public() for k, v in s.connections.values.items()},
"active": len(s.active),
"records": list(s.records),
"codexCheckedModels": sorted(s.codex.checked),
}
)
async def configure(request):
s = service_for(request)
data = await body(request)
result = s.connections.set(data)
s.invalidate()
return web.json_response(result)
async def plan(request):
return web.json_response(await service_for(request).request("llm", await body(request)))
async def decide(request):
return web.json_response(await service_for(request).request("jev", await body(request)))
async def cancel(request):
data = fields(await body(request), ["runId", "requestId"])
if any(not isinstance(v, str) or len(v) > 128 for v in data.values()):
raise DecisionError("invalid_request_id")
service = service_for(request)
key = (data["runId"], data["requestId"])
service.cancelled[key] = time.monotonic()
if len(service.cancelled) > 256:
service.cancelled.pop(next(iter(service.cancelled)))
task = service.active.get(key)
if task:
task.cancel()
return web.json_response({"cancelled": task is not None})
async def test_connection(request):
s = service_for(request)
data = fields(await body(request), ["role"])
if data["role"] not in ("llm", "jev"):
raise DecisionError("invalid_role")
if s.active:
raise DecisionError("request_already_running", 409)
conn = s.connections.get(data["role"])
# Tests are explicit billable calls, count against the same session budget, no retries.
stamp = {
"runId": "connection-tests",
"requestId": secrets.token_hex(12),
"sceneRevision": 0,
"sequence": 0,
"planRevision": 0,
}
async def invoke():
if conn.protocol == "codex":
return await s.codex.check_model(conn.model), {}
if data["role"] == "llm":
value, metrics = await openai.structured(
s.session,
conn,
'Return {"ok":true}.',
{
"type": "object",
"additionalProperties": False,
"required": ["ok"],
"properties": {"ok": {"type": "boolean"}},
},
)
if value != {"ok": True}:
raise DecisionError("connection_test_invalid_response", 502)
else:
result = await post(
s.session,
conn,
"",
{
"model": conn.model,
"state": "Connection test. Choose ok.",
**(
{"provider": {"allow_fallbacks": False}}
if conn.protocol == "openrouter-decisions"
else {}
),
"questions": {"test": jev.question(["ok"], "Choose ok.")},
},
)
jev.answer(result, "test", ["ok"])
metrics = usage(result)
return {"ok": True}, metrics
return web.json_response(await s.execute(data["role"], stamp, conn, invoke))
async def codex_operation(request):
codex = service_for(request).codex
operations = {
"status": codex.status,
"login": codex.login,
"cancel": codex.cancel_login,
"logout": codex.logout,
"models": codex.models,
"limits": codex.limits,
}
name = request.match_info["operation"]
if name not in operations:
raise DecisionError("unknown_codex_operation", 404)
if request.method == "POST":
fields(await body(request), [])
return web.json_response(await operations[name]())
def create_app(directory=None, token=None, origins=None, port=8768):
directory = (directory or Path.home() / ".local/state/mujoco-decision").expanduser().resolve()
service = Service(
directory,
token or secrets.token_urlsafe(32),
origins
or {
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:4173",
"http://127.0.0.1:4173",
},
port,
)
app = web.Application(middlewares=[boundary], client_max_size=65536)
app[STATE] = service
app.router.add_get(PREFIX + "/status", status)
app.router.add_put(PREFIX + "/connections", configure)
app.router.add_post(PREFIX + "/test", test_connection)
app.router.add_post(PREFIX + "/plan", plan)
app.router.add_post(PREFIX + "/decide", decide)
app.router.add_post(PREFIX + "/cancel", cancel)
app.router.add_get(PREFIX + "/codex/{operation:status|models|limits}", codex_operation)
app.router.add_post(PREFIX + "/codex/{operation:login|cancel|logout}", codex_operation)
async def options(_):
return web.Response(status=204)
app.router.add_route("OPTIONS", PREFIX + "/{path:.*}", options)
async def lifecycle(_):
async with aiohttp.ClientSession(trust_env=False) as session:
service.session = session
yield
service.invalidate()
await asyncio.gather(*service.active.values(), return_exceptions=True)
await service.codex.close()
app.cleanup_ctx.append(lifecycle)
return app