@miller-tech/uap 1.84.0 → 1.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.84.0",
3
+ "version": "1.85.0",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -93,6 +93,11 @@ import sys
93
93
  import time
94
94
  import uuid
95
95
  from collections import OrderedDict, defaultdict, deque
96
+
97
+ try:
98
+ import confidence_escalation as _ce # serving-layer Confidence recipe
99
+ except Exception: # pragma: no cover - fail open if module missing
100
+ _ce = None
96
101
  from dataclasses import dataclass, field
97
102
  from pathlib import Path
98
103
 
@@ -2792,6 +2797,16 @@ def _should_use_guarded_non_stream(
2792
2797
  if PROXY_MALFORMED_TOOL_STREAM_STRICT and has_tools:
2793
2798
  return True
2794
2799
 
2800
+ # Confidence-escalation (vLLM "Confidence" recipe) needs the FULL answer
2801
+ # buffered to score it before deciding whether to escalate. Buffer single-
2802
+ # answer (no-tool) streaming turns when escalation is enabled. Default OFF.
2803
+ if not has_tools and _ce is not None:
2804
+ try:
2805
+ if _ce.Settings.from_env().enabled:
2806
+ return True
2807
+ except Exception:
2808
+ pass
2809
+
2795
2810
  # A2: when stream-passthrough is enabled, do NOT buffer required-tool turns
2796
2811
  # (native tool_choice='required' constrains them); force-non-stream and
2797
2812
  # malformed-strict above still apply.
@@ -3351,7 +3366,28 @@ def anthropic_to_openai_response(anthropic_resp: dict) -> dict:
3351
3366
  }
3352
3367
 
3353
3368
 
3369
+ # A3: the anthropic->openai tool conversion + schema sanitize walks every tool's
3370
+ # (often deeply nested) JSON schema. The tool set is IDENTICAL across every turn
3371
+ # of a session, so this recomputed the same result each turn (observed: ~1
3372
+ # SCHEMA SANITIZE log per turn). Cache by a stable hash of the tool definitions.
3373
+ # Downstream only READS the converted dicts and FILTERS the list (narrowing), so
3374
+ # returning the cached object directly is safe.
3375
+ _TOOL_CONVERT_CACHE: "OrderedDict[str, list]" = OrderedDict()
3376
+ _TOOL_CONVERT_CACHE_MAX = 32
3377
+
3378
+
3354
3379
  def _convert_anthropic_tools_to_openai(anthropic_tools: list[dict]) -> list[dict]:
3380
+ cache_key = None
3381
+ try:
3382
+ cache_key = hashlib.sha1(
3383
+ json.dumps(anthropic_tools, sort_keys=True, default=str).encode("utf-8")
3384
+ ).hexdigest()
3385
+ except Exception:
3386
+ cache_key = None
3387
+ if cache_key is not None and cache_key in _TOOL_CONVERT_CACHE:
3388
+ _TOOL_CONVERT_CACHE.move_to_end(cache_key)
3389
+ return _TOOL_CONVERT_CACHE[cache_key]
3390
+
3355
3391
  converted = []
3356
3392
  removed_pattern_fields = 0
3357
3393
  for tool in anthropic_tools:
@@ -3375,6 +3411,10 @@ def _convert_anthropic_tools_to_openai(anthropic_tools: list[dict]) -> list[dict
3375
3411
  removed_pattern_fields,
3376
3412
  len(anthropic_tools),
3377
3413
  )
3414
+ if cache_key is not None:
3415
+ _TOOL_CONVERT_CACHE[cache_key] = converted
3416
+ if len(_TOOL_CONVERT_CACHE) > _TOOL_CONVERT_CACHE_MAX:
3417
+ _TOOL_CONVERT_CACHE.popitem(last=False)
3378
3418
  return converted
3379
3419
 
3380
3420
 
@@ -7963,6 +8003,40 @@ def openai_to_anthropic_response(
7963
8003
  }
7964
8004
 
7965
8005
 
8006
+ async def _maybe_confidence_escalate(anthropic_resp, anthropic_body, client):
8007
+ """Confidence recipe: if the cheap primary answer scores below threshold,
8008
+ escalate the same (non-tool) turn to the stronger backend and return its
8009
+ answer. Default OFF; fails open (returns the original) on any problem."""
8010
+ if _ce is None or not isinstance(anthropic_resp, dict):
8011
+ return anthropic_resp
8012
+ try:
8013
+ settings = _ce.Settings.from_env()
8014
+ text = _ce.extract_text(anthropic_resp)
8015
+ if not _ce.should_escalate(text, settings, _has_tool_definitions(anthropic_body)):
8016
+ return anthropic_resp
8017
+ payload = _ce.build_escalation_payload(anthropic_body, settings)
8018
+ url = settings.endpoint.rstrip("/") + "/v1/messages"
8019
+ headers = {
8020
+ "Content-Type": "application/json",
8021
+ "x-api-key": settings.api_key,
8022
+ "anthropic-version": "2023-06-01",
8023
+ }
8024
+ resp = await client.post(url, json=payload, headers=headers, timeout=120.0)
8025
+ if resp.status_code == 200:
8026
+ logger.warning(
8027
+ "CONFIDENCE ESCALATION: primary confidence below %.2f -> escalated to %s",
8028
+ settings.threshold, settings.model,
8029
+ )
8030
+ return resp.json()
8031
+ logger.warning(
8032
+ "CONFIDENCE ESCALATION: backend %s returned %d; keeping primary",
8033
+ settings.model, resp.status_code,
8034
+ )
8035
+ except Exception as exc:
8036
+ logger.warning("CONFIDENCE ESCALATION: failed (%s); keeping primary", exc)
8037
+ return anthropic_resp
8038
+
8039
+
7966
8040
  async def _heartbeat_then_buffered(produce_coro, model: str):
7967
8041
  """SSE generator: keep-alive heartbeat wrapper for the guarded-non-stream path.
7968
8042
 
@@ -8928,6 +9002,7 @@ async def messages(request: Request):
8928
9002
  "REQUIRED TOOL STREAM GUARDRAIL: served stream response via guarded non-stream path"
8929
9003
  )
8930
9004
 
9005
+ anthropic_resp = await _maybe_confidence_escalate(anthropic_resp, body, client)
8931
9006
  return anthropic_resp
8932
9007
 
8933
9008
  if PROXY_STREAM_HEARTBEAT_SECS > 0:
@@ -0,0 +1,104 @@
1
+ """Confidence-escalation looper (vLLM Semantic Router "Confidence" recipe).
2
+
3
+ Try the cheap primary model first; if a confidence signal on its answer is below
4
+ threshold, escalate the SAME request to a stronger backend and return that
5
+ answer instead. Bounded (one escalation), default OFF, fails open. Scoped to
6
+ non-tool (single-answer) turns — escalating a mid-loop tool turn would need the
7
+ stronger model to share the whole tool/context state, which is out of scope for
8
+ this primitive.
9
+
10
+ The confidence signal is intentionally pluggable. This module ships a cheap
11
+ text heuristic; a stronger signal (token logprob margin, or UAP's real
12
+ execution/acceptance GATE-pass — the differentiator vs logprob) can replace
13
+ ``text_confidence`` without touching the proxy hook.
14
+
15
+ All config is env-driven and OFF by default:
16
+ PROXY_CONFIDENCE_ESCALATE=on enable the looper
17
+ PROXY_CONFIDENCE_THRESHOLD=0.5 escalate when confidence < this
18
+ PROXY_ESCALATE_MODEL=<id> model id sent to the escalation backend
19
+ PROXY_ESCALATE_ENDPOINT=<url> Anthropic-compatible /v1/messages base
20
+ PROXY_ESCALATE_API_KEY=<key> x-api-key for the escalation backend
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ import re
26
+ from dataclasses import dataclass
27
+
28
+
29
+ @dataclass
30
+ class Settings:
31
+ enabled: bool
32
+ threshold: float
33
+ model: str
34
+ endpoint: str
35
+ api_key: str
36
+
37
+ @classmethod
38
+ def from_env(cls) -> "Settings":
39
+ on = os.environ.get("PROXY_CONFIDENCE_ESCALATE", "off").lower() not in {
40
+ "", "0", "off", "false", "no",
41
+ }
42
+ try:
43
+ thr = float(os.environ.get("PROXY_CONFIDENCE_THRESHOLD", "0.5"))
44
+ except ValueError:
45
+ thr = 0.5
46
+ return cls(
47
+ enabled=on,
48
+ threshold=thr,
49
+ model=os.environ.get("PROXY_ESCALATE_MODEL", ""),
50
+ endpoint=os.environ.get("PROXY_ESCALATE_ENDPOINT", ""),
51
+ api_key=os.environ.get("PROXY_ESCALATE_API_KEY", ""),
52
+ )
53
+
54
+ def backend_configured(self) -> bool:
55
+ return bool(self.model and self.endpoint)
56
+
57
+
58
+ _UNCERTAIN = re.compile(
59
+ r"\b(i'?m not sure|i am not sure|i don'?t know|i cannot|i can'?t (?:help|do|determine)|"
60
+ r"unable to|not certain|no idea|as an ai|i'?m sorry,? but)\b",
61
+ re.I,
62
+ )
63
+
64
+
65
+ def text_confidence(text: str) -> float:
66
+ """Cheap heuristic confidence in [0,1]. Conservative: only clearly weak
67
+ answers (empty, refusal/uncertainty, trivially short) score low."""
68
+ t = (text or "").strip()
69
+ if not t:
70
+ return 0.0
71
+ if _UNCERTAIN.search(t):
72
+ return 0.2
73
+ if len(t) < 20:
74
+ return 0.3
75
+ return 0.9
76
+
77
+
78
+ def extract_text(anthropic_resp: dict) -> str:
79
+ """Concatenate text blocks of an Anthropic message response."""
80
+ parts = []
81
+ for blk in (anthropic_resp or {}).get("content", []) or []:
82
+ if isinstance(blk, dict) and blk.get("type") == "text":
83
+ parts.append(blk.get("text", ""))
84
+ return "".join(parts)
85
+
86
+
87
+ def should_escalate(text: str, settings: Settings, has_tools: bool) -> bool:
88
+ if not settings.enabled or not settings.backend_configured():
89
+ return False
90
+ if has_tools: # single-answer scope only
91
+ return False
92
+ return text_confidence(text) < settings.threshold
93
+
94
+
95
+ def build_escalation_payload(anthropic_body: dict, settings: Settings) -> dict:
96
+ """Re-issue the same conversation to the stronger model (no tools)."""
97
+ payload = {
98
+ "model": settings.model,
99
+ "max_tokens": anthropic_body.get("max_tokens", 4096),
100
+ "messages": anthropic_body.get("messages", []),
101
+ }
102
+ if anthropic_body.get("system"):
103
+ payload["system"] = anthropic_body["system"]
104
+ return payload
@@ -0,0 +1,56 @@
1
+ """Tests for the confidence-escalation looper (vLLM Confidence recipe)."""
2
+ import importlib.util
3
+ import unittest
4
+ from pathlib import Path
5
+
6
+ mod_path = Path(__file__).resolve().parents[3] / "tools" / "agents" / "scripts" / "confidence_escalation.py"
7
+ spec = importlib.util.spec_from_file_location("confidence_escalation", mod_path)
8
+ ce = importlib.util.module_from_spec(spec)
9
+ import sys as _sys; _sys.modules["confidence_escalation"]=ce; spec.loader.exec_module(ce)
10
+
11
+
12
+ def S(enabled=True, threshold=0.5, model="opus", endpoint="http://x/", api_key="k"):
13
+ return ce.Settings(enabled=enabled, threshold=threshold, model=model, endpoint=endpoint, api_key=api_key)
14
+
15
+
16
+ class ConfidenceTest(unittest.TestCase):
17
+ def test_text_confidence_levels(self):
18
+ self.assertEqual(ce.text_confidence(""), 0.0)
19
+ self.assertEqual(ce.text_confidence("I don't know"), 0.2)
20
+ self.assertEqual(ce.text_confidence("ok"), 0.3)
21
+ self.assertGreater(ce.text_confidence("Here is a complete, detailed answer to your question."), 0.5)
22
+
23
+ def test_extract_text(self):
24
+ resp = {"content": [{"type": "text", "text": "a"}, {"type": "tool_use"}, {"type": "text", "text": "b"}]}
25
+ self.assertEqual(ce.extract_text(resp), "ab")
26
+
27
+
28
+ class ShouldEscalateTest(unittest.TestCase):
29
+ def test_low_confidence_escalates(self):
30
+ self.assertTrue(ce.should_escalate("I cannot help", S(), has_tools=False))
31
+
32
+ def test_high_confidence_does_not(self):
33
+ self.assertFalse(ce.should_escalate("A thorough and confident full answer here.", S(), has_tools=False))
34
+
35
+ def test_disabled_never(self):
36
+ self.assertFalse(ce.should_escalate("", S(enabled=False), has_tools=False))
37
+
38
+ def test_no_backend_never(self):
39
+ self.assertFalse(ce.should_escalate("", S(endpoint=""), has_tools=False))
40
+
41
+ def test_tool_turn_never(self):
42
+ self.assertFalse(ce.should_escalate("", S(), has_tools=True))
43
+
44
+
45
+ class PayloadTest(unittest.TestCase):
46
+ def test_payload_uses_escalation_model_and_messages(self):
47
+ body = {"model": "qwen", "max_tokens": 100, "system": "sys", "messages": [{"role": "user", "content": "hi"}]}
48
+ p = ce.build_escalation_payload(body, S(model="opus"))
49
+ self.assertEqual(p["model"], "opus")
50
+ self.assertEqual(p["messages"], body["messages"])
51
+ self.assertEqual(p["system"], "sys")
52
+ self.assertNotIn("tools", p)
53
+
54
+
55
+ if __name__ == "__main__":
56
+ unittest.main()
@@ -0,0 +1,49 @@
1
+ """Tests for A3: per-session tool-conversion cache."""
2
+ import importlib.util
3
+ import unittest
4
+ from pathlib import Path
5
+
6
+ proxy_path = Path(__file__).resolve().parents[3] / "tools" / "agents" / "scripts" / "anthropic_proxy.py"
7
+ spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
8
+ ap = importlib.util.module_from_spec(spec)
9
+ spec.loader.exec_module(ap)
10
+
11
+ TOOLS = [
12
+ {"name": "Read", "description": "read", "input_schema": {"type": "object",
13
+ "properties": {"p": {"type": "string", "pattern": "^/.*"}}, "required": ["p"]}},
14
+ {"name": "Bash", "description": "run", "input_schema": {"type": "object",
15
+ "properties": {"cmd": {"type": "string"}}}},
16
+ ]
17
+
18
+
19
+ class ToolConvertCacheTest(unittest.TestCase):
20
+ def setUp(self):
21
+ ap._TOOL_CONVERT_CACHE.clear()
22
+
23
+ def test_correct_conversion_and_sanitize(self):
24
+ out = ap._convert_anthropic_tools_to_openai(TOOLS)
25
+ self.assertEqual(out[0]["function"]["name"], "Read")
26
+ # regex pattern field stripped by sanitize
27
+ self.assertNotIn("pattern", out[0]["function"]["parameters"]["properties"]["p"])
28
+
29
+ def test_second_call_is_cache_hit_same_object(self):
30
+ a = ap._convert_anthropic_tools_to_openai(TOOLS)
31
+ self.assertEqual(len(ap._TOOL_CONVERT_CACHE), 1)
32
+ b = ap._convert_anthropic_tools_to_openai([dict(t) for t in TOOLS]) # equal-by-value
33
+ self.assertIs(a, b, "identical tool set must return the cached object")
34
+
35
+ def test_different_tools_miss(self):
36
+ ap._convert_anthropic_tools_to_openai(TOOLS)
37
+ ap._convert_anthropic_tools_to_openai(TOOLS[:1])
38
+ self.assertEqual(len(ap._TOOL_CONVERT_CACHE), 2)
39
+
40
+ def test_cache_bounded(self):
41
+ for i in range(ap._TOOL_CONVERT_CACHE_MAX + 5):
42
+ ap._convert_anthropic_tools_to_openai(
43
+ [{"name": f"T{i}", "description": "", "input_schema": {"type": "object"}}]
44
+ )
45
+ self.assertLessEqual(len(ap._TOOL_CONVERT_CACHE), ap._TOOL_CONVERT_CACHE_MAX)
46
+
47
+
48
+ if __name__ == "__main__":
49
+ unittest.main()