feat(upgrader): LOOP_TIER=go|zen config — switch weekly upgrade to OpenCode ZEN
GO subscription hit a monthly usage limit (resets in 9 days). Add LOOP_TIER env (default 'zen') to launch-upgrader.py + launch-report.py so the opencode backend can use either subscription: zen→opencode/glm-5.2, go→opencode-go/glm-5.2. The tier selects the default model, the usage-limit probe endpoint, and the auth.json key. The systemd timer reads LOOP_TIER from /srv/cc-ci/upgrader.env.
This commit is contained in:
@ -2,8 +2,8 @@
|
||||
"""cc-ci recipe-report launcher — one-shot agent that runs /recipe-report after the weekly upgrade.
|
||||
|
||||
The report agent's model is configured SEPARATELY from the upgrader (REPORT_BACKEND/REPORT_MODEL),
|
||||
but defaults to the same OpenCode Go subscription on glm-5.2. The model default tracks the backend
|
||||
(opencode→opencode-go/glm-5.2, claude→opus).
|
||||
but defaults to the same OpenCode subscription on glm-5.2 as the upgrader. The model default tracks
|
||||
the backend+tier (opencode+zen→opencode/glm-5.2, opencode+go→opencode-go/glm-5.2, claude→opus).
|
||||
|
||||
Usage:
|
||||
launch-report.py start [DATE] use-or-create the session; runs /recipe-report [DATE]
|
||||
@ -11,7 +11,8 @@ Usage:
|
||||
launch-report.py stop kill the session
|
||||
launch-report.py status show session state
|
||||
|
||||
Env: REPORT_BACKEND (default opencode), REPORT_MODEL (default opencode-go/glm-5.2 | opus for claude),
|
||||
Env: REPORT_BACKEND (default opencode), REPORT_MODEL (default tracks tier),
|
||||
LOOP_TIER / REPORT_TIER (default 'zen'; 'go' or 'zen'; only for opencode),
|
||||
OPENCODE_SHARE (1=attach web server + public --share link), REPORT_SESSION, REPORT_DIR.
|
||||
"""
|
||||
import os, subprocess, sys, time
|
||||
@ -22,7 +23,9 @@ SESSION = os.environ.get("REPORT_SESSION", "cc-ci-report")
|
||||
WORKDIR = os.environ.get("REPORT_DIR", "/srv/cc-ci")
|
||||
LOG_DIR = os.environ.get("LOG_DIR", "/srv/cc-ci/.cc-ci-logs")
|
||||
BACKEND = os.environ.get("REPORT_BACKEND", "opencode")
|
||||
_DEFAULT_MODEL = "opencode-go/glm-5.2" if BACKEND == "opencode" else "opus"
|
||||
TIER = os.environ.get("LOOP_TIER", os.environ.get("REPORT_TIER", "zen"))
|
||||
_TIER_MODEL = {"go": "opencode-go/glm-5.2", "zen": "opencode/glm-5.2"}
|
||||
_DEFAULT_MODEL = _TIER_MODEL.get(TIER, _TIER_MODEL["zen"]) if BACKEND == "opencode" else "opus"
|
||||
MODEL = os.environ.get("REPORT_MODEL", _DEFAULT_MODEL)
|
||||
CLAUDE_BIN = os.environ.get("CLAUDE_BIN", "claude")
|
||||
CLAUDE_FLAGS = os.environ.get("CLAUDE_FLAGS", "--dangerously-skip-permissions")
|
||||
@ -84,7 +87,7 @@ def start(mode, date):
|
||||
)
|
||||
else:
|
||||
die(f"unknown REPORT_BACKEND '{BACKEND}'")
|
||||
log(f"starting {SESSION} (backend={BACKEND}, model={MODEL}, date={date or 'today'})")
|
||||
log(f"starting {SESSION} (backend={BACKEND}, tier={TIER}, model={MODEL}, date={date or 'today'})")
|
||||
subprocess.run(["tmux", "new-session", "-d", "-s", SESSION, "-c", cwd, cmd])
|
||||
subprocess.run(["tmux", "pipe-pane", "-o", "-t", SESSION, f"cat >> '{LOG_DIR}/{SESSION}.log'"])
|
||||
if BACKEND == "opencode":
|
||||
@ -99,13 +102,13 @@ def start(mode, date):
|
||||
LU = "/srv/cc-ci/cc-ci-plan/launch-upgrader.py"
|
||||
rf = Path(LOG_DIR) / f".kickoff-{SESSION}-resume.txt"
|
||||
rf.write_text(
|
||||
"The opencode-go usage limit has reset (or the report stalled). CONTINUE generating and "
|
||||
"The opencode usage limit has reset (or the report stalled). CONTINUE generating and "
|
||||
"publishing this week's public Recipe Report per /recipe-report: survey → write the spec "
|
||||
"JSON → render with recipe-report.py → publish. Do NOT hand-write HTML (render() owns all "
|
||||
"formatting). When the page is live, print 'RECIPE REPORT COMPLETE' and go idle.")
|
||||
wenv = {"HOME": os.environ.get("HOME") or "/home/loops",
|
||||
"UPGRADER_SESSION": SESSION, "UPGRADER_DIR": cwd, "LOG_DIR": LOG_DIR,
|
||||
"UPGRADER_BACKEND": "opencode", "UPGRADER_MODEL": MODEL,
|
||||
"UPGRADER_BACKEND": "opencode", "UPGRADER_MODEL": MODEL, "LOOP_TIER": TIER,
|
||||
"OPENCODE_BIN": OPENCODE_BIN, "OPENCODE_SERVER": OPENCODE_SERVER,
|
||||
"OPENCODE_SHARE": "1" if OPENCODE_SHARE else "0",
|
||||
"UPGRADER_DONE_MARKER": "RECIPE REPORT COMPLETE",
|
||||
|
||||
@ -15,10 +15,14 @@ Usage:
|
||||
|
||||
Env:
|
||||
LOOP_BACKEND opencode (default) | claude — also accepts UPGRADER_BACKEND
|
||||
LOOP_MODEL model flag (overrides UPGRADER_MODEL); default tracks backend —
|
||||
opencode→opencode-go/glm-5.2, claude→sonnet
|
||||
UPGRADER_MODEL provider/model for opencode, e.g. opencode-go/glm-5.2 (OpenCode Go
|
||||
subscription) or tinfoil/deepseek-v4-pro; sonnet etc. for claude
|
||||
LOOP_TIER opencode subscription tier: "zen" (OpenCode ZEN, default) or "go"
|
||||
(OpenCode Go). Selects the default model + the usage-limit probe
|
||||
endpoint/key. Go hit a monthly limit; ZEN is the working alternative.
|
||||
Only affects the opencode backend (ignored for claude).
|
||||
LOOP_MODEL model flag (overrides UPGRADER_MODEL); default tracks backend+tier —
|
||||
opencode+zen→opencode/glm-5.2, opencode+go→opencode-go/glm-5.2, claude→sonnet
|
||||
UPGRADER_MODEL provider/model for opencode, e.g. opencode/glm-5.2 (OpenCode ZEN),
|
||||
opencode-go/glm-5.2 (OpenCode Go), or tinfoil/deepseek-v4-pro; sonnet for claude
|
||||
UPGRADER_ARGS extra args passed to /upgrade-all (e.g. "n8n ghost", "--dry-run")
|
||||
|
||||
claude backend:
|
||||
@ -38,11 +42,22 @@ WORKDIR = os.environ.get("UPGRADER_DIR", "/srv/cc-ci")
|
||||
LOG_DIR = os.environ.get("LOG_DIR", "/srv/cc-ci/.cc-ci-logs")
|
||||
|
||||
# LOOP_BACKEND / LOOP_MODEL take precedence (unified control from the operator).
|
||||
# Default backend+model is the OpenCode Go subscription on glm-5.2; override either via env.
|
||||
# The model default tracks the backend so an explicit `LOOP_BACKEND=claude` (without a model)
|
||||
# still gets a sensible claude model rather than a glm id.
|
||||
# LOOP_TIER selects the OpenCode subscription: "zen" (default) or "go". Go hit a monthly
|
||||
# usage limit; ZEN is the working alternative. Set LOOP_TIER=go when the Go limit resets.
|
||||
# Only affects the opencode backend; claude ignores the tier.
|
||||
BACKEND = os.environ.get("LOOP_BACKEND", os.environ.get("UPGRADER_BACKEND", "opencode"))
|
||||
_DEFAULT_MODEL = "opencode-go/glm-5.2" if BACKEND == "opencode" else "sonnet"
|
||||
TIER = os.environ.get("LOOP_TIER", os.environ.get("UPGRADER_TIER", "zen"))
|
||||
_TIER_CFG = {
|
||||
"go": {"model": "opencode-go/glm-5.2", "endpoint": "https://opencode.ai/zen/go/v1/chat/completions",
|
||||
"auth": "opencode-go", "label": "OpenCode Go"},
|
||||
"zen": {"model": "opencode/glm-5.2", "endpoint": "https://opencode.ai/zen/v1/chat/completions",
|
||||
"auth": "opencode", "label": "OpenCode ZEN"},
|
||||
}
|
||||
if BACKEND == "opencode" and TIER not in _TIER_CFG:
|
||||
print(f"[upgrader] ERROR: unknown LOOP_TIER '{TIER}' — use 'go' or 'zen'", flush=True)
|
||||
sys.exit(1)
|
||||
_tier = _TIER_CFG.get(TIER, _TIER_CFG["zen"])
|
||||
_DEFAULT_MODEL = _tier["model"] if BACKEND == "opencode" else "sonnet"
|
||||
MODEL = os.environ.get("LOOP_MODEL", os.environ.get("UPGRADER_MODEL", _DEFAULT_MODEL))
|
||||
|
||||
CLAUDE_BIN = os.environ.get("CLAUDE_BIN", "claude")
|
||||
@ -173,7 +188,8 @@ def start(mode="use-or-create"):
|
||||
kf.write_text(build_kickoff())
|
||||
|
||||
model_flag = f"--model '{MODEL}'" if MODEL else ""
|
||||
log(f"starting {SESSION} (backend={BACKEND}, model={MODEL}, args='{UPGRADER_ARGS or '<none>'}')")
|
||||
tier_note = f", tier={TIER} ({_tier['label']})" if BACKEND == "opencode" else ""
|
||||
log(f"starting {SESSION} (backend={BACKEND}{tier_note}, model={MODEL}, args='{UPGRADER_ARGS or '<none>'}')")
|
||||
|
||||
if BACKEND == "claude":
|
||||
if not shutil.which(CLAUDE_BIN):
|
||||
@ -201,13 +217,13 @@ def start(mode="use-or-create"):
|
||||
subprocess.run(["tmux", "pipe-pane", "-o", "-t", SESSION,
|
||||
f"cat >> '{LOG_DIR}/{SESSION}.log'"])
|
||||
log(f"started. attach: tmux attach -t {SESSION} log: {LOG_DIR}/{SESSION}.log")
|
||||
# For the opencode backend, spawn a watchdog that auto-resumes the run if the opencode-go
|
||||
# For the opencode backend, spawn a watchdog that auto-resumes the run if the opencode
|
||||
# usage-limit (429) stalls it mid-run (it does NOT self-resume). See watchdog().
|
||||
if BACKEND == "opencode" and os.environ.get("UPGRADER_WATCHDOG", "1") == "1":
|
||||
_spawn_watchdog()
|
||||
|
||||
# ── opencode stall-detect + auto-resume watchdog ────────────────────────────────
|
||||
# The opencode-go subscription enforces a rolling usage-limit (HTTP 429 + retry-after). When it
|
||||
# The opencode subscription (Go or ZEN tier) enforces a rolling usage-limit (HTTP 429 + retry-after). When it
|
||||
# trips mid-run, the `opencode run` agent loop ENDS and does NOT self-resume. This watchdog detects
|
||||
# the stall (the session log stops growing), waits out the limit, and resumes the SAME session —
|
||||
# context preserved — via `opencode run -s <id> --continue`. Standalone: launch-upgrader.py {resume|watchdog}.
|
||||
@ -219,7 +235,7 @@ CHECK_EVERY = int(os.environ.get("UPGRADER_CHECK_SEC", "180")) # watchdog pol
|
||||
# cc-ci-report session with its own marker + resume prompt via these env vars).
|
||||
DONE_MARKER = os.environ.get("UPGRADER_DONE_MARKER", "UPGRADE RUN COMPLETE")
|
||||
RESUME_FILE = os.environ.get("UPGRADER_RESUME_FILE") # optional path to a custom resume prompt
|
||||
GO_ENDPOINT = "https://opencode.ai/zen/go/v1/chat/completions"
|
||||
LIMIT_ENDPOINT = _tier["endpoint"] # tier-aware: zen or go
|
||||
AUTH_JSON = os.path.expanduser("~/.local/share/opencode/auth.json")
|
||||
LOG_FILE = f"{LOG_DIR}/{SESSION}.log"
|
||||
|
||||
@ -279,20 +295,20 @@ def _session_idle_min():
|
||||
# Back-compat alias (older callers / the report watchdog import this name).
|
||||
_log_idle_min = _session_idle_min
|
||||
|
||||
def _go_key():
|
||||
def _limit_key():
|
||||
try:
|
||||
return (_json.load(open(AUTH_JSON)).get("opencode-go") or {}).get("key")
|
||||
return (_json.load(open(AUTH_JSON)).get(_tier["auth"]) or {}).get("key")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _limit_retry_after():
|
||||
"""0 if the opencode-go endpoint is available (HTTP 200); else the 429 retry-after seconds."""
|
||||
key = _go_key()
|
||||
"""0 if the tier's opencode endpoint is available (HTTP 200); else the 429 retry-after seconds."""
|
||||
key = _limit_key()
|
||||
if not key:
|
||||
return 0
|
||||
body = _json.dumps({"model": (MODEL or "").split("/")[-1] or "glm-5.2", "max_tokens": 8,
|
||||
"messages": [{"role": "user", "content": "hi"}]}).encode()
|
||||
req = _ureq.Request(GO_ENDPOINT, data=body, method="POST",
|
||||
req = _ureq.Request(LIMIT_ENDPOINT, data=body, method="POST",
|
||||
headers={"Authorization": "Bearer " + key, "content-type": "application/json"})
|
||||
try:
|
||||
_ureq.urlopen(req, timeout=20).read(); return 0
|
||||
@ -369,7 +385,7 @@ def resume(reason="manual"):
|
||||
kf.write_text(open(RESUME_FILE).read()) # caller-supplied (e.g. the report job)
|
||||
else:
|
||||
kf.write_text(
|
||||
"The opencode-go usage limit has reset (or the run stalled). You were mid-way through the weekly "
|
||||
"The opencode usage limit has reset (or the run stalled). You were mid-way through the weekly "
|
||||
"cc-ci /upgrade-all run. CONTINUE from where you left off — do NOT start over. Process the enrolled "
|
||||
"recipes not yet done this week, alphabetically; SKIP ones already done (their PRs exist — extend, "
|
||||
"never duplicate). Per recipe: run /recipe-upgrade in DEFAULT mode via a subagent, verify with "
|
||||
@ -400,8 +416,8 @@ def _spawn_watchdog():
|
||||
wsess = f"{SESSION}-watchdog"
|
||||
wlog = f"{LOG_DIR}/{SESSION}-watchdog.log"
|
||||
env = {"HOME": os.environ.get("HOME") or os.path.expanduser("~"),
|
||||
"UPGRADER_SESSION": SESSION, "UPGRADER_DIR": WORKDIR, "LOG_DIR": LOG_DIR,
|
||||
"UPGRADER_BACKEND": "opencode", "UPGRADER_MODEL": MODEL,
|
||||
"UPGRADER_SESSION": SESSION, "UPGRADER_DIR": WORKDIR, "LOG_DIR": LOG_DIR,
|
||||
"UPGRADER_BACKEND": "opencode", "UPGRADER_MODEL": MODEL, "LOOP_TIER": TIER,
|
||||
"OPENCODE_BIN": OPENCODE_BIN, "OPENCODE_SERVER": OPENCODE_SERVER,
|
||||
"OPENCODE_SHARE": "1" if OPENCODE_SHARE else "0"}
|
||||
for k in ("UPGRADER_RESUME_FILE", "UPGRADER_DONE_MARKER", "UPGRADER_STALL_MIN", "UPGRADER_CHECK_SEC"):
|
||||
@ -420,7 +436,7 @@ def watchdog():
|
||||
also freezes, so log-idle alone would take 15min to notice a run that died in 5. We detect
|
||||
it directly: no live `opencode run` proc for the session + not completed ⇒ resume promptly.
|
||||
(b) LOG-IDLE — a proc is alive but wedged (no output > STALL_MIN); resume after confirming.
|
||||
Either way, wait out an opencode-go usage-limit (429) first rather than hammering. Exits when the
|
||||
Either way, wait out an opencode usage-limit (429) first rather than hammering. Exits when the
|
||||
model prints DONE_MARKER, or after MAX_RESUMES consecutive resumes fail to get a live proc going
|
||||
(truly broken — hand back to the hourly supervisor / operator). Spawned by start()/resume()."""
|
||||
MAX_RESUMES = int(os.environ.get("UPGRADER_MAX_RESUMES", "20"))
|
||||
@ -485,7 +501,7 @@ def main():
|
||||
shell=True)
|
||||
else:
|
||||
log(f"{SESSION}: stopped")
|
||||
log(f"backend: {BACKEND} model: {MODEL} args: '{UPGRADER_ARGS or '<none>'}'")
|
||||
log(f"backend: {BACKEND} tier: {TIER} model: {MODEL} args: '{UPGRADER_ARGS or '<none>'}'")
|
||||
elif cmd == "attach":
|
||||
os.execvp("tmux", ["tmux", "attach", "-t", SESSION])
|
||||
elif cmd == "resume":
|
||||
@ -504,6 +520,7 @@ def main():
|
||||
launch-upgrader.py watchdog watch + auto-resume the opencode run across usage-limit (429) stalls
|
||||
|
||||
Backend: {BACKEND} (LOOP_BACKEND or UPGRADER_BACKEND env var)
|
||||
Tier: {TIER} (LOOP_TIER or UPGRADER_TIER env var; 'go' or 'zen'; only for opencode)
|
||||
Model: {MODEL} (LOOP_MODEL or UPGRADER_MODEL env var)
|
||||
Args: {UPGRADER_ARGS or '<none>'} (UPGRADER_ARGS env var, passed to /upgrade-all)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user