@miller-tech/uap 1.210.3 → 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.3",
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",
@@ -54,8 +54,10 @@ Configuration (Environment Variables)
54
54
  PROXY_MAX_CONNECTIONS Max concurrent connections to upstream
55
55
  Default: 20
56
56
 
57
- PROXY_CONTEXT_WINDOW Override context window size (auto-detected from
58
- upstream /slots endpoint if not set)
57
+ PROXY_CONTEXT_WINDOW Startup FALLBACK for the context window, used only
58
+ when the upstream /slots probe fails. The detected
59
+ rail wins (re-checked every 60s) and is what
60
+ /v1/models advertises. Not a cap.
59
61
  Default: 0 (auto-detect)
60
62
 
61
63
  PROXY_CONTEXT_PRUNE_THRESHOLD Fraction of context window at which
@@ -1451,12 +1453,48 @@ def _apply_json_response_grammar(openai_body: dict, anthropic_body: dict) -> Non
1451
1453
  if openai_body.get("tools") or openai_body.get("grammar"):
1452
1454
  return
1453
1455
  openai_body["grammar"] = JSON_RESPONSE_GBNF
1454
- openai_body["enable_thinking"] = False
1455
- ctk = openai_body.setdefault("chat_template_kwargs", {})
1456
- ctk["enable_thinking"] = False
1456
+ _set_thinking(openai_body, False)
1457
1457
  logger.info("JSON-RESPONSE grammar applied (evaluator verdict turn)")
1458
1458
 
1459
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
+
1460
1498
  def _apply_thinking_grammar(request_body: dict) -> None:
1461
1499
  """Apply the structured-thinking GBNF grammar to non-tool turns.
1462
1500
 
@@ -1545,6 +1583,13 @@ def _apply_profile_overrides(
1545
1583
  if "stop_sequences" in profile:
1546
1584
  updated["stop_sequences"] = profile["stop_sequences"]
1547
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.
1548
1593
  updated["enable_thinking"] = profile["enable_thinking"]
1549
1594
 
1550
1595
  tool_call_batching = profile.get("tool_call_batching") or {}
@@ -2407,6 +2452,11 @@ class SessionMonitor:
2407
2452
 
2408
2453
  session_monitors: dict[str, SessionMonitor] = {}
2409
2454
  default_context_window = 0
2455
+ # True only when `default_context_window` came from the SERVER (a /slots read)
2456
+ # or from an explicit operator setting — never when it is the hardcoded
2457
+ # fallback. /v1/models publishes the window to clients as fact, and a guess
2458
+ # published as fact is the failure `_model_entry` exists to prevent.
2459
+ _context_window_measured: bool = False
2410
2460
  last_session_id = ""
2411
2461
  _last_ctx_recheck_ts: float = 0.0
2412
2462
  _CTX_RECHECK_INTERVAL: float = 60.0 # Re-detect context window every 60s
@@ -2476,13 +2526,41 @@ def _cleanup_stale_monitors(now_ts: float) -> None:
2476
2526
  session_monitors.pop(sid, None)
2477
2527
 
2478
2528
 
2529
+ def _effective_context_window() -> int:
2530
+ """The window this proxy actually ENFORCES, in tokens (0 = unknown).
2531
+
2532
+ The detected value wins over the env pin because the pin goes stale: the
2533
+ upstream server can restart with a different --ctx-size, and
2534
+ `_maybe_recheck_context_window` tracks that. `PROXY_CONTEXT_WINDOW` is the
2535
+ fallback for when detection has not run or could not reach the server.
2536
+
2537
+ This is the idiom already used at the count_tokens compaction-forcing call
2538
+ sites; it exists as a function so the process-wide consumers — the forcing
2539
+ scale and the /v1/models advertisement — resolve the window from ONE
2540
+ source. They did not: /v1/models stamped the raw env value while everything
2541
+ else used the detected rail. Live on 2026-08-16 that meant the endpoint
2542
+ advertised 65,536 while the proxy enforced 199,680, and the two errors
2543
+ compounded. A client sizing itself to 65,536 while receiving counts
2544
+ inflated 1.73x for compaction forcing compacts at ~35k REAL tokens — 18%
2545
+ of the rail it was given.
2546
+
2547
+ NOT every window in the process: a request carrying a model profile
2548
+ overrides `monitor.context_window` (see `messages`), and the pruner reads
2549
+ that per-session value. This function is the process default, not a claim
2550
+ about every session.
2551
+ """
2552
+ if default_context_window > 0:
2553
+ return default_context_window
2554
+ return max(PROXY_CONTEXT_WINDOW, 0)
2555
+
2556
+
2479
2557
  async def _maybe_recheck_context_window() -> None:
2480
2558
  """Periodically re-query the upstream server's context window.
2481
2559
 
2482
2560
  Handles server restarts with different --ctx-size mid-session.
2483
2561
  Non-blocking: skips if the check interval hasn't elapsed.
2484
2562
  """
2485
- global default_context_window, _last_ctx_recheck_ts
2563
+ global default_context_window, _last_ctx_recheck_ts, _context_window_measured
2486
2564
  now = time.time()
2487
2565
  if now - _last_ctx_recheck_ts < _CTX_RECHECK_INTERVAL:
2488
2566
  return
@@ -2496,6 +2574,8 @@ async def _maybe_recheck_context_window() -> None:
2496
2574
  slots = resp.json()
2497
2575
  if slots and isinstance(slots, list):
2498
2576
  n_ctx = slots[0].get("n_ctx", 0)
2577
+ if n_ctx > 0:
2578
+ _context_window_measured = True
2499
2579
  if n_ctx > 0 and n_ctx != default_context_window:
2500
2580
  old = default_context_window
2501
2581
  default_context_window = n_ctx
@@ -2533,10 +2613,17 @@ async def detect_context_window(client: httpx.AsyncClient) -> int:
2533
2613
 
2534
2614
  Queries the /slots endpoint (llama.cpp) to get the actual n_ctx value.
2535
2615
  Falls back to PROXY_CONTEXT_WINDOW env var, then to a safe default.
2616
+
2617
+ The probe runs FIRST, which is what this docstring always claimed but the
2618
+ code did not do: it returned the env value without asking the server, so a
2619
+ stale setting governed until the first /v1/messages request triggered the
2620
+ 60s recheck — and SDK clients read /v1/models before ever sending a message.
2621
+ The env value is a hand-maintained copy of this same number (the operator
2622
+ file that carries it says "re-derive it whenever --parallel or --ctx-size
2623
+ moves"), so asking the server is strictly better information; the setting
2624
+ stays as the answer for when the server cannot be reached.
2536
2625
  """
2537
- if PROXY_CONTEXT_WINDOW > 0:
2538
- logger.info("Using configured context window: %d tokens", PROXY_CONTEXT_WINDOW)
2539
- return PROXY_CONTEXT_WINDOW
2626
+ global _context_window_measured
2540
2627
 
2541
2628
  try:
2542
2629
  slots_url = LLAMA_CPP_BASE.replace("/v1", "/slots")
@@ -2551,13 +2638,21 @@ async def detect_context_window(client: httpx.AsyncClient) -> int:
2551
2638
  n_ctx,
2552
2639
  len(slots),
2553
2640
  )
2641
+ _context_window_measured = True
2554
2642
  return n_ctx
2555
2643
  except Exception as exc:
2556
2644
  logger.warning("Failed to auto-detect context window: %s", exc)
2557
2645
 
2558
- # Safe default: 128K (common for modern models)
2646
+ if PROXY_CONTEXT_WINDOW > 0:
2647
+ logger.info("Using configured context window: %d tokens", PROXY_CONTEXT_WINDOW)
2648
+ _context_window_measured = True # an operator setting is an assertion
2649
+ return PROXY_CONTEXT_WINDOW
2650
+
2651
+ # Safe default: 128K (common for modern models). NOT measured — the pruner
2652
+ # may use it as a backstop, but it must never be published as fact.
2559
2653
  default = 131072
2560
2654
  logger.warning("Using default context window: %d tokens", default)
2655
+ _context_window_measured = False
2561
2656
  return default
2562
2657
 
2563
2658
 
@@ -6401,14 +6496,26 @@ def build_openai_request(
6401
6496
  # consume the client's max_tokens budget on internal reasoning, leaving
6402
6497
  # nothing for the visible answer.
6403
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.
6404
6515
  if isinstance(anthropic_thinking, dict):
6405
6516
  ttype = (anthropic_thinking.get("type") or "").lower()
6406
- if ttype == "enabled":
6407
- openai_body["enable_thinking"] = True
6408
- else:
6409
- openai_body["enable_thinking"] = False
6517
+ openai_body["enable_thinking"] = ttype == "enabled"
6410
6518
  else:
6411
- # Match Anthropic default: thinking off unless explicitly requested.
6412
6519
  openai_body["enable_thinking"] = False
6413
6520
 
6414
6521
  # Global thinking-off (G): apply to every request, not just tool turns.
@@ -6416,7 +6523,7 @@ def build_openai_request(
6416
6523
  # Per-path tool-turn handling below (DISABLE_THINKING_ON_TOOL_TURNS) is
6417
6524
  # additive — ALWAYS supersedes when set.
6418
6525
  if PROXY_DISABLE_THINKING_ALWAYS:
6419
- openai_body["enable_thinking"] = False
6526
+ _set_thinking(openai_body, False)
6420
6527
 
6421
6528
  # Inject agentic protocol instructions only for tool-enabled turns.
6422
6529
  # Use minimal supplement for qwen models to reduce prompt leak surface.
@@ -6736,7 +6843,7 @@ def build_openai_request(
6736
6843
  monitor.catastrophic_ctx_streak,
6737
6844
  )
6738
6845
  if PROXY_DISABLE_THINKING_ON_TOOL_TURNS:
6739
- openai_body["enable_thinking"] = False
6846
+ _set_thinking(openai_body, False)
6740
6847
  if PROXY_DISABLE_SPEC_ON_TOOL_TURNS:
6741
6848
  openai_body["speculative.n_max"] = 0
6742
6849
  return openai_body
@@ -6761,7 +6868,7 @@ def build_openai_request(
6761
6868
  )
6762
6869
  # Skip all further tool_choice logic — no tools this turn
6763
6870
  if PROXY_DISABLE_THINKING_ON_TOOL_TURNS:
6764
- openai_body["enable_thinking"] = False
6871
+ _set_thinking(openai_body, False)
6765
6872
  if PROXY_DISABLE_SPEC_ON_TOOL_TURNS:
6766
6873
  openai_body["speculative.n_max"] = 0
6767
6874
  return openai_body
@@ -6811,7 +6918,7 @@ def build_openai_request(
6811
6918
  PROXY_HARD_FINALIZE_TURNS,
6812
6919
  )
6813
6920
  if PROXY_DISABLE_THINKING_ON_TOOL_TURNS:
6814
- openai_body["enable_thinking"] = False
6921
+ _set_thinking(openai_body, False)
6815
6922
  if PROXY_DISABLE_SPEC_ON_TOOL_TURNS:
6816
6923
  openai_body["speculative.n_max"] = 0
6817
6924
  return openai_body
@@ -6994,7 +7101,7 @@ def build_openai_request(
6994
7101
 
6995
7102
 
6996
7103
  if PROXY_DISABLE_THINKING_ALWAYS or PROXY_DISABLE_THINKING_ON_TOOL_TURNS:
6997
- openai_body["enable_thinking"] = False
7104
+ _set_thinking(openai_body, False)
6998
7105
  logger.info(
6999
7106
  "Thinking disabled (always=%s tool_turns=%s)",
7000
7107
  PROXY_DISABLE_THINKING_ALWAYS,
@@ -9356,10 +9463,7 @@ async def _apply_empty_maxtokens_recovery(
9356
9463
  if not _is_empty_maxtokens_response(openai_resp):
9357
9464
  return openai_resp
9358
9465
  retry_body = dict(openai_body)
9359
- retry_body["enable_thinking"] = False
9360
- ctk = dict(retry_body.get("chat_template_kwargs") or {})
9361
- ctk["enable_thinking"] = False
9362
- retry_body["chat_template_kwargs"] = ctk
9466
+ _set_thinking(retry_body, False)
9363
9467
  requested = int(openai_body.get("max_tokens") or PROXY_EMPTY_MAXTOKENS_RETRY_MAX_TOKENS)
9364
9468
  retry_body["max_tokens"] = min(requested, PROXY_EMPTY_MAXTOKENS_RETRY_MAX_TOKENS)
9365
9469
  logger.warning(
@@ -9525,7 +9629,7 @@ def _build_malformed_retry_body(
9525
9629
  )
9526
9630
 
9527
9631
  if PROXY_DISABLE_THINKING_ON_TOOL_TURNS:
9528
- retry_body["enable_thinking"] = False
9632
+ _set_thinking(retry_body, False)
9529
9633
 
9530
9634
  # Option 3: Proactively strip grammar from retry when tools are present and
9531
9635
  # grammar+tools is known to be incompatible. Prevents the 400 error
@@ -11485,7 +11589,7 @@ async def count_tokens(request: Request):
11485
11589
  "fires at ~%d real tokens, before the pruner",
11486
11590
  scale,
11487
11591
  PROXY_CLIENT_ASSUMED_WINDOW,
11488
- default_context_window if default_context_window > 0 else PROXY_CONTEXT_WINDOW,
11592
+ _effective_context_window(),
11489
11593
  int(PROXY_CLIENT_ASSUMED_WINDOW * 0.925 / scale),
11490
11594
  )
11491
11595
  return {"input_tokens": scaled}
@@ -11510,7 +11614,7 @@ def _count_tokens_scale() -> float:
11510
11614
  return max(1.0, float(raw))
11511
11615
  except ValueError:
11512
11616
  return 1.0
11513
- window = default_context_window if default_context_window > 0 else PROXY_CONTEXT_WINDOW
11617
+ window = _effective_context_window()
11514
11618
  if window <= 0:
11515
11619
  return 1.0
11516
11620
  frac = (
@@ -12845,9 +12949,10 @@ def _model_entry(model_id: str) -> dict:
12845
12949
  nothing and leave the client on its own defaults.
12846
12950
  """
12847
12951
  entry = {"id": model_id, "object": "model"}
12848
- if PROXY_CONTEXT_WINDOW > 0 and not _should_passthrough_model(model_id):
12952
+ window = _effective_context_window() if _context_window_measured else 0
12953
+ if window > 0 and not _should_passthrough_model(model_id):
12849
12954
  for key in _CONTEXT_WINDOW_KEYS:
12850
- entry[key] = PROXY_CONTEXT_WINDOW
12955
+ entry[key] = window
12851
12956
  return entry
12852
12957
 
12853
12958
 
@@ -12867,6 +12972,12 @@ async def models():
12867
12972
  ANTHROPIC_PASSTHROUGH_MODELS=__local_only__ is set, all IDs (including
12868
12973
  the Claude ones below) are served by the local llama.cpp backend.
12869
12974
  """
12975
+ # Refresh the rail before answering. This endpoint is the FIRST thing SDK
12976
+ # clients call (it is in _PROXY_AUTH_OPEN_PATHS precisely so discovery
12977
+ # works), and clients cache the model list — so answering from a window
12978
+ # that only refreshes on /v1/messages means the number a client keeps for
12979
+ # the whole session is the one from before any traffic existed.
12980
+ await _maybe_recheck_context_window()
12870
12981
  return {"data": [_model_entry(mid) for mid in ADVERTISED_MODEL_IDS]}
12871
12982
 
12872
12983
 
@@ -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
 
@@ -11,6 +11,7 @@ window at all.
11
11
 
12
12
  A client that cannot discover the window cannot size its history to it.
13
13
  """
14
+ import asyncio
14
15
  import importlib.util
15
16
  import os
16
17
  import unittest
@@ -19,9 +20,18 @@ from pathlib import Path
19
20
  proxy_path = Path(__file__).resolve().parents[3] / "tools" / "agents" / "scripts" / "anthropic_proxy.py"
20
21
 
21
22
 
22
- def load_proxy(window="130048", passthrough=None):
23
- """Import a fresh proxy module under the given env (constants bind at import)."""
23
+ def load_proxy(window="130048", passthrough=None, measured=True):
24
+ """Import a fresh proxy module under the given env (constants bind at import).
25
+
26
+ `measured` mirrors what a real startup leaves behind: True once /slots (or
27
+ an explicit setting) supplied the window, False when it is the hardcoded
28
+ guess. /v1/models only publishes a measured window.
29
+ """
24
30
  os.environ["PROXY_CONTEXT_WINDOW"] = window
31
+ # Pin the forcing scale: `auto` is the default, but a developer shell or a
32
+ # systemd unit exporting a fixed value would silently change what
33
+ # _count_tokens_scale returns and fail these tests for unrelated reasons.
34
+ os.environ["PROXY_COUNT_TOKENS_SCALE"] = "auto"
25
35
  if passthrough is None:
26
36
  os.environ.pop("ANTHROPIC_PASSTHROUGH_MODELS", None)
27
37
  else:
@@ -29,6 +39,7 @@ def load_proxy(window="130048", passthrough=None):
29
39
  spec = importlib.util.spec_from_file_location("anthropic_proxy_ctx", proxy_path)
30
40
  mod = importlib.util.module_from_spec(spec)
31
41
  spec.loader.exec_module(mod)
42
+ mod._context_window_measured = measured
32
43
  return mod
33
44
 
34
45
 
@@ -36,6 +47,7 @@ class ModelsAdvertiseContextWindowTest(unittest.TestCase):
36
47
  def tearDown(self):
37
48
  os.environ.pop("PROXY_CONTEXT_WINDOW", None)
38
49
  os.environ.pop("ANTHROPIC_PASSTHROUGH_MODELS", None)
50
+ os.environ.pop("PROXY_COUNT_TOKENS_SCALE", None)
39
51
 
40
52
  def test_local_model_carries_the_window(self):
41
53
  ap = load_proxy()
@@ -57,21 +69,77 @@ class ModelsAdvertiseContextWindowTest(unittest.TestCase):
57
69
  # Stamping the local llama.cpp figure on it would make clients truncate
58
70
  # needlessly — worse than the bug being fixed.
59
71
  ap = load_proxy(passthrough=None) # default patterns: Claude passes through
72
+ ap.default_context_window = 199680 # exercise the DETECTED arm, not the fallback
60
73
  for mid in ("claude-sonnet-4-6", "claude-haiku-4-5-20251001"):
61
74
  self.assertNotIn("context_length", ap._model_entry(mid), mid)
62
75
 
63
76
  def test_local_only_sentinel_means_every_id_is_local(self):
64
77
  ap = load_proxy(passthrough="__local_only__")
78
+ ap.default_context_window = 199680
65
79
  for mid in ap.ADVERTISED_MODEL_IDS:
66
- self.assertEqual(ap._model_entry(mid).get("context_length"), 130048, mid)
80
+ self.assertEqual(ap._model_entry(mid).get("context_length"), 199680, mid)
67
81
 
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")
82
+ def test_unknown_window_advertises_nothing(self):
83
+ # Better to say nothing than to assert a wrong number. "Unknown" means
84
+ # BOTH: no setting and no detected rail.
85
+ ap = load_proxy(window="0", measured=False)
86
+ ap.default_context_window = 0
71
87
  self.assertEqual(
72
88
  ap._model_entry("qwen36-35b-a3b-iq4xs"), {"id": "qwen36-35b-a3b-iq4xs", "object": "model"}
73
89
  )
74
90
 
91
+ def test_a_guessed_window_is_never_published_as_fact(self):
92
+ # detect_context_window falls back to a hardcoded 131072 when the
93
+ # upstream is unreachable. The pruner may use that as a backstop, but
94
+ # advertising it would state a number nobody measured — the very
95
+ # failure this endpoint exists to prevent. Boot with llama down.
96
+ ap = load_proxy(window="0", measured=False)
97
+ ap.default_context_window = 131072
98
+ self.assertEqual(
99
+ ap._model_entry("qwen36-35b-a3b-iq4xs"), {"id": "qwen36-35b-a3b-iq4xs", "object": "model"}
100
+ )
101
+
102
+ def test_advertises_the_detected_rail_when_no_pin_is_set(self):
103
+ # Detection (from /slots) is the normal case: PROXY_CONTEXT_WINDOW=0 is
104
+ # what tells the launcher to auto-detect. Advertising nothing there left
105
+ # exactly the clients this endpoint exists for — the ones that size
106
+ # themselves from it — back on their own defaults.
107
+ ap = load_proxy(window="0")
108
+ ap.default_context_window = 199680
109
+ self.assertEqual(ap._model_entry("qwen36-35b-a3b-iq4xs")["context_length"], 199680)
110
+
111
+ def test_detected_rail_wins_over_a_stale_pin(self):
112
+ # THE BUG (live, 2026-08-16): the pin said 65,536 while the proxy
113
+ # enforced the detected 199,680. Clients were told the small number and
114
+ # compacted against it; the pruner guarded the large one. Whatever the
115
+ # number is, the advertised and enforced windows must be the same one.
116
+ ap = load_proxy(window="65536")
117
+ ap.default_context_window = 199680
118
+ self.assertEqual(ap._model_entry("qwen36-35b-a3b-iq4xs")["context_length"], 199680)
119
+ self.assertEqual(ap._effective_context_window(), 199680)
120
+
121
+ def test_pin_is_the_fallback_until_detection_lands(self):
122
+ # At startup, before the first /slots probe, the pin is all there is.
123
+ ap = load_proxy(window="65536")
124
+ ap.default_context_window = 0
125
+ self.assertEqual(ap._model_entry("qwen36-35b-a3b-iq4xs")["context_length"], 65536)
126
+ self.assertEqual(ap._effective_context_window(), 65536)
127
+
128
+ def test_advertised_window_equals_the_forcing_scale_denominator(self):
129
+ # The compaction-forcing scale and the advertisement are two halves of
130
+ # one contract: the client is told a window AND handed counts scaled to
131
+ # make it compact inside that window. Derived from different numbers,
132
+ # they fight — the 65,536/199,680 split had clients compacting at ~18%
133
+ # of the rail.
134
+ ap = load_proxy(window="65536")
135
+ ap.default_context_window = 199680
136
+ advertised = ap._model_entry("qwen36-35b-a3b-iq4xs")["context_length"]
137
+ frac = ap.PROXY_COMPACT_TARGET_FRACTION
138
+ if not (0 < frac < 1):
139
+ frac = min(0.9, ap.PROXY_CONTEXT_PRUNE_THRESHOLD * 0.95)
140
+ expected = ap.PROXY_CLIENT_ASSUMED_WINDOW / (advertised * frac)
141
+ self.assertAlmostEqual(ap._count_tokens_scale(), max(1.0, expected), places=6)
142
+
75
143
  def test_entry_always_keeps_the_openai_shape(self):
76
144
  ap = load_proxy()
77
145
  for mid in ap.ADVERTISED_MODEL_IDS:
@@ -80,5 +148,54 @@ class ModelsAdvertiseContextWindowTest(unittest.TestCase):
80
148
  self.assertEqual(e["object"], "model")
81
149
 
82
150
 
151
+ class DetectContextWindowTest(unittest.TestCase):
152
+ """Startup resolution: ask the server first, settings are the fallback."""
153
+
154
+ def tearDown(self):
155
+ os.environ.pop("PROXY_CONTEXT_WINDOW", None)
156
+ os.environ.pop("PROXY_COUNT_TOKENS_SCALE", None)
157
+
158
+ @staticmethod
159
+ def _client(n_ctx=None, fail=False):
160
+ class _Resp:
161
+ status_code = 200
162
+
163
+ def json(self):
164
+ return [{"n_ctx": n_ctx}, {"n_ctx": n_ctx}]
165
+
166
+ class _Client:
167
+ async def get(self, url, timeout=None):
168
+ if fail:
169
+ raise RuntimeError("connection refused")
170
+ return _Resp()
171
+
172
+ return _Client()
173
+
174
+ def test_probes_the_server_even_when_a_fallback_is_configured(self):
175
+ # Previously the configured value short-circuited the probe, so a stale
176
+ # setting governed /v1/models until the first /v1/messages — and SDK
177
+ # clients read /v1/models before sending anything.
178
+ ap = load_proxy(window="65536", measured=False)
179
+ w = asyncio.run(ap.detect_context_window(self._client(n_ctx=199680)))
180
+ self.assertEqual(w, 199680)
181
+ self.assertTrue(ap._context_window_measured)
182
+
183
+ def test_falls_back_to_the_configured_value_when_the_probe_fails(self):
184
+ ap = load_proxy(window="65536", measured=False)
185
+ w = asyncio.run(ap.detect_context_window(self._client(fail=True)))
186
+ self.assertEqual(w, 65536)
187
+ # An operator setting is an assertion, so it may be published.
188
+ self.assertTrue(ap._context_window_measured)
189
+
190
+ def test_last_resort_guess_is_marked_unmeasured(self):
191
+ ap = load_proxy(window="0", measured=True)
192
+ w = asyncio.run(ap.detect_context_window(self._client(fail=True)))
193
+ self.assertEqual(w, 131072)
194
+ self.assertFalse(ap._context_window_measured)
195
+ self.assertEqual(
196
+ ap._model_entry("qwen36-35b-a3b-iq4xs"), {"id": "qwen36-35b-a3b-iq4xs", "object": "model"}
197
+ )
198
+
199
+
83
200
  if __name__ == "__main__":
84
201
  unittest.main()
@@ -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()