feat: Update docker-compose configuration to use environment variables for sensitive data, enhance testing documentation with new LaTeX files for capacity planning, and refine README for clarity on compilation and testing processes.
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Mock OpenAI-compatible para ST-EXT-01 (cola ARQ sin coste LLM real).
|
||||
|
||||
Endpoints:
|
||||
GET /v1/models
|
||||
POST /v1/chat/completions
|
||||
|
||||
Responde JSON fijo con delay configurable (default 50 ms).
|
||||
|
||||
Ejemplo:
|
||||
python docs/plan_pruebas/scripts/llm_mock_server.py --port 8099 --delay-ms 50
|
||||
|
||||
Apuntar extraction/workers (Docker):
|
||||
EXTRACTOR_BASE_URL=http://host.docker.internal:8099/v1
|
||||
NORMALIZER_BASE_URL=http://host.docker.internal:8099/v1
|
||||
EXTRACTOR_API_KEY=mock-key
|
||||
ASYNC_QUEUE=true
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
MOCK_SCHEMA = {
|
||||
"numero_factura": "str",
|
||||
"fecha": "str",
|
||||
"nit_prestador": "str",
|
||||
"razon_social": "str",
|
||||
"valor_total": "number",
|
||||
}
|
||||
|
||||
MOCK_EXTRACTION = {
|
||||
"numero_factura": "MOCK-0001",
|
||||
"fecha": "2026-01-15",
|
||||
"nit_prestador": "900000000",
|
||||
"razon_social": "IPS MOCK ST-EXT-01",
|
||||
"valor_total": 1000,
|
||||
}
|
||||
|
||||
|
||||
class Stats:
|
||||
def __init__(self) -> None:
|
||||
self.lock = threading.Lock()
|
||||
self.completions = 0
|
||||
self.models = 0
|
||||
|
||||
def inc(self, kind: str) -> None:
|
||||
with self.lock:
|
||||
if kind == "completions":
|
||||
self.completions += 1
|
||||
else:
|
||||
self.models += 1
|
||||
|
||||
|
||||
STATS = Stats()
|
||||
|
||||
|
||||
def make_handler(delay_ms: float):
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, fmt: str, *args) -> None: # quieter
|
||||
pass
|
||||
|
||||
def _json(self, code: int, payload: dict) -> None:
|
||||
body = json.dumps(payload).encode("utf-8")
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
path = urlparse(self.path).path.rstrip("/")
|
||||
if path in ("/v1/models", "/models"):
|
||||
STATS.inc("models")
|
||||
self._json(
|
||||
200,
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{"id": "mock-llm", "object": "model", "owned_by": "st-ext-01"},
|
||||
{"id": "gpt-5.4-mini", "object": "model", "owned_by": "st-ext-01"},
|
||||
],
|
||||
},
|
||||
)
|
||||
return
|
||||
if path in ("/health", "/v1/health"):
|
||||
self._json(200, {"status": "ok", "completions": STATS.completions})
|
||||
return
|
||||
self._json(404, {"error": {"message": f"not found: {path}", "type": "not_found"}})
|
||||
|
||||
def do_POST(self) -> None: # noqa: N802
|
||||
path = urlparse(self.path).path.rstrip("/")
|
||||
length = int(self.headers.get("Content-Length") or 0)
|
||||
raw = self.rfile.read(length) if length else b"{}"
|
||||
try:
|
||||
req = json.loads(raw.decode("utf-8") or "{}")
|
||||
except Exception:
|
||||
req = {}
|
||||
|
||||
if path not in ("/v1/chat/completions", "/chat/completions"):
|
||||
self._json(404, {"error": {"message": f"not found: {path}", "type": "not_found"}})
|
||||
return
|
||||
|
||||
if delay_ms > 0:
|
||||
time.sleep(delay_ms / 1000.0)
|
||||
|
||||
# Heurística: schema discovery suele pedir "campos" / schema; extracción pide factura.
|
||||
messages = req.get("messages") or []
|
||||
blob = json.dumps(messages, ensure_ascii=False).lower()
|
||||
if "schema" in blob or "campos" in blob or "field" in blob or "descub" in blob:
|
||||
content = json.dumps(MOCK_SCHEMA, ensure_ascii=False)
|
||||
else:
|
||||
content = json.dumps(MOCK_EXTRACTION, ensure_ascii=False)
|
||||
|
||||
model = req.get("model") or "mock-llm"
|
||||
STATS.inc("completions")
|
||||
self._json(
|
||||
200,
|
||||
{
|
||||
"id": f"chatcmpl-mock-{STATS.completions}",
|
||||
"object": "chat.completion",
|
||||
"created": int(time.time()),
|
||||
"model": model,
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {"role": "assistant", "content": content},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 100,
|
||||
"completion_tokens": 50,
|
||||
"total_tokens": 150,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return Handler
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser(description="Mock OpenAI API for ST-EXT-01")
|
||||
p.add_argument("--host", default="0.0.0.0")
|
||||
p.add_argument("--port", type=int, default=int(__import__("os").environ.get("LLM_MOCK_PORT", "8099")))
|
||||
p.add_argument("--delay-ms", type=float, default=float(__import__("os").environ.get("LLM_MOCK_DELAY_MS", "50")))
|
||||
args = p.parse_args()
|
||||
|
||||
httpd = ThreadingHTTPServer((args.host, args.port), make_handler(args.delay_ms))
|
||||
print(f"[llm-mock] http://{args.host}:{args.port}/v1 delay={args.delay_ms}ms")
|
||||
print(f"[llm-mock] health: http://127.0.0.1:{args.port}/health")
|
||||
print("[llm-mock] Ctrl+C para detener")
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\n[llm-mock] stopped")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user