@miller-tech/uap 1.217.0 → 1.219.0

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.
Files changed (38) hide show
  1. package/config/llama-patches/dflash2-pr27342.patch +1365 -0
  2. package/config/llama-profiles/qwen38-27b-dflash2.env +211 -0
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/cli/deliver.js +16 -0
  5. package/dist/cli/deliver.js.map +1 -1
  6. package/dist/cli/guided-setup.d.ts.map +1 -1
  7. package/dist/cli/guided-setup.js +8 -6
  8. package/dist/cli/guided-setup.js.map +1 -1
  9. package/dist/cli/hooks.d.ts.map +1 -1
  10. package/dist/cli/hooks.js +32 -0
  11. package/dist/cli/hooks.js.map +1 -1
  12. package/dist/cli/wizard-config.d.ts +2 -2
  13. package/dist/cli/wizard-config.d.ts.map +1 -1
  14. package/dist/config/settings-registry.d.ts.map +1 -1
  15. package/dist/config/settings-registry.js +22 -7
  16. package/dist/config/settings-registry.js.map +1 -1
  17. package/dist/coordination/reactor.d.ts +8 -0
  18. package/dist/coordination/reactor.d.ts.map +1 -1
  19. package/dist/coordination/reactor.js +43 -12
  20. package/dist/coordination/reactor.js.map +1 -1
  21. package/dist/mcp-router/tools/deliver.js +1 -1
  22. package/dist/mcp-router/tools/deliver.js.map +1 -1
  23. package/dist/types/config.d.ts +40 -16
  24. package/dist/types/config.d.ts.map +1 -1
  25. package/dist/types/config.js +10 -2
  26. package/dist/types/config.js.map +1 -1
  27. package/docs/guides/QWEN36_LLAMACPP.md +96 -37
  28. package/docs/reference/CONFIGURATION_REFERENCE.md +44 -8
  29. package/package.json +1 -1
  30. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  31. package/src/policies/enforcers/delivery_enforcement.py +226 -8
  32. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  33. package/templates/hooks/deliver_autoroute.py +2 -2
  34. package/templates/hooks/escalation_tracker.py +252 -0
  35. package/templates/hooks/stop.sh +10 -0
  36. package/templates/hooks/uap-policy-gate.sh +5 -5
  37. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  38. package/tools/agents/scripts/anthropic_proxy.py +112 -4
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env python3
2
+ """escalation-tracker: the evidence store behind the `escalate` delivery mode.
3
+
4
+ Under `delivery.enforcement: escalate` (or `delivery.localMode: escalate`) a
5
+ coding agent makes ordinary source edits DIRECTLY and `uap deliver` becomes an
6
+ ESCALATION POINT instead of the path every edit is forced through. The gate
7
+ (delivery_enforcement.py) decides *when* to escalate from the state kept here:
8
+
9
+ .uap/escalation-state.json
10
+ failures consecutive verification failures since the last green
11
+ last_failure {ts, source, detail} — what failed, shown to the agent
12
+ last_green ts of the last passing verification
13
+ edits_since_green {relpath: n} — per-file churn with no green in between
14
+ updated ts of the last write
15
+
16
+ Writers (all best-effort, never block):
17
+ escalation_tracker.py fail --source verify|bash|stop --detail "<text>"
18
+ escalation_tracker.py pass --source verify|bash|stop
19
+ escalation_tracker.py edit --file <path> (the gate records this itself)
20
+ escalation_tracker.py reset (after a deliver run lands)
21
+ escalation_tracker.py status [--json]
22
+ escalation_tracker.py classify-bash --command "<cmd>" --output "<text>"
23
+ -> prints pass|fail|none for a build/test command's output, and records it
24
+
25
+ The project root is UAP_MAIN_ROOT (the checkout holding .uap/), else cwd.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import json
31
+ import os
32
+ import re
33
+ import sys
34
+ import time
35
+ from pathlib import Path
36
+
37
+ STATE_FILE = "escalation-state.json"
38
+ DETAIL_MAX = 1200
39
+
40
+ # A shell command that exercises the project's real gates. Only these commands'
41
+ # outcomes count as verification evidence — `ls` failing is not a gate failure.
42
+ _BUILD_TEST_RE = re.compile(
43
+ r"(?:^|[;&|]\s*)(?:"
44
+ r"cargo\s+(?:build|check|test|clippy)"
45
+ r"|npm\s+(?:run\s+)?(?:build|test|lint|typecheck|check)"
46
+ r"|(?:pnpm|yarn|bun)\s+(?:run\s+)?(?:build|test|lint|typecheck|check)"
47
+ r"|(?:npx\s+)?(?:tsc|vitest|jest|mocha|eslint)\b"
48
+ r"|(?:python3?\s+-m\s+)?pytest\b"
49
+ r"|go\s+(?:build|test|vet)"
50
+ r"|make(?:\s+(?:test|build|check|all))?\s*(?:$|[;&|])"
51
+ r"|mvn\s+(?:test|package|verify|compile)"
52
+ r"|gradle(?:w)?\s+(?:test|build|check)"
53
+ r"|dotnet\s+(?:build|test)"
54
+ r"|uap\s+verify\b"
55
+ r")",
56
+ re.IGNORECASE,
57
+ )
58
+ _FAIL_MARKERS = (
59
+ "error[e", # rustc
60
+ "could not compile",
61
+ "test result: failed",
62
+ "failed to compile",
63
+ "npm err!",
64
+ "error ts", # tsc: error TS2345
65
+ "assertionerror",
66
+ "traceback (most recent call last)",
67
+ "build failed",
68
+ "fatal error",
69
+ "compilation failed",
70
+ "panicked at",
71
+ "exit status 1",
72
+ "exit code 1",
73
+ )
74
+ _PASS_MARKERS = (
75
+ "test result: ok",
76
+ "finished `dev`", "finished `release`", "finished dev", "finished release",
77
+ "tests passed", "passed", "all tests passed",
78
+ "compiled successfully", "build succeeded", "build successful",
79
+ "0 errors", "no errors", "ok\n", " ok ",
80
+ "runtime gate: pass", "all gates pass", "verify: pass",
81
+ )
82
+
83
+
84
+ def root_dir() -> Path:
85
+ """The MAIN checkout's root — the same anchor the policy gate uses.
86
+
87
+ The gate resolves MAIN_ROOT by stripping `/.worktrees/NNN-slug` and points
88
+ the enforcer there, so evidence written from a session that runs INSIDE a
89
+ worktree must land in the same place or the trigger never fires."""
90
+ p = str(Path(os.environ.get("UAP_MAIN_ROOT") or ".").resolve())
91
+ if "/.worktrees/" in p:
92
+ p = p.split("/.worktrees/", 1)[0]
93
+ return Path(p)
94
+
95
+
96
+ def state_path(root: Path | None = None) -> Path:
97
+ return (root or root_dir()) / ".uap" / STATE_FILE
98
+
99
+
100
+ def load_state(root: Path | None = None) -> dict:
101
+ try:
102
+ data = json.loads(state_path(root).read_text(encoding="utf-8"))
103
+ if isinstance(data, dict):
104
+ data.setdefault("failures", 0)
105
+ data.setdefault("edits_since_green", {})
106
+ return data
107
+ except Exception:
108
+ pass
109
+ return {"failures": 0, "edits_since_green": {}, "last_failure": None,
110
+ "last_green": None, "updated": 0}
111
+
112
+
113
+ def save_state(state: dict, root: Path | None = None) -> None:
114
+ p = state_path(root)
115
+ try:
116
+ p.parent.mkdir(parents=True, exist_ok=True)
117
+ state["updated"] = int(time.time())
118
+ tmp = p.with_suffix(".tmp")
119
+ tmp.write_text(json.dumps(state, indent=1), encoding="utf-8")
120
+ os.replace(tmp, p)
121
+ except Exception:
122
+ pass # evidence is best-effort; never wedge the hook that records it
123
+
124
+
125
+ def record_fail(source: str, detail: str, root: Path | None = None) -> dict:
126
+ st = load_state(root)
127
+ st["failures"] = int(st.get("failures") or 0) + 1
128
+ st["last_failure"] = {
129
+ "ts": int(time.time()),
130
+ "source": source or "unknown",
131
+ "detail": (detail or "")[-DETAIL_MAX:],
132
+ }
133
+ save_state(st, root)
134
+ return st
135
+
136
+
137
+ def record_pass(source: str, root: Path | None = None) -> dict:
138
+ st = load_state(root)
139
+ st["failures"] = 0
140
+ st["edits_since_green"] = {}
141
+ st["last_green"] = {"ts": int(time.time()), "source": source or "unknown"}
142
+ st["last_failure"] = None
143
+ save_state(st, root)
144
+ return st
145
+
146
+
147
+ def record_edit(rel: str, root: Path | None = None) -> dict:
148
+ st = load_state(root)
149
+ edits = st.get("edits_since_green") or {}
150
+ edits[rel] = int(edits.get(rel, 0)) + 1
151
+ st["edits_since_green"] = edits
152
+ save_state(st, root)
153
+ return st
154
+
155
+
156
+ def reset(root: Path | None = None) -> dict:
157
+ st = {"failures": 0, "edits_since_green": {}, "last_failure": None,
158
+ "last_green": {"ts": int(time.time()), "source": "reset"}, "updated": 0}
159
+ save_state(st, root)
160
+ return st
161
+
162
+
163
+ def is_gate_command(command: str) -> bool:
164
+ return bool(command) and bool(_BUILD_TEST_RE.search(command))
165
+
166
+
167
+ def classify_output(command: str, output: str, exit_code: int | None = None) -> str:
168
+ """pass | fail | none for a shell command's outcome.
169
+
170
+ The exit code is authoritative when the harness supplies one. Without it
171
+ (opencode's after-hook exposes text only) fall back to markers, and to
172
+ `none` when the text says nothing either way — silence is not evidence.
173
+ """
174
+ if not is_gate_command(command):
175
+ return "none"
176
+ if exit_code is not None:
177
+ return "pass" if exit_code == 0 else "fail"
178
+ low = (output or "").lower()
179
+ if not low.strip():
180
+ return "none"
181
+ # A non-zero failure COUNT ("2 failed", "1 failures", "3 errors") or a
182
+ # specific failure marker. A bare "failed" substring is deliberately NOT a
183
+ # marker: `test_failed_login PASSED` and `--- PASS: TestFailedAuth` are green.
184
+ if any(m in low for m in _FAIL_MARKERS) or re.search(r"\b[1-9]\d* (?:failed|failures?|errors?)\b", low):
185
+ return "fail"
186
+ if any(m in low for m in _PASS_MARKERS) or re.search(r"(?m)^ok\b|--- pass:|\b\d+ passed\b", low):
187
+ return "pass"
188
+ return "none"
189
+
190
+
191
+ def main(argv: list[str] | None = None) -> int:
192
+ ap = argparse.ArgumentParser(prog="escalation_tracker.py")
193
+ sub = ap.add_subparsers(dest="cmd", required=True)
194
+ f = sub.add_parser("fail"); f.add_argument("--source", default="verify"); f.add_argument("--detail", default="")
195
+ p = sub.add_parser("pass"); p.add_argument("--source", default="verify"); p.add_argument("--detail", default="")
196
+ e = sub.add_parser("edit"); e.add_argument("--file", required=True)
197
+ sub.add_parser("reset")
198
+ s = sub.add_parser("status"); s.add_argument("--json", action="store_true")
199
+ c = sub.add_parser("classify-bash")
200
+ c.add_argument("--command", required=True); c.add_argument("--output", default="")
201
+ c.add_argument("--exit-code", type=int, default=None); c.add_argument("--dry-run", action="store_true")
202
+ raw = list(sys.argv[1:] if argv is None else argv)
203
+ folded: list[str] = []
204
+ k = 0
205
+ while k < len(raw):
206
+ tok = raw[k]
207
+ if tok in ("--detail", "--output", "--command") and k + 1 < len(raw):
208
+ folded.append(f"{tok}={raw[k + 1]}")
209
+ k += 2
210
+ continue
211
+ folded.append(tok)
212
+ k += 1
213
+ a = ap.parse_args(folded)
214
+
215
+ if a.cmd == "fail":
216
+ detail = a.detail
217
+ if detail == "-":
218
+ detail = sys.stdin.read()
219
+ st = record_fail(a.source, detail)
220
+ print(f"escalation: {st['failures']} consecutive failure(s) recorded ({a.source})")
221
+ elif a.cmd == "pass":
222
+ record_pass(a.source)
223
+ print(f"escalation: green ({a.source}) — counters reset")
224
+ elif a.cmd == "edit":
225
+ record_edit(a.file)
226
+ elif a.cmd == "reset":
227
+ reset()
228
+ print("escalation: state reset")
229
+ elif a.cmd == "status":
230
+ st = load_state()
231
+ if a.json:
232
+ print(json.dumps(st))
233
+ else:
234
+ lf = st.get("last_failure") or {}
235
+ print(f"failures={st.get('failures', 0)} files_churning={len(st.get('edits_since_green') or {})}"
236
+ + (f" last_failure=({lf.get('source')}) {str(lf.get('detail',''))[:120]!r}" if lf else ""))
237
+ elif a.cmd == "classify-bash":
238
+ output = a.output
239
+ if output == "-":
240
+ output = sys.stdin.read()
241
+ verdict = classify_output(a.command, output, a.exit_code)
242
+ if not a.dry_run:
243
+ if verdict == "fail":
244
+ record_fail("bash", f"$ {a.command[:200]}\n{output[-DETAIL_MAX:]}")
245
+ elif verdict == "pass":
246
+ record_pass("bash")
247
+ print(verdict)
248
+ return 0
249
+
250
+
251
+ if __name__ == "__main__":
252
+ sys.exit(main())
@@ -221,6 +221,16 @@ if [ "$CODE_CHANGED" = "true" ] && [ "${UAP_VERIFY_ON_STOP:-1}" != "0" ] && comm
221
221
  fi
222
222
  VERIFY_OUT="$(cd "$PROJECT_DIR" && $TIMEOUT_WRAP uap verify --runtime-only $UV_AUTO $ACC_AUTO --dir "$PROJECT_DIR" 2>&1)"
223
223
  VERIFY_RC=$?
224
+ # Evidence for the `escalate` delivery mode: a red verify counts toward the
225
+ # "escalate to deliver" threshold, a green one clears it. Best-effort.
226
+ TRACKER="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/escalation_tracker.py"
227
+ if [ -f "$TRACKER" ]; then
228
+ if [ "$VERIFY_RC" = "1" ]; then
229
+ (cd "$PROJECT_DIR" && UAP_MAIN_ROOT="$PROJECT_DIR" python3 "$TRACKER" fail --source stop --detail "$(printf '%s' "$VERIFY_OUT" | tail -c 1200)" >/dev/null 2>&1) || true
230
+ elif [ "$VERIFY_RC" = "0" ]; then
231
+ (cd "$PROJECT_DIR" && UAP_MAIN_ROOT="$PROJECT_DIR" python3 "$TRACKER" pass --source stop >/dev/null 2>&1) || true
232
+ fi
233
+ fi
224
234
  fi
225
235
  set -e
226
236
  # RC 1 = a REAL gate failure (code is broken) → block. Everything else fails
@@ -71,7 +71,7 @@ export UAP_WORKTREE_ROOT="$CHECKOUT_ROOT"
71
71
  # in .bashrc) can NOT silently downgrade a project configured to `block`
72
72
  # and let the agent bypass deliver. `delivery.localMode` is read the same
73
73
  # authoritative way and selects how a local-model session resolves block
74
- # (advisory|deliver|block — see delivery_enforcement.py).
74
+ # (advisory|deliver|block|escalate — see delivery_enforcement.py).
75
75
  # 2. ambient UAP_ENFORCE_DELIVERY (operator/CI per-run override) when the
76
76
  # project declares nothing.
77
77
  # 3. default: block.
@@ -83,7 +83,7 @@ import json, sys
83
83
  try:
84
84
  d = (json.load(open(sys.argv[1])).get("delivery") or {})
85
85
  e = str(d.get("enforcement", "")).lower()
86
- print("%s|%s" % (e if e in ("block", "advisory", "off") else "", str(d.get("localMode", "")).lower()))
86
+ print("%s|%s" % (e if e in ("block", "advisory", "off", "escalate") else "", str(d.get("localMode", "")).lower()))
87
87
  except Exception:
88
88
  print("|")
89
89
  ' "$MAIN_ROOT/.uap.json" 2>/dev/null || echo "|")"
@@ -168,7 +168,7 @@ if not hit and cmd:
168
168
  if hit:
169
169
  break
170
170
  bypass = re.search(
171
- r"UAP_DELIVER_BYPASS\s*=\s*[\x27\"]?1|UAP_ENFORCE_DELIVERY\s*=\s*[\x27\"]?(advisory|off|0|false|no)"
171
+ r"UAP_DELIVER_BYPASS\s*=\s*[\x27\"]?1|UAP_ENFORCE_DELIVERY\s*=\s*[\x27\"]?(advisory|off|escalate|0|false|no)"
172
172
  r"|UAP_SELF_PROTECT_OFF\s*=\s*[\x27\"]?1|UAP_NO_WORKTREE\s*=\s*[\x27\"]?1"
173
173
  r"|UAP_WORKDIR_SCOPE_OFF\s*=\s*[\x27\"]?1|UAP_USER_VALIDATION\s*=\s*[\x27\"]?0"
174
174
  r"|UAP_DELIVER_NO_LOCK\s*=\s*[\x27\"]?1|UAP_NO_REVIEW\s*=\s*[\x27\"]?1"
@@ -293,7 +293,7 @@ record_execution() {
293
293
  local esc_args esc_reason
294
294
  esc_args="$(printf '%s' "$ARGS" | sed "s/'/''/g")"
295
295
  esc_reason="$(printf '%s' "$reason" | sed "s/'/''/g" | cut -c1-500)"
296
- sqlite3 "$DB" "INSERT INTO policy_executions
296
+ sqlite3 -cmd ".timeout 5000" "$DB" "INSERT INTO policy_executions
297
297
  (policyId, toolName, operation, args, result, allowed, reason, executedAt)
298
298
  VALUES ('$pid', '${TOOL//\'/\'\'}', '${TOOL//\'/\'\'}', '$esc_args',
299
299
  '{}', $allowed, '$esc_reason', strftime('%Y-%m-%dT%H:%M:%fZ','now'));
@@ -463,7 +463,7 @@ except: print("")' 2>/dev/null || echo "")"
463
463
  # first, and the agent saw the wrong routing message. Honor priority now so
464
464
  # the intended policy fires first. (Blocking short-circuits at exit 2, so the
465
465
  # highest-priority applicable block wins.) Name is the stable tiebreak.
466
- done < <(sqlite3 "$DB" "SELECT p.id, p.name, t.toolName FROM policies p JOIN executable_tools t ON t.policyId=p.id WHERE p.isActive=1 ORDER BY p.priority DESC, p.name ASC;")
466
+ done < <(sqlite3 -cmd ".timeout 5000" "$DB" "SELECT p.id, p.name, t.toolName FROM policies p JOIN executable_tools t ON t.policyId=p.id WHERE p.isActive=1 ORDER BY p.priority DESC, p.name ASC;")
467
467
 
468
468
  # A sensitive op that no self-protect enforcer ever evaluated = the control
469
469
  # surface is unguarded (self-protect not registered/active). Fail closed.
@@ -1945,6 +1945,96 @@ def _apply_profile_overrides(
1945
1945
  return updated, prompt_suffix, grammar_text
1946
1946
 
1947
1947
 
1948
+ # ── Upstream-safe tool names ────────────────────────────────────────────────
1949
+ # llama.cpp's grammar builder cannot parse a tool NAME containing '-' or '.'
1950
+ # under tool_choice=required: the request fails with HTTP 400 "Failed to
1951
+ # initialize samplers: failed to parse grammar" (measured 2026-08-21 against
1952
+ # the live server: `uap-router_deliver` -> 400, the same schema as
1953
+ # `uap_router_deliver` -> 200; `auto` passes either way because the lazy
1954
+ # grammar path never compiles the name). OpenCode prefixes every MCP tool
1955
+ # with "<server>_" and servers are named with hyphens, so the moment a rail
1956
+ # forces `required` with such a tool present, the turn dies and the client
1957
+ # session wedges (observed: RECON hard tier restored `uap-router_deliver`,
1958
+ # forced required, 400, stream error, 10h idle).
1959
+ #
1960
+ # Names are rewritten to [A-Za-z0-9_] on the way UP (tool definitions, the
1961
+ # assistant history's tool_calls, a function-pinned tool_choice) and restored
1962
+ # on the way DOWN, so the client never sees the sanitized form. The map is
1963
+ # module-level: sanitization is a pure function of the name, so one map serves
1964
+ # every session and every response path (stream, strict, non-stream).
1965
+ _UPSTREAM_TOOL_NAME_BAD_RE = re.compile(r"[^A-Za-z0-9_]")
1966
+ _UPSTREAM_TOOL_NAME_MAP: dict[str, str] = {} # sanitized -> original
1967
+
1968
+
1969
+ def _sanitize_tool_name(name: str) -> str:
1970
+ return _UPSTREAM_TOOL_NAME_BAD_RE.sub("_", name or "")
1971
+
1972
+
1973
+ def _sanitize_tool_names_for_upstream(openai_body: dict) -> int:
1974
+ """Rewrite unsafe tool names in-place; returns how many were renamed.
1975
+
1976
+ Idempotent: already-safe names are untouched, so calling it on a retry is
1977
+ harmless. A rename that would collide with another tool's (native or
1978
+ sanitized) name gets a numeric suffix.
1979
+ """
1980
+ tools = openai_body.get("tools") or []
1981
+ originals = {
1982
+ ((t.get("function") or {}).get("name") or "")
1983
+ for t in tools
1984
+ if isinstance(t, dict) and isinstance(t.get("function"), dict)
1985
+ }
1986
+ renamed: dict[str, str] = {} # original -> safe
1987
+ taken: set[str] = set()
1988
+ for t in tools:
1989
+ fn = t.get("function") if isinstance(t, dict) else None
1990
+ if not isinstance(fn, dict):
1991
+ continue
1992
+ name = fn.get("name") or ""
1993
+ safe = _sanitize_tool_name(name)
1994
+ if safe == name:
1995
+ taken.add(name)
1996
+ # A tool that NATIVELY owns this spelling must never be restored
1997
+ # to some other client's hyphenated name left in the global map.
1998
+ _UPSTREAM_TOOL_NAME_MAP.pop(name, None)
1999
+ continue
2000
+ base, i = safe, 2
2001
+ while safe in taken or (safe in originals and safe != name):
2002
+ safe = f"{base}_{i}"
2003
+ i += 1
2004
+ fn["name"] = safe
2005
+ taken.add(safe)
2006
+ renamed[name] = safe
2007
+ _UPSTREAM_TOOL_NAME_MAP[safe] = name
2008
+ if not renamed:
2009
+ return 0
2010
+ # The model must see ONE name per tool: history tool_calls that carry the
2011
+ # original spelling are rewritten too, as is a function-pinned tool_choice.
2012
+ for m in openai_body.get("messages") or []:
2013
+ if not isinstance(m, dict):
2014
+ continue
2015
+ for tc in m.get("tool_calls") or []:
2016
+ fn = tc.get("function") if isinstance(tc, dict) else None
2017
+ if isinstance(fn, dict) and fn.get("name") in renamed:
2018
+ fn["name"] = renamed[fn["name"]]
2019
+ choice = openai_body.get("tool_choice")
2020
+ if isinstance(choice, dict):
2021
+ fn = choice.get("function")
2022
+ if isinstance(fn, dict) and fn.get("name") in renamed:
2023
+ fn["name"] = renamed[fn["name"]]
2024
+ logger.info(
2025
+ "TOOL NAMES: sanitized %d upstream-unsafe tool name(s) for llama.cpp grammar: %s",
2026
+ len(renamed), ", ".join(f"{k}->{v}" for k, v in list(renamed.items())[:6]),
2027
+ )
2028
+ return len(renamed)
2029
+
2030
+
2031
+ def _restore_tool_name(name: str) -> str:
2032
+ """The client-facing name for a tool the upstream called by sanitized name."""
2033
+ if not name or not _UPSTREAM_TOOL_NAME_MAP:
2034
+ return name
2035
+ return _UPSTREAM_TOOL_NAME_MAP.get(name, name)
2036
+
2037
+
1948
2038
  def _is_grammar_tools_incompatibility(status_code: int, error_text: str) -> bool:
1949
2039
  if status_code != 400:
1950
2040
  return False
@@ -1963,9 +2053,15 @@ def _is_gemma4_peg_parse_failure(status_code: int, error_text: str) -> bool:
1963
2053
  tool_choice='auto' so the model can emit prose or a complete call
1964
2054
  without grammar enforcement triggering this failure mode.
1965
2055
  """
2056
+ text = error_text or ""
2057
+ # Same remedy for the other grammar-compile failure: HTTP 400 "Failed to
2058
+ # initialize samplers: failed to parse grammar" (a tool schema/name the
2059
+ # builder cannot express under `required`). Relaxing to `auto` lets the
2060
+ # turn proceed on the lazy grammar path instead of killing the stream.
2061
+ if status_code == 400 and "failed to parse grammar" in text.lower():
2062
+ return True
1966
2063
  if status_code != 500:
1967
2064
  return False
1968
- text = error_text or ""
1969
2065
  return (
1970
2066
  "Failed to parse input at pos" in text
1971
2067
  or "<|tool_call>call:" in text
@@ -8844,6 +8940,12 @@ def _tool_schema_map_from_anthropic_body(anthropic_body: dict) -> dict[str, dict
8844
8940
  if isinstance(name, str) and name:
8845
8941
  schema = tool.get("input_schema")
8846
8942
  schema_map[name] = schema if isinstance(schema, dict) else {}
8943
+ # The upstream calls tools by their SANITIZED name (see
8944
+ # _sanitize_tool_names_for_upstream); a guardrail that only knew the
8945
+ # client's spelling would reject every such call as "unknown tool".
8946
+ safe = _sanitize_tool_name(name)
8947
+ if safe != name and safe not in schema_map:
8948
+ schema_map[safe] = schema_map[name]
8847
8949
  return schema_map
8848
8950
 
8849
8951
 
@@ -9317,6 +9419,9 @@ def _anthropic_tools_by_name(anthropic_body: dict) -> dict[str, dict]:
9317
9419
  else {}
9318
9420
  )
9319
9421
  tool_map[name] = schema or {}
9422
+ safe = _sanitize_tool_name(name) # upstream spelling, see schema map above
9423
+ if safe != name and safe not in tool_map:
9424
+ tool_map[safe] = tool_map[name]
9320
9425
  return tool_map
9321
9426
 
9322
9427
 
@@ -11202,7 +11307,7 @@ def openai_to_anthropic_response(
11202
11307
  {
11203
11308
  "type": "tool_use",
11204
11309
  "id": tool_use_id,
11205
- "name": fn.get("name", ""),
11310
+ "name": _restore_tool_name(fn.get("name", "")),
11206
11311
  "input": args,
11207
11312
  }
11208
11313
  )
@@ -11561,7 +11666,7 @@ async def stream_anthropic_response(
11561
11666
  initial_args = fn.get("arguments", "")
11562
11667
  tool_calls_by_index[tc_idx] = {
11563
11668
  "id": tc_id,
11564
- "name": fn.get("name", ""),
11669
+ "name": _restore_tool_name(fn.get("name", "")),
11565
11670
  "arguments": initial_args,
11566
11671
  "block_index": tool_block_index,
11567
11672
  }
@@ -11749,7 +11854,7 @@ async def stream_anthropic_response(
11749
11854
  for idx, xtc in enumerate(xml_extracted, start=1):
11750
11855
  fn = xtc.get("function", {})
11751
11856
  tc_id = xtc.get("id", f"toolu_{uuid.uuid4().hex[:12]}")
11752
- tc_name = fn.get("name", "")
11857
+ tc_name = _restore_tool_name(fn.get("name", ""))
11753
11858
  tc_args = fn.get("arguments", "{}")
11754
11859
  tool_calls_by_index[idx] = {
11755
11860
  "id": tc_id,
@@ -12380,6 +12485,9 @@ async def messages(request: Request):
12380
12485
  media_type="application/json",
12381
12486
  )
12382
12487
 
12488
+ # Last step before the wire: names llama.cpp's grammar builder can parse.
12489
+ _sanitize_tool_names_for_upstream(openai_body)
12490
+
12383
12491
  use_guarded_non_stream = _should_use_guarded_non_stream(
12384
12492
  is_stream,
12385
12493
  body,