# Judge-side think-block patcher. Stands between Langfuse evaluators and the # headroom-ollama hub (port 8790). Local Ollama does not emit the `signature` # field that Langfuse's Anthropic adapter's Zod schema requires on every # `thinking` content block — without it, the evaluator preflight fails as # "Invalid JSON response". The proxy forwards /v1/* verbatim and adds a dummy # signature to each thinking block before returning. apiVersion: v1 kind: ConfigMap metadata: name: judge-proxy namespace: pragent data: proxy.py: | #!/usr/bin/env python3 """Judge proxy: forward to headroom-ollama, fix thinking blocks.""" import json, sys, urllib.request, urllib.error from http.server import BaseHTTPRequestHandler, HTTPServer from socketserver import ThreadingMixIn UPSTREAM = "http://100.74.17.70:8790" DUMMY_SIG = "kimi-local-judge-no-signature" class H(BaseHTTPRequestHandler): def _proxy(self): n = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(n) if n else b"" h = {k: v for k, v in self.headers.items() if k.lower() not in ("host", "content-length")} req = urllib.request.Request(UPSTREAM + self.path, data=body, headers=h, method=self.command) try: with urllib.request.urlopen(req, timeout=120) as r: resp_body = r.read(); status = r.status; rh = dict(r.headers) except urllib.error.HTTPError as e: resp_body = e.read(); status = e.code; rh = dict(e.headers) ct = rh.get("content-type", "") if status == 200 and "application/json" in ct and self.path.startswith("/v1/messages"): try: obj = json.loads(resp_body) patched = 0 for blk in obj.get("content") or []: if isinstance(blk, dict) and blk.get("type") == "thinking" and "signature" not in blk: blk["signature"] = DUMMY_SIG; patched += 1 if patched: resp_body = json.dumps(obj).encode("utf-8") rh["content-length"] = str(len(resp_body)) print(f"judge-proxy: patched {patched} thinking block(s)", file=sys.stderr, flush=True) except Exception as e: print(f"judge-proxy: patch failed: {e}", file=sys.stderr, flush=True) self.send_response(status) for k, v in rh.items(): if k.lower() not in ("transfer-encoding", "content-length", "connection"): self.send_header(k, v) self.send_header("Content-Length", str(len(resp_body))) self.end_headers(); self.wfile.write(resp_body) def do_POST(self): self._proxy() def do_GET(self): self._proxy() def log_message(self, *a, **k): pass class S(ThreadingMixIn, HTTPServer): daemon_threads = True S(("0.0.0.0", 8802), H).serve_forever() --- apiVersion: v1 kind: Pod metadata: name: judge-proxy namespace: pragent labels: app: judge-proxy spec: nodeSelector: kubernetes.io/hostname: kubernets hostNetwork: true dnsPolicy: ClusterFirstWithHostNet restartPolicy: Always containers: - name: p image: python:3.12-alpine command: ["sh","-c","apk add --no-cache ca-certificates >/dev/null && python3 -u /etc/cfg/proxy.py"] volumeMounts: - {name: cfg, mountPath: /etc/cfg} ports: - {containerPort: 8802, hostPort: 8802} volumes: - name: cfg configMap: name: judge-proxy