222 lines
7.8 KiB
Python
222 lines
7.8 KiB
Python
import os, sys, tempfile, json, socket, threading
|
|
from pathlib import Path
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
os.environ.clear()
|
|
home = tempfile.mkdtemp(prefix="hermes-104360-")
|
|
os.environ.update(
|
|
HOME=home,
|
|
HERMES_HOME=home + "/.hermes",
|
|
PATH="/usr/bin:/bin",
|
|
PYTHONDONTWRITEBYTECODE="1",
|
|
)
|
|
Path(os.environ["HERMES_HOME"]).mkdir()
|
|
sys.dont_write_bytecode = True
|
|
repo = sys.argv[1]
|
|
sys.path.insert(0, repo)
|
|
original = socket.socket.connect
|
|
blocked = []
|
|
|
|
|
|
def connect(s, address):
|
|
if isinstance(address, tuple) and address[0] not in ["127.0.0.1", "::1"]:
|
|
blocked.append(str(address))
|
|
raise RuntimeError("nonloopback blocked")
|
|
return original(s, address)
|
|
|
|
|
|
socket.socket.connect = connect
|
|
captures = []
|
|
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def log_message(self, *a):
|
|
pass
|
|
|
|
def do_POST(self):
|
|
body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
|
auth = self.headers.get("Authorization")
|
|
status = (
|
|
200
|
|
if auth in ("Bearer fixture-command-token", "Bearer rotated-fixture-token")
|
|
else 401
|
|
)
|
|
captures.append(dict(path=self.path, auth=auth, status=status, body=body))
|
|
data = (
|
|
{
|
|
"id": "fixture",
|
|
"object": "chat.completion",
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"message": {"role": "assistant", "content": "fixture-only"},
|
|
"finish_reason": "stop",
|
|
}
|
|
],
|
|
}
|
|
if status == 200
|
|
else {
|
|
"error": {
|
|
"message": "Credential was not sent or was of an unsupported type",
|
|
"type": "auth_error",
|
|
}
|
|
}
|
|
)
|
|
if self.path.endswith("/messages") and status == 200:
|
|
data = {
|
|
"id": "fixture",
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"model": body["model"],
|
|
"content": [{"type": "text", "text": "fixture-only"}],
|
|
"stop_reason": "end_turn",
|
|
"usage": {"input_tokens": 1, "output_tokens": 1},
|
|
}
|
|
raw = json.dumps(data).encode()
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(raw)))
|
|
self.end_headers()
|
|
self.wfile.write(raw)
|
|
|
|
|
|
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
url = f"http://127.0.0.1:{server.server_port}/serving-endpoints"
|
|
import yaml
|
|
|
|
config = {
|
|
"model": {"provider": "fixture-provider", "default": "model-a"},
|
|
"providers": {
|
|
"fixture-provider": {
|
|
"base_url": url,
|
|
"key_cmd": "/usr/bin/printf fixture-command-token",
|
|
"models": ["model-a", "model-b"],
|
|
}
|
|
},
|
|
"fallback_providers": [{"provider": "fixture-provider", "model": "model-b"}],
|
|
}
|
|
Path(os.environ["HERMES_HOME"] + "/config.yaml").write_text(
|
|
yaml.safe_dump(config), encoding="utf-8"
|
|
)
|
|
from hermes_cli.runtime_provider import resolve_runtime_provider
|
|
from agent.auxiliary_client import resolve_provider_client
|
|
|
|
try:
|
|
from agent.client_lifecycle import _swap_fallback_clients
|
|
except ImportError:
|
|
from agent.chat_completion_helpers import _swap_fallback_clients
|
|
from openai import OpenAI
|
|
from types import SimpleNamespace
|
|
|
|
runtime = resolve_runtime_provider(requested="fixture-provider", target_model="model-a")
|
|
out = {"runtime_key_callable": callable(runtime["api_key"]), "cases": []}
|
|
|
|
|
|
def request(label, client, model):
|
|
try:
|
|
client.chat.completions.create(
|
|
model=model, messages=[{"role": "user", "content": "offline fixture probe"}]
|
|
)
|
|
result = "200"
|
|
except Exception as e:
|
|
result = type(e).__name__ + ": " + str(e)
|
|
out["cases"].append({
|
|
"label": label,
|
|
"result": result,
|
|
"sdk_key": client.api_key,
|
|
"sdk_provider_callable": callable(getattr(client, "_api_key_provider", None)),
|
|
})
|
|
|
|
|
|
direct = OpenAI(api_key=runtime["api_key"], base_url=runtime["base_url"], max_retries=0)
|
|
request("direct-runtime", direct, "model-a")
|
|
fb, _ = resolve_provider_client("fixture-provider", model="model-b", raw_codex=True)
|
|
out["fresh_fallback_sdk_key"] = fb.api_key
|
|
out["fresh_fallback_provider_callable"] = callable(
|
|
getattr(fb, "_api_key_provider", None)
|
|
)
|
|
agent = SimpleNamespace()
|
|
_swap_fallback_clients(
|
|
agent, fb, "fixture-provider", "model-b", str(fb.base_url), "chat_completions"
|
|
)
|
|
out["saved_rebuild_key_callable"] = callable(agent._client_kwargs["api_key"])
|
|
request("bare-fallback-first-request", agent.client, "model-b")
|
|
rebuilt = OpenAI(**agent._client_kwargs, max_retries=0)
|
|
request("fallback-rebuilt-from-production-kwargs", rebuilt, "model-b")
|
|
# Configured timeout forces the production swap to rebuild before its first request.
|
|
config["providers"]["fixture-provider"]["request_timeout_seconds"] = 15
|
|
Path(os.environ["HERMES_HOME"] + "/config.yaml").write_text(
|
|
yaml.safe_dump(config), encoding="utf-8"
|
|
)
|
|
from hermes_cli.config import load_config_readonly
|
|
from hermes_cli.timeouts import get_provider_request_timeout
|
|
|
|
out["timeout_resolved"] = get_provider_request_timeout("fixture-provider", "model-b")
|
|
fb2, _ = resolve_provider_client("fixture-provider", model="model-b", raw_codex=True)
|
|
from agent.client_lifecycle import ClientLifecycleMixin
|
|
|
|
|
|
class ProbeAgent(ClientLifecycleMixin):
|
|
pass
|
|
|
|
|
|
a2 = ProbeAgent()
|
|
a2.provider = "fixture-provider"
|
|
a2.model = "model-b"
|
|
a2.base_url = str(fb2.base_url)
|
|
_swap_fallback_clients(
|
|
a2, fb2, "fixture-provider", "model-b", str(fb2.base_url), "chat_completions"
|
|
)
|
|
request("timeout-fallback-first-request", a2.client, "model-b")
|
|
# The rebuild must retain the live source, not just the most recent minted key.
|
|
token_path = Path(home) / "rotating-token"
|
|
token_path.write_text("fixture-command-token")
|
|
for dynamic in (False, True):
|
|
source = (lambda: token_path.read_text()) if dynamic else "fixture-command-token"
|
|
fresh = OpenAI(api_key=source, base_url=url, max_retries=0)
|
|
holder = SimpleNamespace()
|
|
_swap_fallback_clients(
|
|
holder, fresh, "fixture-static", "model-b", url, "codex_responses"
|
|
)
|
|
derived = OpenAI(**holder._client_kwargs, max_retries=0)
|
|
for token in ("fixture-command-token", "rotated-fixture-token"):
|
|
token_path.write_text(token)
|
|
request(f"rotation-{dynamic}-{token}", derived, "model-b")
|
|
assert captures[-1]["auth"] == "Bearer " + (
|
|
token if dynamic else "fixture-command-token"
|
|
)
|
|
fresh.close()
|
|
derived.close()
|
|
token_path.write_text("fixture-command-token")
|
|
source = lambda: token_path.read_text()
|
|
fresh = OpenAI(api_key=source, base_url=url, max_retries=0)
|
|
holder = SimpleNamespace()
|
|
_swap_fallback_clients(holder, fresh, "anthropic", "model-b", url, "anthropic_messages")
|
|
for token in ("fixture-command-token", "rotated-fixture-token"):
|
|
token_path.write_text(token)
|
|
response = holder._anthropic_client.messages.create(
|
|
model="model-b", max_tokens=8, messages=[{"role": "user", "content": "fixture"}]
|
|
)
|
|
assert response.content[0].text == "fixture-only"
|
|
assert captures[-1]["auth"] == "Bearer " + token
|
|
out["anthropic_rotating_handoff"] = "200,200"
|
|
fresh.close()
|
|
holder._anthropic_client.close()
|
|
out["captures"] = captures
|
|
out["blocked"] = blocked
|
|
out["production_imports"] = {
|
|
n: sys.modules[n].__file__
|
|
for n in [
|
|
"hermes_cli.runtime_provider",
|
|
"agent.auxiliary_client",
|
|
"agent.client_lifecycle",
|
|
]
|
|
}
|
|
print(json.dumps(out, indent=2))
|
|
server.shutdown()
|
|
assert captures[0]["status"] == 200
|
|
assert captures[1]["status"] == 200
|
|
assert captures[2]["status"] == 200
|
|
assert captures[3]["status"] == 200
|