fix: soft-fail model discovery timeouts with retries
OpenRouter /models list timed out and aborted the whole review. Retry discovery, and if it still fails skip name validation so parallel review can proceed.
This commit is contained in:
1 parent
c6c2d071fb
commit
f3b62481b0
1 file changed
+56
-21
@@ -118,16 +118,19 @@ def secret_value(key_name: str) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def http_json(url: str, headers: dict) -> dict | list:
|
||||
def http_json(url: str, headers: dict, timeout: float = 25) -> dict | list:
|
||||
req = urllib.request.Request(url, headers=headers, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def normalize_protocol(p: str) -> str:
|
||||
p = (p or "").strip().lower()
|
||||
if p not in PROTOCOL_WIRE:
|
||||
die(f"unknown protocol {p!r} (want {' | '.join(PROTOCOLS)})")
|
||||
die(
|
||||
f"unknown protocol {p!r} (want {' | '.join(PROTOCOLS)})",
|
||||
msg_zh=f"未知 protocol {p!r}(可选 {' | '.join(PROTOCOLS)})",
|
||||
)
|
||||
return p
|
||||
|
||||
|
||||
@@ -135,21 +138,33 @@ def wire_protocol(p: str) -> str:
|
||||
return PROTOCOL_WIRE[normalize_protocol(p)]
|
||||
|
||||
|
||||
def discover_model_ids(base_url: str, protocol: str, api_key: str) -> set[str]:
|
||||
def discover_model_ids(base_url: str, protocol: str, api_key: str) -> set[str] | None:
|
||||
"""Return model id set, or None if discovery failed after retries (skip validation)."""
|
||||
import time
|
||||
|
||||
base = base_url.rstrip("/")
|
||||
wire = wire_protocol(protocol)
|
||||
ids: set[str] = set()
|
||||
if wire not in ("openai-completions", "openai-responses", "anthropic"):
|
||||
die(f"unhandled protocol wire {wire!r}")
|
||||
|
||||
last_err: Exception | None = None
|
||||
attempts = int(os.environ.get("GATEKEEP_DISCOVER_RETRIES", "3"))
|
||||
for attempt in range(max(1, attempts)):
|
||||
try:
|
||||
ids: set[str] = set()
|
||||
if wire in ("openai-completions", "openai-responses"):
|
||||
data = http_json(
|
||||
f"{base}/models",
|
||||
{"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
||||
{
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
for m in data.get("data") or []:
|
||||
mid = m.get("id")
|
||||
if mid:
|
||||
ids.add(mid)
|
||||
elif wire == "anthropic":
|
||||
else:
|
||||
data = http_json(
|
||||
f"{base}/v1/models",
|
||||
{
|
||||
@@ -162,14 +177,33 @@ def discover_model_ids(base_url: str, protocol: str, api_key: str) -> set[str]:
|
||||
mid = m.get("id")
|
||||
if mid:
|
||||
ids.add(mid)
|
||||
else:
|
||||
die(f"unhandled protocol wire {wire!r}")
|
||||
return ids
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode("utf-8", errors="replace")[:400]
|
||||
die(f"model discovery failed for {base}: HTTP {e.code} {body}")
|
||||
last_err = RuntimeError(f"HTTP {e.code} {body}")
|
||||
# 401/403 are configuration errors — fail closed
|
||||
if e.code in (401, 403):
|
||||
die(
|
||||
f"model discovery failed for {base}: {last_err}",
|
||||
msg_zh=f"拉取模型列表失败({base}):{last_err} — 请检查 key",
|
||||
)
|
||||
except Exception as e:
|
||||
die(f"model discovery failed for {base}: {e}")
|
||||
return ids
|
||||
last_err = e
|
||||
if attempt + 1 < attempts:
|
||||
sleep_s = 2**attempt
|
||||
print(
|
||||
f"gatekeep: model discovery retry {attempt + 1}/{attempts} "
|
||||
f"after {sleep_s}s ({last_err})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
time.sleep(sleep_s)
|
||||
|
||||
print(
|
||||
f"gatekeep: model discovery failed for {base} after {attempts} tries "
|
||||
f"({last_err}); skipping name validation and continuing",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def enrich_provider(pid: str, pconf: dict) -> dict:
|
||||
@@ -198,6 +232,12 @@ def main() -> None:
|
||||
override_csv = sys.argv[3] if len(sys.argv) > 3 else ""
|
||||
|
||||
cfg = load_raw(cfg_path)
|
||||
# Resolve language early so die() can speak Chinese when configured
|
||||
override_lang = (os.environ.get("GATEKEEP_LANGUAGE") or "").strip()
|
||||
cfg_lang = str(cfg.get("language") or cfg.get("report_language") or "").strip()
|
||||
language = override_lang or cfg_lang or "en"
|
||||
os.environ["GATEKEEP_LANGUAGE"] = language
|
||||
|
||||
providers_in = cfg["providers"]
|
||||
providers: dict[str, dict] = {
|
||||
pid: enrich_provider(pid, pconf) for pid, pconf in providers_in.items()
|
||||
@@ -278,7 +318,7 @@ def main() -> None:
|
||||
),
|
||||
)
|
||||
available = discover_model_ids(pconf["base_url"], pconf["protocol"], api_key)
|
||||
if not available:
|
||||
if available is not None and len(available) == 0:
|
||||
die(
|
||||
f"provider {pid}: model list empty — check base_url/protocol/key",
|
||||
msg_zh=f"供应商 {pid} 模型列表为空 — 请检查 base_url / protocol / key",
|
||||
@@ -289,14 +329,14 @@ def main() -> None:
|
||||
"wire_protocol": wire_protocol(pconf["protocol"]),
|
||||
"key_name": key_name,
|
||||
"api_key": api_key,
|
||||
"available_count": len(available),
|
||||
"_available": available,
|
||||
"available_count": len(available) if available is not None else -1,
|
||||
"_available": available, # None ⇒ skip validation
|
||||
}
|
||||
|
||||
def to_plan_entry(r: dict) -> dict:
|
||||
pid, model = r["provider"], r["model"]
|
||||
avail = provider_meta[pid]["_available"]
|
||||
if model not in avail:
|
||||
if avail is not None and model not in avail:
|
||||
sample = sorted(avail)[:8]
|
||||
die(
|
||||
f"model {model!r} not available on provider {pid} "
|
||||
@@ -324,11 +364,6 @@ def main() -> None:
|
||||
)
|
||||
os.chmod(secrets_path, 0o600)
|
||||
|
||||
# Action input GATEKEEP_LANGUAGE overrides YAML; empty → YAML; missing → en
|
||||
override_lang = (os.environ.get("GATEKEEP_LANGUAGE") or "").strip()
|
||||
cfg_lang = str(cfg.get("language") or cfg.get("report_language") or "").strip()
|
||||
language = override_lang or cfg_lang or "en"
|
||||
|
||||
plan = {
|
||||
"agreement_min": int(cfg.get("agreement_min", 2)),
|
||||
"temperature": cfg.get("temperature", 0),
|
||||
|
||||
Reference in new issue
Block a user