7166071fca
The compression/aux fallback ladder decides "payment error" via _is_payment_error(), which only knew the spaced "resource exhausted". NVIDIA NIM and gRPC-style wrappers serialize the same quota signal as ResourceExhausted / RESOURCE_EXHAUSTED / resource-exhausted, so a 403/429/status-less body carrying it re-raised instead of walking the configured fallback_chain, and compression fell to the lossy emergency path. Add the three separator variants to _PAYMENT_KEYWORDS (same status gate, no broader "exhausted" matching). evals/auxiliary_resource_exhausted.py drives the real call_llm/async_call_llm against two local OpenAI-compatible listeners (nvidia profile primary, named custom fallback) for the before/after check. #85649
111 lines
5.5 KiB
Python
111 lines
5.5 KiB
Python
"""Local HTTP contract probe; no vendor request or personal Hermes state.
|
|
|
|
Run from the repository with its Python interpreter. JSON output identifies the
|
|
loaded module, observed SDK error, request order, and preserved message payload.
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--spelling", default="ResourceExhausted")
|
|
parser.add_argument("--mode", choices=("sync", "async", "sse"), default="sync")
|
|
parser.add_argument("--status", type=int, default=403)
|
|
args = parser.parse_args()
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
requests = []
|
|
message = f"{args.spelling}: Worker local total request limit reached (32/32)"
|
|
messages = [{"role": "user", "content": "Summarize: keep the deployment decision."}]
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def log_message(self, *args):
|
|
pass
|
|
|
|
def do_POST(self):
|
|
body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
|
if "messages" not in body: # capability probes (/api/show) are not chat requests
|
|
self.send_response(404)
|
|
self.end_headers()
|
|
return
|
|
requests.append(body)
|
|
primary = body["model"] == "primary-probe"
|
|
if primary and args.mode == "sse":
|
|
data = 'data: ' + json.dumps({"error": {"message": message}}) + '\n\ndata: [DONE]\n\n'
|
|
status, kind = 200, "text/event-stream"
|
|
elif primary:
|
|
data = json.dumps({"error": {"message": message, "type": "provider_error"}})
|
|
status, kind = args.status, "application/json"
|
|
else:
|
|
data = json.dumps({"id": "local-summary", "object": "chat.completion", "created": 0,
|
|
"model": body["model"], "choices": [{"index": 0, "finish_reason": "stop",
|
|
"message": {"role": "assistant", "content": "Deployment decision preserved."}}]})
|
|
status, kind = 200, "application/json"
|
|
encoded = data.encode()
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", kind)
|
|
self.send_header("Content-Length", str(len(encoded)))
|
|
self.end_headers()
|
|
self.wfile.write(encoded)
|
|
|
|
with tempfile.TemporaryDirectory(prefix="aux-resource-") as home:
|
|
os.environ["HOME"] = home
|
|
os.environ["HERMES_HOME"] = home
|
|
# Two listeners: a payment/quota error is credential-wide, so the fallback must be a
|
|
# different backend identity (distinct base_url) exactly as NIM -> OpenRouter is in the field.
|
|
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
fallback_server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
fallback_thread = threading.Thread(target=fallback_server.serve_forever, daemon=True)
|
|
thread.start()
|
|
fallback_thread.start()
|
|
base_url = f"http://127.0.0.1:{server.server_port}/v1"
|
|
fallback_url = f"http://127.0.0.1:{fallback_server.server_port}/v1"
|
|
# The primary is the real ``nvidia`` profile pointed at the local listener; the fallback is a
|
|
# named custom provider (its own credential label, like a second vendor in the field).
|
|
config = {"model": {"provider": "local-fallback", "model": "fallback-probe"},
|
|
"providers": {"local-fallback": {"base_url": fallback_url, "api_key": "local-probe"}},
|
|
"auxiliary": {"compression": {"provider": "nvidia", "model": "primary-probe",
|
|
"base_url": base_url, "api_key": "local-probe", "api_mode": "chat_completions",
|
|
"fallback_chain": [{"provider": "local-fallback", "model": "fallback-probe"}]}}}
|
|
# JSON is valid YAML and keeps this standalone probe dependency-free.
|
|
Path(home, "config.yaml").write_text(json.dumps(config), encoding="utf-8")
|
|
import logging
|
|
if os.environ.get("AUX_PROBE_DEBUG"):
|
|
logging.basicConfig(level=logging.DEBUG, stream=sys.stderr)
|
|
import agent.auxiliary_client as aux
|
|
|
|
result = {"module": aux.__file__, "mode": args.mode, "status": args.status,
|
|
"spelling": args.spelling, "surface": "local HTTP contract, not live provider"}
|
|
try:
|
|
if args.mode == "async":
|
|
response = asyncio.run(aux.async_call_llm(task="compression", messages=messages, max_tokens=64))
|
|
else:
|
|
with aux.aux_progress_hook((lambda: None) if args.mode == "sse" else None):
|
|
response = aux.call_llm(task="compression", messages=messages, max_tokens=64)
|
|
result["content"] = response.choices[0].message.content
|
|
except Exception as exc:
|
|
result["error"] = type(exc).__name__
|
|
result["error_status"] = getattr(exc, "status_code", None)
|
|
result["error_message"] = str(exc)
|
|
finally:
|
|
for srv, thr in ((server, thread), (fallback_server, fallback_thread)):
|
|
srv.shutdown()
|
|
srv.server_close()
|
|
thr.join()
|
|
result["models"] = [request["model"] for request in requests]
|
|
result["messages_preserved"] = all(request["messages"] == messages for request in requests)
|
|
print(json.dumps(result))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|