Files
cleveragents-core/robot/e2e/check_openai_key.py
T
hurui200320 e2b127b7e5
CI / lint (pull_request) Successful in 37s
CI / typecheck (pull_request) Successful in 1m18s
CI / security (pull_request) Successful in 56s
CI / quality (pull_request) Successful in 48s
CI / build (pull_request) Successful in 30s
CI / helm (pull_request) Successful in 41s
CI / push-validation (pull_request) Successful in 28s
CI / integration_tests (pull_request) Successful in 4m32s
CI / e2e_tests (pull_request) Successful in 4m42s
CI / coverage (pull_request) Successful in 13m24s
CI / unit_tests (pull_request) Successful in 3m13s
CI / docker (pull_request) Successful in 1m36s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-regression (push) Failing after 0s
CI / benchmark-publish (push) Failing after 0s
CI / push-validation (push) Successful in 12s
CI / build (push) Successful in 15s
CI / helm (push) Successful in 16s
CI / lint (push) Successful in 43s
CI / typecheck (push) Successful in 51s
CI / security (push) Successful in 51s
CI / e2e_tests (push) Successful in 2m14s
CI / quality (push) Successful in 3m44s
CI / integration_tests (push) Successful in 7m0s
CI / unit_tests (push) Successful in 8m33s
CI / coverage (push) Successful in 6m21s
CI / docker (push) Successful in 1m31s
CI / status-check (push) Successful in 2s
fix(e2e): replace naive OpenAI key-presence check with live API probe in E2E suite setups
The existing actor-selection logic in several E2E suite setups checked only
whether OPENAI_API_KEY was present (non-empty). A valid key that has hit its
quota limit passes that check but fails at runtime with HTTP 429, causing the
test to fail even though Anthropic credits are available.

Changes:
- Add robot/e2e/check_openai_key.py: stdlib-only (urllib.request) script that
  sends a minimal chat-completion request ('Hi', max_tokens=1, gpt-4o-mini) to
  the OpenAI API. Exits 0 on HTTP 200; exits 1 for quota (429), auth (401),
  network errors, or any other failure.
- Add 'Resolve LLM Actor' keyword to robot/e2e/common_e2e.resource: runs the
  probe script via ${PYTHON} and returns the openai_model argument (default
  openai/gpt-4o) on success, or the anthropic_model argument (default
  anthropic/claude-sonnet-4-20250514) on failure. Skips the probe entirely when
  OPENAI_API_KEY is not set.
- Update m6_acceptance.robot, wf04_multi_project.robot, wf05_db_migration.robot,
  wf07_cicd.robot, and wf16_devcontainer.robot to use 'Resolve LLM Actor'
  instead of the inline has_openai boolean check.

No production source code (src/) is modified. The decision to fall back to
Anthropic is made once per suite setup, before any test runs.

Closes #10198
2026-04-17 18:00:47 +08:00

97 lines
2.9 KiB
Python

"""Probe the OpenAI API to verify that the key is functional.
This script is used by the E2E test suite setup (via ``Resolve LLM Actor``
in ``common_e2e.resource``) to decide which LLM actor to use before any test
runs. It replaces the naive key-presence check that would select the OpenAI
actor even when the key exists but is quota-exhausted.
Usage::
python check_openai_key.py
Exit codes:
0 — OpenAI API returned HTTP 200; key is functional.
1 — Key is missing, quota-exhausted (HTTP 429), unauthorised (HTTP 401),
or any other error occurred; caller should fall back to Anthropic.
The script uses only Python standard-library modules (``urllib.request``,
``json``, ``os``) — no third-party dependencies are required.
The probe sends the cheapest possible request:
model: gpt-4o-mini
messages: [{"role": "user", "content": "Hi"}]
max_tokens: 1
This costs a fraction of a cent and adds < 5 s to suite setup time.
"""
from __future__ import annotations
import contextlib
import json
import os
import urllib.error
import urllib.request
_OPENAI_URL = "https://api.openai.com/v1/chat/completions"
_PROBE_MODEL = "gpt-4o-mini"
_TIMEOUT_SECONDS = 15
def _probe(api_key: str) -> tuple[bool, str]:
"""Send a minimal chat-completion request to the OpenAI API.
Returns ``(True, "ok")`` when the API responds with HTTP 200.
Returns ``(False, reason)`` for any other outcome.
"""
payload = json.dumps(
{
"model": _PROBE_MODEL,
"messages": [{"role": "user", "content": "Hi"}],
"max_tokens": 1,
}
).encode()
req = urllib.request.Request(
_OPENAI_URL,
data=payload,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=_TIMEOUT_SECONDS) as resp:
if resp.status == 200:
return True, "ok"
# Unexpected non-200 success-range status
return False, f"unexpected HTTP {resp.status}"
except urllib.error.HTTPError as exc:
body = ""
with contextlib.suppress(Exception):
body = exc.read().decode(errors="replace")
return False, f"HTTP {exc.code}: {body[:200]}"
except urllib.error.URLError as exc:
return False, f"network error: {exc.reason}"
except TimeoutError:
return False, f"timed out after {_TIMEOUT_SECONDS}s"
except Exception as exc:
return False, f"unexpected error: {exc}"
def main() -> int:
"""Entrypoint. Returns the process exit code."""
api_key = os.environ.get("OPENAI_API_KEY", "")
if not api_key:
print("OPENAI_API_KEY is not set")
return 1
ok, reason = _probe(api_key)
print(reason)
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())