@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.210.4",
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",
@@ -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["enable_thinking"] = False
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
- if ttype == "enabled":
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["enable_thinking"] = False
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["enable_thinking"] = False
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["enable_thinking"] = False
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["enable_thinking"] = False
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["enable_thinking"] = False
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["enable_thinking"] = False
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["enable_thinking"] = False
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()