@miller-tech/uap 1.15.10 → 1.15.12
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"
|
|
@@ -1321,19 +1343,56 @@ def _last_user_has_tool_result(anthropic_body: dict) -> bool:
|
|
|
1321
1343
|
return False
|
|
1322
1344
|
|
|
1323
1345
|
|
|
1346
|
+
def _sanitize_tool_schema_for_llama(schema):
|
|
1347
|
+
"""Remove JSON Schema keywords that generate unsupported regex grammar.
|
|
1348
|
+
|
|
1349
|
+
llama.cpp's tool grammar generator can fail on regex-heavy schema fields
|
|
1350
|
+
such as "pattern" and "patternProperties" (for example "\\w").
|
|
1351
|
+
"""
|
|
1352
|
+
|
|
1353
|
+
removed = 0
|
|
1354
|
+
|
|
1355
|
+
def _walk(node):
|
|
1356
|
+
nonlocal removed
|
|
1357
|
+
if isinstance(node, dict):
|
|
1358
|
+
cleaned = {}
|
|
1359
|
+
for key, value in node.items():
|
|
1360
|
+
if key in {"pattern", "patternProperties"}:
|
|
1361
|
+
removed += 1
|
|
1362
|
+
continue
|
|
1363
|
+
cleaned[key] = _walk(value)
|
|
1364
|
+
return cleaned
|
|
1365
|
+
if isinstance(node, list):
|
|
1366
|
+
return [_walk(item) for item in node]
|
|
1367
|
+
return node
|
|
1368
|
+
|
|
1369
|
+
return _walk(schema), removed
|
|
1370
|
+
|
|
1371
|
+
|
|
1324
1372
|
def _convert_anthropic_tools_to_openai(anthropic_tools: list[dict]) -> list[dict]:
|
|
1325
1373
|
converted = []
|
|
1374
|
+
removed_pattern_fields = 0
|
|
1326
1375
|
for tool in anthropic_tools:
|
|
1376
|
+
input_schema, removed = _sanitize_tool_schema_for_llama(
|
|
1377
|
+
tool.get("input_schema", {})
|
|
1378
|
+
)
|
|
1379
|
+
removed_pattern_fields += removed
|
|
1327
1380
|
converted.append(
|
|
1328
1381
|
{
|
|
1329
1382
|
"type": "function",
|
|
1330
1383
|
"function": {
|
|
1331
1384
|
"name": tool.get("name", ""),
|
|
1332
1385
|
"description": tool.get("description", ""),
|
|
1333
|
-
"parameters":
|
|
1386
|
+
"parameters": input_schema,
|
|
1334
1387
|
},
|
|
1335
1388
|
}
|
|
1336
1389
|
)
|
|
1390
|
+
if removed_pattern_fields > 0:
|
|
1391
|
+
logger.warning(
|
|
1392
|
+
"TOOL SCHEMA SANITIZE: removed %d regex pattern fields from %d tools",
|
|
1393
|
+
removed_pattern_fields,
|
|
1394
|
+
len(anthropic_tools),
|
|
1395
|
+
)
|
|
1337
1396
|
return converted
|
|
1338
1397
|
|
|
1339
1398
|
|
|
@@ -3550,9 +3609,10 @@ async def messages(request: Request):
|
|
|
3550
3609
|
media_type="application/json",
|
|
3551
3610
|
)
|
|
3552
3611
|
|
|
3553
|
-
use_guarded_non_stream =
|
|
3554
|
-
|
|
3555
|
-
|
|
3612
|
+
use_guarded_non_stream = _should_use_guarded_non_stream(
|
|
3613
|
+
is_stream,
|
|
3614
|
+
body,
|
|
3615
|
+
openai_body,
|
|
3556
3616
|
)
|
|
3557
3617
|
if use_guarded_non_stream:
|
|
3558
3618
|
strict_body = dict(openai_body)
|
|
@@ -3623,10 +3683,14 @@ async def messages(request: Request):
|
|
|
3623
3683
|
logger.info(
|
|
3624
3684
|
"FORCED NON-STREAM: served stream response via guarded non-stream path"
|
|
3625
3685
|
)
|
|
3626
|
-
|
|
3686
|
+
elif PROXY_MALFORMED_TOOL_STREAM_STRICT and _has_tool_definitions(body):
|
|
3627
3687
|
logger.info(
|
|
3628
3688
|
"STRICT STREAM GUARDRAIL: served stream response via guarded non-stream path"
|
|
3629
3689
|
)
|
|
3690
|
+
else:
|
|
3691
|
+
logger.info(
|
|
3692
|
+
"REQUIRED TOOL STREAM GUARDRAIL: served stream response via guarded non-stream path"
|
|
3693
|
+
)
|
|
3630
3694
|
|
|
3631
3695
|
return StreamingResponse(
|
|
3632
3696
|
stream_anthropic_message(anthropic_resp),
|
|
@@ -100,6 +100,109 @@ class TestProxyConfigTuning(unittest.TestCase):
|
|
|
100
100
|
setattr(proxy, "PROXY_CONTEXT_PRUNE_TARGET_FRACTION", old_target)
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
class TestToolSchemaSanitization(unittest.TestCase):
|
|
104
|
+
def test_convert_tools_strips_pattern_fields(self):
|
|
105
|
+
anthropic_tools = [
|
|
106
|
+
{
|
|
107
|
+
"name": "Sample",
|
|
108
|
+
"description": "test",
|
|
109
|
+
"input_schema": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"properties": {
|
|
112
|
+
"id": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"pattern": "^[\\w-]+$",
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"required": ["id"],
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
converted = proxy._convert_anthropic_tools_to_openai(anthropic_tools)
|
|
123
|
+
params = converted[0]["function"]["parameters"]
|
|
124
|
+
self.assertEqual(params["properties"]["id"]["type"], "string")
|
|
125
|
+
self.assertNotIn("pattern", params["properties"]["id"])
|
|
126
|
+
|
|
127
|
+
def test_convert_tools_strips_pattern_properties_fields(self):
|
|
128
|
+
anthropic_tools = [
|
|
129
|
+
{
|
|
130
|
+
"name": "Sample",
|
|
131
|
+
"description": "test",
|
|
132
|
+
"input_schema": {
|
|
133
|
+
"type": "object",
|
|
134
|
+
"patternProperties": {
|
|
135
|
+
"^x-": {"type": "string"},
|
|
136
|
+
},
|
|
137
|
+
"properties": {
|
|
138
|
+
"meta": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"properties": {
|
|
141
|
+
"tag": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"pattern": "^[a-z]+$",
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
converted = proxy._convert_anthropic_tools_to_openai(anthropic_tools)
|
|
153
|
+
params = converted[0]["function"]["parameters"]
|
|
154
|
+
self.assertNotIn("patternProperties", params)
|
|
155
|
+
self.assertNotIn("pattern", params["properties"]["meta"]["properties"]["tag"])
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TestStreamGuardedPathSelection(unittest.TestCase):
|
|
159
|
+
def test_required_tool_turn_uses_guarded_non_stream(self):
|
|
160
|
+
old_force = getattr(proxy, "PROXY_FORCE_NON_STREAM")
|
|
161
|
+
old_strict = getattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT")
|
|
162
|
+
old_guard = getattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL")
|
|
163
|
+
old_retry = getattr(proxy, "PROXY_GUARDRAIL_RETRY")
|
|
164
|
+
try:
|
|
165
|
+
setattr(proxy, "PROXY_FORCE_NON_STREAM", False)
|
|
166
|
+
setattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT", False)
|
|
167
|
+
setattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL", True)
|
|
168
|
+
setattr(proxy, "PROXY_GUARDRAIL_RETRY", True)
|
|
169
|
+
|
|
170
|
+
selected = proxy._should_use_guarded_non_stream(
|
|
171
|
+
True,
|
|
172
|
+
{"tools": [{"name": "Read", "input_schema": {"type": "object"}}]},
|
|
173
|
+
{"tool_choice": "required"},
|
|
174
|
+
)
|
|
175
|
+
self.assertTrue(selected)
|
|
176
|
+
finally:
|
|
177
|
+
setattr(proxy, "PROXY_FORCE_NON_STREAM", old_force)
|
|
178
|
+
setattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT", old_strict)
|
|
179
|
+
setattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL", old_guard)
|
|
180
|
+
setattr(proxy, "PROXY_GUARDRAIL_RETRY", old_retry)
|
|
181
|
+
|
|
182
|
+
def test_auto_tool_turn_keeps_true_stream_when_strict_off(self):
|
|
183
|
+
old_force = getattr(proxy, "PROXY_FORCE_NON_STREAM")
|
|
184
|
+
old_strict = getattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT")
|
|
185
|
+
old_guard = getattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL")
|
|
186
|
+
old_retry = getattr(proxy, "PROXY_GUARDRAIL_RETRY")
|
|
187
|
+
try:
|
|
188
|
+
setattr(proxy, "PROXY_FORCE_NON_STREAM", False)
|
|
189
|
+
setattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT", False)
|
|
190
|
+
setattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL", True)
|
|
191
|
+
setattr(proxy, "PROXY_GUARDRAIL_RETRY", True)
|
|
192
|
+
|
|
193
|
+
selected = proxy._should_use_guarded_non_stream(
|
|
194
|
+
True,
|
|
195
|
+
{"tools": [{"name": "Read", "input_schema": {"type": "object"}}]},
|
|
196
|
+
{"tool_choice": "auto"},
|
|
197
|
+
)
|
|
198
|
+
self.assertFalse(selected)
|
|
199
|
+
finally:
|
|
200
|
+
setattr(proxy, "PROXY_FORCE_NON_STREAM", old_force)
|
|
201
|
+
setattr(proxy, "PROXY_MALFORMED_TOOL_STREAM_STRICT", old_strict)
|
|
202
|
+
setattr(proxy, "PROXY_MALFORMED_TOOL_GUARDRAIL", old_guard)
|
|
203
|
+
setattr(proxy, "PROXY_GUARDRAIL_RETRY", old_retry)
|
|
204
|
+
|
|
205
|
+
|
|
103
206
|
class TestMalformedToolGuardrail(unittest.TestCase):
|
|
104
207
|
def test_detects_malformed_tool_payload(self):
|
|
105
208
|
openai_resp = {
|