Skip to content

opencode harness captures zero completions — config doesn't include gateway baseURL #39

@shiwanghua

Description

@shiwanghua
Image

Title: opencode harness captures zero completions — config doesn't include gateway baseURL

Labels: bug, agent-harness, opencode

Summary

在 SWE-bench Verified 评测中跑 opencode harness,10 个 task 全部 0/10 resolved。诊断发现 agent 容器里 opencode CLI 跑 ~4 秒就退出,根本没发出任何 chat completion 请求——polar gateway 日志里 session id 没有任何 POST /v1/chat/completions(只有 polar node 的 GET /sessions/...
心跳轮询)。

Reproduction

假设 polar 已经在跑:
uv run python examples/swebench_verified/submit_swebench_tasks.py
--harness opencode --max-tasks 5 --num-samples 1
--runtime-backend docker --model-name Qwen3.6-27B

Observed state

  • rollout_results/task_/ses_.json: error: "no completions", record_count: 0
  • trajectory.metadata.api_type: None
  • run_ms: ~4000(仅 4 秒就退出)
  • gateway 日志: 该 session 没有任何 POST /v1/chat/completions,只有极少的 GET /sessions/... 心跳

Root cause

看 src/polar/agent/presets/opencode.py:31-44 的 setup(),写到 $HOME/.config/opencode/opencode.json 的 config 包含 provider 模型但不包含 baseURL 和 apiKey:

config: dict = {
"provider": {provider: {"models": {model_id: {}}}}, # ← 没有 options.baseURL
"permission": {...},
}

opencode CLI 默认会用自己的内置 OpenAI base URL(api.openai.com),不会读 OPENAI_BASE_URL env var(不像 claude_code / openclaw / hermes / openhands_sdk 等需要走 placeholder 替换的 preset)。结果就是:

  • agent 启动 → opencode 不知道该往哪发 → 启动失败 / 静默退出
  • session 4 秒就 end,没有任何 completion 记录

对比 codex.py:80-82(base_url="$OPENAI_BASE_URL")和 openclaw.py:66-68(placeholder + sed 替换),opencode 是这套机制里唯一没配置 baseURL 的 preset。

Suggested fix

在 opencode.py 的 config dict 里加 options.baseURL 和 options.apiKey,用 placeholder 模式(运行时 sed 替换):

  import os                                                                                                                                                                                                                                                                       
  _BASE_URL_PLACEHOLDER = "__POLAR_GATEWAY_BASE_URL__"                                                                                                                                                                                                                          
  _API_KEY_PLACEHOLDER = "__POLAR_GATEWAY_API_KEY__"                                                                                                                                                                                                                            

  config["provider"] = {
      provider: {
          "models": {model_id: {}},
          "options": {
              "baseURL": _BASE_URL_PLACEHOLDER,                                                                                                                                                                                                                                   
              "apiKey": _API_KEY_PLACEHOLDER,
          },                                                                                                                                                                                                                                                                      
      }                                                                                                                                                                                                                                                                         
  }                                                                                                                                                                                                                                                                             

  # setup() 末尾用 sed 替换
  await runtime.exec(
      f"sed -i 's|{_BASE_URL_PLACEHOLDER}|$OPENAI_BASE_URL|g; "
      f"s|{_API_KEY_PLACEHOLDER}|$OPENAI_API_KEY|g' "                                                                                                                                                                                                                             
      f"{self._config_dir}/opencode.json"
  )                                                                                                                                                                                                                                                                               

Environment

  • polar commit: 8bc67cc (stable)
  • opencode-ai: 1.4.6
  • vLLM: Qwen3.6-27B on http://100.8.68.123:55900
  • 对照组: qwen_code 同设置下 5/10 resolved, claude_code 5/10

Issue 2 — Investigation: codex exits after 4 turns with empty patch

Title: codex harness exits early on Qwen3.6-27B — agent stops after a text-only response

Labels: bug, agent-harness, codex, investigation

Summary

codex harness 在 Qwen3.6-27B 上跑 4 轮就退出,patch 为空(empty_generation: true)。对比:同模型同任务下 qwen_code 5/10, claude_code 5/10, codex 0/10。这可能不是 polar bug,但需要排查 gateway 的 Responses 协议实现。

Reproduction

uv run python examples/swebench_verified/submit_swebench_tasks.py
--harness codex --max-tasks 5 --num-samples 1
--runtime-backend docker --model-name Qwen3.6-27B

Observed state(与 opencode 不同的失败模式)

rollout_results/task_/ses_.json:

  • status: COMPLETED, error: None
  • record_count: 4 ← 4 个 completion 成功捕获
  • api_type: "openai_responses" ✓
  • trace_count: 1, 8 messages(3 个 tool call + 1 个 text-only 终止消息)
  • run_ms: ~19000(19 秒)
  • trajectory.metadata.evaluation.report.empty_generation: true(patch 为空)

关键观察:polar gateway 正确记录了 4 个 completion,openai_responses 协议转换工作正常。失败原因在第 4 个 response 是 text-only("Now I can see the issue. Let me trace through the logic..."),codex CLI 收到后就把整个 task 标记完成退出了。

三种可能根因(待排查)

  1. model 行为:Qwen3.6-27B 在 agentic RL 数据上没训到位,应该 tool_call 的地方它 output text。同一模型下 qwen_code / claude_code 都能继续 → 说明模型本身能用。
  2. codex CLI 0.121.0 行为:codex 在 Responses 协议下看到 output_text 而非 function_call 就认为本轮结束。实测 claude_code 也用 OpenAI Chat 协议(polar 转 anthropic),qwen_code 用 OpenAI Chat——这俩都不存在"text-only 终止"的歧义。
  3. polar gateway Responses 协议 bug:看了 src/polar/gateway/transform/openai_responses.py:482-554 的 transform_response()——对 vLLM 返回的 chat completion response,转换为 Responses 输出时只支持 reasoning + message + function_call 三种 output item。没有"refusal /
    incomplete / 隐式停止信号"的处理路径。如果 vLLM 在某种情况返回了 finish_reason != "stop" 或 finish_reason != "tool_calls"(比如 "length"),transformer 可能丢失这个信息,导致 codex 收到一个看起来"正常完成"的 Responses 输出。

排查建议

步骤 1:看 vLLM 实际返回的 chunk 里 finish_reason 是什么。改 polar gateway 临时加一行 log:

# openai_responses.py:572 附近
  if choices and choices[0].get("finish_reason"):
      logger.warning("codex session=%s finish_reason=%s", session_id, choices[0]["finish_reason"])
      events.extend(state.finalize())

步骤 2:检查 vLLM 27B 模型是否对 codex 风格的 system prompt 有兼容问题。codex 的 instructions 字段(openai_responses.py:394-396)被转成 system message。Qwen3.6-27B 的 chat template 可能不会以 codex 期望的格式响应 tool_call。

步骤 3:临时改 agent_env 强制加 streaming,看看 codex 看到 streaming chunks 时会不会继续。具体:在 gateway_url 后追加 &stream=true(如果 codex 支持这个 query param)。

优先级

低。codex 用户少,且可能是 model 行为问题。但 opencode 那条是 blocker——opencode 0/10 完全是 polar 的锅。

Environment

  • polar commit: 8bc67cc
  • @openai/codex: 0.121.0
  • vLLM: Qwen3.6-27B, max_model_len=262144, --reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder
  • 4 calls × 19s = 远少于其他 harness 正常 SWE-bench 任务的 5-15 min

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions