@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.20.6",
3
+ "version": "1.20.7",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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