@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
|
@@ -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", "
|
|
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
|
-
|
|
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):
|