@miller-tech/uap 1.179.7 → 1.179.9
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/dist/.tsbuildinfo +1 -1
- package/dist/delivery/agentic-executor.d.ts +33 -0
- package/dist/delivery/agentic-executor.d.ts.map +1 -1
- package/dist/delivery/agentic-executor.js +100 -3
- package/dist/delivery/agentic-executor.js.map +1 -1
- 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 +38 -3
- package/tools/agents/tests/test_output_token_ceilings.py +155 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Retrying a TRUNCATED write must not ask for less room than already failed.
|
|
3
|
+
|
|
4
|
+
Measured on the Octopus Invaders build (2026-08-01): a 36KB game.js came back
|
|
5
|
+
cut off mid-file and every retry re-truncated.
|
|
6
|
+
|
|
7
|
+
The first diagnosis of this was wrong and is worth recording, because the wrong
|
|
8
|
+
version is the intuitive one:
|
|
9
|
+
|
|
10
|
+
- The `max_tokens=4096` visible in the proxy's REQ: log is NOT the value sent
|
|
11
|
+
upstream. It is logged on the converted body, BEFORE
|
|
12
|
+
`_resolve_max_tokens_request` applies PROXY_MAX_TOKENS_FLOOR — which
|
|
13
|
+
returns max(requested, floor) and so raises it. With the deployed
|
|
14
|
+
FLOOR=32768 the default was never binding.
|
|
15
|
+
- A truncated write is reclassified to `truncated_tool_args` and therefore
|
|
16
|
+
never sets `last_response_garbled`, so PROXY_TOOL_TURN_MAX_TOKENS_GARBLED
|
|
17
|
+
does not govern its retry either.
|
|
18
|
+
- The actual clamp is PROXY_MALFORMED_TOOL_RETRY_MAX_TOKENS (8192 deployed),
|
|
19
|
+
applied via min() on the retry — i.e. the retry of a file that was cut off
|
|
20
|
+
for lack of room was given LESS room than the attempt that failed.
|
|
21
|
+
|
|
22
|
+
So these tests pin the retry-budget branching, which is where the bug was, and
|
|
23
|
+
the ordering properties of the ceiling chain. They deliberately do not assert
|
|
24
|
+
absolute token literals: the deployed EnvironmentFile overrides most of these
|
|
25
|
+
constants, so a test on a code default would pass while production disagreed.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import importlib.util
|
|
29
|
+
import os
|
|
30
|
+
import unittest
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _load_proxy(env=None):
|
|
35
|
+
prev = {}
|
|
36
|
+
env = dict(env or {})
|
|
37
|
+
env.setdefault("UAP_PROXY_ENV_AUTOLOAD", "0")
|
|
38
|
+
for k, v in env.items():
|
|
39
|
+
prev[k] = os.environ.get(k)
|
|
40
|
+
os.environ[k] = v
|
|
41
|
+
try:
|
|
42
|
+
path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
|
|
43
|
+
spec = importlib.util.spec_from_file_location("anthropic_proxy_ceilings", path)
|
|
44
|
+
m = importlib.util.module_from_spec(spec)
|
|
45
|
+
spec.loader.exec_module(m)
|
|
46
|
+
return m
|
|
47
|
+
finally:
|
|
48
|
+
for k, v in prev.items():
|
|
49
|
+
if v is None:
|
|
50
|
+
os.environ.pop(k, None)
|
|
51
|
+
else:
|
|
52
|
+
os.environ[k] = v
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
proxy = _load_proxy({"PROXY_MALFORMED_TOOL_RETRY_MAX_TOKENS": "8192"})
|
|
56
|
+
|
|
57
|
+
BIG = 32768 # the budget a whole-module write actually runs with
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _retry(**kw):
|
|
61
|
+
body = {"model": "m", "messages": [{"role": "user", "content": "hi"}], "max_tokens": BIG}
|
|
62
|
+
return proxy._build_malformed_retry_body(body, {"tools": []}, **kw)["max_tokens"]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TruncatedRetryBudgetTest(unittest.TestCase):
|
|
66
|
+
def test_truncated_retry_keeps_the_budget_that_was_cut_off(self):
|
|
67
|
+
# The whole bug: clamping here guarantees the retry truncates too.
|
|
68
|
+
self.assertEqual(_retry(is_truncated=True), BIG)
|
|
69
|
+
|
|
70
|
+
def test_garbled_retry_still_tightens(self):
|
|
71
|
+
# Genuine malformed args SHOULD get less room — that guard is intact.
|
|
72
|
+
self.assertEqual(
|
|
73
|
+
_retry(is_garbled=True), proxy.PROXY_TOOL_TURN_MAX_TOKENS_GARBLED
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def test_plain_malformed_retry_still_clamps(self):
|
|
77
|
+
self.assertEqual(_retry(), proxy.PROXY_MALFORMED_TOOL_RETRY_MAX_TOKENS)
|
|
78
|
+
|
|
79
|
+
def test_truncation_wins_over_the_generic_clamp_but_not_over_garbled(self):
|
|
80
|
+
# Ordering matters: garbled is checked first by design, since args that
|
|
81
|
+
# are both malformed AND long are a degeneration risk, not a big file.
|
|
82
|
+
self.assertEqual(
|
|
83
|
+
_retry(is_garbled=True, is_truncated=True),
|
|
84
|
+
proxy.PROXY_TOOL_TURN_MAX_TOKENS_GARBLED,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TruncationSignalIsWiredTest(unittest.TestCase):
|
|
89
|
+
"""The helper is only correct if the call site actually tells it.
|
|
90
|
+
|
|
91
|
+
Asserting the branch through the real call site would mean standing up the
|
|
92
|
+
whole async malformed-retry handler with a mocked upstream; the cheaper and
|
|
93
|
+
still-effective guard is that the kwarg is derived from the issue kind
|
|
94
|
+
rather than hardcoded. Without this, deleting the derivation leaves every
|
|
95
|
+
other test in this file green while the bug returns — which is exactly what
|
|
96
|
+
mutation testing showed.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
SRC = (
|
|
100
|
+
Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
|
|
101
|
+
).read_text()
|
|
102
|
+
|
|
103
|
+
def test_call_site_derives_is_truncated_from_the_issue_kind(self):
|
|
104
|
+
self.assertIn(
|
|
105
|
+
'is_truncated=current_issue.kind == "truncated_tool_args"',
|
|
106
|
+
self.SRC,
|
|
107
|
+
"the retry no longer learns that the previous attempt was truncated",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def test_the_kind_it_keys_on_is_the_one_the_classifier_produces(self):
|
|
111
|
+
# Guards a rename on one side only.
|
|
112
|
+
self.assertIn('kind="truncated_tool_args"', self.SRC)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class CeilingChainOrderingTest(unittest.TestCase):
|
|
116
|
+
def test_floor_raises_a_small_request_rather_than_capping_it(self):
|
|
117
|
+
# This is what made the 4096 default a red herring.
|
|
118
|
+
m = _load_proxy({"PROXY_MAX_TOKENS_FLOOR": "32768"})
|
|
119
|
+
self.assertEqual(m._resolve_max_tokens_request(4096), 32768)
|
|
120
|
+
|
|
121
|
+
def test_a_zero_floor_leaves_the_request_alone(self):
|
|
122
|
+
m = _load_proxy({"PROXY_MAX_TOKENS_FLOOR": "0"})
|
|
123
|
+
self.assertEqual(m._resolve_max_tokens_request(4096), 4096)
|
|
124
|
+
|
|
125
|
+
def test_garbled_cap_is_a_tightening_of_the_tool_turn_cap(self):
|
|
126
|
+
self.assertLessEqual(
|
|
127
|
+
proxy.PROXY_TOOL_TURN_MAX_TOKENS_GARBLED, proxy.PROXY_TOOL_TURN_MAX_TOKENS
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def test_default_is_overridable_and_replaces_the_hardcoded_fallback(self):
|
|
131
|
+
m = _load_proxy({"PROXY_DEFAULT_MAX_TOKENS": "12345"})
|
|
132
|
+
out = m.openai_to_anthropic_request(
|
|
133
|
+
{"model": "m", "messages": [{"role": "user", "content": "hi"}]}
|
|
134
|
+
)
|
|
135
|
+
self.assertEqual(out["max_tokens"], 12345)
|
|
136
|
+
|
|
137
|
+
def test_an_explicit_client_value_survives_the_conversion(self):
|
|
138
|
+
# NB: only the conversion. build_openai_request may still raise it via
|
|
139
|
+
# the thinking floor — asserted here at the conversion layer only.
|
|
140
|
+
out = proxy.openai_to_anthropic_request(
|
|
141
|
+
{"model": "m", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 512}
|
|
142
|
+
)
|
|
143
|
+
self.assertEqual(out["max_tokens"], 512)
|
|
144
|
+
|
|
145
|
+
def test_zero_or_null_means_no_opinion_not_emit_nothing(self):
|
|
146
|
+
for val in (0, None):
|
|
147
|
+
with self.subTest(val=val):
|
|
148
|
+
out = proxy.openai_to_anthropic_request(
|
|
149
|
+
{"model": "m", "messages": [{"role": "user", "content": "hi"}], "max_tokens": val}
|
|
150
|
+
)
|
|
151
|
+
self.assertEqual(out["max_tokens"], proxy.PROXY_DEFAULT_MAX_TOKENS)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
unittest.main()
|