@miller-tech/uap 2.19.5 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,74 @@
1
+ # ADR 0005: The Ollama-compatible surface — a third inbound dialect on a second port
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-10-06 (branch `feature/340-ollama-compat-endpoint`)
5
+ - Guide: `docs/guides/PROXY.md`
6
+
7
+ ## Context
8
+
9
+ The anthropic proxy already serves two inbound dialects — Anthropic Messages
10
+ (`/v1/messages`) and OpenAI Chat Completions (`/v1/chat/completions`) — both
11
+ normalized through a single guarded Anthropic pipeline (admission control,
12
+ rail semaphore, loop detection, tool narrowing, malformed-tool retry, context
13
+ pruning). Ollama-native tooling is a third client family that auto-discovers
14
+ `localhost:11434` and generally cannot be re-pointed at a path-prefixed
15
+ alternative, so "just document the OpenAI surface" does not reach it.
16
+
17
+ ## Decision
18
+
19
+ 1. **Same process, second listener port.** The Ollama surface is served by the
20
+ SAME FastAPI app on `PROXY_OLLAMA_PORT` (default 11434; 0 disables). A
21
+ second process was rejected on a hard constraint, not taste: admission
22
+ control and the upstream rail semaphore are per-process in-memory state
23
+ (session admission limit=1, upstream slot limit=1), so a second process
24
+ would silently double the concurrency budget the backend was sized for and
25
+ break the rail. Path-prefixing on one port was rejected because Ollama
26
+ tooling cannot be pointed at it.
27
+
28
+ 2. **Translation only, through the shared guarded core.** Ollama requests are
29
+ translated to OpenAI shape and run through `_guarded_openai_completion` —
30
+ the same core the OpenAI route uses — then translated back to Ollama wire
31
+ shapes. This buys a parity guarantee: an Ollama client cannot drift from
32
+ the other surfaces in guardrail coverage, because there is only one
33
+ pipeline. The cost is double translation: only fields that survive BOTH
34
+ hops are mapped (see consequence 4).
35
+
36
+ 3. **Buffered streaming, same tradeoff as the OpenAI surface.** Streaming is
37
+ buffered through the guardrails, then emitted as Ollama NDJSON (one content
38
+ line plus a final `done: true` line) — not token-by-token from upstream.
39
+ Token-granular streaming would require bypassing the pipeline; correctness
40
+ of the guardrails was ranked above stream granularity. This is the same
41
+ call the OpenAI inbound already made; clients see their reply arrive at
42
+ once instead of incrementally.
43
+
44
+ 4. **Default on at 11434.** The surface's target audience auto-discovers that
45
+ port, so default-off would make the feature unreachable by its own users.
46
+ Blast radius is bounded: loopback bind by default, the same auth token
47
+ gates model-serving routes, a busy port disables the surface without
48
+ touching the Anthropic surface, and `PROXY_OLLAMA_PORT=0` is a config-only
49
+ rollback. The known asymmetry: if this proxy binds 11434 first, a
50
+ later-started real Ollama fails to bind and the error appears in Ollama's
51
+ logs, not ours.
52
+
53
+ ## Consequences
54
+
55
+ - uvicorn serves one app on two ports via a companion listener that installs
56
+ no signal handlers (SIGTERM reaches it by mirroring the primary's exit) and
57
+ runs with `lifespan="off"` so app startup runs exactly once. The proxy
58
+ requires `uvicorn>=0.30.0` for the `capture_signals` override to be
59
+ honored.
60
+ - A companion bind failure must never take the primary down: uvicorn signals
61
+ bind failure with `sys.exit(1)` inside `serve()`, and `SystemExit` is a
62
+ `BaseException` that `gather(return_exceptions=True)` does not contain —
63
+ the companion task is therefore wrapped in an explicit containment guard.
64
+ - Option mapping is deliberately minimal (`temperature`, `top_p`, `top_k`,
65
+ `stop`, `num_predict`): Ollama's `format` (structured output),
66
+ `min_p`/`repeat_penalty`/`seed` and `num_ctx` are dropped honestly at the
67
+ boundary. Wiring `response_format` through the Anthropic pipeline is a
68
+ tracked contract change that deserves its own schema-diff pass.
69
+ - The model card data (`/api/tags`, `/api/show`, `/api/ps`) mirrors only what
70
+ the backend actually exposes (llama.cpp `/props`), cached per model, and
71
+ never invents plausible-looking numbers a client would display as fact.
72
+ - The Claude contract ids that `/v1/models` advertises for Anthropic-SDK
73
+ compatibility are protocol fictions and do NOT appear in `/api/tags` — an
74
+ Ollama client must only see ids the backend actually serves.
@@ -41,6 +41,54 @@ the "keep a small model from wedging" logic lives.
41
41
  | `GET /health` | Liveness (probes the upstream; stays unauthenticated) |
42
42
  | `GET /v1/context` | Per-session token-usage / utilization / guardrail telemetry (dashboards & debug) |
43
43
 
44
+ ### Ollama-compatible surface (`PROXY_OLLAMA_PORT`, default 11434)
45
+
46
+ The same proxy also serves the Ollama wire protocol on a **second port**
47
+ (default `11434`, Ollama's well-known port — the one Ollama-native tooling
48
+ auto-discovers and cannot be re-pointed). Set `PROXY_OLLAMA_PORT=0` to disable
49
+ it (do that on a box running a real Ollama, which owns 11434); if the port is
50
+ busy at startup the surface disables itself and the Anthropic/OpenAI surface
51
+ carries on unaffected.
52
+
53
+ | Route | Purpose |
54
+ |---|---|
55
+ | `GET /api/version` | Ollama discovery handshake |
56
+ | `GET /api/tags` | Model list — backend-served ids only (the Anthropic-protocol Claude ids are not leaked here) |
57
+ | `POST /api/show` | Model card (404s unknown ids like real Ollama) |
58
+ | `GET /api/ps` | Running-model list (the backend stays loaded; capacity policy owns that) |
59
+ | `POST /api/chat` | Chat — messages, options, tools (OpenAI tool schema) |
60
+ | `POST /api/generate` | Text generation — prompt + optional system |
61
+
62
+ Every request runs the **full guardrail path** (admission control, rail
63
+ budgeting, loop detection, tool narrowing, malformed-tool retry, context
64
+ pruning) — an Ollama client gets the same treatment an Anthropic client does.
65
+ The tradeoff matches the OpenAI-compatible surface: streaming is buffered
66
+ through the guardrails, then emitted as Ollama NDJSON (one content line plus a
67
+ final `done: true` line), not token-by-token.
68
+
69
+ Options map only where they survive both translation hops — `temperature`,
70
+ `top_p`, `top_k`, `stop`, and `num_predict` → `max_tokens`. Everything else is
71
+ dropped honestly rather than accepted and silently ignored: Ollama's
72
+ `format` (structured output) is not carried by the underlying pipeline yet
73
+ (wiring `response_format` through is a tracked contract change), `num_ctx`
74
+ would fight the rail budget, and `min_p`/`repeat_penalty`/`seed` are not
75
+ forwarded by the second hop. `keep_alive`/`raw`/`template` are lifecycle
76
+ features with no local equivalent.
77
+
78
+ Two auth behaviors to know about when `PROXY_AUTH_TOKEN` is set: the
79
+ discovery routes above stay unauthenticated (Ollama clients probe them before
80
+ any credential could be presented), but `POST /api/chat`, `POST /api/generate`
81
+ and `POST /api/show` gate exactly like `/v1/messages` — and since most
82
+ Ollama-native tooling has no way to configure a bearer token, auth-on
83
+ realistically means Ollama clients stop at a 401. On the default loopback
84
+ deployment (no token) everything works.
85
+
86
+ If the port is busy at startup (typically a real Ollama), the surface
87
+ disables itself and the Anthropic/OpenAI surface carries on unaffected — the
88
+ error lands in this proxy's journal. The reverse ordering has the opposite
89
+ signature: if this proxy binds 11434 first, a later-started real Ollama is the
90
+ one that fails to bind, and its error shows up in Ollama's logs, not ours.
91
+
44
92
  ---
45
93
 
46
94
  ## Lifecycle: `uap proxy`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "2.19.5",
3
+ "version": "2.20.0",
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_conditional_destructive_verbs tools.agents.tests.test_live_deliver_kill tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_schema_diff_inline 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_repeat_guard tools.agents.tests.test_gate_integrity tools.agents.tests.test_gate_failclosed_indirection tools.agents.tests.test_gate_failclosed_schema_diff tools.agents.tests.test_gate_failclosed_plane_admin 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_anthropic_proxy_tool_arg_types tools.agents.tests.test_anthropic_sse_schema 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_count_tokens_scale_per_session 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_stuck_break_hard 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_plane_admin_protect 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 tools.agents.tests.test_cycle_break_wait_poll tools.agents.tests.test_upstream_model_aliases tools.agents.tests.test_stream_thinking_deltas tools.agents.tests.test_verbatim_compact tools.agents.tests.test_tool_risk tools.agents.tests.test_lockstep_escalation",
24
+ "test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_conditional_destructive_verbs tools.agents.tests.test_live_deliver_kill tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_schema_diff_inline 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_repeat_guard tools.agents.tests.test_gate_integrity tools.agents.tests.test_gate_failclosed_indirection tools.agents.tests.test_gate_failclosed_schema_diff tools.agents.tests.test_gate_failclosed_plane_admin 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_anthropic_proxy_tool_arg_types tools.agents.tests.test_anthropic_sse_schema 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_count_tokens_scale_per_session 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_stuck_break_hard 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_plane_admin_protect 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 tools.agents.tests.test_cycle_break_wait_poll tools.agents.tests.test_upstream_model_aliases tools.agents.tests.test_stream_thinking_deltas tools.agents.tests.test_verbatim_compact tools.agents.tests.test_tool_risk tools.agents.tests.test_lockstep_escalation tools.agents.tests.test_ollama_compat",
25
25
  "test:coverage": "vitest --coverage",
26
26
  "bench": "vitest --config vitest.bench.config.ts",
27
27
  "eval:routing": "tsx scripts/eval-routing.ts",