From 85985dafa3028f927006cf2de89f88acdaa44fba Mon Sep 17 00:00:00 2001 From: hanhainebula <2512674094@qq.com> Date: Sun, 27 Sep 2026 23:27:21 +0800 Subject: [PATCH 1/3] Update pinned agent CLI versions Pin Codex 0.157.1, Claude Code 2.1.283, and Pi 0.87.1 across task images, launcher defaults, tests, and documentation. --- README.md | 2 +- README_zh.md | 2 +- docs/evaluation.md | 6 ++--- docs/network-policy.md | 6 ++--- docs/quickstart.md | 2 +- scripts/harbor_agents.py | 6 ++--- scripts/run_task.py | 6 ++--- scripts/tests/test_network_policies.py | 32 +++++++++++++------------- scripts/tests/test_release.py | 6 ++--- tasks/task-1-1/environment/Dockerfile | 6 ++--- tasks/task-1-2/environment/Dockerfile | 6 ++--- tasks/task-1-3/environment/Dockerfile | 6 ++--- tasks/task-1-4/environment/Dockerfile | 6 ++--- tasks/task-2-1/environment/Dockerfile | 6 ++--- tasks/task-2-2/environment/Dockerfile | 6 ++--- tasks/task-2-3/environment/Dockerfile | 6 ++--- tasks/task-2-4/environment/Dockerfile | 6 ++--- tasks/task-2-5/environment/Dockerfile | 6 ++--- 18 files changed, 61 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index cd1506a..0034e17 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ it. #### Claude Code and the official Anthropic API -Claude Code 2.1.273 is preinstalled in every task image. Set an Anthropic model +Claude Code 2.1.283 is preinstalled in every task image. Set an Anthropic model available to your API account and the dedicated coding-agent key: ```dotenv diff --git a/README_zh.md b/README_zh.md index 1962237..2fb1d8d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -158,7 +158,7 @@ bash scripts/run_task.sh --task task-1-1 --agent codex \ #### Claude Code 和 Anthropic 官方 API -所有任务镜像都预装 Claude Code 2.1.273。填写 API 账户可用的 Anthropic 模型 +所有任务镜像都预装 Claude Code 2.1.283。填写 API 账户可用的 Anthropic 模型 和独立的编码智能体密钥: ```dotenv diff --git a/docs/evaluation.md b/docs/evaluation.md index d7fad87..7830aae 100644 --- a/docs/evaluation.md +++ b/docs/evaluation.md @@ -114,7 +114,7 @@ current directory. ### Claude Code with the official Anthropic API -Claude Code is pinned to version 2.1.273 and preinstalled in every task image. +Claude Code is pinned to version 2.1.283 and preinstalled in every task image. Set a model available to your Anthropic API account and its dedicated coding-agent key: @@ -133,7 +133,7 @@ bash scripts/run_task.sh \ --dry-run ``` -Claude Code 2.1.273 accepts `low`, `medium`, `high`, `xhigh`, and `max` effort. +Claude Code 2.1.283 accepts `low`, `medium`, `high`, `xhigh`, and `max` effort. Use `AGENT_REASONING_EFFORT` as a local default or `--reasoning-effort` for an explicit run. The launcher maps `AGENT_ANTHROPIC_API_KEY` to the agent-only `ANTHROPIC_API_KEY`, permits only `api.anthropic.com` during restricted Agent @@ -148,7 +148,7 @@ variables. ### Pi native providers -Pi is pinned to version 0.85.1 by the launcher. It currently accepts these +Pi is pinned to version 0.87.1 by the launcher. It currently accepts these verified provider/model combinations: | Model | Required variable | diff --git a/docs/network-policy.md b/docs/network-policy.md index fd5fba4..011e691 100644 --- a/docs/network-policy.md +++ b/docs/network-policy.md @@ -114,7 +114,7 @@ launcher: the hostname in `AGENT_OPENAI_BASE_URL` for Codex, `api.deepseek.com` for Pi + DeepSeek, `api.z.ai` for Pi + Z.AI, or the fixed `api.anthropic.com` host for Claude Code. The launcher passes it through Harbor's `--allow-agent-host`, which augments only the `agent.run()` phase. -Codex 0.147.0, Pi 0.85.1, and Claude Code 2.1.273 are preinstalled in every +Codex 0.157.1, Pi 0.87.1, and Claude Code 2.1.283 are preinstalled in every agent image, so agent setup does not need package-registry or general internet access. @@ -144,7 +144,7 @@ invoking Harbor directly on a non-public task, add the matching hostname: PYTHONPATH="$PWD${PYTHONPATH:+:$PYTHONPATH}" harbor run --path tasks/TASK_ID \ --env scripts.harbor_environments:PhaseScopedDocker \ --agent scripts.harbor_agents:PreinstalledCodex --model MODEL_ID \ - --ak version=0.147.0 \ + --ak version=0.157.1 \ --allow-agent-host MODEL_API_HOST ``` @@ -156,7 +156,7 @@ PYTHONPATH="$PWD${PYTHONPATH:+:$PYTHONPATH}" harbor run --path tasks/TASK_ID \ --env scripts.harbor_environments:PhaseScopedDocker \ --agent scripts.harbor_agents:PreinstalledClaudeCode \ --model ANTHROPIC_MODEL_ID \ - --ak version=2.1.273 \ + --ak version=2.1.283 \ --ae 'ANTHROPIC_API_KEY=${AGENT_ANTHROPIC_API_KEY}' \ --allow-agent-host api.anthropic.com ``` diff --git a/docs/quickstart.md b/docs/quickstart.md index 201435b..74a48d1 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -100,7 +100,7 @@ the run as successful. ## Next steps - Read the [full evaluation guide](evaluation.md) before selecting another task - or agent. It documents Codex, Pi, and the pinned Claude Code 2.1.273 launcher, + or agent. It documents Codex, Pi, and the pinned Claude Code 2.1.283 launcher, and its task matrix lists exactly which additional credentials and hardware each task uses. - Read the selected task's `README.md` and `instruction.md` for its resource diff --git a/scripts/harbor_agents.py b/scripts/harbor_agents.py index cc4ec17..8b8fac1 100644 --- a/scripts/harbor_agents.py +++ b/scripts/harbor_agents.py @@ -12,9 +12,9 @@ from scripts.pi_trajectory import convert_events -CODEX_VERSION = "0.147.0" -CLAUDE_CODE_VERSION = "2.1.273" -PI_VERSION = "0.85.1" +CODEX_VERSION = "0.157.1" +CLAUDE_CODE_VERSION = "2.1.283" +PI_VERSION = "0.87.1" async def _require_version( diff --git a/scripts/run_task.py b/scripts/run_task.py index c8de82d..4fbe4ea 100755 --- a/scripts/run_task.py +++ b/scripts/run_task.py @@ -22,8 +22,8 @@ REPO = Path(__file__).resolve().parents[1] TASKS = tuple(sorted(path.parent.name for path in (REPO / "tasks").glob("*/task.toml"))) -CODEX_VERSION = "0.147.0" -CLAUDE_CODE_VERSION = "2.1.273" +CODEX_VERSION = "0.157.1" +CLAUDE_CODE_VERSION = "2.1.283" CLAUDE_CODE_EFFORT_LEVELS = ("low", "medium", "high", "xhigh", "max") ANTHROPIC_HOST = "api.anthropic.com" CLAUDE_HOST_ENV_VARS = ( @@ -36,7 +36,7 @@ "CLAUDE_CODE_USE_VERTEX", "AWS_BEARER_TOKEN_BEDROCK", ) -PI_VERSION = "0.85.1" +PI_VERSION = "0.87.1" PI_THINKING_LEVELS = ("off", "minimal", "low", "medium", "high", "xhigh") PI_MODEL_KEYS = { "deepseek/deepseek-flash": "DEEPSEEK_API_KEY", diff --git a/scripts/tests/test_network_policies.py b/scripts/tests/test_network_policies.py index 1ff3914..d33ea45 100644 --- a/scripts/tests/test_network_policies.py +++ b/scripts/tests/test_network_policies.py @@ -126,9 +126,9 @@ def test_agent_images_pin_all_preinstalled_clis(self): text, ) self.assertIn("@earendil-works/pi-coding-agent@${SEARCH_SWE_PI_VERSION}", text) - self.assertIn("SEARCH_SWE_CODEX_VERSION=0.147.0", text) - self.assertIn("SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.273", text) - self.assertIn("SEARCH_SWE_PI_VERSION=0.85.1", text) + self.assertIn("SEARCH_SWE_CODEX_VERSION=0.157.1", text) + self.assertIn("SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.283", text) + self.assertIn("SEARCH_SWE_PI_VERSION=0.87.1", text) claude_install = text.split("&& npm install --global", 1)[1].split( "&& codex --version", 1 )[0] @@ -158,7 +158,7 @@ def test_claude_effort_levels_are_supported_by_the_harbor_adapter(self): agent = PreinstalledClaudeCode( logs_dir=Path(directory), model_name="fixture", - version="2.1.273", + version="2.1.283", reasoning_effort=effort, ) self.assertIn(f"--effort {effort}", agent.build_cli_flags()) @@ -174,14 +174,14 @@ async def exec(self, command, **kwargs): return SimpleNamespace(return_code=0, stdout=self.output) cases = ( - (PreinstalledCodex, "0.147.0", "codex-cli 0.147.0\n", "codex --version"), + (PreinstalledCodex, "0.157.1", "codex-cli 0.157.1\n", "codex --version"), ( PreinstalledClaudeCode, - "2.1.273", - "2.1.273 (Claude Code)\n", + "2.1.283", + "2.1.283 (Claude Code)\n", "claude --version", ), - (PreinstalledPi, "0.85.1", "0.85.1\n", "pi --version"), + (PreinstalledPi, "0.87.1", "0.87.1\n", "pi --version"), ) for agent_class, version, output, version_command in cases: with self.subTest(agent=agent_class.__name__): @@ -219,34 +219,34 @@ async def exec(self, command, **kwargs): wrong_request = PreinstalledClaudeCode( logs_dir=Path(directory), model_name="fixture", - version="2.1.272", + version="2.1.282", ) with self.assertRaisesRegex( - RuntimeError, "Search-SWE requires Claude Code 2.1.273" + RuntimeError, "Search-SWE requires Claude Code 2.1.283" ): asyncio.run( - wrong_request.install(FakeEnvironment(0, "2.1.273 (Claude Code)")) + wrong_request.install(FakeEnvironment(0, "2.1.283 (Claude Code)")) ) wrong_installed_cli = PreinstalledClaudeCode( logs_dir=Path(directory), model_name="fixture", - version="2.1.273", + version="2.1.283", ) with self.assertRaisesRegex( RuntimeError, - "must provide Claude Code 2.1.273; found 2.1.272", + "must provide Claude Code 2.1.283; found 2.1.282", ): asyncio.run( wrong_installed_cli.install( - FakeEnvironment(0, "2.1.272 (Claude Code)") + FakeEnvironment(0, "2.1.282 (Claude Code)") ) ) missing_cli = PreinstalledClaudeCode( logs_dir=Path(directory), model_name="fixture", - version="2.1.273", + version="2.1.283", ) with self.assertRaisesRegex(RuntimeError, "no usable CLI"): asyncio.run(missing_cli.install(FakeEnvironment(127, ""))) @@ -346,7 +346,7 @@ def test_claude_code_preview_uses_pinned_cli_and_namespaced_key(self): self.assertEqual(result.returncode, 0, result.stderr) self.assertNotIn(secret, result.stdout + result.stderr) argv = self.argv(result) - self.assertIn("version=2.1.273", self.flag_values(argv, "--ak")) + self.assertIn("version=2.1.283", self.flag_values(argv, "--ak")) self.assertIn("reasoning_effort=xhigh", self.flag_values(argv, "--ak")) self.assertEqual( self.flag_values(argv, "--ae"), diff --git a/scripts/tests/test_release.py b/scripts/tests/test_release.py index 6efd759..eff333d 100644 --- a/scripts/tests/test_release.py +++ b/scripts/tests/test_release.py @@ -301,7 +301,7 @@ def test_launcher_builds_supported_pi_commands(self): "scripts.harbor_agents:PreinstalledPi", ) self.assertEqual(argv[argv.index("-m") + 1], model) - self.assertIn("version=0.85.1", self.flag_values(argv, "--ak")) + self.assertIn("version=0.87.1", self.flag_values(argv, "--ak")) self.assertIn("thinking=xhigh", self.flag_values(argv, "--ak")) self.assertFalse(any(value.startswith(("reasoning_effort=", "config=")) for value in self.flag_values(argv, "--ak"))) @@ -340,7 +340,7 @@ def test_launcher_builds_pinned_claude_code_command(self): "scripts.harbor_agents:PreinstalledClaudeCode", ) self.assertEqual(argv[argv.index("-m") + 1], "claude-sonnet-4-6") - self.assertIn("version=2.1.273", self.flag_values(argv, "--ak")) + self.assertIn("version=2.1.283", self.flag_values(argv, "--ak")) self.assertIn("reasoning_effort=max", self.flag_values(argv, "--ak")) self.assertEqual( self.flag_values(argv, "--ae"), @@ -388,7 +388,7 @@ def test_pi_defaults_version_and_validates_environment_thinking(self): capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stderr) argv = shlex.split(result.stdout.splitlines()[-1]) - self.assertIn("version=0.85.1", self.flag_values(argv, "--ak")) + self.assertIn("version=0.87.1", self.flag_values(argv, "--ak")) self.assertFalse(any(value.startswith("thinking=") for value in self.flag_values(argv, "--ak"))) env = self.launcher_env() diff --git a/tasks/task-1-1/environment/Dockerfile b/tasks/task-1-1/environment/Dockerfile index b83d6c2..3358c9f 100644 --- a/tasks/task-1-1/environment/Dockerfile +++ b/tasks/task-1-1/environment/Dockerfile @@ -1,8 +1,8 @@ FROM docker.io/hanhainebula/search-swe-base:cpu-py3.12-1.0.0 -ARG SEARCH_SWE_CODEX_VERSION=0.147.0 -ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.273 -ARG SEARCH_SWE_PI_VERSION=0.85.1 +ARG SEARCH_SWE_CODEX_VERSION=0.157.1 +ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.283 +ARG SEARCH_SWE_PI_VERSION=0.87.1 RUN npm install --global --ignore-scripts \ --registry=https://registry.npmmirror.com \ "@openai/codex@${SEARCH_SWE_CODEX_VERSION}" \ diff --git a/tasks/task-1-2/environment/Dockerfile b/tasks/task-1-2/environment/Dockerfile index b83d6c2..3358c9f 100644 --- a/tasks/task-1-2/environment/Dockerfile +++ b/tasks/task-1-2/environment/Dockerfile @@ -1,8 +1,8 @@ FROM docker.io/hanhainebula/search-swe-base:cpu-py3.12-1.0.0 -ARG SEARCH_SWE_CODEX_VERSION=0.147.0 -ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.273 -ARG SEARCH_SWE_PI_VERSION=0.85.1 +ARG SEARCH_SWE_CODEX_VERSION=0.157.1 +ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.283 +ARG SEARCH_SWE_PI_VERSION=0.87.1 RUN npm install --global --ignore-scripts \ --registry=https://registry.npmmirror.com \ "@openai/codex@${SEARCH_SWE_CODEX_VERSION}" \ diff --git a/tasks/task-1-3/environment/Dockerfile b/tasks/task-1-3/environment/Dockerfile index b83d6c2..3358c9f 100644 --- a/tasks/task-1-3/environment/Dockerfile +++ b/tasks/task-1-3/environment/Dockerfile @@ -1,8 +1,8 @@ FROM docker.io/hanhainebula/search-swe-base:cpu-py3.12-1.0.0 -ARG SEARCH_SWE_CODEX_VERSION=0.147.0 -ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.273 -ARG SEARCH_SWE_PI_VERSION=0.85.1 +ARG SEARCH_SWE_CODEX_VERSION=0.157.1 +ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.283 +ARG SEARCH_SWE_PI_VERSION=0.87.1 RUN npm install --global --ignore-scripts \ --registry=https://registry.npmmirror.com \ "@openai/codex@${SEARCH_SWE_CODEX_VERSION}" \ diff --git a/tasks/task-1-4/environment/Dockerfile b/tasks/task-1-4/environment/Dockerfile index b83d6c2..3358c9f 100644 --- a/tasks/task-1-4/environment/Dockerfile +++ b/tasks/task-1-4/environment/Dockerfile @@ -1,8 +1,8 @@ FROM docker.io/hanhainebula/search-swe-base:cpu-py3.12-1.0.0 -ARG SEARCH_SWE_CODEX_VERSION=0.147.0 -ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.273 -ARG SEARCH_SWE_PI_VERSION=0.85.1 +ARG SEARCH_SWE_CODEX_VERSION=0.157.1 +ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.283 +ARG SEARCH_SWE_PI_VERSION=0.87.1 RUN npm install --global --ignore-scripts \ --registry=https://registry.npmmirror.com \ "@openai/codex@${SEARCH_SWE_CODEX_VERSION}" \ diff --git a/tasks/task-2-1/environment/Dockerfile b/tasks/task-2-1/environment/Dockerfile index de38528..4b5f9ff 100644 --- a/tasks/task-2-1/environment/Dockerfile +++ b/tasks/task-2-1/environment/Dockerfile @@ -1,8 +1,8 @@ FROM docker.io/hanhainebula/search-swe-base:cpu-py3.12-1.0.0 -ARG SEARCH_SWE_CODEX_VERSION=0.147.0 -ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.273 -ARG SEARCH_SWE_PI_VERSION=0.85.1 +ARG SEARCH_SWE_CODEX_VERSION=0.157.1 +ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.283 +ARG SEARCH_SWE_PI_VERSION=0.87.1 RUN npm install --global --ignore-scripts \ --registry=https://registry.npmmirror.com \ "@openai/codex@${SEARCH_SWE_CODEX_VERSION}" \ diff --git a/tasks/task-2-2/environment/Dockerfile b/tasks/task-2-2/environment/Dockerfile index 8a56033..86dfb8d 100644 --- a/tasks/task-2-2/environment/Dockerfile +++ b/tasks/task-2-2/environment/Dockerfile @@ -1,8 +1,8 @@ FROM docker.io/hanhainebula/search-swe-base:gpu-cu13.0-py3.12-1.0.0 -ARG SEARCH_SWE_CODEX_VERSION=0.147.0 -ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.273 -ARG SEARCH_SWE_PI_VERSION=0.85.1 +ARG SEARCH_SWE_CODEX_VERSION=0.157.1 +ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.283 +ARG SEARCH_SWE_PI_VERSION=0.87.1 RUN npm install --global --ignore-scripts \ --registry=https://registry.npmmirror.com \ "@openai/codex@${SEARCH_SWE_CODEX_VERSION}" \ diff --git a/tasks/task-2-3/environment/Dockerfile b/tasks/task-2-3/environment/Dockerfile index 8a56033..86dfb8d 100644 --- a/tasks/task-2-3/environment/Dockerfile +++ b/tasks/task-2-3/environment/Dockerfile @@ -1,8 +1,8 @@ FROM docker.io/hanhainebula/search-swe-base:gpu-cu13.0-py3.12-1.0.0 -ARG SEARCH_SWE_CODEX_VERSION=0.147.0 -ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.273 -ARG SEARCH_SWE_PI_VERSION=0.85.1 +ARG SEARCH_SWE_CODEX_VERSION=0.157.1 +ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.283 +ARG SEARCH_SWE_PI_VERSION=0.87.1 RUN npm install --global --ignore-scripts \ --registry=https://registry.npmmirror.com \ "@openai/codex@${SEARCH_SWE_CODEX_VERSION}" \ diff --git a/tasks/task-2-4/environment/Dockerfile b/tasks/task-2-4/environment/Dockerfile index f4a2db8..70484bf 100644 --- a/tasks/task-2-4/environment/Dockerfile +++ b/tasks/task-2-4/environment/Dockerfile @@ -1,8 +1,8 @@ FROM docker.io/hanhainebula/search-swe-base:cpu-py3.12-1.0.0 -ARG SEARCH_SWE_CODEX_VERSION=0.147.0 -ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.273 -ARG SEARCH_SWE_PI_VERSION=0.85.1 +ARG SEARCH_SWE_CODEX_VERSION=0.157.1 +ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.283 +ARG SEARCH_SWE_PI_VERSION=0.87.1 RUN npm install --global --ignore-scripts \ --registry=https://registry.npmmirror.com \ "@openai/codex@${SEARCH_SWE_CODEX_VERSION}" \ diff --git a/tasks/task-2-5/environment/Dockerfile b/tasks/task-2-5/environment/Dockerfile index f7c94e3..6870ea5 100644 --- a/tasks/task-2-5/environment/Dockerfile +++ b/tasks/task-2-5/environment/Dockerfile @@ -1,8 +1,8 @@ FROM docker.io/hanhainebula/search-swe-base:cpu-py3.12-1.0.0 -ARG SEARCH_SWE_CODEX_VERSION=0.147.0 -ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.273 -ARG SEARCH_SWE_PI_VERSION=0.85.1 +ARG SEARCH_SWE_CODEX_VERSION=0.157.1 +ARG SEARCH_SWE_CLAUDE_CODE_VERSION=2.1.283 +ARG SEARCH_SWE_PI_VERSION=0.87.1 RUN npm install --global --ignore-scripts \ --registry=https://registry.npmmirror.com \ "@openai/codex@${SEARCH_SWE_CODEX_VERSION}" \ From b0293f81c856af06349c4df047dbeb0fbd853b6d Mon Sep 17 00:00:00 2001 From: hanhainebula <2512674094@qq.com> Date: Sun, 27 Sep 2026 23:27:59 +0800 Subject: [PATCH 2/3] Add isolated OpenRouter routing for coding agents Add a dedicated Agent OpenRouter key and explicit Codex and Claude Code routes. Keep the full model slug, use each CLI's supported API endpoint, preserve phase-scoped networking, and document the evaluation commands. --- .env.example | 9 +++- README.md | 32 ++++++++++-- README_zh.md | 30 +++++++++-- docs/evaluation.md | 35 +++++++++++-- docs/network-policy.md | 5 +- scripts/harbor_agents.py | 37 ++++++++++++++ scripts/openrouter_codex.toml | 10 ++++ scripts/run_task.py | 67 +++++++++++++++++++------ scripts/tests/test_network_policies.py | 57 ++++++++++++++++++++- scripts/tests/test_openrouter_agents.py | 62 +++++++++++++++++++++++ scripts/tests/test_release.py | 38 ++++++++++++++ 11 files changed, 349 insertions(+), 33 deletions(-) create mode 100644 scripts/openrouter_codex.toml create mode 100644 scripts/tests/test_openrouter_agents.py diff --git a/.env.example b/.env.example index 2dfefc2..5c52a95 100644 --- a/.env.example +++ b/.env.example @@ -22,10 +22,15 @@ PI_THINKING= AGENT_OPENAI_BASE_URL= AGENT_OPENAI_API_KEY= -# Claude Code agent only: official Anthropic API key. Custom gateways, OAuth, -# Bedrock, and Vertex are not supported by the shared launcher. +# Claude Code direct mode: official Anthropic API key. OpenRouter uses the +# separate Agent key below; other gateways, OAuth, Bedrock, and Vertex are +# unsupported. AGENT_ANTHROPIC_API_KEY= +# OpenRouter Agent mode (--openrouter) for Codex or Claude Code. Separate from +# OPENROUTER_API_KEY used by task submissions and ANSWER_JUDGE_API_KEY. +AGENT_OPENROUTER_API_KEY= + # Optional Codex or Claude Code reasoning effort. AGENT_REASONING_EFFORT= diff --git a/README.md b/README.md index 0034e17..d8bf480 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,9 @@ For other runs, fill only the matching sections already present in `.env`: - Pi + GLM-5.3-Flash: `AGENT_MODEL=zai/glm-5.3-flash` and `ZAI_API_KEY`. - Codex: `AGENT_MODEL`, `AGENT_OPENAI_BASE_URL`, and `AGENT_OPENAI_API_KEY`. - Claude Code: `AGENT_MODEL` and `AGENT_ANTHROPIC_API_KEY`; the launcher uses - only Anthropic's official API. + Anthropic's official API by default. +- OpenRouter Agent mode: `AGENT_OPENROUTER_API_KEY`, `--openrouter`, and a full + `provider/model` slug with Codex or Claude Code. - Task 1-3: the three `ANSWER_JUDGE_*` values are also required. - Optional submission APIs: use `TASK_1_1_OPENROUTER_API_KEY`, `OPENROUTER_API_KEY`, or `JINA_API_KEY` only for the tasks identified by the @@ -182,9 +184,31 @@ bash scripts/run_task.sh --task task-1-1 --agent claude-code \ --reasoning-effort high --output jobs/task-1-1-claude ``` -The shared launcher supports API-key authentication to `api.anthropic.com`; -custom gateways, subscription OAuth, Bedrock, Vertex, ACP, and custom Claude -settings are intentionally outside the initial support scope. The +#### Codex or Claude Code through OpenRouter + +Add a dedicated OpenRouter Agent key to `.env`: + +```dotenv +AGENT_OPENROUTER_API_KEY=YOUR_AGENT_OPENROUTER_KEY +``` + +Choose an Agent and pass its full OpenRouter model ID: + +```bash +bash scripts/run_task.sh --task task-1-1 --agent codex --openrouter \ + --model openai/gpt-6-astra --output jobs/task-1-1-codex-openrouter + +bash scripts/run_task.sh --task task-1-1 --agent claude-code --openrouter \ + --model anthropic/claude-opus-5.5 --output jobs/task-1-1-claude-openrouter +``` + +Add `--dry-run` to either command to preview it before launching. The launcher +sets the OpenRouter API addresses and keeps this key separate from the +submission `OPENROUTER_API_KEY` and verifier credentials. Keep the `VERIFIER_*` +settings from the main example. + +Other custom gateways, subscription OAuth, Bedrock, Vertex, ACP, and custom +Claude settings remain outside the supported scope. The [quick start guide](docs/quickstart.md) covers the default Codex path; the [evaluation guide](docs/evaluation.md) covers the per-task credential and hardware matrix plus GPU, network-policy, and custom-provider options. diff --git a/README_zh.md b/README_zh.md index 2fb1d8d..b719c5e 100644 --- a/README_zh.md +++ b/README_zh.md @@ -93,7 +93,9 @@ VERIFIER_OPENAI_API_KEY=YOUR_DEEPSEEK_KEY - Codex:设置 `AGENT_MODEL`、`AGENT_OPENAI_BASE_URL` 和 `AGENT_OPENAI_API_KEY`。 - Claude Code:设置 `AGENT_MODEL` 和 `AGENT_ANTHROPIC_API_KEY`;共享启动器 - 只使用 Anthropic 官方 API。 + 默认使用 Anthropic 官方 API。 +- OpenRouter Agent 模式:设置 `AGENT_OPENROUTER_API_KEY`,并为 Codex 或 + Claude Code 指定 `--openrouter` 和完整的 `provider/model` 模型 ID。 - Task 1-3:还必须填写三个 `ANSWER_JUDGE_*` 变量。 - 可选 submission API:只在 `.env.example` 注释所列任务确实使用时,填写 `TASK_1_1_OPENROUTER_API_KEY`、`OPENROUTER_API_KEY` 或 `JINA_API_KEY`。 @@ -171,8 +173,30 @@ bash scripts/run_task.sh --task task-1-1 --agent claude-code \ --reasoning-effort high --output jobs/task-1-1-claude ``` -共享启动器只支持通过 API key 访问 `api.anthropic.com`;首版有意不支持自定义 -gateway、订阅 OAuth、Bedrock、Vertex、ACP 和自定义 Claude settings。默认 Codex +#### 通过 OpenRouter 运行 Codex 或 Claude Code + +在 `.env` 中填写独立的 OpenRouter Agent 密钥: + +```dotenv +AGENT_OPENROUTER_API_KEY=YOUR_AGENT_OPENROUTER_KEY +``` + +选择 Agent,并传入完整的 OpenRouter 模型 ID: + +```bash +bash scripts/run_task.sh --task task-1-1 --agent codex --openrouter \ + --model openai/gpt-6-astra --output jobs/task-1-1-codex-openrouter + +bash scripts/run_task.sh --task task-1-1 --agent claude-code --openrouter \ + --model anthropic/claude-opus-5.5 --output jobs/task-1-1-claude-openrouter +``` + +可以先为命令加上 `--dry-run` 预览。启动器已配置 OpenRouter 的 API 地址; +这个 Agent 密钥与任务提交用的 `OPENROUTER_API_KEY` 和裁判密钥分开。 +保留主示例中的 `VERIFIER_*` 配置。 + +其他自定义 gateway、订阅 OAuth、Bedrock、Vertex、ACP 和自定义 Claude settings +仍不受支持。默认 Codex 流程见[快速开始指南](docs/quickstart.md);各任务的凭证与硬件矩阵,以及 GPU、 网络权限和自定义模型服务配置见[评测指南](docs/evaluation.md)。 diff --git a/docs/evaluation.md b/docs/evaluation.md index 7830aae..0f0e739 100644 --- a/docs/evaluation.md +++ b/docs/evaluation.md @@ -112,6 +112,32 @@ AGENT_CODEX_CONFIG=provider.local.toml The equivalent CLI option is `--codex-config`; CLI paths are resolved from the current directory. +### Codex or Claude Code through OpenRouter + +Set a separate Agent key in `.env`: + +```dotenv +AGENT_OPENROUTER_API_KEY=YOUR_AGENT_OPENROUTER_KEY +``` + +Use a full OpenRouter model slug and select the route explicitly: + +```bash +bash scripts/run_task.sh --task task-1-1 --agent codex --openrouter \ + --model openai/gpt-6-astra --dry-run +bash scripts/run_task.sh --task task-1-1 --agent claude-code --openrouter \ + --model anthropic/claude-opus-5.5 --dry-run +``` + +Remove `--dry-run` to launch. This mode fixes Codex's Responses base URL to +`https://openrouter.ai/api/v1` and Claude Code's Anthropic base URL to +`https://openrouter.ai/api`. Claude Code accepts only `anthropic/` slugs here. +The launcher permits `openrouter.ai` in restricted Agent phases and keeps the +OpenRouter Agent key separate from `OPENROUTER_API_KEY` for submissions and +`ANSWER_JUDGE_API_KEY` for task-1-3. Verifier configuration is unchanged. +When using Codex, omit `--codex-config`; the launcher supplies the required +native provider configuration and ignores the direct mode's `AGENT_CODEX_CONFIG`. + ### Claude Code with the official Anthropic API Claude Code is pinned to version 2.1.283 and preinstalled in every task image. @@ -140,11 +166,10 @@ explicit run. The launcher maps `AGENT_ANTHROPIC_API_KEY` to the agent-only phases, and removes inherited Anthropic gateway, OAuth, and Bedrock selectors before starting Harbor. -The shared launcher intentionally does not support custom Anthropic-compatible -gateways, Claude subscription OAuth, Bedrock, Vertex, ACP, or custom Claude -settings. These modes have different credential, executable-configuration, or -network requirements and must not be enabled by adding host environment -variables. +Other custom Anthropic-compatible gateways, Claude subscription OAuth, Bedrock, +Vertex, ACP, and custom Claude settings are not supported. They need different +credentials, executable configuration, or network permissions and must not be +enabled by adding host environment variables. ### Pi native providers diff --git a/docs/network-policy.md b/docs/network-policy.md index 011e691..7042b57 100644 --- a/docs/network-policy.md +++ b/docs/network-policy.md @@ -110,9 +110,10 @@ these counters are not HTTP request counts. ## Current task matrix `model host` below means exactly one coding-model hostname selected by the -launcher: the hostname in `AGENT_OPENAI_BASE_URL` for Codex, +launcher: the hostname in `AGENT_OPENAI_BASE_URL` for direct Codex, `api.deepseek.com` for Pi + DeepSeek, `api.z.ai` for Pi + Z.AI, or the fixed -`api.anthropic.com` host for Claude Code. The launcher passes it through +`api.anthropic.com` host for direct Claude Code. With `--openrouter`, Codex and +Claude Code use `openrouter.ai`. The launcher passes the selected host through Harbor's `--allow-agent-host`, which augments only the `agent.run()` phase. Codex 0.157.1, Pi 0.87.1, and Claude Code 2.1.283 are preinstalled in every agent image, so agent setup does not need package-registry or general internet diff --git a/scripts/harbor_agents.py b/scripts/harbor_agents.py index 8b8fac1..e748375 100644 --- a/scripts/harbor_agents.py +++ b/scripts/harbor_agents.py @@ -5,6 +5,7 @@ from harbor.agents.installed.pi import Pi from harbor.environments.base import BaseEnvironment import json +import shlex import tempfile from pathlib import Path @@ -48,6 +49,27 @@ async def install(self, environment: BaseEnvironment) -> None: ) +class PreinstalledOpenRouterCodex(PreinstalledCodex): + """Keep the full OpenRouter model slug in Harbor's Codex command.""" + + async def exec_as_agent(self, environment, command, **kwargs): + # Harbor 0.22.0 strips the provider prefix in Codex.run(). Replace + # only its model flag; fail if a future Harbor release changes it. + if "codex exec " in command: + flags, separator, instruction = command.partition("-- ") + bare_model = self.model_name.split("/")[-1] + old = f"--model {bare_model} --json " + if not separator or flags.count(old) != 1: + raise RuntimeError( + "Harbor's Codex command format changed; OpenRouter model may be truncated" + ) + command = ( + flags.replace(old, f"--model {shlex.quote(self.model_name)} --json ", 1) + + separator + instruction + ) + return await super().exec_as_agent(environment, command, **kwargs) + + class PreinstalledClaudeCode(ClaudeCode): """Run the pinned Claude Code CLI without runtime package downloads.""" @@ -65,6 +87,21 @@ async def install(self, environment: BaseEnvironment) -> None: ) +class PreinstalledOpenRouterClaudeCode(PreinstalledClaudeCode): + """Use OpenRouter's bearer token with Claude Code's Anthropic endpoint.""" + + def _resolve_auth_env(self): + env = super()._resolve_auth_env() + token = self._get_env("ANTHROPIC_AUTH_TOKEN") + if not token or env.get("ANTHROPIC_BASE_URL") != "https://openrouter.ai/api": + raise RuntimeError( + "OpenRouter Claude Code requires its bearer token and fixed base URL" + ) + env["ANTHROPIC_API_KEY"] = "" + env["ANTHROPIC_AUTH_TOKEN"] = token + return env + + class PreinstalledPi(Pi): """Run the pinned Pi CLI without runtime package downloads.""" diff --git a/scripts/openrouter_codex.toml b/scripts/openrouter_codex.toml new file mode 100644 index 0000000..d6b459c --- /dev/null +++ b/scripts/openrouter_codex.toml @@ -0,0 +1,10 @@ +model_provider = "openrouter" + +[model_providers.openrouter] +name = "OpenRouter" +base_url = "https://openrouter.ai/api/v1" +wire_api = "responses" + +[model_providers.openrouter.auth] +command = "sh" +args = ["-c", "printf %s \"$OPENAI_API_KEY\""] diff --git a/scripts/run_task.py b/scripts/run_task.py index 4fbe4ea..f62a51c 100755 --- a/scripts/run_task.py +++ b/scripts/run_task.py @@ -4,6 +4,7 @@ import argparse import os from pathlib import Path +import re import shlex import shutil import sys @@ -26,6 +27,11 @@ CLAUDE_CODE_VERSION = "2.1.283" CLAUDE_CODE_EFFORT_LEVELS = ("low", "medium", "high", "xhigh", "max") ANTHROPIC_HOST = "api.anthropic.com" +OPENROUTER_HOST = "openrouter.ai" +OPENROUTER_CLAUDE_BASE_URL = "https://openrouter.ai/api" +OPENROUTER_MODEL = re.compile( + r"^[a-z0-9][a-z0-9._-]*/[a-zA-Z0-9][a-zA-Z0-9._:+-]*$" +) CLAUDE_HOST_ENV_VARS = ( "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", @@ -51,6 +57,10 @@ "codex": "scripts.harbor_agents:PreinstalledCodex", "pi": "scripts.harbor_agents:PreinstalledPi", } +OPENROUTER_AGENT_IMPORTS = { + "codex": "scripts.harbor_agents:PreinstalledOpenRouterCodex", + "claude-code": "scripts.harbor_agents:PreinstalledOpenRouterClaudeCode", +} def endpoint_hostname(value, variable): @@ -98,6 +108,10 @@ def main(): selection.add_argument("--task", choices=TASKS) selection.add_argument("--task-path", help="Explicit repository-relative package, including a reviewed submission") parser.add_argument("--agent", default="codex", choices=tuple(AGENT_IMPORTS)) + parser.add_argument( + "--openrouter", action="store_true", + help="Route Codex or Claude Code through OpenRouter using AGENT_OPENROUTER_API_KEY", + ) parser.add_argument("--model", help="Agent model; defaults to AGENT_MODEL") parser.add_argument("--env-file", type=Path, help="Defaults to the repository .env if present") parser.add_argument( @@ -135,6 +149,15 @@ def main(): model = args.model or env.get("AGENT_MODEL") if not model: parser.error("Set --model or AGENT_MODEL") + if args.openrouter: + if args.agent not in OPENROUTER_AGENT_IMPORTS: + parser.error("--openrouter is only valid with --agent codex or claude-code") + if not OPENROUTER_MODEL.fullmatch(model): + parser.error("OpenRouter requires a complete provider/model slug in --model or AGENT_MODEL") + if args.agent == "claude-code" and not model.startswith("anthropic/"): + parser.error("OpenRouter Claude Code requires an anthropic/ model slug") + if args.agent == "codex" and args.codex_config is not None: + parser.error("--openrouter uses its own Codex config; omit --codex-config") if args.agent == "pi": if model not in PI_MODEL_KEYS: parser.error( @@ -169,7 +192,9 @@ def main(): output = args.output.resolve() if args.output else REPO / "jobs" / task_key(REPO, task) command = [ "harbor", "run", "--path", str(task), "--env", "docker", - "--agent", AGENT_IMPORTS[args.agent], "--force-build", "--yes", "-o", str(output), + "--agent", + (OPENROUTER_AGENT_IMPORTS if args.openrouter else AGENT_IMPORTS)[args.agent], + "--force-build", "--yes", "-o", str(output), "--agent-setup-timeout-multiplier", "3", "-m", model, "--n-concurrent", "1", "--n-attempts", "1", "--max-retries", "0", ] @@ -185,7 +210,7 @@ def main(): effort = args.reasoning_effort or env.get("AGENT_REASONING_EFFORT") if effort: command.extend(["--ak", f"reasoning_effort={effort}"]) - config = args.codex_config + config = REPO / "scripts/openrouter_codex.toml" if args.openrouter else args.codex_config if config is None and env.get("AGENT_CODEX_CONFIG"): config = REPO / env["AGENT_CODEX_CONFIG"] if config is not None: @@ -193,12 +218,17 @@ def main(): if not config.is_file(): parser.error(f"Codex configuration does not exist: {config}") command.extend(["--ak", f"config={config}"]) - for name in ("OPENAI_BASE_URL", "OPENAI_API_KEY"): - source = f"AGENT_{name}" - required.append(source) - # Harbor resolves these from its environment. Secrets do not enter argv. - command.extend(["--ae", f"{name}=${{{source}}}"]) - if env.get("AGENT_OPENAI_BASE_URL"): + if args.openrouter: + required.append("AGENT_OPENROUTER_API_KEY") + command.extend(["--ae", "OPENAI_API_KEY=${AGENT_OPENROUTER_API_KEY}"]) + agent_host = OPENROUTER_HOST + else: + for name in ("OPENAI_BASE_URL", "OPENAI_API_KEY"): + source = f"AGENT_{name}" + required.append(source) + # Harbor resolves these from its environment. Secrets do not enter argv. + command.extend(["--ae", f"{name}=${{{source}}}"]) + if not args.openrouter and env.get("AGENT_OPENAI_BASE_URL"): try: agent_host = endpoint_hostname( env["AGENT_OPENAI_BASE_URL"], "AGENT_OPENAI_BASE_URL" @@ -210,14 +240,17 @@ def main(): effort = args.reasoning_effort or env.get("AGENT_REASONING_EFFORT") if effort: command.extend(["--ak", f"reasoning_effort={effort}"]) - required.append("AGENT_ANTHROPIC_API_KEY") - command.extend( - [ - "--ae", - "ANTHROPIC_API_KEY=${AGENT_ANTHROPIC_API_KEY}", - ] - ) - agent_host = ANTHROPIC_HOST + if args.openrouter: + required.append("AGENT_OPENROUTER_API_KEY") + command.extend([ + "--ae", "ANTHROPIC_AUTH_TOKEN=${AGENT_OPENROUTER_API_KEY}", + "--ae", f"ANTHROPIC_BASE_URL={OPENROUTER_CLAUDE_BASE_URL}", + ]) + agent_host = OPENROUTER_HOST + else: + required.append("AGENT_ANTHROPIC_API_KEY") + command.extend(["--ae", "ANTHROPIC_API_KEY=${AGENT_ANTHROPIC_API_KEY}"]) + agent_host = ANTHROPIC_HOST else: thinking = args.thinking or env.get("PI_THINKING") if thinking and thinking not in PI_THINKING_LEVELS: @@ -323,6 +356,8 @@ def main(): # This launcher uses explicit API keys, without consulting a host auth.json. env.pop("CODEX_AUTH_JSON_PATH", None) env.pop("CODEX_FORCE_AUTH_JSON", None) + if args.openrouter: + env.pop("OPENAI_BASE_URL", None) if args.agent == "claude-code": for name in CLAUDE_HOST_ENV_VARS: env.pop(name, None) diff --git a/scripts/tests/test_network_policies.py b/scripts/tests/test_network_policies.py index d33ea45..835fd6b 100644 --- a/scripts/tests/test_network_policies.py +++ b/scripts/tests/test_network_policies.py @@ -266,7 +266,7 @@ def setUp(self): ) } - def run_preview(self, task, agent, model, values): + def run_preview(self, task, agent, model, values, options=()): with tempfile.NamedTemporaryFile("w", delete=False) as env_file: for key, value in values.items(): env_file.write(f"{key}={value}\n") @@ -280,6 +280,7 @@ def run_preview(self, task, agent, model, values): "--agent", agent, "--model", model, "--env-file", str(env_path), + *options, "--dry-run", ], cwd=REPO, @@ -353,6 +354,60 @@ def test_claude_code_preview_uses_pinned_cli_and_namespaced_key(self): ["ANTHROPIC_API_KEY=${AGENT_ANTHROPIC_API_KEY}"], ) + def test_openrouter_agent_routes_are_isolated_from_other_keys(self): + values = { + "AGENT_OPENROUTER_API_KEY": "fixture-openrouter-agent-key", + "AGENT_OPENAI_BASE_URL": "https://unused.example/v1", + "AGENT_OPENAI_API_KEY": "fixture-openai-key", + "AGENT_ANTHROPIC_API_KEY": "fixture-anthropic-key", + "AGENT_CODEX_CONFIG": "missing-direct-provider.toml", + "OPENROUTER_API_KEY": "fixture-submission-key", + "ANSWER_JUDGE_API_KEY": "fixture-answer-key", + } + for agent, model, agent_class, expected_env in ( + ("codex", "openai/gpt-6-astra", "PreinstalledOpenRouterCodex", + ["OPENAI_API_KEY=${AGENT_OPENROUTER_API_KEY}"]), + ("claude-code", "anthropic/claude-opus-5.5", "PreinstalledOpenRouterClaudeCode", + ["ANTHROPIC_AUTH_TOKEN=${AGENT_OPENROUTER_API_KEY}", + "ANTHROPIC_BASE_URL=https://openrouter.ai/api"]), + ): + with self.subTest(agent=agent): + result = self.run_preview("task-1-2", agent, model, values, ("--openrouter",)) + self.assertEqual(result.returncode, 0, result.stderr) + for secret in values.values(): + self.assertNotIn(secret, result.stdout + result.stderr) + argv = self.argv(result) + self.assertEqual(self.flag_values(argv, "--allow-agent-host"), ["openrouter.ai"]) + self.assertEqual(self.flag_values(argv, "--ae"), expected_env) + self.assertTrue(argv[argv.index("--agent") + 1].endswith(agent_class)) + self.assertEqual(argv[argv.index("-m") + 1], model) + self.assertNotIn("AGENT_OPENAI_API_KEY", result.stdout) + self.assertNotIn("AGENT_ANTHROPIC_API_KEY", result.stdout) + self.assertIn("OPENAI_API_KEY=${VERIFIER_OPENAI_API_KEY}", + self.flag_values(argv, "--verifier-env")) + if agent == "codex": + config = next(value.split("=", 1)[1] for value in self.flag_values(argv, "--ak") + if value.startswith("config=")) + self.assertEqual(Path(config).name, "openrouter_codex.toml") + + def test_openrouter_requires_supported_agent_and_complete_slug(self): + cases = ( + ("pi", "deepseek/deepseek-flash", "--openrouter is only valid"), + ("codex", "gpt-6-astra", "complete provider/model slug"), + ("codex", "openai/gpt-6-astra;echo", "complete provider/model slug"), + ("claude-code", "openai/gpt-6-astra", "requires an anthropic/ model"), + ) + for agent, model, message in cases: + with self.subTest(agent=agent, model=model): + result = self.run_preview("task-1-2", agent, model, {}, ("--openrouter",)) + self.assertNotEqual(result.returncode, 0) + self.assertIn(message, result.stderr) + conflict = self.run_preview( + "task-1-2", "codex", "openai/gpt-6-astra", {}, + ("--openrouter", "--codex-config", "missing.toml"), + ) + self.assertIn("omit --codex-config", conflict.stderr) + @staticmethod def flag_values(argv, flag): return [ diff --git a/scripts/tests/test_openrouter_agents.py b/scripts/tests/test_openrouter_agents.py new file mode 100644 index 0000000..6641dd0 --- /dev/null +++ b/scripts/tests/test_openrouter_agents.py @@ -0,0 +1,62 @@ +"""OpenRouter compatibility checks for Harbor's pinned CLI adapters.""" + +import asyncio +from pathlib import Path +import unittest +from unittest.mock import AsyncMock, patch + +from harbor.agents.installed.codex import Codex +from scripts.harbor_agents import ( + PreinstalledOpenRouterClaudeCode, + PreinstalledOpenRouterCodex, +) + + +CONFIG = Path(__file__).resolve().parents[1] / "openrouter_codex.toml" + + +class OpenRouterAgents(unittest.TestCase): + def test_codex_provider_config_and_full_model_slug(self): + agent = PreinstalledOpenRouterCodex( + logs_dir=Path("/tmp"), model_name="openai/gpt-6-astra", config=CONFIG + ) + config = agent._build_effective_config() + self.assertEqual(config["model_provider"], "openrouter") + provider = config["model_providers"]["openrouter"] + self.assertEqual(provider["base_url"], "https://openrouter.ai/api/v1") + self.assertEqual(provider["wire_api"], "responses") + self.assertEqual(provider["auth"]["command"], "sh") + self.assertIn("OPENAI_API_KEY", provider["auth"]["args"][-1]) + + with patch.object(Codex, "exec_as_agent", new_callable=AsyncMock) as execute: + asyncio.run(agent.exec_as_agent( + object(), + "codex exec --model gpt-6-astra --json -- task", + env={"OPENAI_API_KEY": "fixture"}, + )) + self.assertIn("--model openai/gpt-6-astra --json", execute.call_args.args[1]) + + def test_codex_fails_if_harbor_command_shape_changes(self): + agent = PreinstalledOpenRouterCodex( + logs_dir=Path("/tmp"), model_name="openai/gpt-6-astra", config=CONFIG + ) + with self.assertRaisesRegex(RuntimeError, "command format changed"): + asyncio.run(agent.exec_as_agent(object(), "codex exec --model changed --json")) + + def test_claude_uses_bearer_token_and_complete_model(self): + agent = PreinstalledOpenRouterClaudeCode( + logs_dir=Path("/tmp"), model_name="anthropic/claude-opus-5.5", + extra_env={ + "ANTHROPIC_AUTH_TOKEN": "fixture-openrouter-agent-key", + "ANTHROPIC_BASE_URL": "https://openrouter.ai/api", + }, + ) + self.assertEqual(agent._resolved_model_name(), "anthropic/claude-opus-5.5") + auth = agent._resolve_auth_env() + self.assertEqual(auth["ANTHROPIC_AUTH_TOKEN"], "fixture-openrouter-agent-key") + self.assertEqual(auth["ANTHROPIC_API_KEY"], "") + self.assertEqual(auth["ANTHROPIC_BASE_URL"], "https://openrouter.ai/api") + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_release.py b/scripts/tests/test_release.py index eff333d..cbded67 100644 --- a/scripts/tests/test_release.py +++ b/scripts/tests/test_release.py @@ -166,6 +166,44 @@ def test_launcher_separates_agent_and_verifier_keys_without_putting_them_in_argv self.assertIn("OPENAI_API_KEY=${AGENT_OPENAI_API_KEY}", argv) self.assertIn("OPENAI_API_KEY=${VERIFIER_OPENAI_API_KEY}", argv) + def test_openrouter_launch_requires_its_own_key_and_keeps_it_out_of_argv(self): + bin_dir = self.root / "bin" + bin_dir.mkdir() + fake_harbor = bin_dir / "harbor" + fake_harbor.write_text( + f"#!{sys.executable}\nimport json, os, sys\n" + "assert os.environ['AGENT_OPENROUTER_API_KEY'] == 'fixture-agent-openrouter-key'\n" + "assert os.environ['OPENROUTER_API_KEY'] == 'fixture-submission-key'\n" + "assert os.environ['VERIFIER_OPENAI_API_KEY'] == 'fixture-judge-key'\n" + "assert 'OPENAI_BASE_URL' not in os.environ\n" + "print(json.dumps(sys.argv[1:]))\n" + ) + fake_harbor.chmod(0o755) + env = self.launcher_env() + env.update({ + "PATH": str(bin_dir) + os.pathsep + env.get("PATH", ""), + "OPENROUTER_API_KEY": "fixture-submission-key", + "VERIFIER_OPENAI_BASE_URL": "https://judge.example/v1", + "VERIFIER_OPENAI_API_KEY": "fixture-judge-key", + "OPENAI_BASE_URL": "https://wrong-provider.example/v1", + }) + command = [sys.executable, str(self.repo / "scripts/run_task.py"), + "--task", "task-new", "--agent", "codex", "--openrouter", + "--model", "openai/gpt-6-astra"] + missing = subprocess.run(command, cwd=self.root, env=env, + capture_output=True, text=True) + self.assertNotEqual(missing.returncode, 0) + self.assertIn("AGENT_OPENROUTER_API_KEY", missing.stderr) + env["AGENT_OPENROUTER_API_KEY"] = "fixture-agent-openrouter-key" + result = subprocess.run(command, cwd=self.root, env=env, + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + argv = json.loads(result.stdout.splitlines()[-1]) + self.assertIn("OPENAI_API_KEY=${AGENT_OPENROUTER_API_KEY}", argv) + self.assertIn("OPENAI_API_KEY=${VERIFIER_OPENAI_API_KEY}", argv) + self.assertNotIn("fixture-agent-openrouter-key", result.stdout + result.stderr) + self.assertNotIn("fixture-submission-key", result.stdout + result.stderr) + def test_step_restriction_selects_gateway_and_model_host(self): (self.task / "task.toml").write_text( '[environment]\nnetwork_mode = "public"\n' From abc37a50e909ea2ee2b7f5f725e3f836dab6014b Mon Sep 17 00:00:00 2001 From: hanhainebula <2512674094@qq.com> Date: Sun, 27 Sep 2026 23:42:44 +0800 Subject: [PATCH 3/3] Include OpenRouter agent key in environment template checks --- scripts/tests/test_rewardkit_deepseek.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/tests/test_rewardkit_deepseek.py b/scripts/tests/test_rewardkit_deepseek.py index 31f4745..79111f2 100644 --- a/scripts/tests/test_rewardkit_deepseek.py +++ b/scripts/tests/test_rewardkit_deepseek.py @@ -139,6 +139,7 @@ def test_environment_template_lists_every_user_supplied_api_key(self): "ZAI_API_KEY", "AGENT_OPENAI_API_KEY", "AGENT_ANTHROPIC_API_KEY", + "AGENT_OPENROUTER_API_KEY", "VERIFIER_OPENAI_API_KEY", "ANSWER_JUDGE_API_KEY", "TASK_1_1_OPENROUTER_API_KEY", @@ -159,6 +160,7 @@ def test_environment_template_lists_every_user_supplied_api_key(self): "ZAI_API_KEY", "AGENT_OPENAI_API_KEY", "AGENT_ANTHROPIC_API_KEY", + "AGENT_OPENROUTER_API_KEY", "VERIFIER_OPENAI_API_KEY", "ANSWER_JUDGE_*", "OPENROUTER_API_KEY",