fix: G10 断点续跑透传 model_provider(sub2api 续跑失败修法)
断点续跑容器不带 --model-provider 时 Server 的 provider 表里 没有 sub2api,thread/resume 报 provider not found(probe15 实弹)。 Docker/Local 断点方法加可选 model_provider 并转发,worker 两处 调用点传 gateway_provider;补单测断言断点命令含 --model-provider sub2api(旧代码下两新测试 1 失败 1 报错)。
This commit is contained in:
1 parent
0ef896c317
commit
a19b1567bb
3 files changed
+96
-8
No files matched your search
+17
-6
@@ -494,7 +494,8 @@ class LocalExecutor(Executor):
|
|||||||
return self._goal_result_from_runner(
|
return self._goal_result_from_runner(
|
||||||
task["id"], self._run_local(cmd, extra_env))
|
task["id"], self._run_local(cmd, extra_env))
|
||||||
|
|
||||||
def goal_resume_breakpoint_task(self, task) -> dict:
|
def goal_resume_breakpoint_task(self, task,
|
||||||
|
model_provider=None) -> dict:
|
||||||
"""G10:调 host.py goal-resume(无 --goal-text)断点续跑。
|
"""G10:调 host.py goal-resume(无 --goal-text)断点续跑。
|
||||||
|
|
||||||
命令:host.py goal-resume --task <host 名>。不传
|
命令:host.py goal-resume --task <host 名>。不传
|
||||||
@@ -502,14 +503,18 @@ class LocalExecutor(Executor):
|
|||||||
goal/verify_command/goal_max_rounds,从
|
goal/verify_command/goal_max_rounds,从
|
||||||
goal_rounds_done 的下一轮接着跑同一循环,不开新循环、
|
goal_rounds_done 的下一轮接着跑同一循环,不开新循环、
|
||||||
不新建 worktree、不重置轮次。结论解析与 goal_task 同款。
|
不新建 worktree、不重置轮次。结论解析与 goal_task 同款。
|
||||||
(worker 只以 (task) 一个参数调本地断点方法,与 Docker
|
model_provider 可选但 worker 必须传:首轮 thread 在该
|
||||||
断点方法的签名不同,不要顺手统一。)
|
provider 下创建,续跑不带同一 --model-provider 时
|
||||||
|
thread/resume 报 provider not found(与 Docker 断点
|
||||||
|
方法同 bug,2026-10-29 probe15 实弹)。(task) 单参
|
||||||
|
调用仍可用;与 Docker 断点方法的签名不同,不要顺手统一。
|
||||||
"""
|
"""
|
||||||
if not isinstance(task, dict) or task.get("id") is None:
|
if not isinstance(task, dict) or task.get("id") is None:
|
||||||
raise ValueError("任务缺少 id")
|
raise ValueError("任务缺少 id")
|
||||||
cmd = self._base_command(model=task.get("model"),
|
cmd = self._base_command(model=task.get("model"),
|
||||||
effort=task.get("effort"),
|
effort=task.get("effort"),
|
||||||
approval=task.get("approval"))
|
approval=task.get("approval"),
|
||||||
|
model_provider=model_provider)
|
||||||
cmd += ["goal-resume", "--task", self._host_name(task)]
|
cmd += ["goal-resume", "--task", self._host_name(task)]
|
||||||
return self._goal_result_from_runner(
|
return self._goal_result_from_runner(
|
||||||
task["id"], self.runner(cmd))
|
task["id"], self.runner(cmd))
|
||||||
@@ -1424,19 +1429,25 @@ class DockerExecutor(Executor):
|
|||||||
|
|
||||||
def goal_resume_breakpoint_task(self, task, timeout=None,
|
def goal_resume_breakpoint_task(self, task, timeout=None,
|
||||||
extra_mounts=None, network=None,
|
extra_mounts=None, network=None,
|
||||||
extra_env=None) -> dict:
|
extra_env=None,
|
||||||
|
model_provider=None) -> dict:
|
||||||
"""G10:容器内调 host.py goal-resume(无 --goal-text)
|
"""G10:容器内调 host.py goal-resume(无 --goal-text)
|
||||||
断点续跑,与 LocalExecutor.goal_resume_breakpoint_task
|
断点续跑,与 LocalExecutor.goal_resume_breakpoint_task
|
||||||
同语义(从 goal_rounds_done 下一轮接着跑,不开新循环)。
|
同语义(从 goal_rounds_done 下一轮接着跑,不开新循环)。
|
||||||
解析与容器内 goal_resume_task 同款;真 Docker 实弹
|
解析与容器内 goal_resume_task 同款;真 Docker 实弹
|
||||||
未做,命令形状由单测覆盖。
|
未做,命令形状由单测覆盖。
|
||||||
|
model_provider 必须透传:首轮 thread 在该 provider 下
|
||||||
|
创建,续跑容器不带同一 --model-provider 时 Server 的
|
||||||
|
provider 表里没有它,thread/resume 报 provider not found
|
||||||
|
(2026-10-29 probe15 实弹)。
|
||||||
"""
|
"""
|
||||||
if not isinstance(task, dict) or task.get("id") is None:
|
if not isinstance(task, dict) or task.get("id") is None:
|
||||||
raise ValueError("任务缺少 id")
|
raise ValueError("任务缺少 id")
|
||||||
inner = self._inner_host_command(
|
inner = self._inner_host_command(
|
||||||
"goal-resume", "--task", self._host_name(task),
|
"goal-resume", "--task", self._host_name(task),
|
||||||
model=task.get("model"), effort=task.get("effort"),
|
model=task.get("model"), effort=task.get("effort"),
|
||||||
approval=task.get("approval"))
|
approval=task.get("approval"),
|
||||||
|
model_provider=model_provider)
|
||||||
cmd = self.build_run_command(task, inner,
|
cmd = self.build_run_command(task, inner,
|
||||||
extra_mounts=extra_mounts,
|
extra_mounts=extra_mounts,
|
||||||
network=network,
|
network=network,
|
||||||
|
|||||||
@@ -414,6 +414,82 @@ class P1WorkerTestCase(unittest.TestCase):
|
|||||||
self.assertIn("goal-resume", cmd)
|
self.assertIn("goal-resume", cmd)
|
||||||
self.assertNotIn("--goal-text", cmd)
|
self.assertNotIn("--goal-text", cmd)
|
||||||
|
|
||||||
|
# 12b. 断点续跑透传 model_provider:首轮 thread 在该
|
||||||
|
# provider 下创建,续跑命令必须带同一 --model-provider,
|
||||||
|
# 否则 thread/resume 因 provider 表里没有它而失败
|
||||||
|
# (2026-10-29 probe15 实弹:sub2api not found)。
|
||||||
|
def test_goal_resume_breakpoint_forwards_model_provider(self):
|
||||||
|
docker_calls = []
|
||||||
|
|
||||||
|
def dfake(cmd, timeout=None):
|
||||||
|
docker_calls.append(list(cmd))
|
||||||
|
return SimpleNamespace(
|
||||||
|
returncode=0,
|
||||||
|
stdout="goal met after rounds\n"
|
||||||
|
"usage input=10 cached=0 cache_write=0 output=5 "
|
||||||
|
"reasoning=0 total=15 completions=1\n",
|
||||||
|
stderr="")
|
||||||
|
|
||||||
|
task = {"id": "T1", "prompt": "orig goal"}
|
||||||
|
docker = DockerExecutor(runner=dfake, env=FAKE_ENV)
|
||||||
|
with mock.patch("cloud.sandbox.shutil.which",
|
||||||
|
return_value="/usr/bin/docker"):
|
||||||
|
docker.goal_resume_breakpoint_task(
|
||||||
|
task, model_provider="sub2api")
|
||||||
|
cmd = docker_calls[-1]
|
||||||
|
self.assertIn("goal-resume", cmd)
|
||||||
|
self.assertNotIn("--goal-text", cmd)
|
||||||
|
self.assertIn("--model-provider", cmd)
|
||||||
|
self.assertIn("sub2api", cmd)
|
||||||
|
# 不传 provider 时不带该 flag(透传语义,不是强制默认值)。
|
||||||
|
docker_calls.clear()
|
||||||
|
with mock.patch("cloud.sandbox.shutil.which",
|
||||||
|
return_value="/usr/bin/docker"):
|
||||||
|
docker.goal_resume_breakpoint_task(task)
|
||||||
|
self.assertNotIn("--model-provider", docker_calls[-1])
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake(cmd):
|
||||||
|
calls.append(list(cmd))
|
||||||
|
return SimpleNamespace(
|
||||||
|
returncode=0,
|
||||||
|
stdout="goal met after rounds\n"
|
||||||
|
"usage input=10 cached=0 cache_write=0 output=5 "
|
||||||
|
"reasoning=0 total=15 completions=1\n",
|
||||||
|
stderr="")
|
||||||
|
|
||||||
|
local = LocalExecutor("/fake/host.py", runner=fake)
|
||||||
|
local.goal_resume_breakpoint_task(task, model_provider="sub2api")
|
||||||
|
self.assertIn("--model-provider", calls[-1])
|
||||||
|
self.assertIn("sub2api", calls[-1])
|
||||||
|
|
||||||
|
# 12c. worker 本地断点调用点透传网关 provider:run_one
|
||||||
|
# 认领 goal_resume_pending 任务后,本地断点命令必须带
|
||||||
|
# 网关解析出的 --model-provider(Docker 调用点同理,
|
||||||
|
# 沙箱级已覆盖命令形状)。
|
||||||
|
def test_worker_local_breakpoint_forwards_gateway_provider(self):
|
||||||
|
def fake(cmd):
|
||||||
|
self.calls.append(list(cmd))
|
||||||
|
return SimpleNamespace(
|
||||||
|
returncode=0,
|
||||||
|
stdout="goal met after rounds\n"
|
||||||
|
"usage input=10 cached=0 cache_write=0 output=5 "
|
||||||
|
"reasoning=0 total=15 completions=1\n",
|
||||||
|
stderr="")
|
||||||
|
|
||||||
|
worker = self._worker(runner=fake)
|
||||||
|
worker._ensure_gateway = lambda task: ("sub2api", {})
|
||||||
|
_insert_task(self.db_path, "T1", goal_mode=1,
|
||||||
|
goal="orig goal", goal_max_rounds=3,
|
||||||
|
goal_resume_pending=1,
|
||||||
|
model="gpt-5.6-terra")
|
||||||
|
worker.run_one()
|
||||||
|
self.assertEqual(_status(self.db_path, "T1"), "completed")
|
||||||
|
self.assertTrue(any(
|
||||||
|
"--model-provider" in cmd and "sub2api" in cmd
|
||||||
|
for cmd in self.calls))
|
||||||
|
|
||||||
# 13. 开新目标:goal-resume 带 --goal-text(旧行为保留)
|
# 13. 开新目标:goal-resume 带 --goal-text(旧行为保留)
|
||||||
def test_goal_resume_new_goal_keeps_goal_text(self):
|
def test_goal_resume_new_goal_keeps_goal_text(self):
|
||||||
calls = []
|
calls = []
|
||||||
|
|||||||
+3
-2
@@ -3029,7 +3029,8 @@ class Worker:
|
|||||||
extra_mounts=tenant_mounts,
|
extra_mounts=tenant_mounts,
|
||||||
network=tenant_network,
|
network=tenant_network,
|
||||||
extra_env=_merge_env(proxy_env,
|
extra_env=_merge_env(proxy_env,
|
||||||
gateway_env)),
|
gateway_env),
|
||||||
|
model_provider=gateway_provider),
|
||||||
quota_check)
|
quota_check)
|
||||||
elif task.get("_resume"):
|
elif task.get("_resume"):
|
||||||
# P1 断点续跑传 None(不传 --goal-text,
|
# P1 断点续跑传 None(不传 --goal-text,
|
||||||
@@ -3091,7 +3092,7 @@ class Worker:
|
|||||||
if task.get("_goal_resume_breakpoint") and isinstance(
|
if task.get("_goal_resume_breakpoint") and isinstance(
|
||||||
self.executor, LocalExecutor):
|
self.executor, LocalExecutor):
|
||||||
result = self.executor.goal_resume_breakpoint_task(
|
result = self.executor.goal_resume_breakpoint_task(
|
||||||
task)
|
task, model_provider=gateway_provider)
|
||||||
elif task.get("_goal_resume_breakpoint"):
|
elif task.get("_goal_resume_breakpoint"):
|
||||||
# 假执行器/CloudExecutor 没有断点方法时
|
# 假执行器/CloudExecutor 没有断点方法时
|
||||||
# 绝不回落成 goal(会撞已存在)或
|
# 绝不回落成 goal(会撞已存在)或
|
||||||
|
|||||||
Reference in new issue
Block a user