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
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
97 lines
2.9 KiB
Python
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())
|