@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.
@@ -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()