feat(llm): classify 401 as fatal+actionable, 400 as skip-this-model
Browse files- src/llm/explainer.py +20 -0
src/llm/explainer.py
CHANGED
|
@@ -302,6 +302,26 @@ def _llm_explain(payload: ExplainPayload, modality: str = "bbb") -> tuple[str, s
|
|
| 302 |
continue
|
| 303 |
except APIStatusError as e:
|
| 304 |
status = getattr(e, "status_code", None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
# 402 credits / 403 access / 404 retired-id / 5xx upstream → next.
|
| 306 |
if status in (402, 403, 404) or (status is not None and 500 <= status < 600):
|
| 307 |
logger.info("OpenRouter %s on %s; advancing to next free model.", status, model)
|
|
|
|
| 302 |
continue
|
| 303 |
except APIStatusError as e:
|
| 304 |
status = getattr(e, "status_code", None)
|
| 305 |
+
# 401 = unauthorized — the key is bad, no model in this chain
|
| 306 |
+
# will succeed. Surface a loud, actionable hint and bail.
|
| 307 |
+
if status == 401:
|
| 308 |
+
logger.warning(
|
| 309 |
+
"OpenRouter 401 unauthorized on %s. The OPENROUTER_API_KEY "
|
| 310 |
+
"is rejected — verify it is current at "
|
| 311 |
+
"https://openrouter.ai/keys and that free-model data-sharing "
|
| 312 |
+
"is enabled at https://openrouter.ai/settings/privacy. "
|
| 313 |
+
"Falling back to deterministic template.",
|
| 314 |
+
model,
|
| 315 |
+
)
|
| 316 |
+
return None
|
| 317 |
+
# 400 = malformed prompt for this specific model (e.g. it
|
| 318 |
+
# rejected our system role). Skip this model, try the next.
|
| 319 |
+
if status == 400:
|
| 320 |
+
logger.info(
|
| 321 |
+
"OpenRouter 400 on %s (likely prompt-shape mismatch); "
|
| 322 |
+
"advancing to next free model.", model,
|
| 323 |
+
)
|
| 324 |
+
continue
|
| 325 |
# 402 credits / 403 access / 404 retired-id / 5xx upstream → next.
|
| 326 |
if status in (402, 403, 404) or (status is not None and 500 <= status < 600):
|
| 327 |
logger.info("OpenRouter %s on %s; advancing to next free model.", status, model)
|