@miller-tech/uap 1.89.0 → 1.90.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 +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +22 -0
- package/tools/agents/scripts/confidence_escalation.py +36 -7
- package/tools/agents/tests/test_anthropic_proxy_streaming.py +28 -0
- package/tools/agents/tests/test_confidence_escalation.py +37 -0
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -7979,6 +7979,28 @@ def openai_to_anthropic_response(
|
|
|
7979
7979
|
}
|
|
7980
7980
|
)
|
|
7981
7981
|
|
|
7982
|
+
# Empty-output guard (no-tool path). Qwen-style models emit their whole
|
|
7983
|
+
# response inside a <think> block even with enable_thinking=False; when the
|
|
7984
|
+
# client did NOT opt into thinking (expose_thinking=False) the block is
|
|
7985
|
+
# consumed and -- if the model produced nothing after </think> -- the
|
|
7986
|
+
# response collapses to an empty text block. That empty body is useless to
|
|
7987
|
+
# the client and silently breaks downstream consumers (notably the
|
|
7988
|
+
# Fusion/Confidence judge, whose escalate call is a no-tool turn -- an empty
|
|
7989
|
+
# judge reply makes apply_recipe fall back to the primary, so recipes
|
|
7990
|
+
# degrade to single). When there is no text and no tool_use content but we
|
|
7991
|
+
# DID capture thinking, surface the de-tagged thinking as the body rather
|
|
7992
|
+
# than returning nothing.
|
|
7993
|
+
_has_text = any(
|
|
7994
|
+
b.get("type") == "text" and (b.get("text") or "").strip() for b in content
|
|
7995
|
+
)
|
|
7996
|
+
_has_tool_use = any(b.get("type") == "tool_use" for b in content)
|
|
7997
|
+
if not _has_text and not _has_tool_use and thinking_chunks:
|
|
7998
|
+
content.append({"type": "text", "text": "\n\n".join(thinking_chunks)})
|
|
7999
|
+
logger.warning(
|
|
8000
|
+
"EMPTY-OUTPUT GUARD: no-tool turn produced only <think> content; "
|
|
8001
|
+
"promoted de-tagged thinking to the text body (would otherwise be empty)"
|
|
8002
|
+
)
|
|
8003
|
+
|
|
7982
8004
|
stop_reason_map = {
|
|
7983
8005
|
"stop": "end_turn",
|
|
7984
8006
|
"length": "max_tokens",
|
|
@@ -47,6 +47,7 @@ class Settings:
|
|
|
47
47
|
model: str
|
|
48
48
|
endpoint: str
|
|
49
49
|
api_key: str
|
|
50
|
+
allow_self_judge: bool = False
|
|
50
51
|
|
|
51
52
|
@classmethod
|
|
52
53
|
def from_env(cls) -> "Settings":
|
|
@@ -76,11 +77,32 @@ class Settings:
|
|
|
76
77
|
model=os.environ.get("PROXY_ESCALATE_MODEL", ""),
|
|
77
78
|
endpoint=os.environ.get("PROXY_ESCALATE_ENDPOINT", ""),
|
|
78
79
|
api_key=os.environ.get("PROXY_ESCALATE_API_KEY", ""),
|
|
80
|
+
allow_self_judge=flag("PROXY_ALLOW_SELF_JUDGE"),
|
|
79
81
|
)
|
|
80
82
|
|
|
81
83
|
def backend_configured(self) -> bool:
|
|
82
84
|
return bool(self.model and self.endpoint)
|
|
83
85
|
|
|
86
|
+
def judge_is_self(self, primary_model: str = "") -> bool:
|
|
87
|
+
"""True when the configured judge model name matches the primary
|
|
88
|
+
(generator) model -- i.e. qwen judging qwen."""
|
|
89
|
+
pm = (primary_model or "").strip().lower()
|
|
90
|
+
jm = (self.model or "").strip().lower()
|
|
91
|
+
return bool(pm) and bool(jm) and pm == jm
|
|
92
|
+
|
|
93
|
+
def judge_available(self, primary_model: str = "") -> bool:
|
|
94
|
+
"""A judge backend that is configured AND distinct from the primary
|
|
95
|
+
model. A same-model judge (qwen judging qwen) was MEASURED to add no
|
|
96
|
+
quality lift, so judge-dependent recipes (fusion / ratings / remom and
|
|
97
|
+
confidence-escalation) require a distinct judge by default and otherwise
|
|
98
|
+
downgrade to single. Set PROXY_ALLOW_SELF_JUDGE=1 to force self-judging.
|
|
99
|
+
When the primary model is unknown the judge is treated as distinct."""
|
|
100
|
+
if not self.backend_configured():
|
|
101
|
+
return False
|
|
102
|
+
if self.allow_self_judge:
|
|
103
|
+
return True
|
|
104
|
+
return not self.judge_is_self(primary_model)
|
|
105
|
+
|
|
84
106
|
|
|
85
107
|
# ---- helpers --------------------------------------------------------------
|
|
86
108
|
def latest_user_text(anthropic_body: dict) -> str:
|
|
@@ -222,6 +244,7 @@ def select_recipe(anthropic_body: dict, settings: Settings, has_tools: bool) ->
|
|
|
222
244
|
return "single"
|
|
223
245
|
if settings.recipe != "auto":
|
|
224
246
|
return settings.recipe
|
|
247
|
+
pm = (anthropic_body or {}).get("model", "")
|
|
225
248
|
text = latest_user_text(anthropic_body)
|
|
226
249
|
# Prefer the harness reactor's ACTUAL routeResult signal when fresh; else the
|
|
227
250
|
# proxy's own signal extraction (faithful port of query-complexity).
|
|
@@ -229,7 +252,7 @@ def select_recipe(anthropic_body: dict, settings: Settings, has_tools: bool) ->
|
|
|
229
252
|
if rsig is not None:
|
|
230
253
|
rec = rsig.get("recipe")
|
|
231
254
|
if rec in {"single", "confidence", "fusion"}:
|
|
232
|
-
if rec == "fusion" and not settings.
|
|
255
|
+
if rec == "fusion" and not settings.judge_available(pm):
|
|
233
256
|
return "confidence"
|
|
234
257
|
return rec
|
|
235
258
|
complexity = rsig.get("complexity") or query_complexity(text)
|
|
@@ -237,9 +260,9 @@ def select_recipe(anthropic_body: dict, settings: Settings, has_tools: bool) ->
|
|
|
237
260
|
else:
|
|
238
261
|
complexity = query_complexity(text)
|
|
239
262
|
shape = task_shape(text)
|
|
240
|
-
if settings.
|
|
263
|
+
if settings.judge_available(pm) and (complexity == "complex" or shape == "reasoning"):
|
|
241
264
|
return "fusion"
|
|
242
|
-
if settings.
|
|
265
|
+
if settings.judge_available(pm) and len(text) >= settings.auto_fusion_chars:
|
|
243
266
|
return "fusion"
|
|
244
267
|
return "confidence"
|
|
245
268
|
|
|
@@ -351,8 +374,8 @@ def should_escalate(text: str, settings: Settings, has_tools: bool) -> bool:
|
|
|
351
374
|
|
|
352
375
|
|
|
353
376
|
# ---- orchestration (injected callables) -----------------------------------
|
|
354
|
-
async def _confidence_score(text, anthropic_body, settings, call_judge):
|
|
355
|
-
if settings.signal == "selfverify" and settings.
|
|
377
|
+
async def _confidence_score(text, anthropic_body, settings, call_judge, primary_model=""):
|
|
378
|
+
if settings.signal == "selfverify" and settings.judge_available(primary_model) and call_judge is not None:
|
|
356
379
|
jr = await call_judge(build_verify_payload(anthropic_body, text, settings))
|
|
357
380
|
score = parse_verify_score(extract_text(jr)) if isinstance(jr, dict) else None
|
|
358
381
|
if score is not None:
|
|
@@ -370,10 +393,16 @@ async def apply_recipe(primary_resp, anthropic_body, openai_body, settings, has_
|
|
|
370
393
|
if recipe == "single" or not isinstance(primary_resp, dict):
|
|
371
394
|
return primary_resp
|
|
372
395
|
primary_text = extract_text(primary_resp)
|
|
396
|
+
primary_model = (openai_body or {}).get("model", "")
|
|
397
|
+
# Judge-dependent recipes need a configured, DISTINCT (non-self) judge.
|
|
398
|
+
# A same-model judge adds no measured lift, so downgrade to single BEFORE
|
|
399
|
+
# spending fan-out / judge calls.
|
|
400
|
+
if recipe in {"fusion", "ratings", "remom"} and not settings.judge_available(primary_model):
|
|
401
|
+
return primary_resp
|
|
373
402
|
|
|
374
403
|
if recipe == "confidence":
|
|
375
|
-
conf = await _confidence_score(primary_text, anthropic_body, settings, call_judge)
|
|
376
|
-
if conf < settings.threshold and settings.
|
|
404
|
+
conf = await _confidence_score(primary_text, anthropic_body, settings, call_judge, primary_model)
|
|
405
|
+
if conf < settings.threshold and settings.judge_available(primary_model) and call_judge is not None:
|
|
377
406
|
esc = await call_judge(build_escalation_payload(anthropic_body, settings))
|
|
378
407
|
if isinstance(esc, dict):
|
|
379
408
|
return esc
|
|
@@ -3790,6 +3790,34 @@ class TestGenerationHangRecovery(unittest.TestCase):
|
|
|
3790
3790
|
asyncio.run(_run())
|
|
3791
3791
|
|
|
3792
3792
|
|
|
3793
|
+
class EmptyOutputGuardTest(unittest.TestCase):
|
|
3794
|
+
"""#1: a no-tool turn whose entire response was inside <think> must never
|
|
3795
|
+
collapse to an empty body (would silently break the Fusion/Confidence judge)."""
|
|
3796
|
+
|
|
3797
|
+
def test_all_think_no_tool_promotes_thinking_to_body(self):
|
|
3798
|
+
openai_resp = {"choices": [{"message": {"content": "<think>the answer is 42</think>"},
|
|
3799
|
+
"finish_reason": "stop"}]}
|
|
3800
|
+
out = proxy.openai_to_anthropic_response(openai_resp, "qwen", expose_thinking=False)
|
|
3801
|
+
texts = [b.get("text", "") for b in out["content"] if b.get("type") == "text"]
|
|
3802
|
+
self.assertTrue(any("42" in t for t in texts)) # body is non-empty
|
|
3803
|
+
self.assertTrue(all(b.get("type") != "thinking" for b in out["content"]))
|
|
3804
|
+
|
|
3805
|
+
def test_normal_post_think_body_unaffected(self):
|
|
3806
|
+
openai_resp = {"choices": [{"message": {"content": "<think>reason</think>FINAL"},
|
|
3807
|
+
"finish_reason": "stop"}]}
|
|
3808
|
+
out = proxy.openai_to_anthropic_response(openai_resp, "qwen", expose_thinking=False)
|
|
3809
|
+
texts = "".join(b.get("text", "") for b in out["content"] if b.get("type") == "text")
|
|
3810
|
+
self.assertEqual(texts, "FINAL") # guard does not fire
|
|
3811
|
+
|
|
3812
|
+
def test_tool_only_response_not_padded(self):
|
|
3813
|
+
openai_resp = {"choices": [{"message": {"content": "<think>x</think>",
|
|
3814
|
+
"tool_calls": [{"id": "a", "function": {"name": "bash",
|
|
3815
|
+
"arguments": "{}"}}]}, "finish_reason": "tool_calls"}]}
|
|
3816
|
+
out = proxy.openai_to_anthropic_response(openai_resp, "qwen", expose_thinking=False)
|
|
3817
|
+
self.assertTrue(any(b.get("type") == "tool_use" for b in out["content"]))
|
|
3818
|
+
self.assertFalse(any(b.get("type") == "text" for b in out["content"]))
|
|
3819
|
+
|
|
3820
|
+
|
|
3793
3821
|
if __name__ == "__main__":
|
|
3794
3822
|
unittest.main()
|
|
3795
3823
|
|
|
@@ -231,5 +231,42 @@ class SettingsExtTest(unittest.TestCase):
|
|
|
231
231
|
os.environ.pop("PROXY_RECIPE", None)
|
|
232
232
|
|
|
233
233
|
|
|
234
|
+
class JudgeGatingTest(unittest.TestCase): # #2 stronger non-self judge
|
|
235
|
+
def test_judge_available_distinct_vs_self(self):
|
|
236
|
+
s = S(model="opus") # judge=opus, primary=qwen -> distinct
|
|
237
|
+
self.assertTrue(s.judge_available("qwen"))
|
|
238
|
+
self.assertFalse(s.judge_available("opus")) # self-judge blocked
|
|
239
|
+
self.assertFalse(S(model="qwen").judge_available("qwen"))
|
|
240
|
+
self.assertTrue(s.judge_available("")) # unknown primary -> allow
|
|
241
|
+
self.assertFalse(S(model="", endpoint="").judge_available("qwen")) # unconfigured
|
|
242
|
+
|
|
243
|
+
def test_allow_self_judge_override(self):
|
|
244
|
+
s = ce.Settings(enabled=True, recipe="fusion", signal="heuristic", threshold=0.5,
|
|
245
|
+
fusion_n=2, remom_quorum=2, auto_fusion_chars=600, model="qwen",
|
|
246
|
+
endpoint="http://x/", api_key="k", allow_self_judge=True)
|
|
247
|
+
self.assertTrue(s.judge_available("qwen"))
|
|
248
|
+
|
|
249
|
+
def test_self_judge_downgrades_fusion_without_calling_judge(self):
|
|
250
|
+
async def primary(v): raise AssertionError("fan-out must not run on self-judge")
|
|
251
|
+
async def judge(p): raise AssertionError("judge must not be called on self-judge")
|
|
252
|
+
# judge model == primary model ("qwen") -> downgrade to single
|
|
253
|
+
out = run(ce.apply_recipe(resp("c0"), body(), {"model": "qwen", "messages": []},
|
|
254
|
+
S(recipe="fusion", fusion_n=2, model="qwen"), False, primary, judge))
|
|
255
|
+
self.assertEqual(ce.extract_text(out), "c0")
|
|
256
|
+
|
|
257
|
+
def test_distinct_judge_still_runs_fusion(self):
|
|
258
|
+
async def primary(v): return resp("c1")
|
|
259
|
+
async def judge(p): return resp("1")
|
|
260
|
+
out = run(ce.apply_recipe(resp("c0"), body(), {"model": "qwen", "messages": []},
|
|
261
|
+
S(recipe="fusion", fusion_n=2, model="opus"), False, primary, judge))
|
|
262
|
+
self.assertEqual(ce.extract_text(out), "c1")
|
|
263
|
+
|
|
264
|
+
def test_select_recipe_skips_fusion_on_self_judge(self):
|
|
265
|
+
b = {"model": "qwen", "messages": [{"role": "user",
|
|
266
|
+
"content": "prove the theorem and analyze why x is even"}]}
|
|
267
|
+
self.assertEqual(ce.select_recipe(b, S(recipe="auto", model="qwen"), False), "confidence")
|
|
268
|
+
self.assertEqual(ce.select_recipe(b, S(recipe="auto", model="opus"), False), "fusion")
|
|
269
|
+
|
|
270
|
+
|
|
234
271
|
if __name__ == "__main__":
|
|
235
272
|
unittest.main()
|