@miller-tech/uap 1.15.9 → 1.15.11
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
|
@@ -1134,6 +1134,28 @@ def _has_tool_definitions(anthropic_body: dict) -> bool:
|
|
|
1134
1134
|
return isinstance(tools, list) and len(tools) > 0
|
|
1135
1135
|
|
|
1136
1136
|
|
|
1137
|
+
def _should_use_guarded_non_stream(
|
|
1138
|
+
is_stream: bool,
|
|
1139
|
+
anthropic_body: dict,
|
|
1140
|
+
openai_body: dict,
|
|
1141
|
+
) -> bool:
|
|
1142
|
+
if not is_stream:
|
|
1143
|
+
return False
|
|
1144
|
+
|
|
1145
|
+
if PROXY_FORCE_NON_STREAM:
|
|
1146
|
+
return True
|
|
1147
|
+
|
|
1148
|
+
has_tools = _has_tool_definitions(anthropic_body)
|
|
1149
|
+
if PROXY_MALFORMED_TOOL_STREAM_STRICT and has_tools:
|
|
1150
|
+
return True
|
|
1151
|
+
|
|
1152
|
+
return (
|
|
1153
|
+
has_tools
|
|
1154
|
+
and openai_body.get("tool_choice") == "required"
|
|
1155
|
+
and (PROXY_MALFORMED_TOOL_GUARDRAIL or PROXY_GUARDRAIL_RETRY)
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
|
|
1137
1159
|
def _message_has_tool_result(content) -> bool:
|
|
1138
1160
|
return isinstance(content, list) and any(
|
|
1139
1161
|
isinstance(block, dict) and block.get("type") == "tool_result"
|
|
@@ -2479,16 +2501,14 @@ def _classify_tool_response_issue(
|
|
|
2479
2501
|
has_tool_calls = _openai_has_tool_calls(openai_resp)
|
|
2480
2502
|
if not has_tool_calls:
|
|
2481
2503
|
if required_tool_choice:
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
),
|
|
2491
|
-
)
|
|
2504
|
+
return ToolResponseIssue(
|
|
2505
|
+
kind="required_tool_miss",
|
|
2506
|
+
reason="required tool turn returned no tool calls",
|
|
2507
|
+
retry_hint=(
|
|
2508
|
+
"A tool call is mandatory for this turn. Emit exactly one valid tool call now "
|
|
2509
|
+
"with a strict JSON object in `arguments`."
|
|
2510
|
+
),
|
|
2511
|
+
)
|
|
2492
2512
|
return ToolResponseIssue()
|
|
2493
2513
|
|
|
2494
2514
|
if not PROXY_TOOL_ARGS_PREFLIGHT:
|
|
@@ -2566,6 +2586,49 @@ def _looks_malformed_tool_payload(text: str) -> bool:
|
|
|
2566
2586
|
return True
|
|
2567
2587
|
if lowered.count("</parameter") >= 1 and lowered.count('{"description"') >= 1:
|
|
2568
2588
|
return True
|
|
2589
|
+
if _looks_repetitive_policy_echo(text):
|
|
2590
|
+
return True
|
|
2591
|
+
return False
|
|
2592
|
+
|
|
2593
|
+
|
|
2594
|
+
def _looks_repetitive_policy_echo(text: str) -> bool:
|
|
2595
|
+
if not text:
|
|
2596
|
+
return False
|
|
2597
|
+
|
|
2598
|
+
lowered = text.lower()
|
|
2599
|
+
compact = re.sub(r"\s+", " ", lowered).strip()
|
|
2600
|
+
if not compact:
|
|
2601
|
+
return False
|
|
2602
|
+
|
|
2603
|
+
policy_phrase_markers = (
|
|
2604
|
+
"at least 2 new test cases",
|
|
2605
|
+
"tests must be in test/",
|
|
2606
|
+
"describe/it/expect using vitest",
|
|
2607
|
+
)
|
|
2608
|
+
if any(compact.count(marker) >= 4 for marker in policy_phrase_markers):
|
|
2609
|
+
return True
|
|
2610
|
+
|
|
2611
|
+
lines = [
|
|
2612
|
+
re.sub(r"\s+", " ", line.strip().lower())
|
|
2613
|
+
for line in text.splitlines()
|
|
2614
|
+
if line.strip()
|
|
2615
|
+
]
|
|
2616
|
+
if lines:
|
|
2617
|
+
line_counts: dict[str, int] = {}
|
|
2618
|
+
for line in lines:
|
|
2619
|
+
if len(line) < 24:
|
|
2620
|
+
continue
|
|
2621
|
+
line_counts[line] = line_counts.get(line, 0) + 1
|
|
2622
|
+
if line_counts and max(line_counts.values()) >= 8:
|
|
2623
|
+
return True
|
|
2624
|
+
|
|
2625
|
+
repeated_phrase_match = re.search(
|
|
2626
|
+
r"((?:[a-z0-9_./-]+\s+){2,8}[a-z0-9_./-]+)(?:\s+\1){7,}",
|
|
2627
|
+
compact,
|
|
2628
|
+
)
|
|
2629
|
+
if repeated_phrase_match:
|
|
2630
|
+
return True
|
|
2631
|
+
|
|
2569
2632
|
return False
|
|
2570
2633
|
|
|
2571
2634
|
|
|
@@ -3509,9 +3572,10 @@ async def messages(request: Request):
|
|
|
3509
3572
|
media_type="application/json",
|
|
3510
3573
|
)
|
|
3511
3574
|
|
|
3512
|
-
use_guarded_non_stream =
|
|
3513
|
-
|
|
3514
|
-
|
|
3575
|
+
use_guarded_non_stream = _should_use_guarded_non_stream(
|
|
3576
|
+
is_stream,
|
|
3577
|
+
body,
|
|
3578
|
+
openai_body,
|
|
3515
3579
|
)
|
|
3516
3580
|
if use_guarded_non_stream:
|
|
3517
3581
|
strict_body = dict(openai_body)
|
|
@@ -3582,10 +3646,14 @@ async def messages(request: Request):
|
|
|
3582
3646
|
logger.info(
|
|
3583
3647
|
"FORCED NON-STREAM: served stream response via guarded non-stream path"
|
|
3584
3648
|
)
|
|
3585
|
-
|
|
3649
|
+
elif PROXY_MALFORMED_TOOL_STREAM_STRICT and _has_tool_definitions(body):
|
|
3586
3650
|
logger.info(
|
|
3587
3651
|
"STRICT STREAM GUARDRAIL: served stream response via guarded non-stream path"
|
|
3588
3652
|
)
|
|
3653
|
+
else:
|
|
3654
|
+
logger.info(
|
|
3655
|
+
"REQUIRED TOOL STREAM GUARDRAIL: served stream response via guarded non-stream path"
|
|
3656
|
+
)
|
|
3589
3657
|
|
|
3590
3658
|
return StreamingResponse(
|
|
3591
3659
|
stream_anthropic_message(anthropic_resp),
|
|
@@ -100,6 +100,54 @@ class TestProxyConfigTuning(unittest.TestCase):
|
|
|
100
100
|
setattr(proxy, "PROXY_CONTEXT_PRUNE_TARGET_FRACTION", old_target)
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
class TestStreamGuardedPathSelection(unittest.TestCase):
|
|
104
|
+
def test_required_tool_turn_uses_guarded_non_stream(self):
|
|
105
|
+
old_force = getattr(proxy, "PROXY_FORCE_NON_STREAM")
|
|
106
|
+
old_strict = getattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT")
|
|
107
|
+
old_guard = getattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL")
|
|
108
|
+
old_retry = getattr(proxy, "PROXY_GUARDRAIL_RETRY")
|
|
109
|
+
try:
|
|
110
|
+
setattr(proxy, "PROXY_FORCE_NON_STREAM", False)
|
|
111
|
+
setattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT", False)
|
|
112
|
+
setattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL", True)
|
|
113
|
+
setattr(proxy, "PROXY_GUARDRAIL_RETRY", True)
|
|
114
|
+
|
|
115
|
+
selected = proxy._should_use_guarded_non_stream(
|
|
116
|
+
True,
|
|
117
|
+
{"tools": [{"name": "Read", "input_schema": {"type": "object"}}]},
|
|
118
|
+
{"tool_choice": "required"},
|
|
119
|
+
)
|
|
120
|
+
self.assertTrue(selected)
|
|
121
|
+
finally:
|
|
122
|
+
setattr(proxy, "PROXY_FORCE_NON_STREAM", old_force)
|
|
123
|
+
setattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT", old_strict)
|
|
124
|
+
setattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL", old_guard)
|
|
125
|
+
setattr(proxy, "PROXY_GUARDRAIL_RETRY", old_retry)
|
|
126
|
+
|
|
127
|
+
def test_auto_tool_turn_keeps_true_stream_when_strict_off(self):
|
|
128
|
+
old_force = getattr(proxy, "PROXY_FORCE_NON_STREAM")
|
|
129
|
+
old_strict = getattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT")
|
|
130
|
+
old_guard = getattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL")
|
|
131
|
+
old_retry = getattr(proxy, "PROXY_GUARDRAIL_RETRY")
|
|
132
|
+
try:
|
|
133
|
+
setattr(proxy, "PROXY_FORCE_NON_STREAM", False)
|
|
134
|
+
setattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT", False)
|
|
135
|
+
setattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL", True)
|
|
136
|
+
setattr(proxy, "PROXY_GUARDRAIL_RETRY", True)
|
|
137
|
+
|
|
138
|
+
selected = proxy._should_use_guarded_non_stream(
|
|
139
|
+
True,
|
|
140
|
+
{"tools": [{"name": "Read", "input_schema": {"type": "object"}}]},
|
|
141
|
+
{"tool_choice": "auto"},
|
|
142
|
+
)
|
|
143
|
+
self.assertFalse(selected)
|
|
144
|
+
finally:
|
|
145
|
+
setattr(proxy, "PROXY_FORCE_NON_STREAM", old_force)
|
|
146
|
+
setattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT", old_strict)
|
|
147
|
+
setattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL", old_guard)
|
|
148
|
+
setattr(proxy, "PROXY_GUARDRAIL_RETRY", old_retry)
|
|
149
|
+
|
|
150
|
+
|
|
103
151
|
class TestMalformedToolGuardrail(unittest.TestCase):
|
|
104
152
|
def test_detects_malformed_tool_payload(self):
|
|
105
153
|
openai_resp = {
|
|
@@ -805,6 +853,54 @@ class TestMalformedToolGuardrail(unittest.TestCase):
|
|
|
805
853
|
)
|
|
806
854
|
self.assertEqual(issue.kind, "required_tool_miss")
|
|
807
855
|
|
|
856
|
+
def test_required_tool_turn_with_long_text_without_tool_call_is_flagged(self):
|
|
857
|
+
openai_resp = {
|
|
858
|
+
"choices": [
|
|
859
|
+
{
|
|
860
|
+
"finish_reason": "stop",
|
|
861
|
+
"message": {
|
|
862
|
+
"content": (
|
|
863
|
+
"I reviewed the repository and here is a long explanation that still "
|
|
864
|
+
"does not include any valid tool call payload for this required turn."
|
|
865
|
+
),
|
|
866
|
+
"tool_calls": [],
|
|
867
|
+
},
|
|
868
|
+
}
|
|
869
|
+
]
|
|
870
|
+
}
|
|
871
|
+
anthropic_body = {
|
|
872
|
+
"tools": [{"name": "Edit", "input_schema": {"type": "object"}}],
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
issue = proxy._classify_tool_response_issue(
|
|
876
|
+
openai_resp, anthropic_body, required_tool_choice=True
|
|
877
|
+
)
|
|
878
|
+
self.assertEqual(issue.kind, "required_tool_miss")
|
|
879
|
+
|
|
880
|
+
def test_preflight_flags_repetitive_policy_echo_without_tool_call(self):
|
|
881
|
+
repeated = " (describe/it/expect using vitest" * 24
|
|
882
|
+
openai_resp = {
|
|
883
|
+
"choices": [
|
|
884
|
+
{
|
|
885
|
+
"finish_reason": "stop",
|
|
886
|
+
"message": {
|
|
887
|
+
"content": (
|
|
888
|
+
"- At least 2 new test cases before claiming done. "
|
|
889
|
+
"- Tests must be in test/ following existing patterns."
|
|
890
|
+
f"{repeated}"
|
|
891
|
+
),
|
|
892
|
+
"tool_calls": [],
|
|
893
|
+
},
|
|
894
|
+
}
|
|
895
|
+
]
|
|
896
|
+
}
|
|
897
|
+
anthropic_body = {
|
|
898
|
+
"tools": [{"name": "Read", "input_schema": {"type": "object"}}],
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
issue = proxy._classify_tool_response_issue(openai_resp, anthropic_body)
|
|
902
|
+
self.assertEqual(issue.kind, "malformed_payload")
|
|
903
|
+
|
|
808
904
|
def test_markup_repair_sanitizes_tool_arguments(self):
|
|
809
905
|
openai_resp = {
|
|
810
906
|
"choices": [
|