@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.15.10",
3
+ "version": "1.15.12",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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": tool.get("input_schema", {}),
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 = is_stream and (
3554
- PROXY_FORCE_NON_STREAM
3555
- or (PROXY_MALFORMED_TOOL_STREAM_STRICT and "tools" in body)
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
- else:
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 = {