@miller-tech/uap 2.11.0 → 2.16.2

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.
Files changed (166) hide show
  1. package/README.md +7 -6
  2. package/config/capacity-policy.json +15 -3
  3. package/config/model-profiles/qwen38.json +42 -34
  4. package/dist/.tsbuildinfo +1 -1
  5. package/dist/bin/cli.js +39 -0
  6. package/dist/bin/cli.js.map +1 -1
  7. package/dist/capacity/policy.d.ts +15 -0
  8. package/dist/capacity/policy.d.ts.map +1 -1
  9. package/dist/capacity/policy.js +23 -0
  10. package/dist/capacity/policy.js.map +1 -1
  11. package/dist/capacity/probe.d.ts.map +1 -1
  12. package/dist/capacity/probe.js +6 -1
  13. package/dist/capacity/probe.js.map +1 -1
  14. package/dist/cli/deliver.d.ts.map +1 -1
  15. package/dist/cli/deliver.js +148 -2
  16. package/dist/cli/deliver.js.map +1 -1
  17. package/dist/cli/inference.d.ts +13 -0
  18. package/dist/cli/inference.d.ts.map +1 -0
  19. package/dist/cli/inference.js +132 -0
  20. package/dist/cli/inference.js.map +1 -0
  21. package/dist/cli/loops.d.ts +39 -0
  22. package/dist/cli/loops.d.ts.map +1 -0
  23. package/dist/cli/loops.js +154 -0
  24. package/dist/cli/loops.js.map +1 -0
  25. package/dist/cli/wizard-config.d.ts.map +1 -1
  26. package/dist/cli/wizard-config.js +5 -1
  27. package/dist/cli/wizard-config.js.map +1 -1
  28. package/dist/cli/worktree.d.ts.map +1 -1
  29. package/dist/cli/worktree.js +8 -4
  30. package/dist/cli/worktree.js.map +1 -1
  31. package/dist/delivery/adversarial-attack.d.ts +99 -0
  32. package/dist/delivery/adversarial-attack.d.ts.map +1 -0
  33. package/dist/delivery/adversarial-attack.js +326 -0
  34. package/dist/delivery/adversarial-attack.js.map +1 -0
  35. package/dist/delivery/adversarial-gate.d.ts +120 -0
  36. package/dist/delivery/adversarial-gate.d.ts.map +1 -0
  37. package/dist/delivery/adversarial-gate.js +259 -0
  38. package/dist/delivery/adversarial-gate.js.map +1 -0
  39. package/dist/delivery/adversarial-sanction.d.ts +52 -0
  40. package/dist/delivery/adversarial-sanction.d.ts.map +1 -0
  41. package/dist/delivery/adversarial-sanction.js +134 -0
  42. package/dist/delivery/adversarial-sanction.js.map +1 -0
  43. package/dist/delivery/agentic-executor.d.ts +69 -22
  44. package/dist/delivery/agentic-executor.d.ts.map +1 -1
  45. package/dist/delivery/agentic-executor.js +152 -8
  46. package/dist/delivery/agentic-executor.js.map +1 -1
  47. package/dist/delivery/applier.d.ts +8 -0
  48. package/dist/delivery/applier.d.ts.map +1 -1
  49. package/dist/delivery/applier.js +3 -1
  50. package/dist/delivery/applier.js.map +1 -1
  51. package/dist/delivery/convergence-loop.d.ts +15 -0
  52. package/dist/delivery/convergence-loop.d.ts.map +1 -1
  53. package/dist/delivery/convergence-loop.js.map +1 -1
  54. package/dist/delivery/criteria-lint.d.ts +74 -0
  55. package/dist/delivery/criteria-lint.d.ts.map +1 -0
  56. package/dist/delivery/criteria-lint.js +134 -0
  57. package/dist/delivery/criteria-lint.js.map +1 -0
  58. package/dist/delivery/deliver-toggle.d.ts +21 -0
  59. package/dist/delivery/deliver-toggle.d.ts.map +1 -0
  60. package/dist/delivery/deliver-toggle.js +40 -0
  61. package/dist/delivery/deliver-toggle.js.map +1 -0
  62. package/dist/delivery/delivery-evidence.d.ts +109 -0
  63. package/dist/delivery/delivery-evidence.d.ts.map +1 -0
  64. package/dist/delivery/delivery-evidence.js +139 -0
  65. package/dist/delivery/delivery-evidence.js.map +1 -0
  66. package/dist/delivery/epic-mission.d.ts +5 -1
  67. package/dist/delivery/epic-mission.d.ts.map +1 -1
  68. package/dist/delivery/epic-mission.js +34 -6
  69. package/dist/delivery/epic-mission.js.map +1 -1
  70. package/dist/delivery/execution-gate.d.ts.map +1 -1
  71. package/dist/delivery/execution-gate.js +90 -7
  72. package/dist/delivery/execution-gate.js.map +1 -1
  73. package/dist/delivery/index.d.ts +9 -1
  74. package/dist/delivery/index.d.ts.map +1 -1
  75. package/dist/delivery/index.js +9 -1
  76. package/dist/delivery/index.js.map +1 -1
  77. package/dist/delivery/journey-depth.d.ts +61 -0
  78. package/dist/delivery/journey-depth.d.ts.map +1 -0
  79. package/dist/delivery/journey-depth.js +91 -0
  80. package/dist/delivery/journey-depth.js.map +1 -0
  81. package/dist/delivery/master-pipeline.d.ts +50 -0
  82. package/dist/delivery/master-pipeline.d.ts.map +1 -0
  83. package/dist/delivery/master-pipeline.js +52 -0
  84. package/dist/delivery/master-pipeline.js.map +1 -0
  85. package/dist/delivery/mission-acceptance.d.ts.map +1 -1
  86. package/dist/delivery/mission-acceptance.js +54 -2
  87. package/dist/delivery/mission-acceptance.js.map +1 -1
  88. package/dist/delivery/orchestrated-mission.d.ts +4 -1
  89. package/dist/delivery/orchestrated-mission.d.ts.map +1 -1
  90. package/dist/delivery/orchestrated-mission.js +18 -9
  91. package/dist/delivery/orchestrated-mission.js.map +1 -1
  92. package/dist/delivery/plan-check.d.ts +1 -0
  93. package/dist/delivery/plan-check.d.ts.map +1 -1
  94. package/dist/delivery/plan-check.js +48 -0
  95. package/dist/delivery/plan-check.js.map +1 -1
  96. package/dist/delivery/task-orchestrator.d.ts +16 -9
  97. package/dist/delivery/task-orchestrator.d.ts.map +1 -1
  98. package/dist/delivery/task-orchestrator.js +4 -2
  99. package/dist/delivery/task-orchestrator.js.map +1 -1
  100. package/dist/delivery/task-workspace.d.ts +35 -5
  101. package/dist/delivery/task-workspace.d.ts.map +1 -1
  102. package/dist/delivery/task-workspace.js +53 -7
  103. package/dist/delivery/task-workspace.js.map +1 -1
  104. package/dist/delivery/test-oracle-additive.d.ts +26 -6
  105. package/dist/delivery/test-oracle-additive.d.ts.map +1 -1
  106. package/dist/delivery/test-oracle-additive.js +40 -15
  107. package/dist/delivery/test-oracle-additive.js.map +1 -1
  108. package/dist/delivery/user-validation.d.ts +14 -0
  109. package/dist/delivery/user-validation.d.ts.map +1 -1
  110. package/dist/delivery/user-validation.js +103 -9
  111. package/dist/delivery/user-validation.js.map +1 -1
  112. package/dist/delivery/verifier-ladder.d.ts.map +1 -1
  113. package/dist/delivery/verifier-ladder.js +24 -6
  114. package/dist/delivery/verifier-ladder.js.map +1 -1
  115. package/dist/inference/analysis.d.ts +138 -0
  116. package/dist/inference/analysis.d.ts.map +1 -0
  117. package/dist/inference/analysis.js +243 -0
  118. package/dist/inference/analysis.js.map +1 -0
  119. package/dist/inference/probe.d.ts +85 -0
  120. package/dist/inference/probe.d.ts.map +1 -0
  121. package/dist/inference/probe.js +403 -0
  122. package/dist/inference/probe.js.map +1 -0
  123. package/dist/models/types.d.ts.map +1 -1
  124. package/dist/models/types.js +45 -18
  125. package/dist/models/types.js.map +1 -1
  126. package/dist/self-tuning/profiles/qwen38.d.ts +17 -8
  127. package/dist/self-tuning/profiles/qwen38.d.ts.map +1 -1
  128. package/dist/self-tuning/profiles/qwen38.js +21 -11
  129. package/dist/self-tuning/profiles/qwen38.js.map +1 -1
  130. package/dist/utils/model-slots.d.ts +11 -5
  131. package/dist/utils/model-slots.d.ts.map +1 -1
  132. package/dist/utils/model-slots.js +11 -5
  133. package/dist/utils/model-slots.js.map +1 -1
  134. package/docs/INDEX.md +7 -1
  135. package/docs/guides/CAPACITY_POLICY.md +29 -4
  136. package/docs/guides/INFERENCE_HEALTH.md +184 -0
  137. package/docs/guides/PROXY.md +1 -0
  138. package/docs/guides/QWEN38_EXL3.md +268 -0
  139. package/docs/plans/hlg-master-architecture-uplift-2026-09-24.md +67 -0
  140. package/docs/plans/system1-uplift-2026-09-18.md +1 -2
  141. package/docs/reference/CONFIGURATION.md +10 -1
  142. package/docs/reference/CONFIGURATION_REFERENCE.md +14 -0
  143. package/docs/reference/automode-risk.md +1 -1
  144. package/docs/reference/evidence-bound-ship.md +3 -2
  145. package/docs/reference/semantic-supervisor.md +3 -3
  146. package/docs/reference/system1-classifier.md +4 -4
  147. package/package.json +2 -2
  148. package/scripts/lib/llama-upstream.sh +7 -1
  149. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  150. package/src/policies/enforcers/__pycache__/expert_review_required.cpython-312.pyc +0 -0
  151. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
  152. package/src/policies/enforcers/expert_review_required.py +1 -4
  153. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  154. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  155. package/tools/agents/scripts/anthropic_proxy.py +374 -16
  156. package/tools/agents/scripts/confidence_escalation.py +4 -2
  157. package/tools/agents/scripts/project_telemetry.py +1 -0
  158. package/tools/agents/scripts/tool_risk.py +1 -0
  159. package/tools/agents/scripts/tool_risk_calibrate.py +1 -0
  160. package/tools/agents/scripts/toolcall_path_normalizer.py +1 -0
  161. package/tools/agents/tests/test_anthropic_proxy_streaming.py +52 -1
  162. package/tools/agents/tests/test_confidence_escalation.py +3 -2
  163. package/tools/agents/tests/test_count_tokens_scale_per_session.py +126 -0
  164. package/tools/agents/tests/test_expert_review_pr_scope.py +10 -11
  165. package/tools/agents/tests/test_lockstep_escalation.py +324 -0
  166. package/tools/agents/tests/test_thinking_template_kwargs.py +10 -4
@@ -662,6 +662,48 @@ _DOUBLING_FAIL_RE = re.compile(
662
662
  re.IGNORECASE,
663
663
  )
664
664
 
665
+ # ---------------------------------------------------------------------------
666
+ # LOCKSTEP ESCALATION guardrail: the SAME tool call failing with the SAME
667
+ # error, over and over -- the intersection of DOUBLING-DOWN (identical
668
+ # fingerprint) and ERROR-LOOP (identical failure signature). Those two guards
669
+ # used to split this case against itself: DOUBLING-DOWN yielded ("ERROR-LOOP
670
+ # owns the lockstep case") while ERROR-LOOP's remedy assumes a failing FILE to
671
+ # re-read, which is wrong for tool-misuse loops (a remote command that can
672
+ # never succeed). Observed live (2026-09-25): one ssh/docker/python call
673
+ # re-issued 27 times -- ~15 min of full-context prefills at 100% GPU, three
674
+ # ignored re-read nudges -- before the ERROR-LOOP hard stop finally fired.
675
+ #
676
+ # Lockstep owns the case and escalates decisively: a PIVOT directive at
677
+ # PROXY_LOCKSTEP_PIVOT_AT, a final warning one fire later, and a hard stop
678
+ # (400 + loop-incident record) at PROXY_LOCKSTEP_HARD_AT. Both directives are
679
+ # always given before the stop, so with the defaults the stop lands at
680
+ # max(HARD_AT, PIVOT_AT+2) -- never earlier. A fresh human user turn re-arms
681
+ # the guard, mirroring ERROR-LOOP. PROXY_LOCKSTEP_BREAK=off disables the
682
+ # guard; PIVOT_AT=0 disables it too (the ladder has no entry point);
683
+ # HARD_AT=0 disables only the hard stop (advisory tiers still fire).
684
+ #
685
+ # Dependency: the streaks lockstep reads are fed by the ERROR-LOOP and
686
+ # DOUBLING-DOWN trackers, so PROXY_ERROR_LOOP=off or PROXY_DOUBLING_BREAK=off
687
+ # silently disarms lockstep as well. Fail-safe direction, but operator-visible
688
+ # in the docs.
689
+ PROXY_LOCKSTEP_BREAK = os.environ.get("PROXY_LOCKSTEP_BREAK", "on").lower() not in {
690
+ "0", "off", "false", "no",
691
+ }
692
+ PROXY_LOCKSTEP_PIVOT_AT = int(os.environ.get("PROXY_LOCKSTEP_PIVOT_AT", "2"))
693
+ PROXY_LOCKSTEP_HARD_AT = int(os.environ.get("PROXY_LOCKSTEP_HARD_AT", "4"))
694
+
695
+ # Loop-incident ledger: every lockstep escalation tier appends one JSONL
696
+ # record so `uap loops` can show what looped, how hard, and hand the incident
697
+ # to `uap ideate` / `uap deliver` for resolution. Best-effort telemetry -- the
698
+ # proxy never fails a request over it. "" disables; the default honors
699
+ # XDG_CONFIG_HOME like the rest of the UAP config layout.
700
+ _LOOP_INCIDENT_DEFAULT = os.path.join(
701
+ os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config"),
702
+ "uap",
703
+ "loop-incidents.jsonl",
704
+ )
705
+ UAP_LOOP_INCIDENTS = os.environ.get("UAP_LOOP_INCIDENTS", _LOOP_INCIDENT_DEFAULT)
706
+
665
707
  PROXY_TOOL_STATE_MACHINE = os.environ.get(
666
708
  "PROXY_TOOL_STATE_MACHINE", "on"
667
709
  ).lower() not in {
@@ -1360,6 +1402,12 @@ PROXY_DISABLE_THINKING_ALWAYS = os.environ.get(
1360
1402
  # switches, the tool-turn breakers, the JSON-verdict grammar, the empty-response
1361
1403
  # retry and every assistant prefill/continuation.
1362
1404
  #
1405
+ # SUPERSEDED 2026-09-21: that backend is not what runs. The local engine is
1406
+ # buun-llama-cpp with --jinja, which ACCEPTS chat_template_kwargs (verified
1407
+ # with a live 200 carrying enable_thinking). The capability probe below is why
1408
+ # the engine change needed no code change at all — it adapts instead of
1409
+ # assuming. The incident above is kept as the record of why the probe exists.
1410
+ #
1363
1411
  # 'auto' (default) probes the upstream once and remembers; 'on'/'off' pin it for
1364
1412
  # an operator who knows their backend and does not want the probe.
1365
1413
  PROXY_CHAT_TEMPLATE_KWARGS = os.environ.get(
@@ -1544,7 +1592,11 @@ async def _reconcile_wire_model(openai_body: dict) -> None:
1544
1592
 
1545
1593
  THE BUG THIS FIXES, measured 2026-08-19. llama.cpp ignores the OpenAI
1546
1594
  `model` field entirely, so for years the proxy could forward whatever the
1547
- client asked for. ninfer-serve VALIDATES it. With the local-only sentinel
1595
+ client asked for. ninfer-serve VALIDATES it. (SUPERSEDED 2026-09-21: the
1596
+ backend is llama.cpp again, which ignores the field — both `qwen3.8-27b`
1597
+ and the real alias return 200. This reconciliation is now belt-and-braces
1598
+ rather than load-bearing, and stays because the next engine may validate
1599
+ again.) With the local-only sentinel
1548
1600
  (ANTHROPIC_PASSTHROUGH_MODELS=__local_only__ -- the shipped systemd default)
1549
1601
  EVERY advertised id is served locally, including the four `claude-*` ones,
1550
1602
  and every one of them came back:
@@ -2396,6 +2448,13 @@ class SessionMonitor:
2396
2448
  doubling_streak: int = 0 # consecutive FAILED retries of that same call
2397
2449
  doubling_break_fires: int = 0 # monotonic count of injected pivot directives
2398
2450
  last_doubling_obs: str = "" # msg-count:fingerprint key of the last counted observation
2451
+ lockstep_fires: int = 0 # lockstep escalation directives injected this streak
2452
+ lockstep_blocks: int = 0 # monotonic count of lockstep hard stops (400s) served
2453
+ # Streak depth at which the last lockstep tier fired. A client retry resends
2454
+ # an IDENTICAL trailing transcript (5xx, stream abort), which without this
2455
+ # would re-inject the directive and re-append a ledger record per retry.
2456
+ lockstep_last_fire_streak: int = 0
2457
+ session_id: str = "" # owning session id (set by get_session_monitor)
2399
2458
  mandate_deliver_fires: int = 0 # monotonic count of forced deliver-routings (mandate)
2400
2459
  mandate_deliver_active: bool = False # THIS turn is pinned to deliver (beats recon convergence)
2401
2460
  tool_starvation_streak: int = 0 # Consecutive forced turns with no tool_calls produced
@@ -3260,6 +3319,7 @@ def get_session_monitor(session_id: str) -> SessionMonitor:
3260
3319
  monitor = SessionMonitor(context_window=default_context_window)
3261
3320
  session_monitors[session_id] = monitor
3262
3321
 
3322
+ monitor.session_id = session_id
3263
3323
  monitor.touch()
3264
3324
  if monitor.context_window <= 0:
3265
3325
  monitor.context_window = default_context_window
@@ -6234,7 +6294,7 @@ def openai_to_anthropic_request(openai_body: dict) -> dict:
6234
6294
  elif isinstance(block, str):
6235
6295
  blocks.append({"type": "text", "text": block})
6236
6296
 
6237
- for tc in msg.get("tool_calls", []) or []:
6297
+ for tc in msg.get("tool_calls") or []:
6238
6298
  fn = tc.get("function", {})
6239
6299
  try:
6240
6300
  args = json.loads(fn.get("arguments", "{}") or "{}")
@@ -7261,6 +7321,226 @@ def _maybe_inject_stuck_break(openai_body: dict, monitor: "SessionMonitor") -> N
7261
7321
  logger.warning("STUCK-BREAK: forced terminal turn (%s, fires=%d)", reason, monitor.stuck_break_fires)
7262
7322
 
7263
7323
 
7324
+ def _lockstep_streak(monitor: "SessionMonitor") -> int:
7325
+ """Depth of the identical-call-identical-failure lockstep.
7326
+
7327
+ DOUBLING-DOWN tracks consecutive failed retries of one fingerprint;
7328
+ ERROR-LOOP tracks consecutive recurrences of one error signature. When both
7329
+ climb together the loop is provably deterministic -- the same input
7330
+ produces the same failure -- so the lockstep depth is the shallower of the
7331
+ two (either signal resetting breaks the lockstep)."""
7332
+ if not PROXY_LOCKSTEP_BREAK:
7333
+ return 0
7334
+ if not monitor.doubling_fp:
7335
+ return 0
7336
+ return min(monitor.doubling_streak, monitor.error_signature_streak)
7337
+
7338
+
7339
+ def _lockstep_active(monitor: "SessionMonitor") -> bool:
7340
+ """True when the lockstep ladder owns this turn. ERROR-LOOP and
7341
+ DOUBLING-DOWN consult this to yield rather than stack a second (and, for
7342
+ ERROR-LOOP's file-centric remedy, actively wrong) directive."""
7343
+ return (
7344
+ PROXY_LOCKSTEP_BREAK
7345
+ and PROXY_LOCKSTEP_PIVOT_AT > 0
7346
+ and _lockstep_streak(monitor) >= PROXY_LOCKSTEP_PIVOT_AT
7347
+ )
7348
+
7349
+
7350
+ def _record_loop_incident(monitor: "SessionMonitor", outcome: str, detail: str) -> None:
7351
+ """Append one JSONL record to the loop-incident ledger. Best-effort: the
7352
+ proxy never fails a request over telemetry.
7353
+
7354
+ Deliberately lossy -- the fingerprint is hashed and tool ARGUMENTS are
7355
+ never written. The observed loops run remote shells whose command lines
7356
+ can carry tokens, URLs, and credentials; `uap loops` needs the SHAPE of
7357
+ the loop (tool, error signature, streak), not its payload.
7358
+
7359
+ The ledger grows ~3 records per loop episode (pivot / final / hard stop);
7360
+ there is deliberately no rotation -- a wedged session hard-stops instead
7361
+ of streaming records. The file is created 0600 (normalized error
7362
+ fragments can still name internal hosts/services) and opened O_NOFOLLOW
7363
+ so a symlink at the path is refused race-free."""
7364
+ path = UAP_LOOP_INCIDENTS
7365
+ if not path:
7366
+ return
7367
+ try:
7368
+ if os.path.islink(path):
7369
+ logger.warning(
7370
+ "loop-incident ledger is a symlink -- refusing to append (%s)", path
7371
+ )
7372
+ return
7373
+ parent = os.path.dirname(path)
7374
+ if parent:
7375
+ os.makedirs(parent, mode=0o700, exist_ok=True)
7376
+ fp = monitor.doubling_fp or ""
7377
+ record = {
7378
+ "v": 1,
7379
+ "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
7380
+ "guard": "lockstep",
7381
+ "outcome": outcome,
7382
+ "session": (monitor.session_id or "")[:64],
7383
+ "tool": "+".join(sorted(_fingerprint_tool_names(fp)))[:80],
7384
+ "fingerprint": hashlib.sha1(fp.encode("utf-8", "replace")).hexdigest()[:12]
7385
+ if fp
7386
+ else "",
7387
+ "error_signature": (monitor.last_error_signature or "")[:200],
7388
+ "streak": _lockstep_streak(monitor),
7389
+ "doubling_streak": monitor.doubling_streak,
7390
+ "error_signature_streak": monitor.error_signature_streak,
7391
+ "fires": monitor.lockstep_fires,
7392
+ "blocks": monitor.lockstep_blocks,
7393
+ "detail": (detail or "")[:200],
7394
+ }
7395
+ fd = os.open(
7396
+ path,
7397
+ os.O_WRONLY | os.O_CREAT | os.O_APPEND | getattr(os, "O_NOFOLLOW", 0),
7398
+ 0o600,
7399
+ )
7400
+ with os.fdopen(fd, "a", encoding="utf-8") as fh:
7401
+ fh.write(json.dumps(record, ensure_ascii=False) + "\n")
7402
+ except Exception as exc:
7403
+ logger.warning("loop-incident ledger append failed (non-fatal): %s", exc)
7404
+
7405
+
7406
+ def _maybe_lockstep_escalate(openai_body: dict, monitor: "SessionMonitor") -> None:
7407
+ """Decisive escalation for the identical-call-identical-failure loop.
7408
+
7409
+ Owns the lockstep case the older guards split against themselves (see the
7410
+ knob block for the 2026-09-25 incident: 27 identical ssh calls, ~15 min of
7411
+ full-context prefills, three ignored re-read nudges). The ladder:
7412
+ PIVOT_AT -> inject a pivot directive (name the deterministic failure,
7413
+ demand genuinely different approaches, take one);
7414
+ one fire later -> a final warning that the next recurrence hard-fails;
7415
+ HARD_AT -> raise ErrorLoopHardBlock (served as a 400) and record the
7416
+ incident for `uap loops`. A fresh human user turn re-arms,
7417
+ mirroring ERROR-LOOP. Advisory tiers do not touch tool_choice:
7418
+ the model should keep acting, just never with THAT call again."""
7419
+ streak = _lockstep_streak(monitor)
7420
+ if streak < PROXY_LOCKSTEP_PIVOT_AT or PROXY_LOCKSTEP_PIVOT_AT <= 0:
7421
+ # The lockstep broke (a different call, a new error, or a clean
7422
+ # result): reset the ladder so the NEXT loop climbs it from the pivot
7423
+ # tier instead of inheriting "directives ignored" from a previous,
7424
+ # resolved loop.
7425
+ monitor.lockstep_fires = 0
7426
+ monitor.lockstep_last_fire_streak = 0
7427
+ return
7428
+ # Recon-convergence may strip tools / force a terminal summary this turn;
7429
+ # a "take a different approach now" directive would contradict it. Mirrors
7430
+ # DOUBLING-DOWN's yield.
7431
+ if monitor.recon_convergence_pending():
7432
+ return
7433
+ # Deliberately NO yield to STUCK-BREAK: its repeat-call signal is
7434
+ # continuously true during exactly the loop this guard owns, so yielding
7435
+ # would suppress the decisive tier in the target incident shape. The
7436
+ # directives are complementary anyway -- both demand abandoning the
7437
+ # identical call -- and lockstep's diagnosis steps are prose-doable on a
7438
+ # tool-stripped turn.
7439
+ tool = (monitor.doubling_fp or "").split("|", 1)[0].split(":", 1)[0] or "tool"
7440
+ if (
7441
+ PROXY_LOCKSTEP_HARD_AT > 0
7442
+ and streak >= PROXY_LOCKSTEP_HARD_AT
7443
+ and monitor.lockstep_fires >= 2
7444
+ ):
7445
+ if _has_fresh_user_turn(openai_body):
7446
+ monitor.lockstep_fires = 0
7447
+ monitor.lockstep_last_fire_streak = 0
7448
+ logger.warning(
7449
+ "LOCKSTEP: human turn after %d hard stop(s) -- guard re-armed",
7450
+ monitor.lockstep_blocks,
7451
+ )
7452
+ return
7453
+ monitor.lockstep_blocks += 1
7454
+ # Refuse EVERY retry (that is the hard stop) but record the incident
7455
+ # once per new depth -- a client retrying the refused request resends
7456
+ # the identical transcript and must not spam the ledger.
7457
+ if streak > monitor.lockstep_last_fire_streak:
7458
+ monitor.lockstep_last_fire_streak = streak
7459
+ _record_loop_incident(monitor, "hard_blocked", f"{tool} x{streak}")
7460
+ raise LockstepHardBlock(
7461
+ "LOOP-ESCALATE hard stop: the identical "
7462
+ + tool
7463
+ + " call has now failed "
7464
+ + str(streak)
7465
+ + " times with the identical error, and both pivot directives were "
7466
+ "ignored. Retrying a deterministic failure cannot work, so the "
7467
+ "proxy refuses to burn another full-context prefill on this loop "
7468
+ "(operator policy PROXY_LOCKSTEP_HARD_AT="
7469
+ + str(PROXY_LOCKSTEP_HARD_AT)
7470
+ + "). The incident was recorded to the loop ledger -- run "
7471
+ "`uap loops` to inspect it and hand it to `uap ideate` / "
7472
+ "`uap deliver`, or send a normal user message to re-arm the guard. "
7473
+ "Failing signature: \""
7474
+ + monitor.last_error_signature[:120]
7475
+ + "\""
7476
+ )
7477
+ # Duplicate observation of an already-addressed depth: the client resent
7478
+ # the same trailing transcript. The directive for this depth is already in
7479
+ # the conversation; injecting again would stack copies per retry.
7480
+ if monitor.lockstep_fires > 0 and streak <= monitor.lockstep_last_fire_streak:
7481
+ return
7482
+ if monitor.lockstep_fires == 0:
7483
+ outcome = "pivot"
7484
+ directive = (
7485
+ "\n\nLOOP-ESCALATE -- the identical "
7486
+ + tool
7487
+ + " call has failed "
7488
+ + str(streak)
7489
+ + " times with the IDENTICAL error: \""
7490
+ + monitor.last_error_signature[:120]
7491
+ + "\". Retrying this exact call CANNOT work -- the failure is "
7492
+ "deterministic, so do not issue it again. Switch strategy now:\n"
7493
+ "1. State in one sentence WHY the call fails (quote the decisive "
7494
+ "line of the error).\n"
7495
+ "2. List 2-3 genuinely different approaches -- a different tool, a "
7496
+ "different target, or a smaller verifiable step.\n"
7497
+ "3. Take the most promising one on your next tool turn -- or, if "
7498
+ "tools are unavailable this turn, state the choice now.\n"
7499
+ "If no alternative exists, stop and report the blocker in one "
7500
+ "sentence instead of retrying."
7501
+ )
7502
+ else:
7503
+ outcome = "final_warning"
7504
+ # Accurate deadline: the stop lands at max(HARD_AT, the turn both
7505
+ # directives have been given), so phrase from the knob rather than
7506
+ # promising "once more" when HARD_AT sits further out.
7507
+ if streak + 1 >= PROXY_LOCKSTEP_HARD_AT:
7508
+ deadline = "If the same call with the same error recurs once more"
7509
+ else:
7510
+ deadline = (
7511
+ "If the same call keeps failing through streak "
7512
+ + str(PROXY_LOCKSTEP_HARD_AT)
7513
+ )
7514
+ directive = (
7515
+ "\n\nFINAL WARNING -- the identical failing "
7516
+ + tool
7517
+ + " call was retried AGAIN after the pivot directive. "
7518
+ + deadline
7519
+ + ", the proxy HARD-FAILS this session (400) and records the loop "
7520
+ "incident for `uap loops`. Choose one of the alternatives you "
7521
+ "listed, or stop and report the blocker."
7522
+ )
7523
+ monitor.lockstep_fires += 1
7524
+ monitor.lockstep_last_fire_streak = streak
7525
+ _record_loop_incident(monitor, outcome, f"{tool} x{streak}")
7526
+ msgs = openai_body.get("messages")
7527
+ if not isinstance(msgs, list):
7528
+ msgs = []
7529
+ if msgs and msgs[0].get("role") == "system":
7530
+ msgs[0]["content"] = (msgs[0].get("content") or "") + directive
7531
+ else:
7532
+ msgs.insert(0, {"role": "system", "content": directive.strip()})
7533
+ openai_body["messages"] = msgs
7534
+ logger.warning(
7535
+ "LOCKSTEP: %s injected (%s x%d, sig=%r, fires=%d)",
7536
+ outcome,
7537
+ tool,
7538
+ streak,
7539
+ monitor.last_error_signature[:80],
7540
+ monitor.lockstep_fires,
7541
+ )
7542
+
7543
+
7264
7544
  class ErrorLoopHardBlock(Exception):
7265
7545
  """A session kept re-hitting the SAME tool_result error after
7266
7546
  PROXY_ERROR_LOOP_HARD_LIMIT injected re-read nudges. The /v1/messages
@@ -7268,6 +7548,12 @@ class ErrorLoopHardBlock(Exception):
7268
7548
  prefills instead of being nudged yet again."""
7269
7549
 
7270
7550
 
7551
+ class LockstepHardBlock(ErrorLoopHardBlock):
7552
+ """The LOCKSTEP ladder's terminal tier. Subclasses ErrorLoopHardBlock so
7553
+ the single existing 400 handler serves it unchanged; the distinct type
7554
+ lets that handler attribute the stop to the right guard."""
7555
+
7556
+
7271
7557
  def _has_fresh_user_turn(openai_body: dict) -> bool:
7272
7558
  """True when the request's LAST message is a human user turn (text), not a
7273
7559
  tool_result chain. In an unattended tool loop the tail is always a tool
@@ -7304,6 +7590,11 @@ def _maybe_inject_error_loop_break(openai_body: dict, monitor: "SessionMonitor")
7304
7590
  the guard."""
7305
7591
  if not PROXY_ERROR_LOOP:
7306
7592
  return
7593
+ # LOCKSTEP owns the identical-call-identical-error case: its ladder is
7594
+ # faster and its directive correct (pivot to a different approach, not
7595
+ # re-read a file that may not exist). Yield rather than stack directives.
7596
+ if _lockstep_active(monitor):
7597
+ return
7307
7598
  if monitor.error_signature_streak < PROXY_ERROR_LOOP_THRESHOLD:
7308
7599
  return
7309
7600
  if (
@@ -7435,6 +7726,10 @@ def _maybe_inject_doubling_break(openai_body: dict, monitor: "SessionMonitor") -
7435
7726
  should, reason = monitor.should_force_doubling_break()
7436
7727
  if not should:
7437
7728
  return
7729
+ # LOCKSTEP owns the identical-call-identical-error case (faster ladder,
7730
+ # harder stop); only the varied-error doubling shape remains here.
7731
+ if _lockstep_active(monitor):
7732
+ return
7438
7733
  # STUCK-BREAK (a self-aware loop wanting a prose exit) is more urgent.
7439
7734
  stuck, _ = monitor.should_force_stuck_break()
7440
7735
  if stuck:
@@ -8619,6 +8914,12 @@ def build_openai_request(
8619
8914
 
8620
8915
  _maybe_inject_stuck_break(openai_body, monitor)
8621
8916
 
8917
+ # LOCKSTEP ESCALATION: the identical call failing with the identical error.
8918
+ # Runs before ERROR-LOOP/DOUBLING-DOWN, which yield to it -- its ladder
8919
+ # (pivot -> final warning -> hard stop + loop-incident record) is decisive
8920
+ # where their nudges drifted for 27 turns on 2026-09-25.
8921
+ _maybe_lockstep_escalate(openai_body, monitor)
8922
+
8622
8923
  # ERROR-LOOP: same tool_result failure recurring despite varied edits.
8623
8924
  _maybe_inject_error_loop_break(openai_body, monitor)
8624
8925
 
@@ -11441,7 +11742,7 @@ def _build_safe_text_openai_response(
11441
11742
  },
11442
11743
  }
11443
11744
  ],
11444
- "usage": openai_resp.get("usage", {}),
11745
+ "usage": openai_resp.get("usage") or {},
11445
11746
  }
11446
11747
 
11447
11748
 
@@ -11463,7 +11764,7 @@ def _build_clean_guardrail_openai_response(
11463
11764
  },
11464
11765
  }
11465
11766
  ],
11466
- "usage": openai_resp.get("usage", {}),
11767
+ "usage": openai_resp.get("usage") or {},
11467
11768
  }
11468
11769
 
11469
11770
 
@@ -11656,7 +11957,7 @@ async def _apply_malformed_tool_guardrail(
11656
11957
  # Option 2: Log garbled argument content for diagnostics
11657
11958
  arg_excerpt = ""
11658
11959
  if issue.kind == "invalid_tool_args":
11659
- for tc in (working_resp.get("choices", [{}])[0].get("message", {}).get("tool_calls", [])):
11960
+ for tc in (working_resp.get("choices", [{}])[0].get("message", {}).get("tool_calls") or []):
11660
11961
  raw_args = tc.get("function", {}).get("arguments", "")
11661
11962
  if raw_args and _is_garbled_tool_arguments(raw_args):
11662
11963
  arg_excerpt = raw_args[:200].replace("\n", " ")
@@ -11683,7 +11984,7 @@ async def _apply_malformed_tool_guardrail(
11683
11984
  # Track failing tool names for tool narrowing on retry
11684
11985
  failing_tools: set[str] = set()
11685
11986
  if issue.kind == "invalid_tool_args":
11686
- for tc in (working_resp.get("choices", [{}])[0].get("message", {}).get("tool_calls", [])):
11987
+ for tc in (working_resp.get("choices", [{}])[0].get("message", {}).get("tool_calls") or []):
11687
11988
  fn_name = tc.get("function", {}).get("name", "")
11688
11989
  raw_args = tc.get("function", {}).get("arguments", "")
11689
11990
  if fn_name and raw_args and _is_garbled_tool_arguments(raw_args):
@@ -11794,7 +12095,7 @@ async def _apply_malformed_tool_guardrail(
11794
12095
  monitor.invalid_tool_call_streak += 1
11795
12096
  monitor.arg_preflight_rejections += 1
11796
12097
  # Track failing tools from retries for progressive narrowing
11797
- for tc in (retry_working.get("choices", [{}])[0].get("message", {}).get("tool_calls", [])):
12098
+ for tc in (retry_working.get("choices", [{}])[0].get("message", {}).get("tool_calls") or []):
11798
12099
  fn_name = tc.get("function", {}).get("name", "")
11799
12100
  raw_args = tc.get("function", {}).get("arguments", "")
11800
12101
  if fn_name and raw_args and _is_garbled_tool_arguments(raw_args):
@@ -12668,7 +12969,7 @@ def openai_to_anthropic_response(
12668
12969
  content.append({"type": "text", "text": sanitized_text})
12669
12970
 
12670
12971
  # Convert tool calls
12671
- for tc in message.get("tool_calls", []):
12972
+ for tc in message.get("tool_calls") or []:
12672
12973
  fn = tc.get("function", {})
12673
12974
  try:
12674
12975
  args = json.loads(fn.get("arguments", "{}"))
@@ -12735,7 +13036,7 @@ def openai_to_anthropic_response(
12735
13036
  "function_call": "tool_use",
12736
13037
  }
12737
13038
 
12738
- usage = openai_resp.get("usage", {})
13039
+ usage = openai_resp.get("usage") or {}
12739
13040
 
12740
13041
  _log_non_stream_resp(content, finish, usage)
12741
13042
 
@@ -13754,7 +14055,24 @@ async def count_tokens(request: Request):
13754
14055
  media_type="application/json",
13755
14056
  )
13756
14057
  est = estimate_total_tokens(body)
13757
- scale = _count_tokens_scale()
14058
+ # This endpoint has no session contextvar, but the client sends the same
14059
+ # profile header here as on /v1/messages — so the session's real window is
14060
+ # recoverable from the request itself. Without this the scale is computed
14061
+ # from the shared pool and a profile-capped client is told to compact at a
14062
+ # point it can never reach.
14063
+ _profile_window = 0
14064
+ # getattr: this endpoint is also driven directly by tests with a minimal
14065
+ # request stub, and a missing header map must degrade to "no profile",
14066
+ # never raise.
14067
+ _prof = _resolve_profile_name(getattr(request, "headers", {}) or {}, body)
14068
+ if _prof:
14069
+ _cfg = _load_profile_config(_prof)
14070
+ if _cfg and _cfg.get("context_window"):
14071
+ try:
14072
+ _profile_window = int(_cfg["context_window"])
14073
+ except (TypeError, ValueError):
14074
+ _profile_window = 0
14075
+ scale = _count_tokens_scale(_profile_window)
13758
14076
  if scale > 1.0:
13759
14077
  scaled = int(est * scale)
13760
14078
  # Once per scale value, explain the discrepancy an operator would
@@ -13768,7 +14086,7 @@ async def count_tokens(request: Request):
13768
14086
  "fires at ~%d real tokens, before the pruner",
13769
14087
  scale,
13770
14088
  PROXY_CLIENT_ASSUMED_WINDOW,
13771
- _effective_context_window(),
14089
+ _profile_window or _effective_context_window(),
13772
14090
  int(PROXY_CLIENT_ASSUMED_WINDOW * 0.925 / scale),
13773
14091
  )
13774
14092
  return {"input_tokens": scaled}
@@ -13778,12 +14096,50 @@ async def count_tokens(request: Request):
13778
14096
  _count_scale_logged: float = 0.0
13779
14097
 
13780
14098
 
13781
- def _count_tokens_scale() -> float:
14099
+ def _scale_window() -> int:
14100
+ """The window compaction forcing should be computed against.
14101
+
14102
+ The per-SESSION window when one is in force, else the process default.
14103
+
14104
+ This used to be `_effective_context_window()` unconditionally, which is the
14105
+ process-wide value detected from /slots. That silently broke the moment a
14106
+ session carried a profile that capped it BELOW the detected rail, because
14107
+ the two numbers then disagree and only one of them bounds the session:
14108
+
14109
+ detected rail 229376 (shared pool, -np 2 + --kv-unified)
14110
+ profile cap 114688 (what the session may actually use)
14111
+ compact fires 123060 computed from the RAIL -> ABOVE the cap
14112
+ prune fires 80282 computed from the CAP
14113
+
14114
+ Compaction became unreachable and the pruner — designed as a backstop —
14115
+ became the only context manager. That is a real loss, not a wash: the
14116
+ client's compaction writes an LLM summary, while a prune drops the messages
14117
+ and leaves a short breadcrumb list. Resolving the session window first
14118
+ restores the intended ordering (compact ~61.5k, then prune at 80.2k).
14119
+ """
14120
+ try:
14121
+ sid = _current_request_session.get()
14122
+ except LookupError:
14123
+ sid = None
14124
+ if sid:
14125
+ mon = session_monitors.get(sid)
14126
+ win = getattr(mon, "context_window", 0) if mon else 0
14127
+ if win and win > 0:
14128
+ return int(win)
14129
+ return _effective_context_window()
14130
+
14131
+
14132
+ def _count_tokens_scale(window_override: int = 0) -> float:
13782
14133
  """Resolve the count_tokens compaction-forcing scale (>= 1.0; 1.0 = off).
13783
14134
 
13784
14135
  "auto" derives it from the LIVE rail each call so a rail resize (server
13785
14136
  restart with a different --ctx-size) re-tunes the client's compact point
13786
14137
  without a proxy restart.
14138
+
14139
+ `window_override` lets a caller that knows the session's window — the
14140
+ count_tokens endpoint, which resolves it from the request's own profile
14141
+ header — supply it directly, since that endpoint runs outside the
14142
+ request-session contextvar that `_scale_window` reads.
13787
14143
  """
13788
14144
  raw = PROXY_COUNT_TOKENS_SCALE.strip().lower()
13789
14145
  if raw in ("", "off", "none", "0", "1", "1.0"):
@@ -13793,7 +14149,7 @@ def _count_tokens_scale() -> float:
13793
14149
  return max(1.0, float(raw))
13794
14150
  except ValueError:
13795
14151
  return 1.0
13796
- window = _effective_context_window()
14152
+ window = window_override if window_override > 0 else _scale_window()
13797
14153
  if window <= 0:
13798
14154
  return 1.0
13799
14155
  frac = (
@@ -14117,9 +14473,11 @@ async def messages(request: Request):
14117
14473
  profile_grammar=profile_grammar,
14118
14474
  )
14119
14475
  except ErrorLoopHardBlock as exc:
14476
+ lockstep = isinstance(exc, LockstepHardBlock)
14120
14477
  logger.error(
14121
- "ERROR-LOOP hard stop served (blocks=%d): %s",
14122
- monitor.error_loop_blocks,
14478
+ "%s hard stop served (blocks=%d): %s",
14479
+ "LOOP-ESCALATE" if lockstep else "ERROR-LOOP",
14480
+ monitor.lockstep_blocks if lockstep else monitor.error_loop_blocks,
14123
14481
  str(exc)[:160],
14124
14482
  )
14125
14483
  return Response(
@@ -14984,7 +15342,7 @@ async def chat_completions(request: Request):
14984
15342
  yield f"data: {json.dumps(content_chunk)}\n\n".encode()
14985
15343
 
14986
15344
  # Tool call chunks
14987
- for idx, tc in enumerate(message.get("tool_calls", []) or []):
15345
+ for idx, tc in enumerate(message.get("tool_calls") or []):
14988
15346
  tc_chunk = {
14989
15347
  "id": resp_id,
14990
15348
  "object": "chat.completion.chunk",
@@ -50,7 +50,7 @@ class Settings:
50
50
  allow_self_judge: bool = False
51
51
  #: Backend width: how many generations the upstream serves CONCURRENTLY.
52
52
  #: None = UNKNOWN, which must change nothing (not every backend can be
53
- #: probed -- ninfer serves no /slots endpoint -- so silence is not evidence).
53
+ #: probed -- some engines serve no /slots endpoint, and a probe can fail -- so silence is not evidence).
54
54
  #: A FIELD rather than a process-env read at decision time: select_recipe is
55
55
  #: called per request and an ambient UAP_MODEL_SLOTS=1 (which is exactly what
56
56
  #: writeProxyEnv now puts in proxy.env, and what _load_proxy_env_file
@@ -336,7 +336,9 @@ def select_recipe(anthropic_body: dict, settings: Settings, has_tools: bool) ->
336
336
  # N=3 costs 3x the wall clock of a single call for the same answer, and the
337
337
  # deliver run paying it has a fixed wall-clock budget to finish in.
338
338
  #
339
- # Measured live (2026-08-20, ninfer --max-concurrency 1, qwen3.8-27b): tool
339
+ # Measured live on a ONE-RAIL backend (2026-08-20, --max-concurrency 1;
340
+ # that engine has since been replaced by llama.cpp with -np 2, so the
341
+ # downgrade now fires only when the probed width really is 1): tool
340
342
  # -carrying turns already bypass recipes via `has_tools` above, but mission
341
343
  # PLANNING carries no tools -- so it ran under fusion N=3 and took ~20 minutes,
342
344
  # 17% of a 120-minute run budget, before turn 1 existed.
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env python3
1
2
  """Per-project routing/cost telemetry for the shared UAP anthropic proxy.
2
3
 
3
4
  The proxy is a shared singleton that serves *every* project, so per-project
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env python3
1
2
  """AutoMode pre-execution risk classification — advisory heuristic scorer.
2
3
 
3
4
  Uplift 1.3. Classifies each tool call BEFORE dispatch into one of five risk
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env python3
1
2
  """Calibration harness for the AutoMode advisory risk scorer (uplift 1.3).
2
3
 
3
4
  Loads the labeled corpus (tools/agents/tests/fixtures/tool_risk_cases.json),
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env python3
1
2
  """Self-Harness middleware (proxy side) — conversation-aware tool-call path normalizer.
2
3
 
3
4
  The mechanical fix for the `toolcall.path.garbled` failure: a small quant
@@ -5335,7 +5335,10 @@ class TestModelsEndpoint(unittest.TestCase):
5335
5335
 
5336
5336
  # Local model — what the inference server actually serves. Tracks the
5337
5337
  # active model: qwen3.8-27b as of 2026-08-19, served by ninfer-serve
5338
- # (NOT llama.cpp). Requests for this ID route locally even with the
5338
+ # then; as of 2026-09-21 the backend is buun-llama-cpp serving the alias
5339
+ # qwen38-gsq-rco-27b. The id here still routes locally either way —
5340
+ # llama.cpp ignores the OpenAI `model` field — which is the property
5341
+ # under test. Requests for this ID route locally even with the
5339
5342
  # __local_only__ passthrough sentinel set.
5340
5343
  #
5341
5344
  # This assertion is about AGREEMENT with the backend, not about naming.
@@ -6186,5 +6189,53 @@ class TestSendStreamWithRetry(unittest.TestCase):
6186
6189
  self.assertEqual(client.requests[0]["url"], "http://x/v1/chat/completions")
6187
6190
 
6188
6191
 
6192
+ class TestNullToolCallsAndUsage(unittest.TestCase):
6193
+ """tabbyAPI/exllamav3 serializes explicit nulls where llama.cpp omits the
6194
+ keys entirely: "tool_calls": null and "usage": null. dict.get(key, default)
6195
+ returns None for present-but-null keys, so every consumer must coerce with
6196
+ `or []` / `or {}`. Regression coverage for the 2026-09-23 EXL3 cutover
6197
+ 500s (TypeError: 'NoneType' object is not iterable)."""
6198
+
6199
+ def test_null_tool_calls_treated_as_absent(self):
6200
+ openai_resp = {
6201
+ "choices": [{
6202
+ "finish_reason": "stop",
6203
+ "message": {"role": "assistant", "content": "hello", "tool_calls": None},
6204
+ }],
6205
+ "usage": {"prompt_tokens": 5, "completion_tokens": 3},
6206
+ }
6207
+ out = proxy.openai_to_anthropic_response(openai_resp, "qwen3.8")
6208
+ self.assertEqual(out["stop_reason"], "end_turn")
6209
+ self.assertEqual(out["content"], [{"type": "text", "text": "hello"}])
6210
+
6211
+ def test_null_usage_defaults_to_zero_counts(self):
6212
+ openai_resp = {
6213
+ "choices": [{
6214
+ "finish_reason": "stop",
6215
+ "message": {"role": "assistant", "content": "hi"},
6216
+ }],
6217
+ "usage": None,
6218
+ }
6219
+ out = proxy.openai_to_anthropic_response(openai_resp, "qwen3.8")
6220
+ self.assertEqual(out["usage"]["input_tokens"], 0)
6221
+ self.assertEqual(out["usage"]["output_tokens"], 0)
6222
+
6223
+ def test_null_tool_calls_and_null_usage_together(self):
6224
+ # exact response shape observed from tabbyAPI non-streaming completions
6225
+ openai_resp = {
6226
+ "id": "chatcmpl-x",
6227
+ "object": "chat.completion",
6228
+ "choices": [{
6229
+ "index": 0,
6230
+ "finish_reason": "stop",
6231
+ "message": {"role": "assistant", "content": "PROXYOK", "tool_calls": None},
6232
+ }],
6233
+ "usage": None,
6234
+ }
6235
+ out = proxy.openai_to_anthropic_response(openai_resp, "qwen3.8")
6236
+ self.assertEqual(out["content"][0]["text"], "PROXYOK")
6237
+ self.assertEqual(out["usage"]["output_tokens"], 0)
6238
+
6239
+
6189
6240
  if __name__ == "__main__":
6190
6241
  unittest.main()