@miller-tech/uap 1.185.0 → 1.185.1
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 +2 -2
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +52 -9
- package/tools/agents/tests/test_models_context_window.py +84 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miller-tech/uap",
|
|
3
|
-
"version": "1.185.
|
|
3
|
+
"version": "1.185.1",
|
|
4
4
|
"description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"start": "node dist/bin/cli.js",
|
|
22
22
|
"test": "vitest",
|
|
23
23
|
"test:ci": "vitest run",
|
|
24
|
-
"test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer tools.agents.tests.test_gate_evidence tools.agents.tests.test_expert_review_pr_scope tools.agents.tests.test_path_containment tools.agents.tests.test_path_normalizer_hardened tools.agents.tests.test_sandbox_tool_strip tools.agents.tests.test_proxy_env_loader tools.agents.tests.test_stream_required_tool tools.agents.tests.test_tool_call_wrapper_profiles tools.agents.tests.test_tool_convert_cache tools.agents.tests.test_doubling_break tools.agents.tests.test_error_loop_ignores_correctives tools.agents.tests.test_attractor_detection tools.agents.tests.test_client_disconnect tools.agents.tests.test_confidence_escalation tools.agents.tests.test_coordination_ban tools.agents.tests.test_coordination_early_ban tools.agents.tests.test_cycle_break_exploration tools.agents.tests.test_deferral_break tools.agents.tests.test_deliver_autoroute tools.agents.tests.test_delivery_enforcement_all_langs tools.agents.tests.test_delivery_enforcement_exemptions tools.agents.tests.test_delivery_enforcement_filepath tools.agents.tests.test_delivery_enforcement_web_and_bash tools.agents.tests.test_disconnect_watcher tools.agents.tests.test_empty_maxtokens_recovery tools.agents.tests.test_empty_tool_loop_break tools.agents.tests.test_enforcer_escape_hatches tools.agents.tests.test_error_loop_break tools.agents.tests.test_finalize_suppression tools.agents.tests.test_malformed_unclosed_think tools.agents.tests.test_mandate_beats_recon tools.agents.tests.test_mandate_deliver tools.agents.tests.test_overflow_truncate_count_tokens tools.agents.tests.test_passthrough_oauth tools.agents.tests.test_project_telemetry tools.agents.tests.test_proxy_auth_headers tools.agents.tests.test_prune_preserve_force_write tools.agents.tests.test_recon_deliver_gate tools.agents.tests.test_session_admission tools.agents.tests.test_stream_heartbeat tools.agents.tests.test_stuck_break_reattach tools.agents.tests.test_turn_count_breaker_periodic tools.agents.tests.test_upstream_chokepoint tools.agents.tests.test_vision_passthrough tools.agents.tests.test_worktree_required tools.agents.tests.test_enforcer_suite_coverage tools.agents.tests.test_validate_plan_gate tools.agents.tests.test_validate_plan_inside_project tools.agents.tests.test_anthropic_proxy_streaming tools.agents.tests.test_delivery_enforcement_worktree tools.agents.tests.test_output_token_ceilings tools.agents.tests.test_tool_narrowing_core",
|
|
24
|
+
"test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer tools.agents.tests.test_gate_evidence tools.agents.tests.test_expert_review_pr_scope tools.agents.tests.test_path_containment tools.agents.tests.test_path_normalizer_hardened tools.agents.tests.test_sandbox_tool_strip tools.agents.tests.test_proxy_env_loader tools.agents.tests.test_stream_required_tool tools.agents.tests.test_tool_call_wrapper_profiles tools.agents.tests.test_tool_convert_cache tools.agents.tests.test_doubling_break tools.agents.tests.test_error_loop_ignores_correctives tools.agents.tests.test_attractor_detection tools.agents.tests.test_client_disconnect tools.agents.tests.test_confidence_escalation tools.agents.tests.test_coordination_ban tools.agents.tests.test_coordination_early_ban tools.agents.tests.test_cycle_break_exploration tools.agents.tests.test_deferral_break tools.agents.tests.test_deliver_autoroute tools.agents.tests.test_delivery_enforcement_all_langs tools.agents.tests.test_delivery_enforcement_exemptions tools.agents.tests.test_delivery_enforcement_filepath tools.agents.tests.test_delivery_enforcement_web_and_bash tools.agents.tests.test_disconnect_watcher tools.agents.tests.test_empty_maxtokens_recovery tools.agents.tests.test_empty_tool_loop_break tools.agents.tests.test_enforcer_escape_hatches tools.agents.tests.test_error_loop_break tools.agents.tests.test_finalize_suppression tools.agents.tests.test_malformed_unclosed_think tools.agents.tests.test_mandate_beats_recon tools.agents.tests.test_mandate_deliver tools.agents.tests.test_overflow_truncate_count_tokens tools.agents.tests.test_passthrough_oauth tools.agents.tests.test_project_telemetry tools.agents.tests.test_proxy_auth_headers tools.agents.tests.test_prune_preserve_force_write tools.agents.tests.test_recon_deliver_gate tools.agents.tests.test_session_admission tools.agents.tests.test_stream_heartbeat tools.agents.tests.test_stuck_break_reattach tools.agents.tests.test_turn_count_breaker_periodic tools.agents.tests.test_upstream_chokepoint tools.agents.tests.test_vision_passthrough tools.agents.tests.test_worktree_required tools.agents.tests.test_enforcer_suite_coverage tools.agents.tests.test_validate_plan_gate tools.agents.tests.test_validate_plan_inside_project tools.agents.tests.test_anthropic_proxy_streaming tools.agents.tests.test_delivery_enforcement_worktree tools.agents.tests.test_output_token_ceilings tools.agents.tests.test_tool_narrowing_core tools.agents.tests.test_models_context_window",
|
|
25
25
|
"test:coverage": "vitest --coverage",
|
|
26
26
|
"bench": "vitest --config vitest.bench.config.ts",
|
|
27
27
|
"lint": "eslint src --ext .ts",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -12650,6 +12650,57 @@ def _parse_anthropic_sse_to_message(raw: bytes) -> dict | None:
|
|
|
12650
12650
|
}
|
|
12651
12651
|
|
|
12652
12652
|
|
|
12653
|
+
ADVERTISED_MODEL_IDS = (
|
|
12654
|
+
"claude-haiku-4-5-20251001",
|
|
12655
|
+
"claude-sonnet-4-6",
|
|
12656
|
+
"claude-sonnet-5-20250514",
|
|
12657
|
+
"claude-fable-5",
|
|
12658
|
+
"qwen36-35b-a3b-iq4xs",
|
|
12659
|
+
)
|
|
12660
|
+
|
|
12661
|
+
# Keys OpenAI-compatible clients probe for a model's context window. There is no
|
|
12662
|
+
# standard, so emit the common spellings rather than betting on one: hermes reads
|
|
12663
|
+
# context_length / context_window / max_context_length / max_model_len / n_ctx
|
|
12664
|
+
# (agent/model_metadata.py:_CONTEXT_LENGTH_KEYS), LiteLLM and vLLM prefer
|
|
12665
|
+
# max_model_len, LM Studio uses max_context_length. They are all the same number.
|
|
12666
|
+
_CONTEXT_WINDOW_KEYS = (
|
|
12667
|
+
"context_length",
|
|
12668
|
+
"context_window",
|
|
12669
|
+
"max_context_length",
|
|
12670
|
+
"max_model_len",
|
|
12671
|
+
"n_ctx",
|
|
12672
|
+
)
|
|
12673
|
+
|
|
12674
|
+
|
|
12675
|
+
def _model_entry(model_id: str) -> dict:
|
|
12676
|
+
"""One /v1/models row, carrying the context window when we know it.
|
|
12677
|
+
|
|
12678
|
+
Advertising this is not cosmetic. A client that cannot discover the window
|
|
12679
|
+
cannot size its own history to it, so it grows unbounded and the FIRST thing
|
|
12680
|
+
that notices is this proxy — which can then only prune blind, after the
|
|
12681
|
+
prompt is already built.
|
|
12682
|
+
|
|
12683
|
+
Live, 2026-08-04: hermes has a context compressor and probes for exactly
|
|
12684
|
+
these keys. We advertised bare {"id", "object"} rows, its model cache held no
|
|
12685
|
+
entry for our model, so the compressor never engaged. It sent 470 messages /
|
|
12686
|
+
219,957 tokens against a 130,048 window (169%), and the proxy CRITICAL PRUNEd
|
|
12687
|
+
290 of them to fit — 61 such events in 18 hours. Raising the window from
|
|
12688
|
+
86,784 to 130,048 had not helped, because the growth was never sized to the
|
|
12689
|
+
window in the first place.
|
|
12690
|
+
|
|
12691
|
+
Only advertised for models this proxy serves LOCALLY. A model that
|
|
12692
|
+
round-trips to api.anthropic.com has its own (much larger) window, and
|
|
12693
|
+
stamping the local llama.cpp figure on it would make clients truncate
|
|
12694
|
+
needlessly — a worse bug than the one being fixed, so when in doubt emit
|
|
12695
|
+
nothing and leave the client on its own defaults.
|
|
12696
|
+
"""
|
|
12697
|
+
entry = {"id": model_id, "object": "model"}
|
|
12698
|
+
if PROXY_CONTEXT_WINDOW > 0 and not _should_passthrough_model(model_id):
|
|
12699
|
+
for key in _CONTEXT_WINDOW_KEYS:
|
|
12700
|
+
entry[key] = PROXY_CONTEXT_WINDOW
|
|
12701
|
+
return entry
|
|
12702
|
+
|
|
12703
|
+
|
|
12653
12704
|
@app.get("/v1/models")
|
|
12654
12705
|
async def models():
|
|
12655
12706
|
"""Return available model list.
|
|
@@ -12666,15 +12717,7 @@ async def models():
|
|
|
12666
12717
|
ANTHROPIC_PASSTHROUGH_MODELS=__local_only__ is set, all IDs (including
|
|
12667
12718
|
the Claude ones below) are served by the local llama.cpp backend.
|
|
12668
12719
|
"""
|
|
12669
|
-
return {
|
|
12670
|
-
"data": [
|
|
12671
|
-
{"id": "claude-haiku-4-5-20251001", "object": "model"},
|
|
12672
|
-
{"id": "claude-sonnet-4-6", "object": "model"},
|
|
12673
|
-
{"id": "claude-sonnet-5-20250514", "object": "model"},
|
|
12674
|
-
{"id": "claude-fable-5", "object": "model"},
|
|
12675
|
-
{"id": "qwen36-35b-a3b-iq4xs", "object": "model"},
|
|
12676
|
-
]
|
|
12677
|
-
}
|
|
12720
|
+
return {"data": [_model_entry(mid) for mid in ADVERTISED_MODEL_IDS]}
|
|
12678
12721
|
|
|
12679
12722
|
|
|
12680
12723
|
@app.get("/health")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""/v1/models must advertise the context window for locally-served models.
|
|
2
|
+
|
|
3
|
+
Regression (hermes, 2026-08-04): the endpoint returned bare {"id","object"}
|
|
4
|
+
rows. Hermes HAS a context compressor and probes for context_length /
|
|
5
|
+
context_window / max_context_length / max_model_len / n_ctx, found none, and its
|
|
6
|
+
model cache held no entry for our model — so the compressor never engaged. It
|
|
7
|
+
sent 470 messages / 219,957 tokens against a 130,048 window (169%) and the proxy
|
|
8
|
+
CRITICAL PRUNEd 290 of them; 61 such events in 18 hours. Raising the window from
|
|
9
|
+
86,784 to 130,048 had not helped, because the growth was never sized to the
|
|
10
|
+
window at all.
|
|
11
|
+
|
|
12
|
+
A client that cannot discover the window cannot size its history to it.
|
|
13
|
+
"""
|
|
14
|
+
import importlib.util
|
|
15
|
+
import os
|
|
16
|
+
import unittest
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
proxy_path = Path(__file__).resolve().parents[3] / "tools" / "agents" / "scripts" / "anthropic_proxy.py"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_proxy(window="130048", passthrough=None):
|
|
23
|
+
"""Import a fresh proxy module under the given env (constants bind at import)."""
|
|
24
|
+
os.environ["PROXY_CONTEXT_WINDOW"] = window
|
|
25
|
+
if passthrough is None:
|
|
26
|
+
os.environ.pop("ANTHROPIC_PASSTHROUGH_MODELS", None)
|
|
27
|
+
else:
|
|
28
|
+
os.environ["ANTHROPIC_PASSTHROUGH_MODELS"] = passthrough
|
|
29
|
+
spec = importlib.util.spec_from_file_location("anthropic_proxy_ctx", proxy_path)
|
|
30
|
+
mod = importlib.util.module_from_spec(spec)
|
|
31
|
+
spec.loader.exec_module(mod)
|
|
32
|
+
return mod
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ModelsAdvertiseContextWindowTest(unittest.TestCase):
|
|
36
|
+
def tearDown(self):
|
|
37
|
+
os.environ.pop("PROXY_CONTEXT_WINDOW", None)
|
|
38
|
+
os.environ.pop("ANTHROPIC_PASSTHROUGH_MODELS", None)
|
|
39
|
+
|
|
40
|
+
def test_local_model_carries_the_window(self):
|
|
41
|
+
ap = load_proxy()
|
|
42
|
+
e = ap._model_entry("qwen36-35b-a3b-iq4xs")
|
|
43
|
+
self.assertEqual(e["context_length"], 130048)
|
|
44
|
+
|
|
45
|
+
def test_every_probed_key_is_emitted(self):
|
|
46
|
+
# There is no standard key. Clients disagree, so emit the common
|
|
47
|
+
# spellings — missing the one a client happens to read is the same
|
|
48
|
+
# failure as advertising nothing.
|
|
49
|
+
ap = load_proxy()
|
|
50
|
+
e = ap._model_entry("qwen36-35b-a3b-iq4xs")
|
|
51
|
+
for key in ("context_length", "context_window", "max_context_length",
|
|
52
|
+
"max_model_len", "n_ctx"):
|
|
53
|
+
self.assertEqual(e.get(key), 130048, key)
|
|
54
|
+
|
|
55
|
+
def test_passthrough_models_do_not_get_the_local_window(self):
|
|
56
|
+
# A model that round-trips to api.anthropic.com has a much larger window.
|
|
57
|
+
# Stamping the local llama.cpp figure on it would make clients truncate
|
|
58
|
+
# needlessly — worse than the bug being fixed.
|
|
59
|
+
ap = load_proxy(passthrough=None) # default patterns: Claude passes through
|
|
60
|
+
for mid in ("claude-sonnet-4-6", "claude-haiku-4-5-20251001"):
|
|
61
|
+
self.assertNotIn("context_length", ap._model_entry(mid), mid)
|
|
62
|
+
|
|
63
|
+
def test_local_only_sentinel_means_every_id_is_local(self):
|
|
64
|
+
ap = load_proxy(passthrough="__local_only__")
|
|
65
|
+
for mid in ap.ADVERTISED_MODEL_IDS:
|
|
66
|
+
self.assertEqual(ap._model_entry(mid).get("context_length"), 130048, mid)
|
|
67
|
+
|
|
68
|
+
def test_unset_window_advertises_nothing(self):
|
|
69
|
+
# Better to say nothing than to assert a wrong number.
|
|
70
|
+
ap = load_proxy(window="0")
|
|
71
|
+
self.assertEqual(
|
|
72
|
+
ap._model_entry("qwen36-35b-a3b-iq4xs"), {"id": "qwen36-35b-a3b-iq4xs", "object": "model"}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def test_entry_always_keeps_the_openai_shape(self):
|
|
76
|
+
ap = load_proxy()
|
|
77
|
+
for mid in ap.ADVERTISED_MODEL_IDS:
|
|
78
|
+
e = ap._model_entry(mid)
|
|
79
|
+
self.assertEqual(e["id"], mid)
|
|
80
|
+
self.assertEqual(e["object"], "model")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
unittest.main()
|