@miller-tech/uap 1.210.4 → 1.210.5
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 +70 -18
- package/tools/agents/tests/test_anthropic_proxy_streaming.py +11 -0
- package/tools/agents/tests/test_thinking_template_kwargs.py +155 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miller-tech/uap",
|
|
3
|
-
"version": "1.210.
|
|
3
|
+
"version": "1.210.5",
|
|
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_live_deliver_kill 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_repeat_call_guard tools.agents.tests.test_gate_integrity tools.agents.tests.test_gate_failclosed_indirection 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_write_complexity tools.agents.tests.test_fastpath_config 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_non_stream_resp_log 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",
|
|
24
|
+
"test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_live_deliver_kill 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_repeat_call_guard tools.agents.tests.test_gate_integrity tools.agents.tests.test_gate_failclosed_indirection 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_write_complexity tools.agents.tests.test_fastpath_config 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_non_stream_resp_log 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 tools.agents.tests.test_thinking_template_kwargs",
|
|
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
|
|
@@ -1453,12 +1453,48 @@ def _apply_json_response_grammar(openai_body: dict, anthropic_body: dict) -> Non
|
|
|
1453
1453
|
if openai_body.get("tools") or openai_body.get("grammar"):
|
|
1454
1454
|
return
|
|
1455
1455
|
openai_body["grammar"] = JSON_RESPONSE_GBNF
|
|
1456
|
-
openai_body
|
|
1457
|
-
ctk = openai_body.setdefault("chat_template_kwargs", {})
|
|
1458
|
-
ctk["enable_thinking"] = False
|
|
1456
|
+
_set_thinking(openai_body, False)
|
|
1459
1457
|
logger.info("JSON-RESPONSE grammar applied (evaluator verdict turn)")
|
|
1460
1458
|
|
|
1461
1459
|
|
|
1460
|
+
def _set_thinking(body: dict, enabled: bool) -> None:
|
|
1461
|
+
"""Turn upstream reasoning on/off, in BOTH places llama.cpp reads.
|
|
1462
|
+
|
|
1463
|
+
The top-level `enable_thinking` field alone does NOTHING on a `--jinja`
|
|
1464
|
+
server: the Qwen chat template reads `chat_template_kwargs`, and this one is
|
|
1465
|
+
launched with `--chat-template-kwargs {"enable_thinking": true}`, so an
|
|
1466
|
+
unqualified top-level flag is silently overridden by the template default.
|
|
1467
|
+
|
|
1468
|
+
Measured on the live server, identical tool-call prompt:
|
|
1469
|
+
top-level only -> 703 chars of reasoning, 196 tokens
|
|
1470
|
+
chat_template_kwargs -> 0 chars, 27 tokens
|
|
1471
|
+
and on a planning-flavoured prompt with max_tokens=700, reasoning consumed
|
|
1472
|
+
the ENTIRE budget and no tool call was emitted at all.
|
|
1473
|
+
|
|
1474
|
+
The OPERATOR switches — PROXY_DISABLE_THINKING_ALWAYS and
|
|
1475
|
+
PROXY_DISABLE_THINKING_ON_TOOL_TURNS, plus the tool-turn breakers — set only
|
|
1476
|
+
the top-level field, so they were no-ops against a jinja server: they
|
|
1477
|
+
existed, logged themselves as active, and changed nothing. The paths that
|
|
1478
|
+
already set both (the JSON-verdict grammar, the empty-max_tokens retry, and
|
|
1479
|
+
the prefill/continuation turn) worked, which is why evaluator turns were
|
|
1480
|
+
unaffected and this went unnoticed elsewhere.
|
|
1481
|
+
|
|
1482
|
+
Use this for a switch that expresses INTENT ("this turn must not reason").
|
|
1483
|
+
The Anthropic protocol-default translation deliberately does NOT use it —
|
|
1484
|
+
see the comment there; making a default authoritative would change serving
|
|
1485
|
+
policy for every client rather than fix a broken control.
|
|
1486
|
+
"""
|
|
1487
|
+
body["enable_thinking"] = enabled
|
|
1488
|
+
# REPLACE the nested dict rather than mutate it: callers build retry bodies
|
|
1489
|
+
# with `dict(openai_body)`, a SHALLOW copy that still shares this dict, so
|
|
1490
|
+
# mutating in place would silently disable thinking on the original request
|
|
1491
|
+
# too. Copying keeps any other template kwargs the server was launched with.
|
|
1492
|
+
existing = body.get("chat_template_kwargs")
|
|
1493
|
+
ctk = dict(existing) if isinstance(existing, dict) else {}
|
|
1494
|
+
ctk["enable_thinking"] = enabled
|
|
1495
|
+
body["chat_template_kwargs"] = ctk
|
|
1496
|
+
|
|
1497
|
+
|
|
1462
1498
|
def _apply_thinking_grammar(request_body: dict) -> None:
|
|
1463
1499
|
"""Apply the structured-thinking GBNF grammar to non-tool turns.
|
|
1464
1500
|
|
|
@@ -1547,6 +1583,13 @@ def _apply_profile_overrides(
|
|
|
1547
1583
|
if "stop_sequences" in profile:
|
|
1548
1584
|
updated["stop_sequences"] = profile["stop_sequences"]
|
|
1549
1585
|
if "enable_thinking" in profile:
|
|
1586
|
+
# Left as a plain assignment: `updated` is the ANTHROPIC-shaped body,
|
|
1587
|
+
# and build_openai_request constructs the upstream body from a fresh
|
|
1588
|
+
# literal that never reads this key — so this has never reached the
|
|
1589
|
+
# wire, and writing a llama.cpp template kwarg into an Anthropic body
|
|
1590
|
+
# would only add a field that passthrough requests must not carry.
|
|
1591
|
+
# Wiring profiles to thinking is a separate change from making the
|
|
1592
|
+
# operator switches work.
|
|
1550
1593
|
updated["enable_thinking"] = profile["enable_thinking"]
|
|
1551
1594
|
|
|
1552
1595
|
tool_call_batching = profile.get("tool_call_batching") or {}
|
|
@@ -6453,14 +6496,26 @@ def build_openai_request(
|
|
|
6453
6496
|
# consume the client's max_tokens budget on internal reasoning, leaving
|
|
6454
6497
|
# nothing for the visible answer.
|
|
6455
6498
|
anthropic_thinking = anthropic_body.get("thinking")
|
|
6499
|
+
# PROTOCOL DEFAULT — deliberately top-level only, NOT via _set_thinking.
|
|
6500
|
+
#
|
|
6501
|
+
# Anthropic's default is "thinking off unless asked", so this branch sets
|
|
6502
|
+
# off for almost every request. Against a jinja server the top-level field
|
|
6503
|
+
# is inert, which is why the upstream has been reasoning by default all
|
|
6504
|
+
# along. Routing this through _set_thinking would make it authoritative and
|
|
6505
|
+
# silently flip EVERY client that never asked for thinking — Claude Code,
|
|
6506
|
+
# opencode, benchmarks, all OpenAI-compat traffic — from the server's
|
|
6507
|
+
# configured default to off. That is a serving-policy change, not a bug fix.
|
|
6508
|
+
#
|
|
6509
|
+
# The same reasoning applies to the "enabled" case in reverse: making it
|
|
6510
|
+
# authoritative would let a client turn reasoning ON against a server
|
|
6511
|
+
# deliberately launched with it off. The measured bug was one-directional
|
|
6512
|
+
# (an operator's OFF switch didn't stick); the fix stays one-directional.
|
|
6513
|
+
# Operators who want thinking off across the board have an explicit switch
|
|
6514
|
+
# below (PROXY_DISABLE_THINKING_ALWAYS), and it now works.
|
|
6456
6515
|
if isinstance(anthropic_thinking, dict):
|
|
6457
6516
|
ttype = (anthropic_thinking.get("type") or "").lower()
|
|
6458
|
-
|
|
6459
|
-
openai_body["enable_thinking"] = True
|
|
6460
|
-
else:
|
|
6461
|
-
openai_body["enable_thinking"] = False
|
|
6517
|
+
openai_body["enable_thinking"] = ttype == "enabled"
|
|
6462
6518
|
else:
|
|
6463
|
-
# Match Anthropic default: thinking off unless explicitly requested.
|
|
6464
6519
|
openai_body["enable_thinking"] = False
|
|
6465
6520
|
|
|
6466
6521
|
# Global thinking-off (G): apply to every request, not just tool turns.
|
|
@@ -6468,7 +6523,7 @@ def build_openai_request(
|
|
|
6468
6523
|
# Per-path tool-turn handling below (DISABLE_THINKING_ON_TOOL_TURNS) is
|
|
6469
6524
|
# additive — ALWAYS supersedes when set.
|
|
6470
6525
|
if PROXY_DISABLE_THINKING_ALWAYS:
|
|
6471
|
-
openai_body
|
|
6526
|
+
_set_thinking(openai_body, False)
|
|
6472
6527
|
|
|
6473
6528
|
# Inject agentic protocol instructions only for tool-enabled turns.
|
|
6474
6529
|
# Use minimal supplement for qwen models to reduce prompt leak surface.
|
|
@@ -6788,7 +6843,7 @@ def build_openai_request(
|
|
|
6788
6843
|
monitor.catastrophic_ctx_streak,
|
|
6789
6844
|
)
|
|
6790
6845
|
if PROXY_DISABLE_THINKING_ON_TOOL_TURNS:
|
|
6791
|
-
openai_body
|
|
6846
|
+
_set_thinking(openai_body, False)
|
|
6792
6847
|
if PROXY_DISABLE_SPEC_ON_TOOL_TURNS:
|
|
6793
6848
|
openai_body["speculative.n_max"] = 0
|
|
6794
6849
|
return openai_body
|
|
@@ -6813,7 +6868,7 @@ def build_openai_request(
|
|
|
6813
6868
|
)
|
|
6814
6869
|
# Skip all further tool_choice logic — no tools this turn
|
|
6815
6870
|
if PROXY_DISABLE_THINKING_ON_TOOL_TURNS:
|
|
6816
|
-
openai_body
|
|
6871
|
+
_set_thinking(openai_body, False)
|
|
6817
6872
|
if PROXY_DISABLE_SPEC_ON_TOOL_TURNS:
|
|
6818
6873
|
openai_body["speculative.n_max"] = 0
|
|
6819
6874
|
return openai_body
|
|
@@ -6863,7 +6918,7 @@ def build_openai_request(
|
|
|
6863
6918
|
PROXY_HARD_FINALIZE_TURNS,
|
|
6864
6919
|
)
|
|
6865
6920
|
if PROXY_DISABLE_THINKING_ON_TOOL_TURNS:
|
|
6866
|
-
openai_body
|
|
6921
|
+
_set_thinking(openai_body, False)
|
|
6867
6922
|
if PROXY_DISABLE_SPEC_ON_TOOL_TURNS:
|
|
6868
6923
|
openai_body["speculative.n_max"] = 0
|
|
6869
6924
|
return openai_body
|
|
@@ -7046,7 +7101,7 @@ def build_openai_request(
|
|
|
7046
7101
|
|
|
7047
7102
|
|
|
7048
7103
|
if PROXY_DISABLE_THINKING_ALWAYS or PROXY_DISABLE_THINKING_ON_TOOL_TURNS:
|
|
7049
|
-
openai_body
|
|
7104
|
+
_set_thinking(openai_body, False)
|
|
7050
7105
|
logger.info(
|
|
7051
7106
|
"Thinking disabled (always=%s tool_turns=%s)",
|
|
7052
7107
|
PROXY_DISABLE_THINKING_ALWAYS,
|
|
@@ -9408,10 +9463,7 @@ async def _apply_empty_maxtokens_recovery(
|
|
|
9408
9463
|
if not _is_empty_maxtokens_response(openai_resp):
|
|
9409
9464
|
return openai_resp
|
|
9410
9465
|
retry_body = dict(openai_body)
|
|
9411
|
-
retry_body
|
|
9412
|
-
ctk = dict(retry_body.get("chat_template_kwargs") or {})
|
|
9413
|
-
ctk["enable_thinking"] = False
|
|
9414
|
-
retry_body["chat_template_kwargs"] = ctk
|
|
9466
|
+
_set_thinking(retry_body, False)
|
|
9415
9467
|
requested = int(openai_body.get("max_tokens") or PROXY_EMPTY_MAXTOKENS_RETRY_MAX_TOKENS)
|
|
9416
9468
|
retry_body["max_tokens"] = min(requested, PROXY_EMPTY_MAXTOKENS_RETRY_MAX_TOKENS)
|
|
9417
9469
|
logger.warning(
|
|
@@ -9577,7 +9629,7 @@ def _build_malformed_retry_body(
|
|
|
9577
9629
|
)
|
|
9578
9630
|
|
|
9579
9631
|
if PROXY_DISABLE_THINKING_ON_TOOL_TURNS:
|
|
9580
|
-
retry_body
|
|
9632
|
+
_set_thinking(retry_body, False)
|
|
9581
9633
|
|
|
9582
9634
|
# Option 3: Proactively strip grammar from retry when tools are present and
|
|
9583
9635
|
# grammar+tools is known to be incompatible. Prevents the 400 error
|
|
@@ -2043,6 +2043,17 @@ class TestToolTurnControls(unittest.TestCase):
|
|
|
2043
2043
|
)
|
|
2044
2044
|
self.assertIn("enable_thinking", openai)
|
|
2045
2045
|
self.assertFalse(openai["enable_thinking"])
|
|
2046
|
+
# THE assertion whose absence let the switch be a no-op for months:
|
|
2047
|
+
# a `--jinja` server reads chat_template_kwargs and ignores the
|
|
2048
|
+
# top-level field entirely, so asserting only the line above passes
|
|
2049
|
+
# while the upstream keeps reasoning. Measured on the live server:
|
|
2050
|
+
# top-level only -> 703 chars of reasoning; this key -> 0.
|
|
2051
|
+
self.assertIs(
|
|
2052
|
+
openai.get("chat_template_kwargs", {}).get("enable_thinking"),
|
|
2053
|
+
False,
|
|
2054
|
+
"tool-turn thinking switch must reach chat_template_kwargs, "
|
|
2055
|
+
"or a --jinja upstream ignores it",
|
|
2056
|
+
)
|
|
2046
2057
|
finally:
|
|
2047
2058
|
setattr(proxy, "PROXY_DISABLE_THINKING_ON_TOOL_TURNS", old_disable)
|
|
2048
2059
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Turning reasoning off must reach the place llama.cpp actually reads it.
|
|
2
|
+
|
|
3
|
+
Measured on the live server (Qwen3.6-35B-A3B, launched `--jinja
|
|
4
|
+
--chat-template-kwargs {"enable_thinking": true}`), identical tool-call prompt:
|
|
5
|
+
|
|
6
|
+
top-level "enable_thinking": false -> 703 chars of reasoning, 196 tokens
|
|
7
|
+
chat_template_kwargs.enable_thinking -> 0 chars, 27 tokens
|
|
8
|
+
|
|
9
|
+
and on a planning-flavoured prompt with max_tokens=700, reasoning consumed the
|
|
10
|
+
ENTIRE budget and NO tool call was emitted — the "stuck planning" symptom.
|
|
11
|
+
|
|
12
|
+
Every thinking switch in the proxy set only the top-level field, so
|
|
13
|
+
PROXY_DISABLE_THINKING_ALWAYS, PROXY_DISABLE_THINKING_ON_TOOL_TURNS and the
|
|
14
|
+
Anthropic `thinking: {type: disabled}` parameter were all no-ops against a jinja
|
|
15
|
+
server: the controls existed, logged themselves as active, and changed nothing.
|
|
16
|
+
"""
|
|
17
|
+
import importlib.util
|
|
18
|
+
import os
|
|
19
|
+
import unittest
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
proxy_path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_PRIOR_AUTOLOAD = os.environ.get("UAP_PROXY_ENV_AUTOLOAD")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def tearDownModule():
|
|
29
|
+
# Restore rather than leak into any test sharing this process.
|
|
30
|
+
if _PRIOR_AUTOLOAD is None:
|
|
31
|
+
os.environ.pop("UAP_PROXY_ENV_AUTOLOAD", None)
|
|
32
|
+
else:
|
|
33
|
+
os.environ["UAP_PROXY_ENV_AUTOLOAD"] = _PRIOR_AUTOLOAD
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_proxy(**env):
|
|
37
|
+
# Explicit, so a project .uap/proxy.env cannot leak in when this module is
|
|
38
|
+
# run directly rather than through the npm script.
|
|
39
|
+
os.environ["UAP_PROXY_ENV_AUTOLOAD"] = "0"
|
|
40
|
+
for k in ("PROXY_DISABLE_THINKING_ALWAYS", "PROXY_DISABLE_THINKING_ON_TOOL_TURNS"):
|
|
41
|
+
os.environ.pop(k, None)
|
|
42
|
+
os.environ.update({k: v for k, v in env.items()})
|
|
43
|
+
spec = importlib.util.spec_from_file_location("anthropic_proxy_thinking", proxy_path)
|
|
44
|
+
mod = importlib.util.module_from_spec(spec)
|
|
45
|
+
spec.loader.exec_module(mod)
|
|
46
|
+
return mod
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SetThinkingTest(unittest.TestCase):
|
|
50
|
+
def tearDown(self):
|
|
51
|
+
for k in ("PROXY_DISABLE_THINKING_ALWAYS", "PROXY_DISABLE_THINKING_ON_TOOL_TURNS"):
|
|
52
|
+
os.environ.pop(k, None)
|
|
53
|
+
|
|
54
|
+
def test_disabling_reaches_chat_template_kwargs(self):
|
|
55
|
+
# THE BUG: the top-level field alone is overridden by the server's
|
|
56
|
+
# --chat-template-kwargs default, so reasoning kept flowing.
|
|
57
|
+
ap = load_proxy()
|
|
58
|
+
body = {}
|
|
59
|
+
ap._set_thinking(body, False)
|
|
60
|
+
self.assertIs(body["chat_template_kwargs"]["enable_thinking"], False)
|
|
61
|
+
|
|
62
|
+
def test_top_level_is_still_set_for_servers_that_read_it(self):
|
|
63
|
+
# Belt and braces: non-jinja / older builds read the top-level field.
|
|
64
|
+
ap = load_proxy()
|
|
65
|
+
body = {}
|
|
66
|
+
ap._set_thinking(body, False)
|
|
67
|
+
self.assertIs(body["enable_thinking"], False)
|
|
68
|
+
|
|
69
|
+
def test_enabling_reaches_both_places_too(self):
|
|
70
|
+
ap = load_proxy()
|
|
71
|
+
body = {}
|
|
72
|
+
ap._set_thinking(body, True)
|
|
73
|
+
self.assertIs(body["enable_thinking"], True)
|
|
74
|
+
self.assertIs(body["chat_template_kwargs"]["enable_thinking"], True)
|
|
75
|
+
|
|
76
|
+
def test_existing_chat_template_kwargs_are_preserved(self):
|
|
77
|
+
# The server is launched with other template kwargs; clobbering the dict
|
|
78
|
+
# would drop them.
|
|
79
|
+
ap = load_proxy()
|
|
80
|
+
body = {"chat_template_kwargs": {"preserve_thinking": False, "custom": 1}}
|
|
81
|
+
ap._set_thinking(body, False)
|
|
82
|
+
self.assertEqual(body["chat_template_kwargs"]["custom"], 1)
|
|
83
|
+
self.assertIs(body["chat_template_kwargs"]["preserve_thinking"], False)
|
|
84
|
+
self.assertIs(body["chat_template_kwargs"]["enable_thinking"], False)
|
|
85
|
+
|
|
86
|
+
def test_a_non_dict_kwargs_value_does_not_raise(self):
|
|
87
|
+
# Defensive: a malformed client body must not 500 the turn.
|
|
88
|
+
ap = load_proxy()
|
|
89
|
+
body = {"chat_template_kwargs": "nonsense"}
|
|
90
|
+
ap._set_thinking(body, False)
|
|
91
|
+
self.assertIs(body["enable_thinking"], False)
|
|
92
|
+
|
|
93
|
+
def test_a_shallow_retry_copy_does_not_disable_thinking_on_the_original(self):
|
|
94
|
+
# The helper's whole rationale: retry bodies are built as
|
|
95
|
+
# `dict(openai_body)`, which SHARES this nested dict. Mutating it in
|
|
96
|
+
# place would disable thinking on the in-flight request too.
|
|
97
|
+
ap = load_proxy()
|
|
98
|
+
original = {"chat_template_kwargs": {"enable_thinking": True}}
|
|
99
|
+
retry = dict(original)
|
|
100
|
+
ap._set_thinking(retry, False)
|
|
101
|
+
self.assertIs(original["chat_template_kwargs"]["enable_thinking"], True)
|
|
102
|
+
self.assertIs(retry["chat_template_kwargs"]["enable_thinking"], False)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class SwitchesReachTheTemplateTest(unittest.TestCase):
|
|
106
|
+
"""The switches, exercised through the real request builder."""
|
|
107
|
+
|
|
108
|
+
def tearDown(self):
|
|
109
|
+
for k in ("PROXY_DISABLE_THINKING_ALWAYS", "PROXY_DISABLE_THINKING_ON_TOOL_TURNS"):
|
|
110
|
+
os.environ.pop(k, None)
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _build(ap, tools=False):
|
|
114
|
+
body = {"model": "test", "messages": [{"role": "user", "content": "hi"}]}
|
|
115
|
+
if tools:
|
|
116
|
+
body["tools"] = [
|
|
117
|
+
{"name": "Read", "description": "Read file", "input_schema": {"type": "object"}}
|
|
118
|
+
]
|
|
119
|
+
return ap.build_openai_request(body, ap.SessionMonitor(context_window=262144))
|
|
120
|
+
|
|
121
|
+
def test_always_switch_reaches_chat_template_kwargs(self):
|
|
122
|
+
ap = load_proxy(PROXY_DISABLE_THINKING_ALWAYS="on")
|
|
123
|
+
built = self._build(ap)
|
|
124
|
+
self.assertIs(built["chat_template_kwargs"]["enable_thinking"], False)
|
|
125
|
+
|
|
126
|
+
def test_protocol_default_does_NOT_force_the_template(self):
|
|
127
|
+
# Deliberate: Anthropic defaults thinking off, and making that
|
|
128
|
+
# authoritative would flip every client that never asked for it from
|
|
129
|
+
# the server's configured default to off. A serving-policy change is
|
|
130
|
+
# not a bug fix; operators have an explicit switch for that.
|
|
131
|
+
ap = load_proxy()
|
|
132
|
+
built = self._build(ap)
|
|
133
|
+
self.assertIs(built["enable_thinking"], False)
|
|
134
|
+
self.assertNotIn(
|
|
135
|
+
"enable_thinking",
|
|
136
|
+
built.get("chat_template_kwargs", {}),
|
|
137
|
+
"the protocol default must not override the server's configured default",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def test_a_client_cannot_turn_thinking_ON_against_the_server_default(self):
|
|
141
|
+
# The measured bug was one-directional (an OFF switch that didn't
|
|
142
|
+
# stick); the fix stays one-directional.
|
|
143
|
+
ap = load_proxy()
|
|
144
|
+
body = {
|
|
145
|
+
"model": "test",
|
|
146
|
+
"messages": [{"role": "user", "content": "hi"}],
|
|
147
|
+
"thinking": {"type": "enabled"},
|
|
148
|
+
}
|
|
149
|
+
built = ap.build_openai_request(body, ap.SessionMonitor(context_window=262144))
|
|
150
|
+
self.assertIs(built["enable_thinking"], True)
|
|
151
|
+
self.assertNotIn("enable_thinking", built.get("chat_template_kwargs", {}))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
unittest.main()
|