@miller-tech/uap 1.88.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 +95 -8
- package/tools/agents/tests/test_anthropic_proxy_streaming.py +28 -0
- package/tools/agents/tests/test_confidence_escalation.py +91 -3
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",
|
|
@@ -42,10 +42,12 @@ class Settings:
|
|
|
42
42
|
signal: str
|
|
43
43
|
threshold: float
|
|
44
44
|
fusion_n: int
|
|
45
|
+
remom_quorum: int
|
|
45
46
|
auto_fusion_chars: int
|
|
46
47
|
model: str
|
|
47
48
|
endpoint: str
|
|
48
49
|
api_key: str
|
|
50
|
+
allow_self_judge: bool = False
|
|
49
51
|
|
|
50
52
|
@classmethod
|
|
51
53
|
def from_env(cls) -> "Settings":
|
|
@@ -59,7 +61,7 @@ class Settings:
|
|
|
59
61
|
return default
|
|
60
62
|
|
|
61
63
|
recipe = os.environ.get("PROXY_RECIPE", "auto").lower()
|
|
62
|
-
if recipe not in {"auto", "single", "confidence", "fusion"}:
|
|
64
|
+
if recipe not in {"auto", "single", "confidence", "fusion", "ratings", "remom", "workflow"}:
|
|
63
65
|
recipe = "auto"
|
|
64
66
|
signal = os.environ.get("PROXY_CONFIDENCE_SIGNAL", "heuristic").lower()
|
|
65
67
|
if signal not in {"heuristic", "selfverify"}:
|
|
@@ -70,15 +72,37 @@ class Settings:
|
|
|
70
72
|
signal=signal,
|
|
71
73
|
threshold=num("PROXY_CONFIDENCE_THRESHOLD", 0.5, float),
|
|
72
74
|
fusion_n=max(2, min(6, num("PROXY_FUSION_N", 3, int))),
|
|
75
|
+
remom_quorum=max(1, min(6, num("PROXY_REMOM_QUORUM", 2, int))),
|
|
73
76
|
auto_fusion_chars=num("PROXY_AUTO_FUSION_CHARS", 600, int),
|
|
74
77
|
model=os.environ.get("PROXY_ESCALATE_MODEL", ""),
|
|
75
78
|
endpoint=os.environ.get("PROXY_ESCALATE_ENDPOINT", ""),
|
|
76
79
|
api_key=os.environ.get("PROXY_ESCALATE_API_KEY", ""),
|
|
80
|
+
allow_self_judge=flag("PROXY_ALLOW_SELF_JUDGE"),
|
|
77
81
|
)
|
|
78
82
|
|
|
79
83
|
def backend_configured(self) -> bool:
|
|
80
84
|
return bool(self.model and self.endpoint)
|
|
81
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
|
+
|
|
82
106
|
|
|
83
107
|
# ---- helpers --------------------------------------------------------------
|
|
84
108
|
def latest_user_text(anthropic_body: dict) -> str:
|
|
@@ -220,6 +244,7 @@ def select_recipe(anthropic_body: dict, settings: Settings, has_tools: bool) ->
|
|
|
220
244
|
return "single"
|
|
221
245
|
if settings.recipe != "auto":
|
|
222
246
|
return settings.recipe
|
|
247
|
+
pm = (anthropic_body or {}).get("model", "")
|
|
223
248
|
text = latest_user_text(anthropic_body)
|
|
224
249
|
# Prefer the harness reactor's ACTUAL routeResult signal when fresh; else the
|
|
225
250
|
# proxy's own signal extraction (faithful port of query-complexity).
|
|
@@ -227,7 +252,7 @@ def select_recipe(anthropic_body: dict, settings: Settings, has_tools: bool) ->
|
|
|
227
252
|
if rsig is not None:
|
|
228
253
|
rec = rsig.get("recipe")
|
|
229
254
|
if rec in {"single", "confidence", "fusion"}:
|
|
230
|
-
if rec == "fusion" and not settings.
|
|
255
|
+
if rec == "fusion" and not settings.judge_available(pm):
|
|
231
256
|
return "confidence"
|
|
232
257
|
return rec
|
|
233
258
|
complexity = rsig.get("complexity") or query_complexity(text)
|
|
@@ -235,9 +260,9 @@ def select_recipe(anthropic_body: dict, settings: Settings, has_tools: bool) ->
|
|
|
235
260
|
else:
|
|
236
261
|
complexity = query_complexity(text)
|
|
237
262
|
shape = task_shape(text)
|
|
238
|
-
if settings.
|
|
263
|
+
if settings.judge_available(pm) and (complexity == "complex" or shape == "reasoning"):
|
|
239
264
|
return "fusion"
|
|
240
|
-
if settings.
|
|
265
|
+
if settings.judge_available(pm) and len(text) >= settings.auto_fusion_chars:
|
|
241
266
|
return "fusion"
|
|
242
267
|
return "confidence"
|
|
243
268
|
|
|
@@ -321,6 +346,26 @@ def parse_judge_index(text: str, n: int):
|
|
|
321
346
|
return i if 0 <= i < n else None
|
|
322
347
|
|
|
323
348
|
|
|
349
|
+
def build_synthesis_payload(anthropic_body: dict, candidate_texts: list[str], settings: Settings) -> dict:
|
|
350
|
+
q = latest_user_text(anthropic_body)
|
|
351
|
+
listing = "\n\n".join(f"[{i}]\n{t}" for i, t in enumerate(candidate_texts))
|
|
352
|
+
prompt = (
|
|
353
|
+
"Synthesize a SINGLE best answer to the REQUEST by merging the correct, "
|
|
354
|
+
"complementary parts of the candidate answers below. Resolve disagreements "
|
|
355
|
+
"and keep the required output format. Reply with ONLY the final answer."
|
|
356
|
+
"\n\nREQUEST:\n" + q + "\n\nCANDIDATES:\n" + listing
|
|
357
|
+
)
|
|
358
|
+
return {"model": settings.model, "max_tokens": anthropic_body.get("max_tokens", 4096),
|
|
359
|
+
"messages": [{"role": "user", "content": prompt}]}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
async def _fanout_candidates(primary_resp, openai_body, settings, call_primary):
|
|
363
|
+
"""[primary] + up to fusion_n-1 temperature-varied breadth candidates."""
|
|
364
|
+
variants = build_fusion_variants(openai_body or {}, settings.fusion_n)
|
|
365
|
+
results = await asyncio.gather(*[call_primary(v) for v in variants], return_exceptions=True)
|
|
366
|
+
return [primary_resp] + [r for r in results if isinstance(r, dict)]
|
|
367
|
+
|
|
368
|
+
|
|
324
369
|
def should_escalate(text: str, settings: Settings, has_tools: bool) -> bool:
|
|
325
370
|
"""Back-compat (heuristic confidence path)."""
|
|
326
371
|
if not settings.enabled or not settings.backend_configured() or has_tools:
|
|
@@ -329,8 +374,8 @@ def should_escalate(text: str, settings: Settings, has_tools: bool) -> bool:
|
|
|
329
374
|
|
|
330
375
|
|
|
331
376
|
# ---- orchestration (injected callables) -----------------------------------
|
|
332
|
-
async def _confidence_score(text, anthropic_body, settings, call_judge):
|
|
333
|
-
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:
|
|
334
379
|
jr = await call_judge(build_verify_payload(anthropic_body, text, settings))
|
|
335
380
|
score = parse_verify_score(extract_text(jr)) if isinstance(jr, dict) else None
|
|
336
381
|
if score is not None:
|
|
@@ -348,10 +393,16 @@ async def apply_recipe(primary_resp, anthropic_body, openai_body, settings, has_
|
|
|
348
393
|
if recipe == "single" or not isinstance(primary_resp, dict):
|
|
349
394
|
return primary_resp
|
|
350
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
|
|
351
402
|
|
|
352
403
|
if recipe == "confidence":
|
|
353
|
-
conf = await _confidence_score(primary_text, anthropic_body, settings, call_judge)
|
|
354
|
-
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:
|
|
355
406
|
esc = await call_judge(build_escalation_payload(anthropic_body, settings))
|
|
356
407
|
if isinstance(esc, dict):
|
|
357
408
|
return esc
|
|
@@ -370,6 +421,42 @@ async def apply_recipe(primary_resp, anthropic_body, openai_body, settings, has_
|
|
|
370
421
|
if idx is not None:
|
|
371
422
|
return candidates[idx]
|
|
372
423
|
return primary_resp # fallback to best valid evidence
|
|
424
|
+
|
|
425
|
+
if recipe == "ratings":
|
|
426
|
+
# Bounded ensemble: rate each candidate independently, pick the best.
|
|
427
|
+
candidates = await _fanout_candidates(primary_resp, openai_body, settings, call_primary)
|
|
428
|
+
if len(candidates) <= 1 or not settings.backend_configured() or call_judge is None:
|
|
429
|
+
return primary_resp
|
|
430
|
+
rs = await asyncio.gather(
|
|
431
|
+
*[call_judge(build_verify_payload(anthropic_body, extract_text(c), settings))
|
|
432
|
+
for c in candidates],
|
|
433
|
+
return_exceptions=True,
|
|
434
|
+
)
|
|
435
|
+
scored = []
|
|
436
|
+
for c, jr2 in zip(candidates, rs):
|
|
437
|
+
sc = parse_verify_score(extract_text(jr2)) if isinstance(jr2, dict) else None
|
|
438
|
+
scored.append((sc if sc is not None else text_confidence(extract_text(c)), c))
|
|
439
|
+
return max(scored, key=lambda x: x[0])[1]
|
|
440
|
+
|
|
441
|
+
if recipe == "remom":
|
|
442
|
+
# Breadth -> quorum -> synthesis into the output contract; fall back
|
|
443
|
+
# to the best valid evidence if synthesis fails (no API error).
|
|
444
|
+
candidates = await _fanout_candidates(primary_resp, openai_body, settings, call_primary)
|
|
445
|
+
valid = [c for c in candidates if extract_text(c).strip()]
|
|
446
|
+
if len(valid) >= settings.remom_quorum and settings.backend_configured() and call_judge is not None:
|
|
447
|
+
synth = await call_judge(
|
|
448
|
+
build_synthesis_payload(anthropic_body, [extract_text(c) for c in valid], settings)
|
|
449
|
+
)
|
|
450
|
+
if isinstance(synth, dict) and extract_text(synth).strip():
|
|
451
|
+
return synth
|
|
452
|
+
return max(valid, key=lambda c: len(extract_text(c))) if valid else primary_resp
|
|
453
|
+
|
|
454
|
+
if recipe == "workflow":
|
|
455
|
+
# Workflows (planner/patcher/verifier under a contract) are the
|
|
456
|
+
# deliver convergence loop's job — it owns the real execution +
|
|
457
|
+
# acceptance gates and repo state a stateless serving turn lacks.
|
|
458
|
+
# Pass through; the harness routes workflow tasks through uap deliver.
|
|
459
|
+
return primary_resp
|
|
373
460
|
except Exception:
|
|
374
461
|
return primary_resp
|
|
375
462
|
return primary_resp
|
|
@@ -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
|
|
|
@@ -15,10 +15,10 @@ spec.loader.exec_module(ce)
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def S(enabled=True, recipe="auto", signal="heuristic", threshold=0.5, fusion_n=3,
|
|
18
|
-
auto_chars=600, model="opus", endpoint="http://x/", key="k"):
|
|
18
|
+
auto_chars=600, model="opus", endpoint="http://x/", key="k", remom_quorum=2):
|
|
19
19
|
return ce.Settings(enabled=enabled, recipe=recipe, signal=signal, threshold=threshold,
|
|
20
|
-
fusion_n=fusion_n,
|
|
21
|
-
endpoint=endpoint, api_key=key)
|
|
20
|
+
fusion_n=fusion_n, remom_quorum=remom_quorum, auto_fusion_chars=auto_chars,
|
|
21
|
+
model=model, endpoint=endpoint, api_key=key)
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def resp(text):
|
|
@@ -180,5 +180,93 @@ class CrossProcessSignalTest(unittest.TestCase): # reactor -> proxy
|
|
|
180
180
|
self.assertEqual(r, "fusion") # reactor signal overrides the simple self-classification
|
|
181
181
|
|
|
182
182
|
|
|
183
|
+
|
|
184
|
+
class ApplyRatingsTest(unittest.TestCase): # Ratings
|
|
185
|
+
def test_picks_highest_rated_candidate(self):
|
|
186
|
+
async def primary(v): return resp("cand-1")
|
|
187
|
+
scores = iter(["3", "9"]) # primary rated 3, fanout rated 9
|
|
188
|
+
async def judge(p): return resp(next(scores))
|
|
189
|
+
out = run(ce.apply_recipe(resp("cand-0"), body(), {"model": "q", "messages": []},
|
|
190
|
+
S(recipe="ratings", fusion_n=2), False, primary, judge))
|
|
191
|
+
self.assertEqual(ce.extract_text(out), "cand-1")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class ApplyReMoMTest(unittest.TestCase): # ReMoM
|
|
195
|
+
def test_synthesizes_when_quorum_met(self):
|
|
196
|
+
async def primary(v): return resp("evidence B")
|
|
197
|
+
async def judge(p): return resp("MERGED ANSWER")
|
|
198
|
+
out = run(ce.apply_recipe(resp("evidence A"), body(), {"model": "q", "messages": []},
|
|
199
|
+
S(recipe="remom", fusion_n=2), False, primary, judge))
|
|
200
|
+
self.assertEqual(ce.extract_text(out), "MERGED ANSWER")
|
|
201
|
+
|
|
202
|
+
def test_falls_back_to_best_evidence_when_synthesis_fails(self):
|
|
203
|
+
async def primary(v): return resp("a longer, more complete candidate answer")
|
|
204
|
+
async def judge(p): return None # synthesis failed
|
|
205
|
+
out = run(ce.apply_recipe(resp("short"), body(), {"model": "q", "messages": []},
|
|
206
|
+
S(recipe="remom", fusion_n=2), False, primary, judge))
|
|
207
|
+
self.assertEqual(ce.extract_text(out), "a longer, more complete candidate answer")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class WorkflowTest(unittest.TestCase):
|
|
211
|
+
def test_workflow_passes_through(self): # Workflows = deliver's job
|
|
212
|
+
async def primary(v): return resp("x")
|
|
213
|
+
async def judge(p): return resp("y")
|
|
214
|
+
out = run(ce.apply_recipe(resp("PRIMARY"), body(), {}, S(recipe="workflow"), False, primary, judge))
|
|
215
|
+
self.assertEqual(ce.extract_text(out), "PRIMARY")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class SettingsExtTest(unittest.TestCase):
|
|
219
|
+
def test_remom_quorum_default(self):
|
|
220
|
+
import os, tempfile
|
|
221
|
+
for k in ("PROXY_REMOM_QUORUM",): os.environ.pop(k, None)
|
|
222
|
+
self.assertEqual(ce.Settings.from_env().remom_quorum, 2)
|
|
223
|
+
|
|
224
|
+
def test_new_recipes_valid(self):
|
|
225
|
+
import os
|
|
226
|
+
for r in ("ratings", "remom", "workflow"):
|
|
227
|
+
os.environ["PROXY_RECIPE"] = r
|
|
228
|
+
try:
|
|
229
|
+
self.assertEqual(ce.Settings.from_env().recipe, r)
|
|
230
|
+
finally:
|
|
231
|
+
os.environ.pop("PROXY_RECIPE", None)
|
|
232
|
+
|
|
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
|
+
|
|
183
271
|
if __name__ == "__main__":
|
|
184
272
|
unittest.main()
|