@miller-tech/uap 1.15.11 → 1.15.13

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.11",
3
+ "version": "1.15.13",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1343,19 +1343,65 @@ def _last_user_has_tool_result(anthropic_body: dict) -> bool:
1343
1343
  return False
1344
1344
 
1345
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
+ property_map_keys = {"properties", "definitions", "$defs", "dependentSchemas"}
1355
+
1356
+ def _walk(node, parent_key=None):
1357
+ nonlocal removed
1358
+ if isinstance(node, dict):
1359
+ cleaned = {}
1360
+ for key, value in node.items():
1361
+ key_is_property_name = parent_key in property_map_keys
1362
+ if (
1363
+ key == "pattern"
1364
+ and isinstance(value, str)
1365
+ and not key_is_property_name
1366
+ ):
1367
+ removed += 1
1368
+ continue
1369
+ if key == "patternProperties" and not key_is_property_name:
1370
+ removed += 1
1371
+ continue
1372
+ cleaned[key] = _walk(value, key)
1373
+ return cleaned
1374
+ if isinstance(node, list):
1375
+ return [_walk(item, parent_key) for item in node]
1376
+ return node
1377
+
1378
+ return _walk(schema), removed
1379
+
1380
+
1346
1381
  def _convert_anthropic_tools_to_openai(anthropic_tools: list[dict]) -> list[dict]:
1347
1382
  converted = []
1383
+ removed_pattern_fields = 0
1348
1384
  for tool in anthropic_tools:
1385
+ input_schema, removed = _sanitize_tool_schema_for_llama(
1386
+ tool.get("input_schema", {})
1387
+ )
1388
+ removed_pattern_fields += removed
1349
1389
  converted.append(
1350
1390
  {
1351
1391
  "type": "function",
1352
1392
  "function": {
1353
1393
  "name": tool.get("name", ""),
1354
1394
  "description": tool.get("description", ""),
1355
- "parameters": tool.get("input_schema", {}),
1395
+ "parameters": input_schema,
1356
1396
  },
1357
1397
  }
1358
1398
  )
1399
+ if removed_pattern_fields > 0:
1400
+ logger.warning(
1401
+ "TOOL SCHEMA SANITIZE: removed %d regex pattern fields from %d tools",
1402
+ removed_pattern_fields,
1403
+ len(anthropic_tools),
1404
+ )
1359
1405
  return converted
1360
1406
 
1361
1407
 
@@ -100,6 +100,85 @@ 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
+ def test_convert_tools_keeps_property_named_pattern(self):
158
+ anthropic_tools = [
159
+ {
160
+ "name": "ScheduleTool",
161
+ "description": "test",
162
+ "input_schema": {
163
+ "type": "object",
164
+ "required": ["pattern", "subject"],
165
+ "properties": {
166
+ "pattern": {
167
+ "type": "string",
168
+ "description": "User-provided matching pattern",
169
+ },
170
+ "subject": {"type": "string"},
171
+ },
172
+ },
173
+ }
174
+ ]
175
+
176
+ converted = proxy._convert_anthropic_tools_to_openai(anthropic_tools)
177
+ params = converted[0]["function"]["parameters"]
178
+ self.assertIn("pattern", params["required"])
179
+ self.assertEqual(params["properties"]["pattern"]["type"], "string")
180
+
181
+
103
182
  class TestStreamGuardedPathSelection(unittest.TestCase):
104
183
  def test_required_tool_turn_uses_guarded_non_stream(self):
105
184
  old_force = getattr(proxy, "PROXY_FORCE_NON_STREAM")