@miller-tech/uap 1.20.6 → 1.20.7
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
|
@@ -227,6 +227,9 @@ PROXY_MALFORMED_TOOL_RETRY_MAX_TOKENS = int(
|
|
|
227
227
|
PROXY_MALFORMED_TOOL_RETRY_TEMPERATURE = float(
|
|
228
228
|
os.environ.get("PROXY_MALFORMED_TOOL_RETRY_TEMPERATURE", "0")
|
|
229
229
|
)
|
|
230
|
+
PROXY_TOOL_TURN_TEMPERATURE = float(
|
|
231
|
+
os.environ.get("PROXY_TOOL_TURN_TEMPERATURE", "0.3")
|
|
232
|
+
)
|
|
230
233
|
PROXY_MALFORMED_TOOL_STREAM_STRICT = os.environ.get(
|
|
231
234
|
"PROXY_MALFORMED_TOOL_STREAM_STRICT", "off"
|
|
232
235
|
).lower() not in {
|
|
@@ -2208,6 +2211,17 @@ def build_openai_request(
|
|
|
2208
2211
|
if "stop_sequences" in anthropic_body:
|
|
2209
2212
|
openai_body["stop"] = anthropic_body["stop_sequences"]
|
|
2210
2213
|
|
|
2214
|
+
# Force controlled temperature for tool-call turns to reduce garbled output
|
|
2215
|
+
if has_tools:
|
|
2216
|
+
client_temp = openai_body.get("temperature")
|
|
2217
|
+
if client_temp is None or client_temp > PROXY_TOOL_TURN_TEMPERATURE:
|
|
2218
|
+
openai_body["temperature"] = PROXY_TOOL_TURN_TEMPERATURE
|
|
2219
|
+
logger.info(
|
|
2220
|
+
"TOOL TURN TEMP: forcing temperature=%.2f (was %s) for tool-enabled request",
|
|
2221
|
+
PROXY_TOOL_TURN_TEMPERATURE,
|
|
2222
|
+
client_temp,
|
|
2223
|
+
)
|
|
2224
|
+
|
|
2211
2225
|
# Convert Anthropic tools to OpenAI function-calling tools
|
|
2212
2226
|
if has_tools:
|
|
2213
2227
|
openai_body["tools"] = _convert_anthropic_tools_to_openai(
|
|
@@ -2655,6 +2669,91 @@ def _extract_tool_calls_from_text(text: str) -> tuple[list[dict], str]:
|
|
|
2655
2669
|
return extracted, remaining
|
|
2656
2670
|
|
|
2657
2671
|
|
|
2672
|
+
# Pattern: runaway closing braces like }}}}}
|
|
2673
|
+
_GARBLED_RUNAWAY_BRACES_RE = re.compile(r"\}{4,}")
|
|
2674
|
+
# Pattern: repetitive digit sequences like 000000 or 398859738398859738
|
|
2675
|
+
_GARBLED_REPETITIVE_DIGITS_RE = re.compile(r"(\d{3,})\1{2,}")
|
|
2676
|
+
# Pattern: long runs of zeros
|
|
2677
|
+
_GARBLED_ZEROS_RE = re.compile(r"0{8,}")
|
|
2678
|
+
# Pattern: extremely long unbroken digit strings (>30 digits)
|
|
2679
|
+
_GARBLED_LONG_DIGITS_RE = re.compile(r"\d{30,}")
|
|
2680
|
+
|
|
2681
|
+
|
|
2682
|
+
def _is_garbled_tool_arguments(arguments_str: str) -> bool:
|
|
2683
|
+
"""Detect garbled/degenerate tool call arguments.
|
|
2684
|
+
|
|
2685
|
+
Returns True if the arguments string shows signs of degenerate generation:
|
|
2686
|
+
- Runaway closing braces (}}}}})
|
|
2687
|
+
- Repetitive digit patterns (000000, 398859738398859738)
|
|
2688
|
+
- Extremely long digit strings
|
|
2689
|
+
- Unbalanced braces suggesting truncated/corrupt JSON
|
|
2690
|
+
"""
|
|
2691
|
+
if not arguments_str or arguments_str == "{}":
|
|
2692
|
+
return False
|
|
2693
|
+
|
|
2694
|
+
if _GARBLED_RUNAWAY_BRACES_RE.search(arguments_str):
|
|
2695
|
+
return True
|
|
2696
|
+
if _GARBLED_REPETITIVE_DIGITS_RE.search(arguments_str):
|
|
2697
|
+
return True
|
|
2698
|
+
if _GARBLED_ZEROS_RE.search(arguments_str):
|
|
2699
|
+
return True
|
|
2700
|
+
if _GARBLED_LONG_DIGITS_RE.search(arguments_str):
|
|
2701
|
+
return True
|
|
2702
|
+
|
|
2703
|
+
# Check brace balance — more than 2 unmatched braces suggests corruption
|
|
2704
|
+
open_count = arguments_str.count("{")
|
|
2705
|
+
close_count = arguments_str.count("}")
|
|
2706
|
+
if abs(open_count - close_count) > 2:
|
|
2707
|
+
return True
|
|
2708
|
+
|
|
2709
|
+
return False
|
|
2710
|
+
|
|
2711
|
+
|
|
2712
|
+
def _sanitize_garbled_tool_calls(openai_resp: dict) -> bool:
|
|
2713
|
+
"""Check tool calls in an OpenAI response for garbled arguments.
|
|
2714
|
+
|
|
2715
|
+
If garbled arguments are detected, removes the affected tool calls
|
|
2716
|
+
and logs a warning. Returns True if any tool calls were removed.
|
|
2717
|
+
"""
|
|
2718
|
+
choice = (openai_resp.get("choices") or [{}])[0]
|
|
2719
|
+
message = choice.get("message", {})
|
|
2720
|
+
tool_calls = message.get("tool_calls")
|
|
2721
|
+
if not tool_calls:
|
|
2722
|
+
return False
|
|
2723
|
+
|
|
2724
|
+
clean = []
|
|
2725
|
+
garbled_count = 0
|
|
2726
|
+
for tc in tool_calls:
|
|
2727
|
+
fn = tc.get("function", {})
|
|
2728
|
+
args_str = fn.get("arguments", "{}")
|
|
2729
|
+
if _is_garbled_tool_arguments(args_str):
|
|
2730
|
+
garbled_count += 1
|
|
2731
|
+
logger.warning(
|
|
2732
|
+
"GARBLED TOOL ARGS: name=%s args_preview=%.120s",
|
|
2733
|
+
fn.get("name", "?"),
|
|
2734
|
+
args_str,
|
|
2735
|
+
)
|
|
2736
|
+
else:
|
|
2737
|
+
clean.append(tc)
|
|
2738
|
+
|
|
2739
|
+
if garbled_count == 0:
|
|
2740
|
+
return False
|
|
2741
|
+
|
|
2742
|
+
if clean:
|
|
2743
|
+
message["tool_calls"] = clean
|
|
2744
|
+
else:
|
|
2745
|
+
# All tool calls were garbled — remove tool_calls entirely
|
|
2746
|
+
message.pop("tool_calls", None)
|
|
2747
|
+
choice["finish_reason"] = "stop"
|
|
2748
|
+
|
|
2749
|
+
logger.warning(
|
|
2750
|
+
"GARBLED TOOL ARGS: removed %d garbled tool call(s), %d clean remaining",
|
|
2751
|
+
garbled_count,
|
|
2752
|
+
len(clean),
|
|
2753
|
+
)
|
|
2754
|
+
return True
|
|
2755
|
+
|
|
2756
|
+
|
|
2658
2757
|
def _tool_schema_map_from_anthropic_body(anthropic_body: dict) -> dict[str, dict]:
|
|
2659
2758
|
schema_map: dict[str, dict] = {}
|
|
2660
2759
|
for tool in anthropic_body.get("tools", []) or []:
|
|
@@ -4048,6 +4147,8 @@ def openai_to_anthropic_response(openai_resp: dict, model: str) -> dict:
|
|
|
4048
4147
|
"""Convert an OpenAI Chat Completions response to Anthropic Messages format."""
|
|
4049
4148
|
# First: try to recover tool calls trapped in text XML tags
|
|
4050
4149
|
_maybe_extract_text_tool_calls(openai_resp)
|
|
4150
|
+
# Second: strip garbled/degenerate tool call arguments
|
|
4151
|
+
_sanitize_garbled_tool_calls(openai_resp)
|
|
4051
4152
|
|
|
4052
4153
|
choice = openai_resp.get("choices", [{}])[0]
|
|
4053
4154
|
message = choice.get("message", {})
|
|
@@ -2925,6 +2925,108 @@ class TestToolCallXMLExtraction(unittest.TestCase):
|
|
|
2925
2925
|
self.assertEqual(anthropic["stop_reason"], "tool_use")
|
|
2926
2926
|
|
|
2927
2927
|
|
|
2928
|
+
class TestGarbledToolArgDetection(unittest.TestCase):
|
|
2929
|
+
"""Tests for detecting and sanitizing garbled tool call arguments."""
|
|
2930
|
+
|
|
2931
|
+
def test_runaway_braces_detected(self):
|
|
2932
|
+
self.assertTrue(proxy._is_garbled_tool_arguments('{"command":"echo test}}}}}'))
|
|
2933
|
+
|
|
2934
|
+
def test_repetitive_digits_detected(self):
|
|
2935
|
+
self.assertTrue(proxy._is_garbled_tool_arguments('{"command":"echo 398398398398398398"}'))
|
|
2936
|
+
|
|
2937
|
+
def test_long_zeros_detected(self):
|
|
2938
|
+
self.assertTrue(proxy._is_garbled_tool_arguments('{"command":"echo 00000000000"}'))
|
|
2939
|
+
|
|
2940
|
+
def test_extremely_long_digits_detected(self):
|
|
2941
|
+
self.assertTrue(proxy._is_garbled_tool_arguments('{"x":"' + "1" * 35 + '"}'))
|
|
2942
|
+
|
|
2943
|
+
def test_unbalanced_braces_detected(self):
|
|
2944
|
+
self.assertTrue(proxy._is_garbled_tool_arguments('{"a":{"b":{"c":"d"'))
|
|
2945
|
+
|
|
2946
|
+
def test_normal_args_not_flagged(self):
|
|
2947
|
+
self.assertFalse(proxy._is_garbled_tool_arguments('{"command":"ls -la /tmp"}'))
|
|
2948
|
+
self.assertFalse(proxy._is_garbled_tool_arguments('{"file_path":"/home/user/test.py"}'))
|
|
2949
|
+
|
|
2950
|
+
def test_empty_args_not_flagged(self):
|
|
2951
|
+
self.assertFalse(proxy._is_garbled_tool_arguments("{}"))
|
|
2952
|
+
self.assertFalse(proxy._is_garbled_tool_arguments(""))
|
|
2953
|
+
|
|
2954
|
+
def test_sanitize_removes_garbled_calls(self):
|
|
2955
|
+
openai_resp = {
|
|
2956
|
+
"choices": [{
|
|
2957
|
+
"finish_reason": "tool_calls",
|
|
2958
|
+
"message": {
|
|
2959
|
+
"tool_calls": [
|
|
2960
|
+
{"function": {"name": "Bash", "arguments": '{"command":"ls"}'}},
|
|
2961
|
+
{"function": {"name": "Bash", "arguments": '{"command":"echo test}}}}}}'}},
|
|
2962
|
+
],
|
|
2963
|
+
},
|
|
2964
|
+
}]
|
|
2965
|
+
}
|
|
2966
|
+
removed = proxy._sanitize_garbled_tool_calls(openai_resp)
|
|
2967
|
+
self.assertTrue(removed)
|
|
2968
|
+
msg = openai_resp["choices"][0]["message"]
|
|
2969
|
+
self.assertEqual(len(msg["tool_calls"]), 1)
|
|
2970
|
+
self.assertEqual(msg["tool_calls"][0]["function"]["name"], "Bash")
|
|
2971
|
+
|
|
2972
|
+
def test_sanitize_all_garbled_removes_tool_calls(self):
|
|
2973
|
+
openai_resp = {
|
|
2974
|
+
"choices": [{
|
|
2975
|
+
"finish_reason": "tool_calls",
|
|
2976
|
+
"message": {
|
|
2977
|
+
"tool_calls": [
|
|
2978
|
+
{"function": {"name": "Bash", "arguments": '{"command":"echo }}}}}}'}},
|
|
2979
|
+
],
|
|
2980
|
+
},
|
|
2981
|
+
}]
|
|
2982
|
+
}
|
|
2983
|
+
removed = proxy._sanitize_garbled_tool_calls(openai_resp)
|
|
2984
|
+
self.assertTrue(removed)
|
|
2985
|
+
msg = openai_resp["choices"][0]["message"]
|
|
2986
|
+
self.assertNotIn("tool_calls", msg)
|
|
2987
|
+
self.assertEqual(openai_resp["choices"][0]["finish_reason"], "stop")
|
|
2988
|
+
|
|
2989
|
+
def test_sanitize_clean_args_noop(self):
|
|
2990
|
+
openai_resp = {
|
|
2991
|
+
"choices": [{
|
|
2992
|
+
"finish_reason": "tool_calls",
|
|
2993
|
+
"message": {
|
|
2994
|
+
"tool_calls": [
|
|
2995
|
+
{"function": {"name": "Read", "arguments": '{"file_path":"/x.py"}'}},
|
|
2996
|
+
],
|
|
2997
|
+
},
|
|
2998
|
+
}]
|
|
2999
|
+
}
|
|
3000
|
+
removed = proxy._sanitize_garbled_tool_calls(openai_resp)
|
|
3001
|
+
self.assertFalse(removed)
|
|
3002
|
+
|
|
3003
|
+
|
|
3004
|
+
class TestToolTurnTemperature(unittest.TestCase):
|
|
3005
|
+
"""Tests for per-request temperature forcing on tool-enabled turns."""
|
|
3006
|
+
|
|
3007
|
+
def _make_monitor(self):
|
|
3008
|
+
return proxy.SessionMonitor()
|
|
3009
|
+
|
|
3010
|
+
def test_tool_turn_forces_temperature(self):
|
|
3011
|
+
body = {
|
|
3012
|
+
"model": "qwen3.5",
|
|
3013
|
+
"messages": [{"role": "user", "content": "hello"}],
|
|
3014
|
+
"tools": [{"name": "Bash", "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}}}],
|
|
3015
|
+
"temperature": 0.8,
|
|
3016
|
+
}
|
|
3017
|
+
result = proxy.build_openai_request(body, self._make_monitor())
|
|
3018
|
+
self.assertLessEqual(result["temperature"], proxy.PROXY_TOOL_TURN_TEMPERATURE)
|
|
3019
|
+
|
|
3020
|
+
def test_no_tools_preserves_temperature(self):
|
|
3021
|
+
body = {
|
|
3022
|
+
"model": "qwen3.5",
|
|
3023
|
+
"messages": [{"role": "user", "content": "hello"}],
|
|
3024
|
+
"temperature": 0.8,
|
|
3025
|
+
}
|
|
3026
|
+
result = proxy.build_openai_request(body, self._make_monitor())
|
|
3027
|
+
self.assertEqual(result["temperature"], 0.8)
|
|
3028
|
+
|
|
3029
|
+
|
|
2928
3030
|
if __name__ == "__main__":
|
|
2929
3031
|
unittest.main()
|
|
2930
3032
|
|