feat: per-task model/effort/approval + relay models endpoint (#59)

Co-authored-by: host <host@test.local>
This commit is contained in:
openqjandhost authored and GitHub committed 2026-10-27 01:44:53 +08:00
1 parent cff152c5a2
commit 8b2f3968dc
9 files changed
+521 -16

No files matched your search

+54 -3
View File
@@ -83,6 +83,8 @@ from cloud.oauth import (
start_oauth_login,
unbind_external_identity,
)
from cloud.relay import RelayError as _RelayError
from cloud.relay import get_relay_models as _get_relay_models
from cloud.repos import validate_repo_url
from cloud.push import (
create_subscription as _push_create,
@@ -389,6 +391,12 @@ def migrate_tasks_table(conn):
# HA 阶段 1:认领任务的 worker 所在主机名;/pr 据此判断
# worktree 在本机(同步跑)还是别机(排 pr 任务给该主机)。
"worktree_host": "TEXT",
# 任务级选项(console 选模型/强度/权限用):NULL = 跟随
# worker 配置默认值;model 不做白名单(中转目录会变,
# 下拉只是建议值),effort/approval 由 _create_task 校验。
"model": "TEXT",
"effort": "TEXT",
"approval": "TEXT",
}
for name, coltype in new_cols.items():
if name not in cols:
@@ -524,6 +532,9 @@ def _task_to_dict(row):
"verify_command": _row_get(row, "verify_command"),
"goal_max_rounds": _row_get(row, "goal_max_rounds"),
"worktree_host": _row_get(row, "worktree_host"),
"model": _row_get(row, "model"),
"effort": _row_get(row, "effort"),
"approval": _row_get(row, "approval"),
}
@@ -817,6 +828,8 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
self._metrics(user_id)
elif path == "/models" and method == "GET":
self._models_overview(user_id)
elif path == "/relay/models" and method == "GET":
self._relay_models(user_id)
elif path.startswith("/projects/"):
self._dispatch_project(user_id, method, path, query)
elif path == "/tasks" and method == "POST":
@@ -1295,6 +1308,20 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
self._send_json(200, {"task_id": task_id,
"project_id": task["project_id"], **report})
def _relay_models(self, user_id):
# 中转可用模型列表:服务端凭环境变量里的 relay key 现取
# 现回,只把模型 id 交给调用者。key 绝不进响应、日志、
# 缓存(见 cloud/relay.py)。单测可经 server.relay_fetcher
# 注入假取数函数;中转挂了或未配 key 时 502,不编造列表。
fetcher = getattr(self.server, "relay_fetcher", None)
try:
models, cached, updated_at = _get_relay_models(fetcher=fetcher)
except _RelayError as exc:
self._send_json(502, {"error": str(exc)})
return
self._send_json(200, {
"models": models, "cached": cached, "updated_at": updated_at})
def _models_overview(self, user_id):
# 优先级列表来自环境变量 MODELS_PRIORITY(逗号分隔),未设时
# 用默认列表;这只是控制面的默认配置,不是 provider 的承诺。
@@ -1382,6 +1409,28 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
self._send_json(400, {
"error": "executor must be local, docker or cloud"})
return
# 任务级选项:model 不做白名单(中转目录会变,下拉只是
# 建议值);effort 必须是 codex 的 effort 档位;approval
# 是 console 三档(ask/auto/full),非法值 400 且不建任务。
# 三者都缺省(NULL)时 worker 走配置默认值,行为与过去一致。
model = body.get("model")
if model is not None:
if not isinstance(model, str) or not model.strip():
self._send_json(400, {
"error": "model must be a non-empty string"})
return
model = model.strip()
effort = body.get("effort")
if effort is not None and effort not in (
"low", "medium", "high", "xhigh", "max"):
self._send_json(400, {
"error": "effort must be low, medium, high, xhigh or max"})
return
approval = body.get("approval")
if approval is not None and approval not in ("ask", "auto", "full"):
self._send_json(400, {
"error": "approval must be ask, auto or full"})
return
# goal 字段:verify_command 非空即 goal 模式(worker 走
# host.py goal 循环);goal 文本缺省用 prompt;轮次缺省 3
# (与 host.py 默认一致),非法值 400 且不建任务。
@@ -1431,12 +1480,14 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
"INSERT INTO tasks(id, project_id, prompt, status,"
" idempotency_key, depends_on, priority, created_by,"
" created_at, updated_at, last_heartbeat, executor,"
" goal, verify_command, goal_max_rounds)"
" goal, verify_command, goal_max_rounds,"
" model, effort, approval)"
" VALUES(?, ?, ?, 'queued', ?, ?, ?, ?, ?, ?, NULL, ?,"
" ?, ?, ?)",
" ?, ?, ?, ?, ?, ?)",
(task_id, project_id, prompt, idempotency_key,
json.dumps(depends_on), priority, user_id, now, now,
executor, goal, verify_command, goal_max_rounds),
executor, goal, verify_command, goal_max_rounds,
model, effort, approval),
)
conn.commit()
except (sqlite3.IntegrityError, _PgIntegrityError):