mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-09 04:06:42 +00:00
A coding agent ships aimed at its vendor's API, so on a host that also runs
svc-ai-litellm the deployment pays twice: the gateway it already operates sits
unused, and the workstation's key leaves the machine to reach a third party.
claude, pi and the new codex role now resolve the gateway the same way.
An explicit services.<agent>.gateway_url wins, because a workstation whose
gateway lives in another inventory carries the address by hand. Otherwise the
loopback URL applies, and only while LITELLM_USABLE. agnt_<agent>_configured
requires a url and a key together and gates the write, so an agent this
deployment cannot point anywhere is left alone rather than half configured.
codex needs its own role rather than a fourth branch in an existing one: it
reads its key from an environment variable instead of its config file, so it
writes config.toml plus an environment.d entry, where claude and pi merge into
a JSON file the user may already keep.
The three agents share one probe. It matches the written config as text rather
than parsing it, because the three write TOML, JSON and INI while the question
asked of all three is the same one, whether the file names this deployment's
gateway. It then asks for a completion with the key the role wrote and requires
content in the answer, since an empty completion reads as success to any caller
that only checks the status code.
utils/once/flag.yml moves to the first task in claude and codex. Registering
claude and codex as shared services makes the roles loadable through
load/app.yml, which re-enters a role whose run_once flag is not yet set.
Verified: 396 lints with 363 subtests, shellcheck on the four scripts, ruff on
the probe. Not verified: no config has been rendered and no agent has yet
reached a gateway, which is also the first exercise of the host branch added in
95bf5898a8.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
84 lines
2.7 KiB
Python
84 lines
2.7 KiB
Python
"""Check that a workstation agent's configured gateway actually answers it.
|
|
|
|
Env (rendered into test.env from the staging role's templates/test.env.j2):
|
|
GATEWAY_CONFIG_PATH the config file the role writes
|
|
GATEWAY_URL the OpenAI-compatible base URL the agent was pointed at
|
|
GATEWAY_KEY the key the agent presents
|
|
GATEWAY_MODEL the alias the agent asks for
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
TIMEOUT = 300
|
|
PROMPT = "Reply with the single word pong."
|
|
|
|
|
|
def failures() -> list[str]:
|
|
config_path = Path(os.environ["GATEWAY_CONFIG_PATH"])
|
|
url = os.environ["GATEWAY_URL"].rstrip("/")
|
|
model = os.environ["GATEWAY_MODEL"]
|
|
found: list[str] = []
|
|
|
|
if not config_path.is_file():
|
|
return [
|
|
f"{config_path} does not exist, so the agent was never pointed anywhere"
|
|
]
|
|
|
|
written = config_path.read_text(encoding="utf-8")
|
|
if url not in written:
|
|
found.append(
|
|
f"{config_path} does not name {url}, so the agent would reach the vendor API"
|
|
)
|
|
|
|
payload = json.dumps(
|
|
{
|
|
"model": model,
|
|
"messages": [{"role": "user", "content": PROMPT}],
|
|
"max_tokens": 16,
|
|
}
|
|
).encode()
|
|
request = urllib.request.Request( # noqa: S310 the URL is the loopback gateway this deployment published
|
|
f"{url}/chat/completions",
|
|
data=payload,
|
|
headers={
|
|
"Authorization": f"Bearer {os.environ['GATEWAY_KEY']}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
method="POST",
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=TIMEOUT) as response: # noqa: S310 loopback gateway
|
|
body = json.loads(response.read())
|
|
except urllib.error.HTTPError as error:
|
|
return [*found, f"the gateway refused the agent's key with HTTP {error.code}"]
|
|
except (urllib.error.URLError, TimeoutError) as error:
|
|
return [*found, f"the gateway at {url} could not be reached: {error}"]
|
|
|
|
choices = body.get("choices") or [{}]
|
|
content = ((choices[0].get("message") or {}).get("content") or "").strip()
|
|
if not content:
|
|
found.append(f"{model} answered through the gateway without any content")
|
|
else:
|
|
print(f"[OK] {model} answered through {url}: {content[:40]}")
|
|
return found
|
|
|
|
|
|
def main() -> int:
|
|
found = failures()
|
|
for failure in found:
|
|
print(f"[FAIL] {failure}", file=sys.stderr)
|
|
if not found:
|
|
print("[OK] the agent reaches the gateway and gets an answer")
|
|
return 1 if found else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|