@miller-tech/uap 1.15.0 → 1.15.2

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.15.0",
3
+ "version": "1.15.2",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -162,7 +162,7 @@ PROXY_MALFORMED_TOOL_GUARDRAIL = os.environ.get(
162
162
  "no",
163
163
  }
164
164
  PROXY_MALFORMED_TOOL_RETRY_MAX = int(
165
- os.environ.get("PROXY_MALFORMED_TOOL_RETRY_MAX", "1")
165
+ os.environ.get("PROXY_MALFORMED_TOOL_RETRY_MAX", "2")
166
166
  )
167
167
  PROXY_MALFORMED_TOOL_RETRY_MAX_TOKENS = int(
168
168
  os.environ.get("PROXY_MALFORMED_TOOL_RETRY_MAX_TOKENS", "2048")
@@ -898,6 +898,28 @@ def _extract_text(content) -> str:
898
898
  return str(content)
899
899
 
900
900
 
901
+ _TOOL_CALL_APOLOGY_MARKERS = (
902
+ "i could not produce a valid tool-call format in this turn",
903
+ "i will issue exactly one valid tool call next",
904
+ )
905
+
906
+ _TOOL_CALL_RETRY_MESSAGE = (
907
+ "Tool-call formatting failed after automatic retries. "
908
+ "Please retry the same request."
909
+ )
910
+
911
+
912
+ def _contains_tool_call_apology(text: str) -> bool:
913
+ if not text:
914
+ return False
915
+ lowered = text.lower()
916
+ return any(marker in lowered for marker in _TOOL_CALL_APOLOGY_MARKERS)
917
+
918
+
919
+ def _sanitize_tool_call_apology_text(text: str) -> str:
920
+ return _TOOL_CALL_RETRY_MESSAGE if _contains_tool_call_apology(text) else text
921
+
922
+
901
923
  def _has_tool_definitions(anthropic_body: dict) -> bool:
902
924
  tools = anthropic_body.get("tools")
903
925
  return isinstance(tools, list) and len(tools) > 0
@@ -1647,6 +1669,9 @@ def _looks_malformed_tool_payload(text: str) -> bool:
1647
1669
  return False
1648
1670
 
1649
1671
  lowered = text.lower()
1672
+ if _contains_tool_call_apology(text):
1673
+ return True
1674
+
1650
1675
  primary_markers = ("</parameter", "<parameter", "<tool_call", "<function=")
1651
1676
  if any(marker in lowered for marker in primary_markers):
1652
1677
  return True
@@ -1705,6 +1730,18 @@ def _build_malformed_retry_body(openai_body: dict, anthropic_body: dict) -> dict
1705
1730
  retry_body["tool_choice"] = "required"
1706
1731
  retry_body["temperature"] = PROXY_MALFORMED_TOOL_RETRY_TEMPERATURE
1707
1732
 
1733
+ malformed_retry_instruction = {
1734
+ "role": "user",
1735
+ "content": (
1736
+ "Your previous response had invalid tool-call formatting. "
1737
+ "Respond with exactly one valid tool call using the provided tools. "
1738
+ "Do not output prose, markdown, XML tags, or schema snippets."
1739
+ ),
1740
+ }
1741
+ existing_messages = retry_body.get("messages")
1742
+ if isinstance(existing_messages, list) and existing_messages:
1743
+ retry_body["messages"] = [*existing_messages, malformed_retry_instruction]
1744
+
1708
1745
  if PROXY_MALFORMED_TOOL_RETRY_MAX_TOKENS > 0:
1709
1746
  current_max = int(
1710
1747
  retry_body.get("max_tokens", PROXY_MALFORMED_TOOL_RETRY_MAX_TOKENS)
@@ -1737,10 +1774,7 @@ def _build_clean_guardrail_openai_response(openai_resp: dict) -> dict:
1737
1774
  "finish_reason": "stop",
1738
1775
  "message": {
1739
1776
  "role": "assistant",
1740
- "content": (
1741
- "I could not produce a valid tool-call format in this turn. "
1742
- "Please continue; I will issue exactly one valid tool call next."
1743
- ),
1777
+ "content": _TOOL_CALL_RETRY_MESSAGE,
1744
1778
  },
1745
1779
  }
1746
1780
  ],
@@ -1940,7 +1974,17 @@ def openai_to_anthropic_response(openai_resp: dict, model: str) -> dict:
1940
1974
 
1941
1975
  content = []
1942
1976
  if message.get("content"):
1943
- content.append({"type": "text", "text": message["content"]})
1977
+ raw_text = (
1978
+ message["content"]
1979
+ if isinstance(message["content"], str)
1980
+ else str(message["content"])
1981
+ )
1982
+ sanitized_text = _sanitize_tool_call_apology_text(raw_text)
1983
+ if sanitized_text != raw_text:
1984
+ logger.warning(
1985
+ "SANITIZE: replaced known malformed tool-call apology text in assistant response"
1986
+ )
1987
+ content.append({"type": "text", "text": sanitized_text})
1944
1988
 
1945
1989
  # Convert tool calls
1946
1990
  for tc in message.get("tool_calls", []):
@@ -164,6 +164,35 @@ class TestMalformedToolGuardrail(unittest.TestCase):
164
164
  }
165
165
  self.assertTrue(proxy._is_malformed_tool_response(openai_resp, anthropic_body))
166
166
 
167
+ def test_detects_tool_call_apology_text_as_malformed(self):
168
+ openai_resp = {
169
+ "choices": [
170
+ {
171
+ "finish_reason": "stop",
172
+ "message": {
173
+ "content": (
174
+ "I could not produce a valid tool-call format in this turn. "
175
+ "Please continue; I will issue exactly one valid tool call next."
176
+ ),
177
+ "tool_calls": [],
178
+ },
179
+ }
180
+ ]
181
+ }
182
+ anthropic_body = {
183
+ "tools": [{"name": "Read", "input_schema": {"type": "object"}}],
184
+ "messages": [{"role": "user", "content": "fix this"}],
185
+ }
186
+ self.assertTrue(proxy._is_malformed_tool_response(openai_resp, anthropic_body))
187
+
188
+ def test_tool_call_apology_helper_detects_phrase(self):
189
+ apology_text = (
190
+ "I could not produce a valid tool-call format in this turn. "
191
+ "Please continue; I will issue exactly one valid tool call next."
192
+ )
193
+ self.assertTrue(proxy._contains_tool_call_apology(apology_text))
194
+ self.assertFalse(proxy._contains_tool_call_apology("normal assistant response"))
195
+
167
196
  def test_clean_tool_call_response_is_not_malformed(self):
168
197
  openai_resp = {
169
198
  "choices": [
@@ -385,6 +414,7 @@ class TestMalformedToolGuardrail(unittest.TestCase):
385
414
  openai_body = {
386
415
  "model": "test",
387
416
  "max_tokens": 4000,
417
+ "messages": [{"role": "user", "content": "fix the issue"}],
388
418
  "tools": [{"type": "function", "function": {"name": "Read"}}],
389
419
  }
390
420
  anthropic_body = {
@@ -402,11 +432,45 @@ class TestMalformedToolGuardrail(unittest.TestCase):
402
432
  self.assertEqual(retry["max_tokens"], 512)
403
433
  self.assertEqual(len(retry["tools"]), 3)
404
434
  self.assertFalse(retry["enable_thinking"])
435
+ self.assertEqual(retry["messages"][-1]["role"], "user")
436
+ self.assertIn(
437
+ "invalid tool-call formatting",
438
+ retry["messages"][-1]["content"],
439
+ )
405
440
  finally:
406
441
  setattr(proxy, "PROXY_MALFORMED_TOOL_RETRY_MAX_TOKENS", old_cap)
407
442
  setattr(proxy, "PROXY_MALFORMED_TOOL_RETRY_TEMPERATURE", old_temp)
408
443
  setattr(proxy, "PROXY_DISABLE_THINKING_ON_TOOL_TURNS", old_disable)
409
444
 
445
+ def test_clean_guardrail_response_does_not_promise_future_tool_call(self):
446
+ guardrail = proxy._build_clean_guardrail_openai_response(
447
+ {"model": "test-model"}
448
+ )
449
+ text = guardrail["choices"][0]["message"]["content"]
450
+ self.assertIn("Please retry the same request", text)
451
+ self.assertNotIn("I will issue exactly one valid tool call next", text)
452
+
453
+ def test_openai_to_anthropic_response_sanitizes_tool_call_apology(self):
454
+ openai_resp = {
455
+ "choices": [
456
+ {
457
+ "finish_reason": "stop",
458
+ "message": {
459
+ "content": (
460
+ "I could not produce a valid tool-call format in this turn. "
461
+ "Please continue; I will issue exactly one valid tool call next."
462
+ ),
463
+ "tool_calls": [],
464
+ },
465
+ }
466
+ ]
467
+ }
468
+
469
+ converted = proxy.openai_to_anthropic_response(openai_resp, "test-model")
470
+ text = converted["content"][0]["text"]
471
+ self.assertIn("Please retry the same request", text)
472
+ self.assertNotIn("I will issue exactly one valid tool call next", text)
473
+
410
474
 
411
475
  class TestToolTurnControls(unittest.TestCase):
412
476
  def test_tool_narrowing_reduces_tool_count(self):