Skip to content

fix: surface opencode errors to CI and catch model-not-found in fallback#100

Merged
Svtter merged 2 commits into
mainfrom
worktree-fix-99-surface-errors
May 21, 2026
Merged

fix: surface opencode errors to CI and catch model-not-found in fallback#100
Svtter merged 2 commits into
mainfrom
worktree-fix-99-surface-errors

Conversation

@Svtter
Copy link
Copy Markdown
Collaborator

@Svtter Svtter commented May 21, 2026

Summary

  • Add --print-logs --log-level ERROR flags to the opencode invocation in run-github-opencode.py, so fatal errors like ProviderModelNotFoundError appear in CI stdout/stderr instead of only in the local log file (~/.local/share/opencode/log/)
  • Add ProviderModelNotFoundError to the default fallback_on_regex in both github-run-opencode and multi-review, so model-not-found errors trigger fallback to the next candidate model

Root Cause

opencode 1.15.x writes ProviderModelNotFoundError only to its internal log file, not to stdout/stderr. When a model (e.g. glm-5) is deprecated/removed, the action crashes with exit code 1 but the CI log shows only a generic failure — the actual error is invisible without SSH access to the runner.

The multi-review action already used --print-logs --log-level ERROR (see run-multi-review.py:226,244), but the main run-opencode.sh path used by all other actions did not.

Note

The default model is already glm-5.1 in both scripts — no model name update was needed.

Fixes #99

Test plan

  • All 39 existing tests pass
  • fallback_on_regex tests (test_fallback_on_regex, test_builtin_model_fallback) continue to pass
  • Manual: configure a deprecated model and verify the error appears in CI logs
  • Manual: configure fallback models and verify model-not-found triggers fallback

🤖 Generated with Claude Code

- Add `--print-logs --log-level ERROR` to opencode invocation so fatal
  errors like ProviderModelNotFoundError appear in CI stdout/stderr
  instead of only in the local log file
- Add ProviderModelNotFoundError to default fallback_on_regex in both
  github-run-opencode and multi-review, so model-not-found errors
  trigger fallback to the next candidate model

Fixes #99

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

安全无虞

安全分析总结

此 PR 仅包含两处非功能性变更:

  1. github-run-opencode/run-github-opencode.py:在 OPENCODE_ARGS 环境变量中添加 --print-logs --log-level ERROR 参数,并将 ProviderModelNotFoundError 加入默认 fallback_on_regex 正则表达式。
  2. multi-review/run-multi-review.py:将 ProviderModelNotFoundError 加入默认 fallback_on_regex 正则表达式。

两处修改均不涉及用户输入处理、身份认证、敏感数据传输或外部 API 调用逻辑。

详细检查

检查项 结论
输入验证与清理 ProviderModelNotFoundError 为纯字母,无正则特殊字符;--print-logs --log-level ERROR 为硬编码字符串,无注入点
认证与授权 ✅ 未改动任何认证/授权逻辑
注入漏洞(命令注入/XSS/SQL) fallback_on_regex 已有 validate_regex() 校验(line 327),新添加的模式不会引入注入
敏感数据暴露 --log-level ERROR 仅输出 ERROR 级别日志,暴露程度极低
不安全的依赖/API ✅ 未引入新依赖
OWASP Top 10 ✅ 不涉及

阻塞项:无

建议项:无

New%20session%20-%202026-05-21T09%3A56%3A27.295Z
opencode session  |  github run

@github-actions
Copy link
Copy Markdown

架构合理

本次 PR 变更极小(3 行新增,3 行删除),属于纯配置层面的修补:

  1. github-run-opencode/run-github-opencode.py:272 — 补齐了 --print-logs --log-level ERROR 参数。该参数在 multi-review/run-multi-review.py:226,244 已经使用,本次变更是让两个代码路径保持一致,消除了原有的配置不一致问题。

  2. 两文件的 fallback_on_regex 默认值 — 同步添加了 ProviderModelNotFoundError 模式。fallback_on_regex 机制在这两处是同一模式的平行实现,同时更新是必要的,不存在跨层的偏离。

从架构角度看:

  • 耦合:未引入新依赖或循环引用,纯配置变更
  • 模块放置:不涉及新文件
  • 分层:变更范围停留在编排层(orchestration)的 fallback 决策逻辑内,不跨层
  • 接口设计:无新 API
  • 霰弹式修改:两处都是同一概念(fallback 触发正则)的平行实现,修改受限于可预见的两处,没有扩散到不相关的层
  • 一致性--print-logs 原本就有缺失,本次补齐了,反而提升了一致性

阻塞项:无

建议项:无

New%20session%20-%202026-05-21T09%3A57%3A11.064Z
opencode session  |  github run

@github-actions
Copy link
Copy Markdown

所有改动都聚焦于 PR 描述的两个目标,逻辑正确无缺陷。

审查结论:

可合并

本次 PR 改动量极小(+3/-3),分别修改两个文件的 fallback_on_regex 默认值(添加 ProviderModelNotFoundError)以及 github-run-opencode 路径的 OPENCODE_ARGS(添加 --print-logs --log-level ERROR)。

  • 阻塞项:无
  • 建议项:无

New%20session%20-%202026-05-21T09%3A56%3A27.361Z
opencode session  |  github run

@github-actions
Copy link
Copy Markdown

可合并

综合评审总结

三位 reviewer(security、quality 以及 PR 自身的 code review)均对本次 PR 给出了无阻塞项、无建议项的结论,一致认为变更合理。

变更摘要:

  • github-run-opencode/run-github-opencode.py(+2/-2):添加 --print-logs --log-level ERROR 参数,补齐与 multi-review 路径的一致性;在 fallback_on_regex 默认值中添加 ProviderModelNotFoundError
  • multi-review/run-multi-review.py(+1/-1):在 fallback_on_regex 默认值中添加 ProviderModelNotFoundError

多 reviewer 确认:

  • --print-logs --log-level ERROR 参数补齐 → security、quality 均确认无安全/架构风险(多 reviewer 确认)
  • fallback_on_regex 添加 ProviderModelNotFoundError → 三位 reviewer 均判断为纯配置变更,逻辑正确(多 reviewer 确认)

冲突解决: 无冲突。三位 reviewer 结论一致。

阻塞项:无

建议项:无

New%20session%20-%202026-05-21T09%3A57%3A33.713Z
opencode session  |  github run

@github-actions
Copy link
Copy Markdown

Asserting permissions for user Svtter...
permission: admin
Adding reaction...
opencode session ses_1b607f26effe87im5e0VnvEMH8
Fetching prompt data for PR...
Checking out local branch...
Sending message to opencode...
[09:57:34.795] INFO (#278): loop {
service: "session.prompt",
"session.id": "ses_1b607f26effe87im5e0VnvEMH8",
step: 0,
}
[09:57:34.829] INFO (#292): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}
[09:57:34.914] INFO (#278): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}
[09:57:39.880] INFO (#278): loop {
service: "session.prompt",
"session.id": "ses_1b607f26effe87im5e0VnvEMH8",
step: 1,
}
[09:57:39.883] INFO (#278): exiting loop {
service: "session.prompt",
"session.id": "ses_1b607f26effe87im5e0VnvEMH8",
}
Checking if branch is dirty...
Creating comment...
Removing reaction...


详细审查报告:

✅ security

Asserting permissions for user Svtter...
permission: admin
Adding reaction...
opencode session ses_1b608f5e0ffejpqqTcYX6R0zgb
Fetching prompt data for PR...
Checking out local branch...
Sending message to opencode...
[09:56:28.441] INFO (#289): loop {
service: "session.prompt",
"session.id": "ses_1b608f5e0ffejpqqTcYX6R0zgb",
step: 0,
}
[09:56:28.501] INFO (#303): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}
[09:56:28.647] INFO (#289): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}
[09:56:30.766] INFO (#289): loop {
service: "session.prompt",
"session.id": "ses_1b608f5e0ffejpqqTcYX6R0zgb",
step: 1,
}
[09:56:30.800] INFO (#289): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[09:56:34.283] INFO (#289): loop {
service: "session.prompt",
"session.id": "ses_1b608f5e0ffejpqqTcYX6R0zgb",
step: 2,
}
[09:56:34.314] INFO (#289): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[09:56:36.521] INFO (#289): loop {
service: "session.prompt",
"session.id": "ses_1b608f5e0ffejpqqTcYX6R0zgb",
step: 3,
}
[09:56:36.547] INFO (#289): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[09:56:38.459] INFO (#289): loop {
service: "session.prompt",
"session.id": "ses_1b608f5e0ffejpqqTcYX6R0zgb",
step: 4,
}
[09:56:38.480] INFO (#289): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}
[09:56:51.644] INFO (#289): loop {
service: "session.prompt",
"session.id": "ses_1b608f5e0ffejpqqTcYX6R0zgb",
step: 5,
}
[09:56:51.646] INFO (#289): exiting loop {
service: "session.prompt",
"session.id": "ses_1b608f5e0ffejpqqTcYX6R0zgb",
}
Checking if branch is dirty...
Creating comment...
Removing reaction...

✅ quality

Asserting permissions for user Svtter...
permission: admin
Adding reaction...
opencode session ses_1b608f59fffeU7J9yPm9icCEAX
Fetching prompt data for PR...
Checking out local branch...
Sending message to opencode...
[09:56:28.539] INFO (#295): loop {
service: "session.prompt",
"session.id": "ses_1b608f59fffeU7J9yPm9icCEAX",
step: 0,
}
[09:56:28.603] INFO (#303): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}
[09:56:28.921] INFO (#295): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[09:56:31.272] INFO (#295): loop {
service: "session.prompt",
"session.id": "ses_1b608f59fffeU7J9yPm9icCEAX",
step: 1,
}
[09:56:31.305] INFO (#295): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[09:56:33.999] INFO (#295): loop {
service: "session.prompt",
"session.id": "ses_1b608f59fffeU7J9yPm9icCEAX",
step: 2,
}
[09:56:34.021] INFO (#295): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[09:56:38.577] INFO (#295): loop {
service: "session.prompt",
"session.id": "ses_1b608f59fffeU7J9yPm9icCEAX",
step: 3,
}
[09:56:38.597] INFO (#295): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[09:56:41.103] INFO (#295): loop {
service: "session.prompt",
"session.id": "ses_1b608f59fffeU7J9yPm9icCEAX",
step: 4,
}
[09:56:41.124] INFO (#295): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[09:56:43.531] INFO (#295): loop {
service: "session.prompt",
"session.id": "ses_1b608f59fffeU7J9yPm9icCEAX",
step: 5,
}
[09:56:43.549] INFO (#295): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[09:56:45.668] INFO (#295): loop {
service: "session.prompt",
"session.id": "ses_1b608f59fffeU7J9yPm9icCEAX",
step: 6,
}
[09:56:45.687] INFO (#295): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[09:57:01.447] INFO (#295): loop {
service: "session.prompt",
"session.id": "ses_1b608f59fffeU7J9yPm9icCEAX",
step: 7,
}
[09:57:01.469] INFO (#295): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[09:57:03.805] INFO (#295): loop {
service: "session.prompt",
"session.id": "ses_1b608f59fffeU7J9yPm9icCEAX",
step: 8,
}
[09:57:03.829] INFO (#295): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[09:57:05.885] INFO (#295): loop {
service: "session.prompt",
"session.id": "ses_1b608f59fffeU7J9yPm9icCEAX",
step: 9,
}
[09:57:05.904] INFO (#295): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[09:57:07.785] INFO (#295): loop {
service: "session.prompt",
"session.id": "ses_1b608f59fffeU7J9yPm9icCEAX",
step: 10,
}
[09:57:07.823] INFO (#295): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[09:57:14.508] INFO (#295): loop {
service: "session.prompt",
"session.id": "ses_1b608f59fffeU7J9yPm9icCEAX",
step: 11,
}
[09:57:14.583] INFO (#295): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[09:57:16.418] INFO (#295): loop {
service: "session.prompt",
"session.id": "ses_1b608f59fffeU7J9yPm9icCEAX",
step: 12,
}
[09:57:16.437] INFO (#295): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[09:57:18.925] INFO (#295): loop {
service: "session.prompt",
"session.id": "ses_1b608f59fffeU7J9yPm9icCEAX",
step: 13,
}
[09:57:18.947] INFO (#295): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}
[09:57:30.630] INFO (#295): loop {
service: "session.prompt",
"session.id": "ses_1b608f59fffeU7J9yPm9icCEAX",
step: 14,
}
[09:57:30.632] INFO (#295): exiting loop {
service: "session.prompt",
"session.id": "ses_1b608f59fffeU7J9yPm9icCEAX",
}
Checking if branch is dirty...
Creating comment...
Removing reaction...

@github-actions
Copy link
Copy Markdown

可合并

该 PR 在两个文件中做了三处修改:为 github-run-opencodeOPENCODE_ARGS 添加 --print-logs --log-level ERROR 标志,以及在两个文件的 fallback_on_regex 默认值中追加 ProviderModelNotFoundError。改动意图清晰,与 PR 描述一致,解决 issue #99 中模型错误无法透传至 CI 日志的问题。

阻塞项:无

建议项:

  1. multi-review/run-multi-review.py:317 的 fallback regex 匹配的是 _run_opencode 返回的 stdout,而 --print-logs 产生的日志输出可能写入 stderr(取决于 opencode CLI 的实现)。这意味着 ProviderModelNotFoundError 可能在 stderr 中、不会出现在 stdout 里,导致 fallback regex 不匹配。不过此问题与多 review 中已有的 timed out/deadline exceeded 等 pattern 面临相同处境(这些模式同样已存在于现有代码的 fallback_regex 中),属于非本 PR 引入的预存设计问题。建议确认 opencode CLI 的 --print-logs 实际写入哪个流,必要时在 _run_opencode_raw/_run_opencode_in_dir 中合并 stdout+stderr 后返回。

New%20session%20-%202026-05-21T09%3A56%3A27.431Z
opencode session  |  github run

@github-actions
Copy link
Copy Markdown

Now I have all the information needed. Let me present the audit results.

发现遗漏

总结

PR 在 Python 代码层正确添加了 ProviderModelNotFoundErrorfallback_on_regex 默认值,但在 action.yml 中遗漏了对应的默认值更新,导致该功能在实际使用时不会生效。


CRITICAL

ProviderModelNotFoundError 未加入 action.yml 的 fallback-on-regex 默认值

PR 修改了两个 Python 脚本的 fallback_on_regex 默认值,使其包含 ProviderModelNotFoundError。然而,所有的 action.yml 文件(共 6 个)都通过以下方式传递环境变量:

GITHUB_RUN_OPENCODE_FALLBACK_ON_REGEX: ${{ inputs.fallback-on-regex }}

当用户没有显式设置 fallback-on-regex 输入时,action.yml 的 default 值(timed out|timeout|deadline exceeded|context deadline exceeded|operation timed out|connection timed out,不含 ProviderModelNotFoundError)会被赋值给环境变量,导致 Python 代码的默认值永远无法被命中。这意味着 ProviderModelNotFoundError 回退机制在通过 composite action 使用时不生效,整个功能的实现目的被架空。

涉及文件(6 个):

  • github-run-opencode/action.yml:76default 字段
  • review/action.yml:76default 字段
  • feature-missing/action.yml:154default 字段
  • architect-review/action.yml:76default 字段
  • spec-coverage/action.yml:173default 字段
  • multi-review/action.yml:76default 字段

修复方向: 在上述 6 个 action.yml 的 fallback-on-regexdefault 值末尾追加 |ProviderModelNotFoundError,与 Python 代码默认值保持一致。


已正确实现的部分(无问题)

  • --print-logs --log-level ERROR 已正确添加到 github-run-opencode/run-github-opencode.pyOPENCODE_ARGS(行 272),且 OPENCODE_ARGS 不受 action.yml 覆写,功能正常。
  • multi-review--print-logs --log-level ERROR 在修改前已存在(行 226, 244),无需变动。
  • 默认模型已为 glm-5.1:经确认 github-run-opencode/run-github-opencode.py:285multi-review/run-multi-review.py:270,569 均使用 glm-5.1,符合 issue 要求。

New%20session%20-%202026-05-21T09%3A56%3A27.236Z
opencode session  |  github run

The Python code default was updated but action.yml input defaults
override it — without this change ProviderModelNotFoundError would
never be matched when using the composite actions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

安全无虞

安全分析总结

本次 PR 变更仅涉及:

  1. github-run-opencode/run-github-opencode.pyOPENCODE_ARGS 添加硬编码的 --print-logs --log-level ERRORfallback_on_regex 默认值添加 ProviderModelNotFoundError
  2. multi-review/run-multi-review.pyfallback_on_regex 默认值添加 ProviderModelNotFoundError
  3. 6 个 action.yml:同步更新 fallback-on-regexdefault

逐项安全检查

检查项 结论及理由
输入验证与清理 ProviderModelNotFoundError 为纯字母字符串,不含正则特殊字符,不存在 ReDoS 风险。fallback_on_regex 取自环境变量后经过 validate_regex() 编译校验(run-github-opencode.py:327),无效正则直接退出。
认证与授权 ✅ 未修改任何认证逻辑。GITHUB_TOKENZHIPU_API_KEY 等凭据传递方式不变。
注入漏洞 --print-logs --log-level ERROR 为硬编码字符串(run-github-opencode.py:272),非用户输入拼接,无命令注入风险。fallback_on_regex 仅用于 re.search() 匹配,不会传给 shell。
敏感数据暴露 --log-level ERROR 降低了日志输出级别,暴露范围反而缩小。--print-logs 将 opencode 内部错误日志同步到 CI stdout/stderr,有助于调试且不增传输环节。opencode CLI 不会在 ERROR 级别日志中记录 API key。
不安全的依赖/API ✅ 未引入新依赖。
OWASP Top 10 ✅ 不涉及用户数据处理、外部 URL 引用、反序列化等攻击面。

阻塞项:无

建议项:无

New%20session%20-%202026-05-21T10%3A01%3A35.310Z
opencode session  |  github run

@github-actions
Copy link
Copy Markdown

可合并

变更覆盖全面:6 个 action.yml 和 2 个 Python 文件的 fallback-on-regex 默认值均同步添加了 ProviderModelNotFoundError,Python 端的默认值与 action.yml 的 input default 一致(早先的遗漏已由最新 commit 修复)。--print-logs --log-level ERROR 已正确添加到 github-run-opencode 路径,补齐了与 multi-review 路径的一致性。validate_regex() 对新增的正则做校验,ProviderModelNotFoundError 纯字母无特殊字符,校验通过。测试均通过。

阻塞项:无

建议项:

  1. multi-review/run-multi-review.py 未对 fallback_on_regex 做正则有效性校验(github-run-opencode 路径有 validate_regex()),若用户传入非法正则会在运行时崩溃,可考虑同步加入校验。
  2. multi-review_run_opencode_raw/_run_opencode_in_dir 仅返回 stdout,若 --print-logs 实际将错误日志写入 stderr 会导致 ProviderModelNotFoundError 无法匹配 fallback(此问题对所有 fallback pattern 均存在,属预存设计问题,建议确认 opencode CLI 的日志输出流)。

New%20session%20-%202026-05-21T10%3A01%3A35.391Z
opencode session  |  github run

@github-actions
Copy link
Copy Markdown

可合并

综合评审总结

三位 reviewer(security、quality、code review)在最新版本(含 action.yml 同步修复)上结论一致,均无阻塞项。

关键发现历程

  • 审计 reviewer (09:58:41) 发现初始版本遗漏了 6 个 action.yml 的 fallback-on-regex default 值的同步更新,导致 ProviderModelNotFoundError 回退机制在 composite action 使用时不生效。
  • 该问题已被后续 commit 修复(8 个文件均被修改,6 个 action.yml 同步追加了 ProviderModelNotFoundError)。
  • 修复后所有 reviewer 均确认无阻塞项。

共识确认

发现项 来源 多 reviewer 确认
--print-logs --log-level ERROR 补齐 github-run-opencode 路径 security, quality ✅ 多 reviewer 确认:安全无风险,架构一致性提升
fallback_on_regex 添加 ProviderModelNotFoundError(Python + 6 action.yml) security, quality, code review ✅ 多 reviewer 确认:三端一致,纯配置变更,逻辑正确
action.yml 默认值遗漏(已修复) 审计 reviewer → 后续 commit 修复 已修复,不再构成阻塞

阻塞项

建议项

  1. multi-review 缺少 validate_regex() 校验(reviewer: quality)—— github-run-opencode 路径对 fallback_on_regexvalidate_regex() 编译校验(run-github-opencode.py:327),而 multi-review 路径没有,用户传入非法正则时会在运行时崩溃。建议 multi-review/run-multi-review.py 同步加入正则有效性校验。

  2. _run_opencode_raw 仅返回 stdout 问题(reviewer: multiple)—— multi-review/run-multi-review.py_run_opencode_raw/_run_opencode_in_dir 仅返回 stdout,而 --print-logs 可能将错误日志写入 stderr,导致 ProviderModelNotFoundError 等 fallback pattern 无法匹配。此问题对所有 fallback pattern 均存在(属预存设计问题,非本 PR 引入),建议确认 opencode CLI 的日志输出流,必要时合并 stdout+stderr 后做 fallback 匹配。

New%20session%20-%202026-05-21T10%3A03%3A03.632Z
opencode session  |  github run

@github-actions
Copy link
Copy Markdown

Asserting permissions for user Svtter...
permission: admin
Adding reaction...
opencode session ses_1b602e9b0ffekzhssfR3InUd2N
Fetching prompt data for PR...
Checking out local branch...
Sending message to opencode...
[10:03:04.547] INFO (#276): loop {
service: "session.prompt",
"session.id": "ses_1b602e9b0ffekzhssfR3InUd2N",
step: 0,
}
[10:03:04.601] INFO (#290): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}
[10:03:04.755] INFO (#276): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}
[10:03:19.806] INFO (#276): loop {
service: "session.prompt",
"session.id": "ses_1b602e9b0ffekzhssfR3InUd2N",
step: 1,
}
[10:03:19.810] INFO (#276): exiting loop {
service: "session.prompt",
"session.id": "ses_1b602e9b0ffekzhssfR3InUd2N",
}
Checking if branch is dirty...
Creating comment...
Removing reaction...


详细审查报告:

✅ security

Asserting permissions for user Svtter...
permission: admin
Adding reaction...
opencode session ses_1b60442b2ffenEcOkdeSfVMkEe
Fetching prompt data for PR...
Checking out local branch...
Sending message to opencode...
[10:01:36.212] INFO (#283): loop {
service: "session.prompt",
"session.id": "ses_1b60442b2ffenEcOkdeSfVMkEe",
step: 0,
}
[10:01:36.371] INFO (#297): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}
[10:01:36.731] INFO (#283): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:01:42.869] INFO (#283): loop {
service: "session.prompt",
"session.id": "ses_1b60442b2ffenEcOkdeSfVMkEe",
step: 1,
}
[10:01:42.908] INFO (#283): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:01:45.142] INFO (#283): loop {
service: "session.prompt",
"session.id": "ses_1b60442b2ffenEcOkdeSfVMkEe",
step: 2,
}
[10:01:45.182] INFO (#283): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:01:48.003] INFO (#283): loop {
service: "session.prompt",
"session.id": "ses_1b60442b2ffenEcOkdeSfVMkEe",
step: 3,
}
[10:01:48.038] INFO (#283): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:01:50.301] INFO (#283): loop {
service: "session.prompt",
"session.id": "ses_1b60442b2ffenEcOkdeSfVMkEe",
step: 4,
}
[10:01:50.417] INFO (#283): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:01:53.499] INFO (#283): loop {
service: "session.prompt",
"session.id": "ses_1b60442b2ffenEcOkdeSfVMkEe",
step: 5,
}
[10:01:53.535] INFO (#283): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:01:58.241] INFO (#283): loop {
service: "session.prompt",
"session.id": "ses_1b60442b2ffenEcOkdeSfVMkEe",
step: 6,
}
[10:01:58.274] INFO (#283): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:02:02.196] INFO (#283): loop {
service: "session.prompt",
"session.id": "ses_1b60442b2ffenEcOkdeSfVMkEe",
step: 7,
}
[10:02:02.234] INFO (#283): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:02:04.621] INFO (#283): loop {
service: "session.prompt",
"session.id": "ses_1b60442b2ffenEcOkdeSfVMkEe",
step: 8,
}
[10:02:04.662] INFO (#283): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:02:06.897] INFO (#283): loop {
service: "session.prompt",
"session.id": "ses_1b60442b2ffenEcOkdeSfVMkEe",
step: 9,
}
[10:02:06.932] INFO (#283): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:02:08.774] INFO (#283): loop {
service: "session.prompt",
"session.id": "ses_1b60442b2ffenEcOkdeSfVMkEe",
step: 10,
}
[10:02:08.817] INFO (#283): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:02:16.074] INFO (#283): loop {
service: "session.prompt",
"session.id": "ses_1b60442b2ffenEcOkdeSfVMkEe",
step: 11,
}
[10:02:16.113] INFO (#283): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}
[10:02:33.710] INFO (#283): loop {
service: "session.prompt",
"session.id": "ses_1b60442b2ffenEcOkdeSfVMkEe",
step: 12,
}
[10:02:33.714] INFO (#283): exiting loop {
service: "session.prompt",
"session.id": "ses_1b60442b2ffenEcOkdeSfVMkEe",
}
Checking if branch is dirty...
Creating comment...
Removing reaction...

✅ quality

Asserting permissions for user Svtter...
permission: admin
Adding reaction...
opencode session ses_1b6044260ffeGFJo9bz0kYLY0t
Fetching prompt data for PR...
Checking out local branch...
Sending message to opencode...
[10:01:36.613] INFO (#281): loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
step: 0,
}
[10:01:36.688] INFO (#295): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}
[10:01:36.913] INFO (#281): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:01:41.386] INFO (#281): loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
step: 1,
}
[10:01:41.427] INFO (#281): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:01:43.263] INFO (#281): loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
step: 2,
}
[10:01:43.300] INFO (#281): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:01:46.176] INFO (#281): loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
step: 3,
}
[10:01:46.211] INFO (#281): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:01:48.483] INFO (#281): loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
step: 4,
}
[10:01:48.516] INFO (#281): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:01:50.081] INFO (#281): loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
step: 5,
}
[10:01:50.114] INFO (#281): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:01:58.957] INFO (#281): loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
step: 6,
}
[10:01:58.992] INFO (#281): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:02:02.470] INFO (#281): loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
step: 7,
}
[10:02:02.503] INFO (#281): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:02:04.662] INFO (#281): loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
step: 8,
}
[10:02:04.701] INFO (#281): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:02:06.832] INFO (#281): loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
step: 9,
}
[10:02:06.867] INFO (#281): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:02:09.030] INFO (#281): loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
step: 10,
}
[10:02:09.067] INFO (#281): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:02:25.203] INFO (#281): loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
step: 11,
}
[10:02:25.258] INFO (#281): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:02:31.228] INFO (#281): loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
step: 12,
}
[10:02:31.261] INFO (#281): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:02:49.377] INFO (#281): loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
step: 13,
}
[10:02:49.411] INFO (#281): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}

[10:02:51.915] INFO (#281): loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
step: 14,
}
[10:02:51.946] INFO (#281): llm runtime selected {
"llm.runtime": "ai-sdk",
"llm.provider": "deepseek",
"llm.model": "deepseek-v4-flash",
}
[10:03:00.232] INFO (#281): loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
step: 15,
}
[10:03:00.235] INFO (#281): exiting loop {
service: "session.prompt",
"session.id": "ses_1b6044260ffeGFJo9bz0kYLY0t",
}
Checking if branch is dirty...
Creating comment...
Removing reaction...

@github-actions
Copy link
Copy Markdown

现在我已经拥有所有需要的信息。以下是完整的审查:

可合并

本次 PR 修正了 issue #99,让 ProviderModelNotFoundError 在 CI 日志中可见并触发 fallback。两个 commit 共修改 8 个文件,变更一致、逻辑正确。

阻塞项:无

建议项:无


审查细节:

变更总览

文件 变更
github-run-opencode/run-github-opencode.py:272 OPENCODE_ARGS: "github run""github run --print-logs --log-level ERROR"
github-run-opencode/run-github-opencode.py:270 fallback_on_regex 默认值追加 |ProviderModelNotFoundError
multi-review/run-multi-review.py:544 fallback_on_regex 默认值追加 |ProviderModelNotFoundError
6 个 action.ymlfallback-on-regex 默认值 追加 |ProviderModelNotFoundError

验证结果

  1. --print-logs --log-level ERROR 补齐正确。 github-run-opencode 路径的 run_model() 使用 stderr=subprocess.STDOUT 合并两个流,且底层 run-opencode.sh 使用 2>&1,无论 opencode CLI 将日志写入 stdout 还是 stderr,都能被 CI 捕获和被 fallback regex 匹配。multi-review 路径此前已使用了这些参数,本次 PR 让两条路径保持一致。

  2. fallback_on_regex 默认值一致更新。 第一版 commit (1627bb8) 只更新了 Python 脚本,第二版 commit (435a086) 补充了全部 6 个 action.yml。现在 Python 默认值和 action.yml 默认值完全一致,不存在用户不传参时 action.yml 默认值覆盖 Python 默认值导致功能失效的问题。

  3. ProviderModelNotFoundError 是纯字母字符串,不含正则特殊字符,安全地嵌入 | 分隔的 alternation 中。代码中已有 validate_regex() 校验。

  4. 非该 PR 引入的预存问题: multi-review 路径的 _run_opencode_raw/_run_opencode_in_dir 将 stdout 和 stderr 分开捕获,fallback regex 只检查 stdout。如果 opencode CLI 将 --log-level ERROR 的输出写入 stderr 而非 stdout,则 regex 不会命中。但该设计限制在 PR 之前就已存在(multi-review 此前已使用 --print-logs --log-level ERROR),且已有 pattern("timed out"、"timeout" 等)面临相同的处境,因此不构成本 PR 的阻塞点。

New%20session%20-%202026-05-21T10%3A01%3A38.954Z
opencode session  |  github run

@github-actions
Copy link
Copy Markdown

架构合理

本次 PR 变更基于两个 commit:首先在 Python 脚本中添加 ProviderModelNotFoundError 回退模式,然后在第二个 commit 同步更新所有 6 个 action.ymlfallback-on-regex 默认值。从架构角度看:

  1. 一致性提升:补齐了 github-run-opencode 路径缺少的 --print-logs --log-level ERROR,使之与 multi-review 路径一致;同时修复了 action.yml 默认值与 Python 脚本默认值不一致的预存架构缺陷(action.ymldefault 始终通过环境变量覆盖 Python 侧的 fallback 默认值,因此只改 Python 而不改 action.yml 会导致实际生效的是旧值)。

  2. 分层清晰action.yml 负责 GitHub Actions 输入接口定义,Python 脚本负责编排逻辑,run-opencode.sh 负责执行。本次变更不跨层,不引入新的层间依赖。

  3. 霰弹式修改属预存结构:8 个文件改动量虽大,但其中 6 个 action.yml 的修改完全相同、是 6 个独立 composite action 共享同一 fallback 概念所导致的必然结果,非本 PR 引入。第二个 commit 正确识别并补齐了这一约束,反而是架构意识的体现。

  4. OPENCODE_ARGS 硬编码无问题run-github-opencode.py:272--print-logs --log-level ERROR 硬写入 OPENCODE_ARGS,这是 PR 设计意图(始终在 CI 暴露错误),且 github-run-opencode/action.yml 本就未暴露 args 输入给用户,因此不构成架构问题。

阻塞项:无

建议项:无

New%20session%20-%202026-05-21T10%3A02%3A35.371Z
opencode session  |  github run

@github-actions
Copy link
Copy Markdown

发现遗漏

总结

PR 正确实现了 issue #99 的核心需求:添加了 --print-logs --log-level ERRORrun-github-opencode.py,并在两个 Python 脚本及所有 6 个 action.yml 的 fallback_on_regex 默认值中添加了 ProviderModelNotFoundError。默认模型已为 glm-5.1,无需修改。发现一处 LOW 级别的潜在问题。


LOW

multi-review 路径中 fallback regex 可能无法匹配到 stderr 输出的 ProviderModelNotFoundError

PR 在 multi-review/run-multi-review.py:544fallback_on_regex 默认值中添加了 ProviderModelNotFoundError,但 multi-review 的子进程调用(_run_opencode_raw 第 234 行、_run_opencode_in_dir 第 252 行)使用 stdout=subprocess.PIPE, stderr=subprocess.PIPE 分别捕获输出,且仅返回 stdout(第 238 行:return result.returncode, result.stdout.decode(...))。而 fallback regex 匹配(_run_reviewer_inner 第 317 行)对 output(仅 stdout)进行匹配。

opencode 的 --print-logs 产生的日志输出通常写入 stderr(CLI 通用惯例),因此 ProviderModelNotFoundError 可能出现在 stderr 而非 stdout 中,导致 fallback regex 不会匹配、无法触发模型回退。

注意:这是 multi-review 路径长期存在的架构问题(所有已有 fallback pattern 如 timed outdeadline exceeded 都面临相同处境),非本 PR 引入。但本 PR 声称在 multi-review 中通过此 regex 实现 fallback 触发的目标可能无法达成。

修复方向:_run_opencode_raw(第 238 行)和 _run_opencode_in_dir(第 254 行附近)返回时,将 stderr 内容合并到 stdout 中返回:

combined = (result.stdout + result.stderr).decode("utf-8", errors="replace")
return result.returncode, combined

或至少在 _run_reviewer_inner 的 regex 匹配前将 output 与 stderr 合并。

New%20session%20-%202026-05-21T10%3A01%3A39.036Z
opencode session  |  github run

@Svtter Svtter merged commit 398abaf into main May 21, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

opencode 1.15.x crashes with ProviderModelNotFoundError when model glm-5 is unavailable, no useful error in CI output

1 participant