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
+79
-44
@@ -118,16 +118,19 @@ def secret_value(key_name: str) -> str:
|
|||||||
return ""
|
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")
|
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"))
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def normalize_protocol(p: str) -> str:
|
def normalize_protocol(p: str) -> str:
|
||||||
p = (p or "").strip().lower()
|
p = (p or "").strip().lower()
|
||||||
if p not in PROTOCOL_WIRE:
|
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
|
return p
|
||||||
|
|
||||||
|
|
||||||
@@ -135,41 +138,72 @@ def wire_protocol(p: str) -> str:
|
|||||||
return PROTOCOL_WIRE[normalize_protocol(p)]
|
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("/")
|
base = base_url.rstrip("/")
|
||||||
wire = wire_protocol(protocol)
|
wire = wire_protocol(protocol)
|
||||||
ids: set[str] = set()
|
if wire not in ("openai-completions", "openai-responses", "anthropic"):
|
||||||
try:
|
die(f"unhandled protocol wire {wire!r}")
|
||||||
if wire in ("openai-completions", "openai-responses"):
|
|
||||||
data = http_json(
|
last_err: Exception | None = None
|
||||||
f"{base}/models",
|
attempts = int(os.environ.get("GATEKEEP_DISCOVER_RETRIES", "3"))
|
||||||
{"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
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",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
for m in data.get("data") or []:
|
||||||
|
mid = m.get("id")
|
||||||
|
if mid:
|
||||||
|
ids.add(mid)
|
||||||
|
else:
|
||||||
|
data = http_json(
|
||||||
|
f"{base}/v1/models",
|
||||||
|
{
|
||||||
|
"x-api-key": api_key,
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
for m in data.get("data") or []:
|
||||||
|
mid = m.get("id")
|
||||||
|
if mid:
|
||||||
|
ids.add(mid)
|
||||||
|
return ids
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
body = e.read().decode("utf-8", errors="replace")[:400]
|
||||||
|
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:
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
for m in data.get("data") or []:
|
time.sleep(sleep_s)
|
||||||
mid = m.get("id")
|
|
||||||
if mid:
|
print(
|
||||||
ids.add(mid)
|
f"gatekeep: model discovery failed for {base} after {attempts} tries "
|
||||||
elif wire == "anthropic":
|
f"({last_err}); skipping name validation and continuing",
|
||||||
data = http_json(
|
file=sys.stderr,
|
||||||
f"{base}/v1/models",
|
)
|
||||||
{
|
return None
|
||||||
"x-api-key": api_key,
|
|
||||||
"anthropic-version": "2023-06-01",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
for m in data.get("data") or []:
|
|
||||||
mid = m.get("id")
|
|
||||||
if mid:
|
|
||||||
ids.add(mid)
|
|
||||||
else:
|
|
||||||
die(f"unhandled protocol wire {wire!r}")
|
|
||||||
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}")
|
|
||||||
except Exception as e:
|
|
||||||
die(f"model discovery failed for {base}: {e}")
|
|
||||||
return ids
|
|
||||||
|
|
||||||
|
|
||||||
def enrich_provider(pid: str, pconf: dict) -> dict:
|
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 ""
|
override_csv = sys.argv[3] if len(sys.argv) > 3 else ""
|
||||||
|
|
||||||
cfg = load_raw(cfg_path)
|
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_in = cfg["providers"]
|
||||||
providers: dict[str, dict] = {
|
providers: dict[str, dict] = {
|
||||||
pid: enrich_provider(pid, pconf) for pid, pconf in providers_in.items()
|
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)
|
available = discover_model_ids(pconf["base_url"], pconf["protocol"], api_key)
|
||||||
if not available:
|
if available is not None and len(available) == 0:
|
||||||
die(
|
die(
|
||||||
f"provider {pid}: model list empty — check base_url/protocol/key",
|
f"provider {pid}: model list empty — check base_url/protocol/key",
|
||||||
msg_zh=f"供应商 {pid} 模型列表为空 — 请检查 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"]),
|
"wire_protocol": wire_protocol(pconf["protocol"]),
|
||||||
"key_name": key_name,
|
"key_name": key_name,
|
||||||
"api_key": api_key,
|
"api_key": api_key,
|
||||||
"available_count": len(available),
|
"available_count": len(available) if available is not None else -1,
|
||||||
"_available": available,
|
"_available": available, # None ⇒ skip validation
|
||||||
}
|
}
|
||||||
|
|
||||||
def to_plan_entry(r: dict) -> dict:
|
def to_plan_entry(r: dict) -> dict:
|
||||||
pid, model = r["provider"], r["model"]
|
pid, model = r["provider"], r["model"]
|
||||||
avail = provider_meta[pid]["_available"]
|
avail = provider_meta[pid]["_available"]
|
||||||
if model not in avail:
|
if avail is not None and model not in avail:
|
||||||
sample = sorted(avail)[:8]
|
sample = sorted(avail)[:8]
|
||||||
die(
|
die(
|
||||||
f"model {model!r} not available on provider {pid} "
|
f"model {model!r} not available on provider {pid} "
|
||||||
@@ -324,11 +364,6 @@ def main() -> None:
|
|||||||
)
|
)
|
||||||
os.chmod(secrets_path, 0o600)
|
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 = {
|
plan = {
|
||||||
"agreement_min": int(cfg.get("agreement_min", 2)),
|
"agreement_min": int(cfg.get("agreement_min", 2)),
|
||||||
"temperature": cfg.get("temperature", 0),
|
"temperature": cfg.get("temperature", 0),
|
||||||
|
|||||||
Reference in new issue
Block a user